/** * GET /api/me — returns the authenticated user's identity and assigned color. * * Flow (normal — OIDC active): * 1. getAuth(c) reads iss + sub from the OIDC session JWT cookie * (validated and refreshed by oidcAuthMiddleware — never reaches here unauthenticated) * 2. Derives displayName from OIDC claims (name → preferred_username → email → sub fallback) * then calls upsertUser(iss, sub, displayName) — writes on first visit, corrects a * previously blank displayName on subsequent visits (idempotent, keyed on iss+sub, D-10) * 3. Returns { user: { id, displayName, color } } * * Flow (dev bypass — DEV_AUTH_BYPASS=true, non-production): * devAuthBypass() injects DEV_USER via c.set('user', DEV_USER). oidcAuthMiddleware * is NOT mounted in index.ts when the bypass is active, so getAuth(c) is never called. * This handler reads c.get('user') first and short-circuits to return the dev identity * directly, skipping the DB upsert. * * The OIDC session cookie is httpOnly + Secure + SameSite (T-02-03). * No credential or refresh-token data is included in the response (T-02-04). */ import { Hono } from 'hono' import { getAuth } from '../auth/middleware.js' import { upsertUser } from '../auth/user.js' // Side-effect import: brings in the ContextVariableMap augmentation for c.get('user') import '../auth/devBypass.js' export const meRouter = new Hono() meRouter.get('/', async (c) => { // Dev-auth bypass path: devAuthBypass() sets c.get('user') to DEV_USER when active. // Return the injected dev identity directly — no DB round-trip, no OIDC session needed. const devUser = c.get('user') if (devUser) { return c.json({ user: { id: devUser.id, displayName: devUser.displayName, color: devUser.color, }, }) } // Normal OIDC path: getAuth returns null only if the session is invalid. // oidcAuthMiddleware on /api/* redirects unauthenticated requests before this handler // is reached, so null here indicates a genuine session error. const auth = await getAuth(c) if (!auth) { return c.json({ error: 'Unauthorized' }, 401) } // iss and sub are the stable identity fields — identity is always keyed on iss+sub (D-10). const iss = (auth.iss as string | undefined) ?? '' const sub = auth.sub ?? '' // Derive the best available display name from OIDC claims, in preference order: // 1. name — full name set by the IdP (most human-friendly) // 2. preferred_username — often the login handle; still readable // 3. email — readable but reveals contact info; acceptable fallback // 4. sub — always present; not human-friendly but never blank // // Each candidate is tested defensively — Authelia may omit or blank-out any claim. // Whether Authelia emits name/preferred_username is an operator configuration concern // (e.g. userinfo scope, claim mappings in authelia config) — out of scope here. const claimStr = (v: unknown): string | undefined => typeof v === 'string' && v.trim() !== '' ? v.trim() : undefined const displayName = claimStr(auth.name) ?? claimStr(auth.preferred_username) ?? claimStr(auth.email) ?? `Member ${String(sub).slice(0, 8)}` const user = await upsertUser(iss, sub, displayName) if (!user) { return c.json({ error: 'Could not resolve user' }, 500) } return c.json({ user: { id: user.id, displayName: user.displayName, color: user.color, }, }) })