mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-06 05:39:05 +00:00
Initial commit
This commit is contained in:
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}`);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user