const passport = require("passport");
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const FacebookStrategy = require("passport-facebook").Strategy;
const crypto = require("crypto");
const prisma = require("./prisma");
const transporter = require("./mailer");
const getFrontendUrl = require("../utils/frontendUrl");
const { normalizeEmail } = require("../validations/email.validation");
const PROVIDER_LABELS = { GOOGLE: "Google", FACEBOOK: "Facebook" };
// Un compte créé via un provider externe n'a pas de mot de passe. On lui en
// propose un dès l'inscription, via le même mécanisme de jeton que la
// réinitialisation : sans mot de passe, l'utilisateur dépend entièrement du
// provider pour se connecter, et le formulaire de changement de son profil lui
// reste inaccessible.
const PASSWORD_SETUP_EXPIRY_MS = 1000 * 60 * 60 * 24; // 24 h
async function sendPasswordSetupEmail(user, providerLabel) {
const token = crypto.randomBytes(32).toString("hex");
await prisma.user.update({
where: { id: user.id },
data: {
resetPasswordToken: token,
resetPasswordExpires: new Date(Date.now() + PASSWORD_SETUP_EXPIRY_MS),
},
});
const setupUrl = `${getFrontendUrl()}/definir-mot-de-passe/${token}`;
await transporter.sendMail({
from: process.env.MAIL_FROM,
to: user.email,
subject: "Définissez votre mot de passe Thé Tip Top",
html: `
Bienvenue ${user.firstname} !
Votre compte a été créé avec ${providerLabel}. Vous pouvez continuer à vous
connecter de cette façon, ou définir un mot de passe pour accéder
également à votre compte sans passer par ${providerLabel}.
Définir mon mot de passe
Ce lien expire dans 24 heures. Si vous
ne souhaitez pas de mot de passe, ignorez simplement cet email : votre
connexion ${providerLabel} continuera de fonctionner.
`,
});
}
// Callback de vérification commun à tous les providers : passport normalise
// le profil (id, name, emails, photos), seule la valeur de `provider` change.
function verifyOAuthProfile(provider) {
const providerLabel = PROVIDER_LABELS[provider];
return async (accessToken, refreshToken, profile, done) => {
try {
const email = normalizeEmail(profile.emails?.[0]?.value);
// Cas fréquent chez Facebook : compte ouvert avec un numéro de téléphone.
if (!email) {
return done(null, false, {
message: `Aucune adresse email n'est associée à ce compte ${providerLabel}.`,
});
}
// 1. Compte déjà lié à ce compte externe : connexion directe.
const byProvider = await prisma.user.findUnique({
where: {
provider_providerId: { provider, providerId: profile.id },
},
});
if (byProvider) return done(null, byProvider);
// 2. Un compte existe avec le même email : on le rattache au provider.
// L'email étant déjà validé par le provider, on en profite pour lever
// la vérification si elle était en attente.
const byEmail = await prisma.user.findUnique({ where: { email } });
if (byEmail) {
const linked = await prisma.user.update({
where: { id: byEmail.id },
data: {
provider,
providerId: profile.id,
isEmailVerified: true,
emailVerifyToken: null,
emailVerifyExpires: null,
avatarUrl: byEmail.avatarUrl || profile.photos?.[0]?.value || null,
},
});
return done(null, linked);
}
// 3. Première connexion : création du compte.
const created = await prisma.user.create({
data: {
firstname: profile.name?.givenName || profile.displayName || "Utilisateur",
lastname: profile.name?.familyName || "",
email,
password: null,
provider,
providerId: profile.id,
avatarUrl: profile.photos?.[0]?.value || null,
isEmailVerified: true,
},
});
// Un envoi qui échoue ne doit pas bloquer la connexion : le compte
// est valide, et « mot de passe oublié » permet de rattraper.
try {
await sendPasswordSetupEmail(created, providerLabel);
} catch (mailError) {
console.error("Erreur envoi email de definition du mot de passe:", mailError);
}
return done(null, created);
} catch (error) {
return done(error);
}
};
}
// Authentification stateless : le callback OAuth émet directement notre JWT,
// donc pas de session Express ni de serialize/deserialize à brancher.
const apiUrl = process.env.APP_URL || "http://localhost:5000";
const googleEnabled =
Boolean(process.env.GOOGLE_CLIENT_ID) && Boolean(process.env.GOOGLE_CLIENT_SECRET);
const facebookEnabled =
Boolean(process.env.FACEBOOK_APP_ID) && Boolean(process.env.FACEBOOK_APP_SECRET);
if (googleEnabled) {
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: `${apiUrl}/api/auth/google/callback`,
scope: ["profile", "email"],
},
verifyOAuthProfile("GOOGLE")
)
);
}
if (facebookEnabled) {
passport.use(
new FacebookStrategy(
{
clientID: process.env.FACEBOOK_APP_ID,
clientSecret: process.env.FACEBOOK_APP_SECRET,
callbackURL: `${apiUrl}/api/auth/facebook/callback`,
// Sans profileFields, la Graph API ne renvoie que l'id et le nom.
profileFields: ["id", "displayName", "name", "emails", "photos"],
// appsecret_proof : empêche l'usage d'un access token volé ailleurs.
enableProof: true,
},
verifyOAuthProfile("FACEBOOK")
)
);
}
module.exports = { passport, googleEnabled, facebookEnabled, verifyOAuthProfile };