mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-06 21:29:05 +00:00
Initial commit
This commit is contained in:
81
routes/api/auth/ldap/+server.ts
Normal file
81
routes/api/auth/ldap/+server.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { getLdapConfigs, createLdapConfig } from '$lib/server/db';
|
||||
|
||||
// GET /api/auth/ldap - List all LDAP configurations
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
// Allow access when auth is disabled (setup mode) or when user is admin
|
||||
if (auth.authEnabled && (!auth.isAuthenticated || !auth.isAdmin)) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!auth.isEnterprise) {
|
||||
return json({ error: 'Enterprise license required' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const configs = await getLdapConfigs();
|
||||
// Don't return passwords
|
||||
const sanitized = configs.map(config => ({
|
||||
...config,
|
||||
bindPassword: config.bindPassword ? '********' : undefined
|
||||
}));
|
||||
return json(sanitized);
|
||||
} catch (error) {
|
||||
console.error('Failed to get LDAP configs:', error);
|
||||
return json({ error: 'Failed to get LDAP configurations' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// POST /api/auth/ldap - Create a new LDAP configuration
|
||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
// Allow access when auth is disabled (setup mode) or when user is admin
|
||||
if (auth.authEnabled && (!auth.isAuthenticated || !auth.isAdmin)) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!auth.isEnterprise) {
|
||||
return json({ error: 'Enterprise license required' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await request.json();
|
||||
|
||||
// Validate required fields
|
||||
if (!data.name || !data.serverUrl || !data.baseDn) {
|
||||
return json({ error: 'Name, server URL, and base DN are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const config = await createLdapConfig({
|
||||
name: data.name,
|
||||
enabled: data.enabled ?? false,
|
||||
serverUrl: data.serverUrl,
|
||||
bindDn: data.bindDn || undefined,
|
||||
bindPassword: data.bindPassword || undefined,
|
||||
baseDn: data.baseDn,
|
||||
userFilter: data.userFilter || '(uid={{username}})',
|
||||
usernameAttribute: data.usernameAttribute || 'uid',
|
||||
emailAttribute: data.emailAttribute || 'mail',
|
||||
displayNameAttribute: data.displayNameAttribute || 'cn',
|
||||
groupBaseDn: data.groupBaseDn || undefined,
|
||||
groupFilter: data.groupFilter || undefined,
|
||||
adminGroup: data.adminGroup || undefined,
|
||||
roleMappings: data.roleMappings || undefined,
|
||||
tlsEnabled: data.tlsEnabled ?? false,
|
||||
tlsCa: data.tlsCa || undefined
|
||||
});
|
||||
|
||||
return json({
|
||||
...config,
|
||||
bindPassword: config.bindPassword ? '********' : undefined
|
||||
}, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error('Failed to create LDAP config:', error);
|
||||
return json({ error: 'Failed to create LDAP configuration' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
131
routes/api/auth/ldap/[id]/+server.ts
Normal file
131
routes/api/auth/ldap/[id]/+server.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { getLdapConfig, updateLdapConfig, deleteLdapConfig } from '$lib/server/db';
|
||||
|
||||
// GET /api/auth/ldap/[id] - Get a specific LDAP configuration
|
||||
export const GET: RequestHandler = async ({ params, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
// Allow access when auth is disabled (setup mode) or when user is admin
|
||||
if (auth.authEnabled && (!auth.isAuthenticated || !auth.isAdmin)) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!auth.isEnterprise) {
|
||||
return json({ error: 'Enterprise license required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const id = parseInt(params.id!, 10);
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Invalid ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const config = await getLdapConfig(id);
|
||||
if (!config) {
|
||||
return json({ error: 'LDAP configuration not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({
|
||||
...config,
|
||||
bindPassword: config.bindPassword ? '********' : undefined
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get LDAP config:', error);
|
||||
return json({ error: 'Failed to get LDAP configuration' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// PUT /api/auth/ldap/[id] - Update a LDAP configuration
|
||||
export const PUT: RequestHandler = async ({ params, request, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
// Allow access when auth is disabled (setup mode) or when user is admin
|
||||
if (auth.authEnabled && (!auth.isAuthenticated || !auth.isAdmin)) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!auth.isEnterprise) {
|
||||
return json({ error: 'Enterprise license required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const id = parseInt(params.id!, 10);
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Invalid ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const existing = await getLdapConfig(id);
|
||||
if (!existing) {
|
||||
return json({ error: 'LDAP configuration not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
|
||||
// Don't update password if it's the masked value
|
||||
const updateData: any = {};
|
||||
if (data.name !== undefined) updateData.name = data.name;
|
||||
if (data.enabled !== undefined) updateData.enabled = data.enabled;
|
||||
if (data.serverUrl !== undefined) updateData.serverUrl = data.serverUrl;
|
||||
if (data.bindDn !== undefined) updateData.bindDn = data.bindDn;
|
||||
if (data.bindPassword !== undefined && data.bindPassword !== '********') {
|
||||
updateData.bindPassword = data.bindPassword;
|
||||
}
|
||||
if (data.baseDn !== undefined) updateData.baseDn = data.baseDn;
|
||||
if (data.userFilter !== undefined) updateData.userFilter = data.userFilter;
|
||||
if (data.usernameAttribute !== undefined) updateData.usernameAttribute = data.usernameAttribute;
|
||||
if (data.emailAttribute !== undefined) updateData.emailAttribute = data.emailAttribute;
|
||||
if (data.displayNameAttribute !== undefined) updateData.displayNameAttribute = data.displayNameAttribute;
|
||||
if (data.groupBaseDn !== undefined) updateData.groupBaseDn = data.groupBaseDn;
|
||||
if (data.groupFilter !== undefined) updateData.groupFilter = data.groupFilter;
|
||||
if (data.adminGroup !== undefined) updateData.adminGroup = data.adminGroup;
|
||||
if (data.roleMappings !== undefined) updateData.roleMappings = data.roleMappings;
|
||||
if (data.tlsEnabled !== undefined) updateData.tlsEnabled = data.tlsEnabled;
|
||||
if (data.tlsCa !== undefined) updateData.tlsCa = data.tlsCa;
|
||||
|
||||
const config = await updateLdapConfig(id, updateData);
|
||||
if (!config) {
|
||||
return json({ error: 'Failed to update configuration' }, { status: 500 });
|
||||
}
|
||||
|
||||
return json({
|
||||
...config,
|
||||
bindPassword: config.bindPassword ? '********' : undefined
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update LDAP config:', error);
|
||||
return json({ error: 'Failed to update LDAP configuration' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// DELETE /api/auth/ldap/[id] - Delete a LDAP configuration
|
||||
export const DELETE: RequestHandler = async ({ params, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
// Allow access when auth is disabled (setup mode) or when user is admin
|
||||
if (auth.authEnabled && (!auth.isAuthenticated || !auth.isAdmin)) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!auth.isEnterprise) {
|
||||
return json({ error: 'Enterprise license required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const id = parseInt(params.id!, 10);
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Invalid ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const deleted = await deleteLdapConfig(id);
|
||||
if (!deleted) {
|
||||
return json({ error: 'LDAP configuration not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to delete LDAP config:', error);
|
||||
return json({ error: 'Failed to delete LDAP configuration' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
37
routes/api/auth/ldap/[id]/test/+server.ts
Normal file
37
routes/api/auth/ldap/[id]/test/+server.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { testLdapConnection } from '$lib/server/auth';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { getLdapConfig } from '$lib/server/db';
|
||||
|
||||
// POST /api/auth/ldap/[id]/test - Test LDAP connection
|
||||
export const POST: RequestHandler = async ({ params, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
// Allow access when auth is disabled (setup mode) or when user is admin
|
||||
if (auth.authEnabled && (!auth.isAuthenticated || !auth.isAdmin)) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!auth.isEnterprise) {
|
||||
return json({ error: 'Enterprise license required' }, { status: 403 });
|
||||
}
|
||||
|
||||
const id = parseInt(params.id!, 10);
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Invalid ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const config = await getLdapConfig(id);
|
||||
if (!config) {
|
||||
return json({ error: 'LDAP configuration not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const result = await testLdapConnection(id);
|
||||
return json(result);
|
||||
} catch (error) {
|
||||
console.error('Failed to test LDAP connection:', error);
|
||||
return json({ error: 'Failed to test LDAP connection' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
117
routes/api/auth/login/+server.ts
Normal file
117
routes/api/auth/login/+server.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import {
|
||||
authenticateLocal,
|
||||
authenticateLdap,
|
||||
getEnabledLdapConfigs,
|
||||
createUserSession,
|
||||
isRateLimited,
|
||||
recordFailedAttempt,
|
||||
clearRateLimit,
|
||||
verifyMfaToken,
|
||||
isAuthEnabled
|
||||
} from '$lib/server/auth';
|
||||
import { getUser, getUserByUsername } from '$lib/server/db';
|
||||
|
||||
// POST /api/auth/login - Authenticate user
|
||||
export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => {
|
||||
// Check if auth is enabled
|
||||
if (!(await isAuthEnabled())) {
|
||||
return json({ error: 'Authentication is not enabled' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { username, password, mfaToken, provider = 'local' } = await request.json();
|
||||
|
||||
if (!username || !password) {
|
||||
return json({ error: 'Username and password are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Rate limiting by IP and username
|
||||
const clientIp = getClientAddress();
|
||||
const rateLimitKey = `${clientIp}:${username}`;
|
||||
|
||||
const { limited, retryAfter } = isRateLimited(rateLimitKey);
|
||||
if (limited) {
|
||||
return json(
|
||||
{ error: `Too many login attempts. Please try again in ${retryAfter} seconds.` },
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
// Attempt authentication based on provider
|
||||
let result: any;
|
||||
let authProviderType: 'local' | 'ldap' | 'oidc' = 'local';
|
||||
|
||||
if (provider.startsWith('ldap:')) {
|
||||
// LDAP provider with specific config ID (e.g., "ldap:1")
|
||||
const configId = parseInt(provider.split(':')[1], 10);
|
||||
result = await authenticateLdap(username, password, configId);
|
||||
authProviderType = 'ldap';
|
||||
} else if (provider === 'ldap') {
|
||||
// Generic LDAP (will try all enabled configs)
|
||||
result = await authenticateLdap(username, password);
|
||||
authProviderType = 'ldap';
|
||||
} else {
|
||||
result = await authenticateLocal(username, password);
|
||||
authProviderType = 'local';
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
recordFailedAttempt(rateLimitKey);
|
||||
return json({ error: result.error || 'Authentication failed' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Handle MFA if required
|
||||
if (result.requiresMfa) {
|
||||
if (!mfaToken) {
|
||||
// Return that MFA is required
|
||||
return json({ requiresMfa: true }, { status: 200 });
|
||||
}
|
||||
|
||||
// Verify MFA token
|
||||
const user = await getUserByUsername(username);
|
||||
if (!user || !(await verifyMfaToken(user.id, mfaToken))) {
|
||||
recordFailedAttempt(rateLimitKey);
|
||||
return json({ error: 'Invalid MFA code' }, { status: 401 });
|
||||
}
|
||||
|
||||
// MFA verified, create session
|
||||
const session = await createUserSession(user.id, authProviderType, cookies);
|
||||
clearRateLimit(rateLimitKey);
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
displayName: user.displayName,
|
||||
isAdmin: user.isAdmin
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// No MFA, create session directly
|
||||
if (result.user) {
|
||||
const session = await createUserSession(result.user.id, authProviderType, cookies);
|
||||
clearRateLimit(rateLimitKey);
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
user: {
|
||||
id: result.user.id,
|
||||
username: result.user.username,
|
||||
email: result.user.email,
|
||||
displayName: result.user.displayName,
|
||||
isAdmin: result.user.isAdmin
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return json({ error: 'Authentication failed' }, { status: 401 });
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
return json({ error: 'Login failed' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
14
routes/api/auth/logout/+server.ts
Normal file
14
routes/api/auth/logout/+server.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { destroySession } from '$lib/server/auth';
|
||||
|
||||
// POST /api/auth/logout - End session
|
||||
export const POST: RequestHandler = async ({ cookies }) => {
|
||||
try {
|
||||
await destroySession(cookies);
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
return json({ error: 'Logout failed' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
88
routes/api/auth/oidc/+server.ts
Normal file
88
routes/api/auth/oidc/+server.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import {
|
||||
getOidcConfigs,
|
||||
createOidcConfig,
|
||||
type OidcConfig
|
||||
} from '$lib/server/db';
|
||||
|
||||
// GET /api/auth/oidc - List all OIDC configurations
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
// When auth is enabled, require authentication and settings:view permission
|
||||
if (auth.authEnabled) {
|
||||
if (!auth.isAuthenticated) {
|
||||
return json({ error: 'Authentication required' }, { status: 401 });
|
||||
}
|
||||
if (!await auth.can('settings', 'view')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const configs = await getOidcConfigs();
|
||||
// Sanitize sensitive data
|
||||
const sanitized = configs.map(config => ({
|
||||
...config,
|
||||
clientSecret: config.clientSecret ? '********' : ''
|
||||
}));
|
||||
return json(sanitized);
|
||||
} catch (error) {
|
||||
console.error('Failed to get OIDC configs:', error);
|
||||
return json({ error: 'Failed to get OIDC configurations' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// POST /api/auth/oidc - Create new OIDC configuration
|
||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
// When auth is enabled, require authentication and settings:edit permission
|
||||
if (auth.authEnabled) {
|
||||
if (!auth.isAuthenticated) {
|
||||
return json({ error: 'Authentication required' }, { status: 401 });
|
||||
}
|
||||
if (!await auth.can('settings', 'edit')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await request.json();
|
||||
|
||||
// Validate required fields
|
||||
const required = ['name', 'issuerUrl', 'clientId', 'clientSecret', 'redirectUri'];
|
||||
for (const field of required) {
|
||||
if (!data[field]) {
|
||||
return json({ error: `Missing required field: ${field}` }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const config = await createOidcConfig({
|
||||
name: data.name,
|
||||
enabled: data.enabled ?? false,
|
||||
issuerUrl: data.issuerUrl,
|
||||
clientId: data.clientId,
|
||||
clientSecret: data.clientSecret,
|
||||
redirectUri: data.redirectUri,
|
||||
scopes: data.scopes || 'openid profile email',
|
||||
usernameClaim: data.usernameClaim || 'preferred_username',
|
||||
emailClaim: data.emailClaim || 'email',
|
||||
displayNameClaim: data.displayNameClaim || 'name',
|
||||
adminClaim: data.adminClaim || undefined,
|
||||
adminValue: data.adminValue || undefined,
|
||||
roleMappingsClaim: data.roleMappingsClaim || 'groups',
|
||||
roleMappings: data.roleMappings || undefined
|
||||
});
|
||||
|
||||
return json({
|
||||
...config,
|
||||
clientSecret: '********'
|
||||
}, { status: 201 });
|
||||
} catch (error: any) {
|
||||
console.error('Failed to create OIDC config:', error);
|
||||
return json({ error: error.message || 'Failed to create OIDC configuration' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
136
routes/api/auth/oidc/[id]/+server.ts
Normal file
136
routes/api/auth/oidc/[id]/+server.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import {
|
||||
getOidcConfig,
|
||||
updateOidcConfig,
|
||||
deleteOidcConfig
|
||||
} from '$lib/server/db';
|
||||
|
||||
// GET /api/auth/oidc/[id] - Get specific OIDC configuration
|
||||
export const GET: RequestHandler = async ({ params, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
// When auth is enabled, require authentication and settings:view permission
|
||||
if (auth.authEnabled) {
|
||||
if (!auth.isAuthenticated) {
|
||||
return json({ error: 'Authentication required' }, { status: 401 });
|
||||
}
|
||||
if (!await auth.can('settings', 'view')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const id = parseInt(params.id || '');
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Invalid configuration ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const config = await getOidcConfig(id);
|
||||
if (!config) {
|
||||
return json({ error: 'OIDC configuration not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({
|
||||
...config,
|
||||
clientSecret: config.clientSecret ? '********' : ''
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get OIDC config:', error);
|
||||
return json({ error: 'Failed to get OIDC configuration' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// PUT /api/auth/oidc/[id] - Update OIDC configuration
|
||||
export const PUT: RequestHandler = async ({ params, request, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
// When auth is enabled, require authentication and settings:edit permission
|
||||
if (auth.authEnabled) {
|
||||
if (!auth.isAuthenticated) {
|
||||
return json({ error: 'Authentication required' }, { status: 401 });
|
||||
}
|
||||
if (!await auth.can('settings', 'edit')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const id = parseInt(params.id || '');
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Invalid configuration ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const existing = await getOidcConfig(id);
|
||||
if (!existing) {
|
||||
return json({ error: 'OIDC configuration not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const data = await request.json();
|
||||
|
||||
// Don't update clientSecret if it's the masked value
|
||||
const updateData: any = {};
|
||||
if (data.name !== undefined) updateData.name = data.name;
|
||||
if (data.enabled !== undefined) updateData.enabled = data.enabled;
|
||||
if (data.issuerUrl !== undefined) updateData.issuerUrl = data.issuerUrl;
|
||||
if (data.clientId !== undefined) updateData.clientId = data.clientId;
|
||||
if (data.clientSecret !== undefined && data.clientSecret !== '********') {
|
||||
updateData.clientSecret = data.clientSecret;
|
||||
}
|
||||
if (data.redirectUri !== undefined) updateData.redirectUri = data.redirectUri;
|
||||
if (data.scopes !== undefined) updateData.scopes = data.scopes;
|
||||
if (data.usernameClaim !== undefined) updateData.usernameClaim = data.usernameClaim;
|
||||
if (data.emailClaim !== undefined) updateData.emailClaim = data.emailClaim;
|
||||
if (data.displayNameClaim !== undefined) updateData.displayNameClaim = data.displayNameClaim;
|
||||
if (data.adminClaim !== undefined) updateData.adminClaim = data.adminClaim;
|
||||
if (data.adminValue !== undefined) updateData.adminValue = data.adminValue;
|
||||
if (data.roleMappingsClaim !== undefined) updateData.roleMappingsClaim = data.roleMappingsClaim;
|
||||
if (data.roleMappings !== undefined) updateData.roleMappings = data.roleMappings;
|
||||
|
||||
const config = await updateOidcConfig(id, updateData);
|
||||
if (!config) {
|
||||
return json({ error: 'Failed to update OIDC configuration' }, { status: 500 });
|
||||
}
|
||||
|
||||
return json({
|
||||
...config,
|
||||
clientSecret: config.clientSecret ? '********' : ''
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Failed to update OIDC config:', error);
|
||||
return json({ error: error.message || 'Failed to update OIDC configuration' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// DELETE /api/auth/oidc/[id] - Delete OIDC configuration
|
||||
export const DELETE: RequestHandler = async ({ params, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
// When auth is enabled, require authentication and settings:edit permission
|
||||
if (auth.authEnabled) {
|
||||
if (!auth.isAuthenticated) {
|
||||
return json({ error: 'Authentication required' }, { status: 401 });
|
||||
}
|
||||
if (!await auth.can('settings', 'edit')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const id = parseInt(params.id || '');
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Invalid configuration ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const deleted = await deleteOidcConfig(id);
|
||||
if (!deleted) {
|
||||
return json({ error: 'OIDC configuration not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to delete OIDC config:', error);
|
||||
return json({ error: 'Failed to delete OIDC configuration' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
77
routes/api/auth/oidc/[id]/initiate/+server.ts
Normal file
77
routes/api/auth/oidc/[id]/initiate/+server.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { json, redirect } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { buildOidcAuthorizationUrl, isAuthEnabled } from '$lib/server/auth';
|
||||
import { getOidcConfig } from '$lib/server/db';
|
||||
|
||||
// GET /api/auth/oidc/[id]/initiate - Start OIDC authentication flow
|
||||
export const GET: RequestHandler = async ({ params, url }) => {
|
||||
// Check if auth is enabled
|
||||
if (!isAuthEnabled()) {
|
||||
return json({ error: 'Authentication is not enabled' }, { status: 400 });
|
||||
}
|
||||
|
||||
const id = parseInt(params.id || '');
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Invalid configuration ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get redirect URL from query params
|
||||
const redirectUrl = url.searchParams.get('redirect') || '/';
|
||||
|
||||
try {
|
||||
const config = await getOidcConfig(id);
|
||||
if (!config || !config.enabled) {
|
||||
return json({ error: 'OIDC provider not found or disabled' }, { status: 404 });
|
||||
}
|
||||
|
||||
const result = await buildOidcAuthorizationUrl(id, redirectUrl);
|
||||
|
||||
if ('error' in result) {
|
||||
return json({ error: result.error }, { status: 500 });
|
||||
}
|
||||
|
||||
// Redirect to the IdP
|
||||
throw redirect(302, result.url);
|
||||
} catch (error: any) {
|
||||
// Re-throw redirect
|
||||
if (error.status === 302) {
|
||||
throw error;
|
||||
}
|
||||
console.error('Failed to initiate OIDC:', error);
|
||||
return json({ error: error.message || 'Failed to initiate SSO' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// POST /api/auth/oidc/[id]/initiate - Get authorization URL without redirect
|
||||
export const POST: RequestHandler = async ({ params, request }) => {
|
||||
// Check if auth is enabled
|
||||
if (!isAuthEnabled()) {
|
||||
return json({ error: 'Authentication is not enabled' }, { status: 400 });
|
||||
}
|
||||
|
||||
const id = parseInt(params.id || '');
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Invalid configuration ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const redirectUrl = body.redirect || '/';
|
||||
|
||||
const config = await getOidcConfig(id);
|
||||
if (!config || !config.enabled) {
|
||||
return json({ error: 'OIDC provider not found or disabled' }, { status: 404 });
|
||||
}
|
||||
|
||||
const result = await buildOidcAuthorizationUrl(id, redirectUrl);
|
||||
|
||||
if ('error' in result) {
|
||||
return json({ error: result.error }, { status: 500 });
|
||||
}
|
||||
|
||||
return json({ url: result.url });
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get OIDC authorization URL:', error);
|
||||
return json({ error: error.message || 'Failed to initiate SSO' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
28
routes/api/auth/oidc/[id]/test/+server.ts
Normal file
28
routes/api/auth/oidc/[id]/test/+server.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { validateSession, testOidcConnection, isAuthEnabled } from '$lib/server/auth';
|
||||
|
||||
// POST /api/auth/oidc/[id]/test - Test OIDC connection
|
||||
export const POST: RequestHandler = async ({ params, cookies }) => {
|
||||
// When auth is disabled, allow access (for initial setup)
|
||||
// When auth is enabled, require admin
|
||||
if (isAuthEnabled()) {
|
||||
const user = await validateSession(cookies);
|
||||
if (!user || !user.isAdmin) {
|
||||
return json({ error: 'Admin access required' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const id = parseInt(params.id || '');
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Invalid configuration ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await testOidcConnection(id);
|
||||
return json(result);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to test OIDC connection:', error);
|
||||
return json({ success: false, error: error.message || 'Test failed' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
53
routes/api/auth/oidc/callback/+server.ts
Normal file
53
routes/api/auth/oidc/callback/+server.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { json, redirect } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { handleOidcCallback, createUserSession, isAuthEnabled } from '$lib/server/auth';
|
||||
|
||||
// GET /api/auth/oidc/callback - Handle OIDC callback from IdP
|
||||
export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||
// Check if auth is enabled
|
||||
if (!isAuthEnabled()) {
|
||||
throw redirect(302, '/login?error=auth_disabled');
|
||||
}
|
||||
|
||||
// Get parameters from URL
|
||||
const code = url.searchParams.get('code');
|
||||
const state = url.searchParams.get('state');
|
||||
const error = url.searchParams.get('error');
|
||||
const errorDescription = url.searchParams.get('error_description');
|
||||
|
||||
// Handle error from IdP
|
||||
if (error) {
|
||||
console.error('OIDC error from IdP:', error, errorDescription);
|
||||
const errorMsg = encodeURIComponent(errorDescription || error);
|
||||
throw redirect(302, `/login?error=${errorMsg}`);
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!code || !state) {
|
||||
throw redirect(302, '/login?error=invalid_callback');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handleOidcCallback(code, state);
|
||||
|
||||
if (!result.success || !result.user) {
|
||||
const errorMsg = encodeURIComponent(result.error || 'Authentication failed');
|
||||
throw redirect(302, `/login?error=${errorMsg}`);
|
||||
}
|
||||
|
||||
// Create session
|
||||
await createUserSession(result.user.id, 'oidc', cookies);
|
||||
|
||||
// Redirect to the original destination or home
|
||||
const redirectUrl = result.redirectUrl || '/';
|
||||
throw redirect(302, redirectUrl);
|
||||
} catch (error: any) {
|
||||
// Re-throw redirect
|
||||
if (error.status === 302) {
|
||||
throw error;
|
||||
}
|
||||
console.error('OIDC callback error:', error);
|
||||
const errorMsg = encodeURIComponent(error.message || 'Authentication failed');
|
||||
throw redirect(302, `/login?error=${errorMsg}`);
|
||||
}
|
||||
};
|
||||
54
routes/api/auth/providers/+server.ts
Normal file
54
routes/api/auth/providers/+server.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { isAuthEnabled, getEnabledLdapConfigs, getEnabledOidcConfigs } from '$lib/server/auth';
|
||||
import { getAuthSettings } from '$lib/server/db';
|
||||
import { isEnterprise } from '$lib/server/license';
|
||||
|
||||
// GET /api/auth/providers - Get available authentication providers
|
||||
export const GET: RequestHandler = async () => {
|
||||
if (!(await isAuthEnabled())) {
|
||||
return json({ providers: [] });
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch all provider configs in parallel
|
||||
const [settings, enterpriseEnabled, oidcConfigs] = await Promise.all([
|
||||
getAuthSettings(),
|
||||
isEnterprise(),
|
||||
getEnabledOidcConfigs()
|
||||
]);
|
||||
const ldapConfigs = enterpriseEnabled ? await getEnabledLdapConfigs() : [];
|
||||
|
||||
const providers: { id: string; name: string; type: 'local' | 'ldap' | 'oidc'; initiateUrl?: string }[] = [];
|
||||
|
||||
// Local auth is always available when auth is enabled
|
||||
providers.push({ id: 'local', name: 'Local', type: 'local' });
|
||||
|
||||
// Add enabled LDAP providers (enterprise only)
|
||||
for (const config of ldapConfigs) {
|
||||
providers.push({
|
||||
id: `ldap:${config.id}`,
|
||||
name: config.name,
|
||||
type: 'ldap'
|
||||
});
|
||||
}
|
||||
|
||||
// Add enabled OIDC providers (free for all)
|
||||
for (const config of oidcConfigs) {
|
||||
providers.push({
|
||||
id: `oidc:${config.id}`,
|
||||
name: config.name,
|
||||
type: 'oidc',
|
||||
initiateUrl: `/api/auth/oidc/${config.id}/initiate`
|
||||
});
|
||||
}
|
||||
|
||||
return json({
|
||||
providers,
|
||||
defaultProvider: settings.defaultProvider || 'local'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get auth providers:', error);
|
||||
return json({ providers: [{ id: 'local', name: 'Local', type: 'local' }] });
|
||||
}
|
||||
};
|
||||
46
routes/api/auth/session/+server.ts
Normal file
46
routes/api/auth/session/+server.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { validateSession, isAuthEnabled } from '$lib/server/auth';
|
||||
import { getAuthSettings } from '$lib/server/db';
|
||||
|
||||
// GET /api/auth/session - Get current session/user
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
try {
|
||||
const authEnabled = await isAuthEnabled();
|
||||
|
||||
if (!authEnabled) {
|
||||
// Auth is disabled, return anonymous session
|
||||
return json({
|
||||
authenticated: false,
|
||||
authEnabled: false
|
||||
});
|
||||
}
|
||||
|
||||
const user = await validateSession(cookies);
|
||||
|
||||
if (!user) {
|
||||
return json({
|
||||
authenticated: false,
|
||||
authEnabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return json({
|
||||
authenticated: true,
|
||||
authEnabled: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
displayName: user.displayName,
|
||||
avatar: user.avatar,
|
||||
isAdmin: user.isAdmin,
|
||||
provider: user.provider,
|
||||
permissions: user.permissions
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Session check error:', error);
|
||||
return json({ error: 'Failed to check session' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
71
routes/api/auth/settings/+server.ts
Normal file
71
routes/api/auth/settings/+server.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { getAuthSettings, updateAuthSettings, countAdminUsers } from '$lib/server/db';
|
||||
import { isEnterprise } from '$lib/server/license';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
|
||||
// GET /api/auth/settings - Get auth settings
|
||||
// Public when auth is disabled, requires authentication when enabled
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
// When auth is enabled, require authentication first, then settings:view permission
|
||||
if (auth.authEnabled) {
|
||||
if (!auth.isAuthenticated) {
|
||||
return json({ error: 'Authentication required' }, { status: 401 });
|
||||
}
|
||||
if (!await auth.can('settings', 'view')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const settings = await getAuthSettings();
|
||||
return json(settings);
|
||||
} catch (error) {
|
||||
console.error('Failed to get auth settings:', error);
|
||||
return json({ error: 'Failed to get auth settings' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
// PUT /api/auth/settings - Update auth settings
|
||||
// Requires authentication and settings:edit permission
|
||||
export const PUT: RequestHandler = async ({ request, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
// When auth is enabled, require authentication first, then settings:edit permission
|
||||
if (auth.authEnabled) {
|
||||
if (!auth.isAuthenticated) {
|
||||
return json({ error: 'Authentication required' }, { status: 401 });
|
||||
}
|
||||
if (!await auth.can('settings', 'edit')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await request.json();
|
||||
|
||||
// Check if trying to enable auth without required users
|
||||
if (data.authEnabled === true) {
|
||||
const userCount = await countAdminUsers();
|
||||
// PostgreSQL returns bigint for count(*), convert to number for comparison
|
||||
if (Number(userCount) === 0) {
|
||||
const enterprise = await isEnterprise();
|
||||
const errorMessage = enterprise
|
||||
? 'Cannot enable authentication without an admin user. Create a user and assign them the Admin role first.'
|
||||
: 'Cannot enable authentication without any users. Create a user first.';
|
||||
return json({
|
||||
error: errorMessage,
|
||||
requiresUser: true
|
||||
}, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const settings = await updateAuthSettings(data);
|
||||
return json(settings);
|
||||
} catch (error) {
|
||||
console.error('Failed to update auth settings:', error);
|
||||
return json({ error: 'Failed to update auth settings' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user