JavaScript 56.7%
TypeScript 42.6%
1/**2 * Finalisation d'une connexion (partagée par mot de passe, 2FA et code courriel) :3 * analyse IA de sécurité, enregistrement LoginSession, écriture de la session iron-session.4 */5import { NextRequest } from "next/server";6import { randomBytes } from "crypto";7import type { IronSession } from "iron-session";8import { prisma } from "./prisma";9import { logActivity } from "./activity";10import { parseUserAgent } from "./ua-parser";11import { getGeoFromIp } from "./geo";12import { analyzeLogin } from "./ai-security";13import type { SessionData } from "./session";1415export function clientIp(request: NextRequest): string {16 return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "127.0.0.1";17}1819export function describeClient(request: NextRequest) {20 const ua = request.headers.get("user-agent") || "";21 const parsed = parseUserAgent(ua);22 return { ua, parsed, label: `${parsed.browser} / ${parsed.os}${parsed.device ? ` (${parsed.device})` : ""}` };23}2425export type LoginMethod = "password" | "2fa" | "email_code";2627/**28 * Termine la connexion pour `user`. Retourne { ok:false, error, status } si l'agent IA refuse.29 * La `session` est sauvegardée en cas de succès.30 */31export async function completeLogin(32 request: NextRequest,33 session: IronSession<SessionData>,34 user: { id: string; role: string },35 method: LoginMethod36): Promise<{ ok: true } | { ok: false; error: string; status: number }> {37 const ip = clientIp(request);38 const { ua, parsed } = describeClient(request);39 const geo = await getGeoFromIp(ip);4041 const aiResult = await analyzeLogin({ userId: user.id, ip, country: geo.country, city: geo.city, device: parsed.device, browser: parsed.browser, os: parsed.os });4243 const base = { userId: user.id, ip, userAgent: ua, device: parsed.device, browser: parsed.browser, os: parsed.os, country: geo.country, city: geo.city };44 if (aiResult.decision === "refuse") {45 await prisma.loginSession.create({ data: { ...base, status: "refused_by_ai", aiDecision: "refused", aiReason: aiResult.reason, aiRiskLevel: aiResult.riskLevel, endedAt: new Date() } });46 await logActivity("LOGIN_REFUSED_AI", null, `Connexion (${method}) refusée par IA depuis ${ip} (${geo.country}) — ${aiResult.reason}`);47 return { ok: false, error: `Connexion refusée par l'agent IA de sécurité. ${aiResult.reason}`, status: 403 };48 }4950 const loginSession = await prisma.loginSession.create({ data: { ...base, status: "active", aiDecision: "allowed", aiReason: aiResult.reason, aiRiskLevel: aiResult.riskLevel } });5152 session.isLoggedIn = true;53 session.userId = user.id;54 session.userRole = user.role;55 session.pendingTwoFactor = false;56 session.pendingUserId = undefined;57 session.otp = undefined;58 session.sessionToken = loginSession.sessionToken;59 session.csrfToken = randomBytes(32).toString("hex");60 await session.save();6162 const methodLabel = method === "email_code" ? "code courriel" : method === "2fa" ? "2FA" : "mot de passe";63 await logActivity("LOGIN", null, `Connexion (${methodLabel}) depuis ${ip} (${parsed.browser} / ${parsed.os}) — IA : ${aiResult.riskLevel}`);64 return { ok: true };65}66