Initial commit

This commit is contained in:
Jarek Krochmalski
2025-12-28 21:16:03 +01:00
commit 62e3c6439e
552 changed files with 104858 additions and 0 deletions

View 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 });
}
};

View 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 });
}
};

View 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 });
}
};

View 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 });
}
};

View 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 });
}
};

View 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 });
}
};

View 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 });
}
};

View 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 });
}
};

View 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 });
}
};

View 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}`);
}
};

View 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' }] });
}
};

View 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 });
}
};

View 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 });
}
};