import NextAuth from 'next-auth'; import Authentik from 'next-auth/providers/authentik'; import { groupsFromClaim, roleFromGroups, type Role } from './roles'; /** * Authentication against Authentik. * * Sessions are JWTs with no database adapter, which keeps this module usable * from the edge middleware and means a sign-in costs no query. * * The trade-off is worth stating: the role is read from the token, so removing * someone from the admin group does not end a session already in flight — it * takes effect at the next sign-in, or when the eight-hour session expires. * Immediate revocation would mean asking Authentik's API on every request, * which is what api_llm_loxi does and what this application deliberately does * not: it drives a shop window, not a fleet. */ const ADMIN_GROUP = process.env.AUTHENTIK_ADMIN_GROUP?.trim() || 'horaires-admins'; declare module 'next-auth' { interface Session { user: { email?: string | null; name?: string | null; image?: string | null; role: Role; }; } } declare module '@auth/core/jwt' { interface JWT { role?: Role; } } const nextAuth = NextAuth({ providers: [Authentik({ name: process.env.AUTHENTIK_DISPLAY_NAME?.trim() || 'Loxi' })], // The application sits behind a reverse proxy; the forwarded host is the // real one. trustHost: true, session: { strategy: 'jwt', maxAge: 8 * 60 * 60 }, pages: { signIn: '/login', error: '/login' }, callbacks: { jwt({ token, profile }) { // `profile` is only present on the request that follows a sign-in, so // the group membership is resolved once and carried in the token. if (profile) { token.role = roleFromGroups(groupsFromClaim(profile.groups), ADMIN_GROUP); } return token; }, session({ session, token }) { session.user.role = token.role ?? 'viewer'; return session; }, }, }); export const { auth, signIn, signOut } = nextAuth; export const { GET, POST } = nextAuth.handlers;