// Auteur : Simon-Pierre Boucher — contact@spboucher.ai // KA ID — sessions (JWT HS256 en témoin HttpOnly) + hachage scrypt. import { SignJWT, jwtVerify } from "jose"; import { cookies } from "next/headers"; import crypto from "crypto"; import { findUserById, type UserRow } from "./db"; export const SESSION_COOKIE = "ka_session"; export const BASE_URL = process.env.BASE_URL ?? "https://www.groupe-ka.com"; function secret(): Uint8Array { const s = process.env.KA_AUTH_SECRET; if (!s) { if (process.env.NODE_ENV === "production") throw new Error("KA_AUTH_SECRET manquant"); return new TextEncoder().encode("ka-dev-secret-non-production"); } return new TextEncoder().encode(s); } /* ---------- mots de passe (scrypt natif, zéro dépendance) ---------- */ export function hashPassword(password: string): string { const salt = crypto.randomBytes(16).toString("hex"); const hash = crypto.scryptSync(password, salt, 64).toString("hex"); return `${salt}:${hash}`; } export function verifyPassword(password: string, stored: string): boolean { const [salt, hash] = stored.split(":"); if (!salt || !hash) return false; const test = crypto.scryptSync(password, salt, 64); return crypto.timingSafeEqual(test, Buffer.from(hash, "hex")); } /* ---------- sessions ---------- */ export type SessionUser = { id: number; email: string; name: string; avatarUrl: string | null; hasGoogle: boolean; hasApple: boolean; hasPassword: boolean; kaId: string | null; createdAt: string; lastLogin: string | null; role: string | null; }; export async function mintSessionToken(userId: number): Promise { return await new SignJWT({ uid: userId }) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setIssuer(BASE_URL) .setExpirationTime("30d") .sign(secret()); } export const sessionCookieOptions = { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax" as const, path: "/", maxAge: 60 * 60 * 24 * 30, }; export async function getSessionUser(): Promise { const jar = await cookies(); const token = jar.get(SESSION_COOKIE)?.value; if (!token) return null; try { const { payload } = await jwtVerify(token, secret(), { issuer: BASE_URL, }); const row = findUserById(Number(payload.uid)) as UserRow | undefined; if (!row) return null; return { id: row.id, email: row.email, name: row.name, avatarUrl: row.avatar_url, hasGoogle: !!row.google_sub, hasApple: !!row.apple_sub, hasPassword: !!row.password_hash, kaId: row.ka_id, createdAt: row.created_at, lastLogin: row.last_login, role: row.role, }; } catch { return null; } } /* ---------- redirections sûres ---------- */ // N'accepte que les chemins internes (« /compte ») — jamais d'URL absolue, // sauf un retour SSO interne déjà validé par /sso/authorize. export function safeNext(next: string | null | undefined): string { if (!next) return "/compte"; if (next.startsWith("/") && !next.startsWith("//")) return next; return "/compte"; }