import pkg from '@supabase/ssr'; const { createServerClient } = pkg; import { type Handle, redirect } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; import { env } from '$env/dynamic/private'; import type { Database } from '$lib/types/database'; // Use internal URL for server-side operations (Docker network), fallback to public URL const SERVER_SUPABASE_URL = env.SUPABASE_INTERNAL_URL || PUBLIC_SUPABASE_URL; /** * Supabase authentication hook * Sets up the Supabase client with cookie handling for SSR */ const supabaseHandle: Handle = async ({ event, resolve }) => { event.locals.supabase = createServerClient( SERVER_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { cookies: { getAll: () => event.cookies.getAll(), setAll: (cookiesToSet) => { cookiesToSet.forEach(({ name, value, options }) => { event.cookies.set(name, value, { ...options, path: '/' }); }); } } } ); /** * Safe session getter that validates the JWT * Returns session, user, and member data */ event.locals.safeGetSession = async () => { const { data: { session } } = await event.locals.supabase.auth.getSession(); if (!session) { return { session: null, user: null, member: null }; } // Validate the session by getting the user const { data: { user }, error: userError } = await event.locals.supabase.auth.getUser(); if (userError || !user) { return { session: null, user: null, member: null }; } // Fetch member profile with dues status const { data: member } = await event.locals.supabase .from('members_with_dues') .select('*') .eq('id', user.id) .single(); return { session, user, member }; }; return resolve(event, { filterSerializedResponseHeaders(name) { return name === 'content-range' || name === 'x-supabase-api-version'; } }); }; /** * Authorization hook * Protects routes based on authentication and role requirements */ const authorizationHandle: Handle = async ({ event, resolve }) => { const { session, member } = await event.locals.safeGetSession(); const path = event.url.pathname; // API routes handle their own authentication if (path.startsWith('/api/')) { return resolve(event); } // Auth callback routes should always be accessible if (path.startsWith('/auth/')) { return resolve(event); } // Logout route should always be accessible if (path === '/logout') { return resolve(event); } // Protected routes - require authentication const protectedPrefixes = ['/dashboard', '/profile', '/payments', '/documents', '/board', '/admin']; const isProtectedRoute = protectedPrefixes.some((prefix) => path.startsWith(prefix)); if (isProtectedRoute && !session) { throw redirect(303, `/login?redirectTo=${encodeURIComponent(path)}`); } // Handle authenticated users without a member profile // This can happen if member record creation failed or was deleted if (isProtectedRoute && session && !member) { console.error('Authenticated user has no member profile:', session.user?.id); // Sign them out and redirect to login with an error await event.locals.supabase.auth.signOut(); throw redirect(303, '/login?error=no_profile'); } // Board routes - require board or admin role if (path.startsWith('/board') && member) { if (member.role !== 'board' && member.role !== 'admin') { throw redirect(303, '/dashboard'); } } // Admin routes - require admin role if (path.startsWith('/admin') && member) { if (member.role !== 'admin') { throw redirect(303, '/dashboard'); } } // Redirect authenticated users away from auth pages if (session && (path === '/login' || path === '/signup')) { throw redirect(303, '/dashboard'); } return resolve(event); }; export const handle: Handle = sequence(supabaseHandle, authorizationHandle);