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%
2.9 KB · 61 lines typescript
Raw Blame History
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