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
|
import type { OrgConfig } from '$lib/server/config';
|
||||||
// for information about these interfaces
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
interface Locals {
|
||||||
// interface Locals {}
|
user?: {
|
||||||
// interface PageData {}
|
id: number;
|
||||||
// interface PageState {}
|
email: string;
|
||||||
// interface Platform {}
|
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