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 };