TypeScript 98.3%
CSS 0.9%
Shell 0.7%
1import { NextResponse } from "next/server";2import { z } from "zod";3import { apiError, parseBody } from "@/lib/api.ts";4import { hashPassword, passwordPolicyError } from "@/lib/auth/password.ts";5import { rateLimit } from "@/lib/auth/rate-limit.ts";6import { SESSION_COOKIE, assertSameOrigin, createSession, logAuthEvent } from "@/lib/auth/session.ts";7import { all, get, run } from "@/lib/db/index.ts";89const schema = z.object({10 username: z.string().min(3, "Identifiant : 3 caractères minimum").max(60).regex(/^[a-zA-Z0-9._-]+$/, "Identifiant : lettres, chiffres, . _ - seulement"),11 displayName: z.string().min(1).max(120),12 email: z.string().email("Courriel invalide").max(200).optional().or(z.literal("")),13 password: z.string().max(200),14 accessCode: z.string().max(100).optional(),15 courses: z.array(z.enum(["IMM1003", "IMM1033"])).min(1, "Choisissez au moins un cours"),16});1718export async function POST(req: Request) {19 try {20 await assertSameOrigin();21 const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "local";22 const rl = rateLimit(`register:${ip}`, 10, 60 * 60 * 1000);23 if (!rl.ok) return NextResponse.json({ error: "Trop d'inscriptions depuis cette adresse. Réessayez plus tard." }, { status: 429 });2425 const body = await parseBody(req, schema);2627 const requiredCode = process.env.SIGNUP_ACCESS_CODE || "";28 if (requiredCode && body.accessCode !== requiredCode) {29 return NextResponse.json({ error: "Code d'accès invalide. Il est fourni par votre professeur." }, { status: 403 });30 }31 const policyErr = passwordPolicyError(body.password);32 if (policyErr) return NextResponse.json({ error: policyErr }, { status: 400 });3334 if (get("SELECT 1 as ok FROM users WHERE username = ?", body.username)) {35 return NextResponse.json({ error: "Cet identifiant est déjà utilisé." }, { status: 409 });36 }3738 const r = run(39 "INSERT INTO users (username, display_name, email, password_hash, role) VALUES (?, ?, ?, ?, 'student')",40 body.username, body.displayName, body.email || null, hashPassword(body.password)41 );42 const userId = Number(r.lastInsertRowid);43 const validCourses = new Set(all<{ code: string }>("SELECT code FROM courses WHERE active = 1").map((c) => c.code));44 for (const c of body.courses) if (validCourses.has(c)) run("INSERT OR IGNORE INTO enrollments (user_id, course_code) VALUES (?, ?)", userId, c);4546 logAuthEvent("register", { userId, username: body.username, ip });47 const { token, expiresAt } = createSession(userId, ip, req.headers.get("user-agent") ?? undefined);48 const res = NextResponse.json({ ok: true });49 res.cookies.set(SESSION_COOKIE, token, {50 httpOnly: true,51 sameSite: "lax",52 secure: process.env.NODE_ENV === "production" && (process.env.APP_URL ?? "").startsWith("https"),53 expires: expiresAt,54 path: "/",55 });56 return res;57 } catch (e) {58 return apiError(e);59 }60}61