SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
1 h agolast push
JavaScript 56.7% TypeScript 42.6%
3.1 KB · 66 lines typescript
Raw Blame History
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