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 { verifyPassword } 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 { get, run } from "@/lib/db/index.ts";89const schema = z.object({10 username: z.string().min(1).max(100),11 password: z.string().min(1).max(200),12});1314export async function POST(req: Request) {15 try {16 await assertSameOrigin();17 const { username, password } = await parseBody(req, schema);18 const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "local";1920 const rl = rateLimit(`login:${ip}:${username.toLowerCase()}`, 8, 15 * 60 * 1000);21 if (!rl.ok) {22 logAuthEvent("login-rate-limited", { username, ip });23 return NextResponse.json(24 { error: `Trop de tentatives. Réessayez dans ${Math.ceil(rl.retryAfterS / 60)} min.` },25 { status: 429 }26 );27 }2829 const user = get<{ id: number; password_hash: string; disabled: number; must_change_password: number; role: string }>(30 "SELECT id, password_hash, disabled, must_change_password, role FROM users WHERE username = ?",31 username32 );33 if (!user || !verifyPassword(password, user.password_hash)) {34 logAuthEvent("login-failed", { username, ip });35 return NextResponse.json({ error: "Identifiant ou mot de passe incorrect." }, { status: 401 });36 }37 if (user.disabled) {38 logAuthEvent("login-disabled", { userId: user.id, username, ip });39 return NextResponse.json({ error: "Ce compte est désactivé. Contactez le professeur." }, { status: 403 });40 }4142 const { token, expiresAt } = createSession(user.id, ip, req.headers.get("user-agent") ?? undefined);43 run("UPDATE users SET last_login_at = datetime('now') WHERE id = ?", user.id);44 logAuthEvent("login-success", { userId: user.id, username, ip });4546 const res = NextResponse.json({47 ok: true,48 mustChangePassword: !!user.must_change_password,49 role: user.role,50 });51 res.cookies.set(SESSION_COOKIE, token, {52 httpOnly: true,53 sameSite: "lax",54 secure: process.env.NODE_ENV === "production" && (process.env.APP_URL ?? "").startsWith("https"),55 expires: expiresAt,56 path: "/",57 });58 return res;59 } catch (e) {60 return apiError(e);61 }62}63