166 lines
No EOL
5.7 KiB
TypeScript
166 lines
No EOL
5.7 KiB
TypeScript
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 });
|
||
}
|
||
} |