import { NextResponse } from "next/server"; import { z } from "zod"; import { apiError, parseBody } from "@/lib/api.ts"; import { hashPassword, passwordPolicyError } from "@/lib/auth/password.ts"; import { rateLimit } from "@/lib/auth/rate-limit.ts"; import { SESSION_COOKIE, assertSameOrigin, createSession, logAuthEvent } from "@/lib/auth/session.ts"; import { all, get, run } from "@/lib/db/index.ts"; const schema = z.object({ username: z.string().min(3, "Identifiant : 3 caractères minimum").max(60).regex(/^[a-zA-Z0-9._-]+$/, "Identifiant : lettres, chiffres, . _ - seulement"), displayName: z.string().min(1).max(120), email: z.string().email("Courriel invalide").max(200).optional().or(z.literal("")), password: z.string().max(200), accessCode: z.string().max(100).optional(), courses: z.array(z.enum(["IMM1003", "IMM1033"])).min(1, "Choisissez au moins un cours"), }); export async function POST(req: Request) { try { await assertSameOrigin(); const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "local"; const rl = rateLimit(`register:${ip}`, 10, 60 * 60 * 1000); if (!rl.ok) return NextResponse.json({ error: "Trop d'inscriptions depuis cette adresse. Réessayez plus tard." }, { status: 429 }); const body = await parseBody(req, schema); const requiredCode = process.env.SIGNUP_ACCESS_CODE || ""; if (requiredCode && body.accessCode !== requiredCode) { return NextResponse.json({ error: "Code d'accès invalide. Il est fourni par votre professeur." }, { status: 403 }); } const policyErr = passwordPolicyError(body.password); if (policyErr) return NextResponse.json({ error: policyErr }, { status: 400 }); if (get("SELECT 1 as ok FROM users WHERE username = ?", body.username)) { return NextResponse.json({ error: "Cet identifiant est déjà utilisé." }, { status: 409 }); } const r = run( "INSERT INTO users (username, display_name, email, password_hash, role) VALUES (?, ?, ?, ?, 'student')", body.username, body.displayName, body.email || null, hashPassword(body.password) ); const userId = Number(r.lastInsertRowid); const validCourses = new Set(all<{ code: string }>("SELECT code FROM courses WHERE active = 1").map((c) => c.code)); for (const c of body.courses) if (validCourses.has(c)) run("INSERT OR IGNORE INTO enrollments (user_id, course_code) VALUES (?, ?)", userId, c); logAuthEvent("register", { userId, username: body.username, ip }); const { token, expiresAt } = createSession(userId, ip, req.headers.get("user-agent") ?? undefined); const res = NextResponse.json({ ok: true }); res.cookies.set(SESSION_COOKIE, token, { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production" && (process.env.APP_URL ?? "").startsWith("https"), expires: expiresAt, path: "/", }); return res; } catch (e) { return apiError(e); } }