import { createServerClient } from '@supabase/ssr' import { type Handle, redirect } from '@sveltejs/kit' import { sequence } from '@sveltejs/kit/hooks' import { env } from '$env/dynamic/public' const supabase: Handle = async ({ event, resolve }) => { /** * Creates a Supabase client specific to this server request. * * The Supabase client gets the Auth token from the request cookies. */ event.locals.supabase = createServerClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY, { cookies: { getAll: () => event.cookies.getAll(), /** * SvelteKit's cookies API requires `path` to be explicitly set in * the cookie options. Setting `path` to `/` replicates previous/ * standard behavior. */ setAll: (cookiesToSet) => { cookiesToSet.forEach(({ name, value, options }) => { event.cookies.set(name, value, { ...options, path: '/' }) }) }, }, }) /** * Unlike `supabase.auth.getSession()`, which returns the session _without_ * validating the JWT, this function also calls `getUser()` to validate the * JWT before returning the session. */ event.locals.safeGetSession = async () => { const { data: { session }, } = await event.locals.supabase.auth.getSession() if (!session) { return { session: null, user: null } } const { data: { user }, error, } = await event.locals.supabase.auth.getUser() if (error) { // JWT validation has failed return { session: null, user: null } } return { session, user } } /** * Fetch user profile data including display name, section position, and section name */ event.locals.getUserProfile = async (userId) => { if (!userId) return null const { data: profile, error } = await event.locals.supabase .from('profiles') .select('display_name, section_position, section:sections (name)') .eq('id', userId) .single() if (error) return null return profile } return resolve(event, { filterSerializedResponseHeaders(name) { /** * Supabase libraries use the `content-range` and `x-supabase-api-version` * headers, so we need to tell SvelteKit to pass it through. */ return name === 'content-range' || name === 'x-supabase-api-version' }, }) } const authGuard: Handle = async ({ event, resolve }) => { const { session, user } = await event.locals.safeGetSession() event.locals.session = session event.locals.user = user // Fetch the user's profile if they're authenticated if (user) { event.locals.profile = await event.locals.getUserProfile(user.id) } if (!event.locals.session && event.url.pathname.startsWith('/private')) { redirect(303, '/auth/login') } if (event.locals.session && event.url.pathname === '/auth') { redirect(303, '/private/home') } // Role-based access control for events routes if (event.url.pathname.startsWith('/private/events')) { if (!event.locals.profile || event.locals.profile.section_position !== 'events_manager') { redirect(303, '/private/errors/events/denied') } } return resolve(event) } export const handle: Handle = sequence(supabase, authGuard)