SPB Git forge

spb/immbot-ai

Public
1commits 1branches 0releases
1.5 MBsize
maindefault branch
20 days agolast push
TypeScript 98.3% CSS 0.9% Shell 0.7%
3.9 KB · 121 lines typescript
Raw Blame History
1// Sessions serveur : jeton aléatoire 256 bits remis en cookie httpOnly,2// haché (SHA-256) avant stockage en base — un vol de BD ne permet pas de rejouer une session.3import { createHash, randomBytes } from "node:crypto";4import { cookies, headers } from "next/headers";5import { get, run } from "../db/index.ts";67export const SESSION_COOKIE = "immbot_session";8const SESSION_DAYS = 14;910export type SessionUser = {11  id: number;12  username: string;13  display_name: string;14  email: string | null;15  role: "student" | "instructor" | "admin";16  must_change_password: number;17  is_initial_admin: number;18  disabled: number;19};2021function hashToken(token: string): string {22  return createHash("sha256").update(token).digest("hex");23}2425export function createSession(userId: number, ip?: string, userAgent?: string): { token: string; expiresAt: Date } {26  const token = randomBytes(32).toString("hex");27  const expiresAt = new Date(Date.now() + SESSION_DAYS * 86400_000);28  run(29    "INSERT INTO sessions (token_hash, user_id, expires_at, ip, user_agent) VALUES (?, ?, ?, ?, ?)",30    hashToken(token),31    userId,32    expiresAt.toISOString(),33    ip ?? null,34    (userAgent ?? "").slice(0, 300)35  );36  return { token, expiresAt };37}3839export function destroySession(token: string) {40  run("DELETE FROM sessions WHERE token_hash = ?", hashToken(token));41}4243export function destroyAllSessions(userId: number) {44  run("DELETE FROM sessions WHERE user_id = ?", userId);45}4647export function userForToken(token: string | undefined): SessionUser | null {48  if (!token) return null;49  const row = get<SessionUser & { expires_at: string }>(50    `SELECT u.id, u.username, u.display_name, u.email, u.role, u.must_change_password,51            u.is_initial_admin, u.disabled, s.expires_at52     FROM sessions s JOIN users u ON u.id = s.user_id53     WHERE s.token_hash = ?`,54    hashToken(token)55  );56  if (!row) return null;57  if (new Date(row.expires_at) < new Date()) {58    destroySession(token);59    return null;60  }61  if (row.disabled) return null;62  return row;63}6465/** Utilisateur courant (Server Components et Route Handlers). */66export async function currentUser(): Promise<SessionUser | null> {67  const jar = await cookies();68  return userForToken(jar.get(SESSION_COOKIE)?.value);69}7071export async function requireUser(): Promise<SessionUser> {72  const u = await currentUser();73  if (!u) throw new AuthError(401, "Authentification requise.");74  return u;75}7677export async function requireRole(role: "instructor" | "admin"): Promise<SessionUser> {78  const u = await requireUser();79  const ok = u.role === "admin" || (role === "instructor" && u.role === "instructor");80  if (!ok) throw new AuthError(403, "Accès refusé.");81  return u;82}8384export class AuthError extends Error {85  status: number;86  constructor(status: number, message: string) {87    super(message);88    this.status = status;89  }90}9192/** Défense CSRF : les mutations doivent venir de la même origine. */93export async function assertSameOrigin(): Promise<void> {94  const h = await headers();95  const site = h.get("sec-fetch-site");96  if (site && site !== "same-origin" && site !== "none") throw new AuthError(403, "Origine non autorisée.");97  const origin = h.get("origin");98  if (origin) {99    const expected = process.env.APP_URL || "";100    const host = h.get("host") || "";101    try {102      const o = new URL(origin);103      if (o.host !== host && (!expected || origin !== expected)) throw new AuthError(403, "Origine non autorisée.");104    } catch (e) {105      if (e instanceof AuthError) throw e;106      throw new AuthError(403, "Origine invalide.");107    }108  }109}110111export function logAuthEvent(event: string, opts: { userId?: number; username?: string; ip?: string; detail?: string }) {112  run(113    "INSERT INTO auth_events (user_id, username, event, ip, detail) VALUES (?, ?, ?, ?, ?)",114    opts.userId ?? null,115    opts.username ?? null,116    event,117    opts.ip ?? null,118    opts.detail ?? null119  );120}121