feat(phase2): OAuth/Authentik integration + drag-and-drop reordering
- Add OIDC/OAuth2 login via openid-client with PKCE flow - Auto-provision OAuth users with group mapping - Conditional login page (OAuth/local/both based on auth mode) - Admin OAuth test connection button - Install svelte-dnd-action for board editor DnD - Draggable sections and widgets with cross-section moves - Reorder APIs with atomic Prisma transactions - Visual drag handles and drop zone indicators
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { redirect, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types.js';
|
||||
import * as oauthService from '$lib/server/services/oauthService.js';
|
||||
import * as userService from '$lib/server/services/userService.js';
|
||||
import * as authService from '$lib/server/services/authService.js';
|
||||
|
||||
const COOKIE_BASE = {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax' as const,
|
||||
path: '/'
|
||||
};
|
||||
|
||||
export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||
try {
|
||||
// Check for error response from the provider
|
||||
const oauthError = url.searchParams.get('error');
|
||||
if (oauthError) {
|
||||
const description = url.searchParams.get('error_description') || oauthError;
|
||||
throw new Error(`OAuth provider returned an error: ${description}`);
|
||||
}
|
||||
|
||||
// Ensure we have an authorization code
|
||||
const code = url.searchParams.get('code');
|
||||
if (!code) {
|
||||
throw new Error('No authorization code received from OAuth provider');
|
||||
}
|
||||
|
||||
// Retrieve the code_verifier from the cookie
|
||||
const codeVerifier = cookies.get('oauth_code_verifier');
|
||||
if (!codeVerifier) {
|
||||
throw new Error('OAuth session expired. Please try logging in again.');
|
||||
}
|
||||
|
||||
// Clear the code_verifier cookie
|
||||
cookies.delete('oauth_code_verifier', { path: '/' });
|
||||
|
||||
// Exchange the authorization code for tokens and get user info
|
||||
const userInfo = await oauthService.handleCallback(url, codeVerifier);
|
||||
|
||||
// Find or create local user from OAuth info
|
||||
const user = await userService.findOrCreateByOAuth({
|
||||
email: userInfo.email,
|
||||
displayName: userInfo.name || userInfo.preferred_username || userInfo.email.split('@')[0],
|
||||
avatarUrl: userInfo.picture,
|
||||
groups: userInfo.groups ? [...userInfo.groups] : undefined
|
||||
});
|
||||
|
||||
// Issue local JWT tokens (same as local auth flow)
|
||||
const accessToken = authService.signAccessToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role
|
||||
});
|
||||
const refreshToken = authService.generateRefreshToken();
|
||||
await authService.saveRefreshToken(user.id, refreshToken);
|
||||
|
||||
// Set session cookies
|
||||
cookies.set('access_token', accessToken, {
|
||||
...COOKIE_BASE,
|
||||
maxAge: 900 // 15 minutes
|
||||
});
|
||||
cookies.set('refresh_token', refreshToken, {
|
||||
...COOKIE_BASE,
|
||||
maxAge: 604800 // 7 days
|
||||
});
|
||||
cookies.set('refresh_user_id', user.id, {
|
||||
...COOKIE_BASE,
|
||||
maxAge: 604800 // 7 days
|
||||
});
|
||||
|
||||
throw redirect(302, '/');
|
||||
} catch (err) {
|
||||
// Re-throw redirects
|
||||
if (err && typeof err === 'object' && 'status' in err && (err as { status: number }).status === 302) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const message = err instanceof Error ? err.message : 'OAuth authentication failed';
|
||||
throw error(500, message);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user