import { NextResponse } from "next/server"; import { z } from "zod"; import { apiError, parseBody } from "@/lib/api.ts"; import { verifyPassword } 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 { get, run } from "@/lib/db/index.ts"; const schema = z.object({ username: z.string().min(1).max(100), password: z.string().min(1).max(200), }); export async function POST(req: Request) { try { await assertSameOrigin(); const { username, password } = await parseBody(req, schema); const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "local"; const rl = rateLimit(`login:${ip}:${username.toLowerCase()}`, 8, 15 * 60 * 1000); if (!rl.ok) { logAuthEvent("login-rate-limited", { username, ip }); return NextResponse.json( { error: `Trop de tentatives. Réessayez dans ${Math.ceil(rl.retryAfterS / 60)} min.` }, { status: 429 } ); } const user = get<{ id: number; password_hash: string; disabled: number; must_change_password: number; role: string }>( "SELECT id, password_hash, disabled, must_change_password, role FROM users WHERE username = ?", username ); if (!user || !verifyPassword(password, user.password_hash)) { logAuthEvent("login-failed", { username, ip }); return NextResponse.json({ error: "Identifiant ou mot de passe incorrect." }, { status: 401 }); } if (user.disabled) { logAuthEvent("login-disabled", { userId: user.id, username, ip }); return NextResponse.json({ error: "Ce compte est désactivé. Contactez le professeur." }, { status: 403 }); } const { token, expiresAt } = createSession(user.id, ip, req.headers.get("user-agent") ?? undefined); run("UPDATE users SET last_login_at = datetime('now') WHERE id = ?", user.id); logAuthEvent("login-success", { userId: user.id, username, ip }); const res = NextResponse.json({ ok: true, mustChangePassword: !!user.must_change_password, role: user.role, }); 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); } }