1c0a7cb850
Phase 4 — New Widget Types: - Clock/Weather, System Stats, RSS/Feed, Calendar, Markdown, Metric/Counter, Link Group, Camera/Stream widgets - Backend services with caching for each data source - Full creation form with dynamic config fields per type Phase 5 — Visual & Styling Enhancements: - Glassmorphism card style (solid/glass/outline) - Board-level themes with per-board hue/saturation - Animated SVG status rings replacing static dots - Card size options (compact/medium/large) - Custom CSS injection (admin + per-board, sanitized) - Wallpaper backgrounds with blur/overlay/parallax Phase 6 — Functional Features: - Favorites bar with drag-and-drop reordering - Recent apps tracking with privacy toggle - Uptime dashboard page (/status, guest-accessible) - Notifications system (Discord/Slack/Telegram/HTTP webhooks) - App tags with filtering in board view - Multi-URL app cards with expandable sub-links - Personal API tokens with scoped permissions - Audit log with retention and admin viewer Phase 7 — Quality of Life: - Onboarding wizard (5-step first-launch setup) - App URL health preview with favicon/title detection - Board templates (4 built-in + custom import/export) - Keyboard shortcut overlay (j/k nav, 1-9 boards, ? help) 212 files changed, 15641 insertions, 980 deletions. Build, lint, type check, and 222 tests all pass.
100 lines
3.1 KiB
TypeScript
100 lines
3.1 KiB
TypeScript
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 and state from cookies
|
|
const codeVerifier = cookies.get('oauth_code_verifier');
|
|
if (!codeVerifier) {
|
|
throw new Error('OAuth session expired. Please try logging in again.');
|
|
}
|
|
|
|
const expectedState = cookies.get('oauth_state');
|
|
if (!expectedState) {
|
|
throw new Error('OAuth session expired. Please try logging in again.');
|
|
}
|
|
|
|
// Validate the state parameter matches to prevent CSRF
|
|
const returnedState = url.searchParams.get('state');
|
|
if (returnedState !== expectedState) {
|
|
throw new Error('OAuth state mismatch. Possible CSRF attack. Please try logging in again.');
|
|
}
|
|
|
|
// Clear the OAuth cookies
|
|
cookies.delete('oauth_code_verifier', { path: '/' });
|
|
cookies.delete('oauth_state', { path: '/' });
|
|
|
|
// Exchange the authorization code for tokens and get user info
|
|
const userInfo = await oauthService.handleCallback(url, codeVerifier, expectedState);
|
|
|
|
// 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);
|
|
}
|
|
};
|