import { SvelteKitAuth } from '@auth/sveltekit'; import type { OIDCConfig } from '@auth/core/providers'; import { env } from '$env/dynamic/private'; interface AuthentikProfile { sub: string; email: string; name: string; preferred_username: string; groups: string[]; } /** * Fetch the user's node_id from maskinrommet using the Authentik access token. * Called once at sign-in so the node_id is cached in the JWT for future requests. */ async function fetchNodeId(accessToken: string): Promise { const url = env.MASKINROMMET_URL ?? 'https://api.sidelinja.org'; try { const res = await fetch(`${url}/me`, { headers: { Authorization: `Bearer ${accessToken}` } }); if (res.ok) { const data = await res.json(); return data.node_id ?? null; } console.error(`[auth] /me returned ${res.status}: ${await res.text()}`); } catch (e) { console.error('[auth] Failed to fetch node_id from maskinrommet:', e); } return null; } /** * Sync user profile (username) to maskinrommet on sign-in. * Stores Authentik preferred_username in auth_identities.username. */ async function syncUsername(accessToken: string, username: string): Promise { const url = env.MASKINROMMET_URL ?? 'https://api.sidelinja.org'; try { const res = await fetch(`${url}/auth/sync`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ username }) }); if (!res.ok) { console.error(`[auth] /auth/sync returned ${res.status}: ${await res.text()}`); } } catch (e) { console.error('[auth] Failed to sync username to maskinrommet:', e); } } export const { handle, signIn, signOut } = SvelteKitAuth({ trustHost: true, providers: [ { id: 'authentik', name: 'Authentik', type: 'oidc', issuer: env.AUTHENTIK_ISSUER, clientId: env.AUTHENTIK_CLIENT_ID, clientSecret: env.AUTHENTIK_CLIENT_SECRET, authorization: { params: { scope: 'openid email profile offline_access' } }, checks: ['pkce', 'state'], profile(profile: AuthentikProfile) { return { id: profile.sub, name: profile.name ?? profile.preferred_username, email: profile.email }; } } satisfies OIDCConfig ], callbacks: { async jwt({ token, account, profile }) { // profile is only available on initial sign-in, not on refresh. // Store authentik_sub in the token so it persists across refreshes. if (profile?.sub) { token.authentik_sub = profile.sub; } // On initial sign-in, fetch node_id and store access_token if (account?.access_token) { token.access_token = account.access_token; if (!token.node_id) { token.node_id = await fetchNodeId(account.access_token); } // Sync username from Authentik preferred_username on each sign-in const p = profile as AuthentikProfile | undefined; if (p?.preferred_username) { await syncUsername(account.access_token, p.preferred_username); } } return token; }, session({ session, token }) { if (session.user) { // Use Authentik sub as user ID (not @auth/sveltekit's internal ID) session.user.id = (token.authentik_sub ?? token.sub) as string; } // Expose node_id and access_token so frontend can call maskinrommet // eslint-disable-next-line @typescript-eslint/no-explicit-any const s = session as any; s.nodeId = token.node_id as string | undefined; s.accessToken = token.access_token as string | undefined; return session; } } });