espace-paie-odentas/app/api/auth/signin-password/route.ts

166 lines
No EOL
5.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { createClient } from "@supabase/supabase-js";
export async function POST(req: Request) {
try {
const { email, password, mfaCode, rememberMe } = await req.json();
if (!email || !password) {
return NextResponse.json({ error: "Email et mot de passe requis" }, { status: 400 });
}
// Utiliser le client Supabase avec gestion des cookies
const supabase = createRouteHandlerClient({ cookies });
// Tentative de connexion avec mot de passe
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
// Erreurs spécifiques
if (error.message.includes("Invalid login credentials")) {
return NextResponse.json({ error: "Email ou mot de passe incorrect" }, { status: 401 });
}
if (error.message.includes("Email not confirmed")) {
return NextResponse.json({ error: "Email non confirmé" }, { status: 401 });
}
return NextResponse.json({ error: error.message }, { status: 400 });
}
const userId = data.user?.id;
if (!userId) {
return NextResponse.json({ error: "Utilisateur introuvable" }, { status: 400 });
}
// Vérifier si l'utilisateur a activé le MFA
const { data: factors } = await supabase.auth.mfa.listFactors();
const hasMfa = factors?.totp && factors.totp.length > 0;
if (hasMfa && !mfaCode) {
// L'utilisateur a activé le 2FA mais n'a pas fourni de code
// On renvoie une réponse spécifique indiquant qu'un code MFA est requis
await supabase.auth.signOut(); // Déconnecter temporairement
return NextResponse.json({
error: "MFA_REQUIRED",
message: "Code 2FA requis"
}, { status: 206 }); // 206 = Partial Content (nécessite action supplémentaire)
}
if (hasMfa && mfaCode) {
// Vérifier le code MFA
try {
const totpFactor = factors?.totp?.[0];
if (!totpFactor) {
return NextResponse.json({ error: "Facteur MFA introuvable" }, { status: 400 });
}
// Créer un challenge pour le facteur TOTP
const { data: challengeData, error: challengeError } = await supabase.auth.mfa.challenge({
factorId: totpFactor.id,
});
if (challengeError) {
console.error("Erreur challenge MFA:", challengeError);
return NextResponse.json({ error: "Erreur lors du challenge MFA" }, { status: 500 });
}
// Vérifier le code TOTP
const { data: verifyData, error: verifyError } = await supabase.auth.mfa.verify({
factorId: totpFactor.id,
challengeId: challengeData.id,
code: mfaCode,
});
if (verifyError) {
console.error("Erreur vérification MFA:", verifyError);
await supabase.auth.signOut(); // Déconnecter en cas d'échec
return NextResponse.json({ error: "Code 2FA invalide" }, { status: 401 });
}
// Le code MFA est valide, continuer avec la connexion
} catch (mfaError: any) {
console.error("Erreur MFA:", mfaError);
await supabase.auth.signOut();
return NextResponse.json({ error: "Erreur lors de la vérification 2FA" }, { status: 500 });
}
}
// Vérifications de révocation (identiques à l'original)
const url = process.env.SUPABASE_URL!;
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const srv = createClient(url, serviceKey, {
auth: { autoRefreshToken: false, persistSession: false }
});
// 1) Si Staff → OK direct
const { data: su } = await srv
.from("staff_users")
.select("is_staff")
.eq("user_id", userId)
.maybeSingle();
if (su?.is_staff) {
return NextResponse.json({ ok: true });
}
// 2) Sinon, vérifier la révocation (user à une seule org)
const { data: mem } = await srv
.from("organization_members")
.select("revoked")
.eq("user_id", userId)
.maybeSingle();
if (!mem || mem.revoked) {
// Déconnecter l'utilisateur si révoqué
await supabase.auth.signOut();
return NextResponse.json({ error: "Compte révoqué" }, { status: 403 });
}
// 3) Gérer les cookies persistants si "rememberMe" est activé
const response = NextResponse.json({ ok: true });
if (rememberMe) {
// Définir un cookie remember_me pour le middleware
response.cookies.set("remember_me", "true", {
maxAge: 60 * 60 * 24 * 30, // 30 jours
path: "/",
sameSite: "lax",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
});
// Prolonger la durée des cookies Supabase
const cookieStore = cookies();
const allCookies = cookieStore.getAll();
allCookies.forEach((cookie) => {
if (cookie.name.startsWith("sb-")) {
response.cookies.set(cookie.name, cookie.value, {
maxAge: 60 * 60 * 24 * 30, // 30 jours
path: "/",
sameSite: "lax",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
});
}
});
console.log("✅ [signin-password] Cookies persistants activés pour 30 jours");
} else {
// Supprimer le cookie remember_me s'il existe
response.cookies.set("remember_me", "", {
maxAge: 0,
path: "/",
});
console.log(" [signin-password] Cookies de session (non persistants)");
}
return response;
} catch (e: any) {
console.error("Erreur signin-password:", e);
return NextResponse.json({ error: e?.message || "Erreur interne" }, { status: 500 });
}
}