feat: add toast state, server hooks (auth, guards, tracking, security headers)
This commit is contained in:
Vendored
+14
-7
@@ -1,12 +1,19 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
import type { OrgConfig } from '$lib/server/config';
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
interface Locals {
|
||||
user?: {
|
||||
id: number;
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
active: boolean;
|
||||
mustChangePassword: boolean;
|
||||
};
|
||||
sessionToken?: string;
|
||||
orgConfig: OrgConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { validateSession } from '$lib/server/auth';
|
||||
import { getOrgConfig } from '$lib/server/config';
|
||||
import { trackPageView } from '$lib/server/track';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Load org config (cached)
|
||||
event.locals.orgConfig = await getOrgConfig();
|
||||
|
||||
// Session validation
|
||||
const sessionToken = event.cookies.get('session');
|
||||
if (sessionToken) {
|
||||
const result = await validateSession(sessionToken);
|
||||
if (result) {
|
||||
event.locals.user = result.user;
|
||||
event.locals.sessionToken = sessionToken;
|
||||
} else {
|
||||
event.cookies.delete('session', { path: '/' });
|
||||
}
|
||||
}
|
||||
|
||||
// Force password change
|
||||
if (event.locals.user?.mustChangePassword) {
|
||||
const path = event.url.pathname;
|
||||
if (path !== '/change-password' && path !== '/logout' && !path.startsWith('/api/')) {
|
||||
throw redirect(302, '/change-password');
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to setup wizard if not complete
|
||||
if (event.locals.user && !event.locals.orgConfig.setupComplete) {
|
||||
const path = event.url.pathname;
|
||||
if (
|
||||
path.startsWith('/admin') &&
|
||||
path !== '/admin' &&
|
||||
!path.startsWith('/setup') &&
|
||||
!path.startsWith('/api/') &&
|
||||
path !== '/logout'
|
||||
) {
|
||||
throw redirect(302, '/setup');
|
||||
}
|
||||
}
|
||||
|
||||
// Auth guards
|
||||
if (event.url.pathname.startsWith('/admin')) {
|
||||
if (!event.locals.user) {
|
||||
throw redirect(302, '/login');
|
||||
}
|
||||
}
|
||||
|
||||
if (event.url.pathname.startsWith('/foster')) {
|
||||
if (!event.locals.user) {
|
||||
throw redirect(302, '/login');
|
||||
}
|
||||
}
|
||||
|
||||
// Page view tracking (async, don't await)
|
||||
const path = event.url.pathname;
|
||||
if (!path.startsWith('/admin') && !path.startsWith('/api/') && !path.startsWith('/foster')) {
|
||||
const ipAddress = event.request.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
|
||||
|| event.getClientAddress();
|
||||
trackPageView({
|
||||
path,
|
||||
ipAddress,
|
||||
userAgent: event.request.headers.get('user-agent') || undefined,
|
||||
referrer: event.request.headers.get('referer') || undefined
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve with security headers
|
||||
const response = await resolve(event);
|
||||
|
||||
response.headers.set('X-Frame-Options', 'SAMEORIGIN');
|
||||
response.headers.set('X-Content-Type-Options', 'nosniff');
|
||||
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
||||
|
||||
return response;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
interface Toast {
|
||||
id: number;
|
||||
message: string;
|
||||
type: 'success' | 'error' | 'info';
|
||||
}
|
||||
|
||||
let toasts = $state<Toast[]>([]);
|
||||
let nextId = 0;
|
||||
|
||||
export function addToast(message: string, type: Toast['type'] = 'info'): void {
|
||||
const id = nextId++;
|
||||
toasts.push({ id, message, type });
|
||||
setTimeout(() => removeToast(id), 5000);
|
||||
}
|
||||
|
||||
export function removeToast(id: number): void {
|
||||
toasts = toasts.filter((t) => t.id !== id);
|
||||
}
|
||||
|
||||
export function getToasts(): Toast[] {
|
||||
return toasts;
|
||||
}
|
||||
Reference in New Issue
Block a user