/** * Finalisation d'une connexion (partagée par mot de passe, 2FA et code courriel) : * analyse IA de sécurité, enregistrement LoginSession, écriture de la session iron-session. */ import { NextRequest } from "next/server"; import { randomBytes } from "crypto"; import type { IronSession } from "iron-session"; import { prisma } from "./prisma"; import { logActivity } from "./activity"; import { parseUserAgent } from "./ua-parser"; import { getGeoFromIp } from "./geo"; import { analyzeLogin } from "./ai-security"; import type { SessionData } from "./session"; export function clientIp(request: NextRequest): string { return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || request.headers.get("x-real-ip") || "127.0.0.1"; } export function describeClient(request: NextRequest) { const ua = request.headers.get("user-agent") || ""; const parsed = parseUserAgent(ua); return { ua, parsed, label: `${parsed.browser} / ${parsed.os}${parsed.device ? ` (${parsed.device})` : ""}` }; } export type LoginMethod = "password" | "2fa" | "email_code"; /** * Termine la connexion pour `user`. Retourne { ok:false, error, status } si l'agent IA refuse. * La `session` est sauvegardée en cas de succès. */ export async function completeLogin( request: NextRequest, session: IronSession, user: { id: string; role: string }, method: LoginMethod ): Promise<{ ok: true } | { ok: false; error: string; status: number }> { const ip = clientIp(request); const { ua, parsed } = describeClient(request); const geo = await getGeoFromIp(ip); const aiResult = await analyzeLogin({ userId: user.id, ip, country: geo.country, city: geo.city, device: parsed.device, browser: parsed.browser, os: parsed.os }); const base = { userId: user.id, ip, userAgent: ua, device: parsed.device, browser: parsed.browser, os: parsed.os, country: geo.country, city: geo.city }; if (aiResult.decision === "refuse") { await prisma.loginSession.create({ data: { ...base, status: "refused_by_ai", aiDecision: "refused", aiReason: aiResult.reason, aiRiskLevel: aiResult.riskLevel, endedAt: new Date() } }); await logActivity("LOGIN_REFUSED_AI", null, `Connexion (${method}) refusée par IA depuis ${ip} (${geo.country}) — ${aiResult.reason}`); return { ok: false, error: `Connexion refusée par l'agent IA de sécurité. ${aiResult.reason}`, status: 403 }; } const loginSession = await prisma.loginSession.create({ data: { ...base, status: "active", aiDecision: "allowed", aiReason: aiResult.reason, aiRiskLevel: aiResult.riskLevel } }); session.isLoggedIn = true; session.userId = user.id; session.userRole = user.role; session.pendingTwoFactor = false; session.pendingUserId = undefined; session.otp = undefined; session.sessionToken = loginSession.sessionToken; session.csrfToken = randomBytes(32).toString("hex"); await session.save(); const methodLabel = method === "email_code" ? "code courriel" : method === "2fa" ? "2FA" : "mot de passe"; await logActivity("LOGIN", null, `Connexion (${methodLabel}) depuis ${ip} (${parsed.browser} / ${parsed.os}) — IA : ${aiResult.riskLevel}`); return { ok: true }; }