171 lines
6.1 KiB
JavaScript
171 lines
6.1 KiB
JavaScript
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 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: `
|
|
<div style="font-family:sans-serif;max-width:480px;margin:auto">
|
|
<h2 style="color:#006B6B">Bienvenue ${user.firstname} !</h2>
|
|
<p>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}.</p>
|
|
<p style="text-align:center;padding:20px 0">
|
|
<a href="${setupUrl}" style="background:#006B6B;color:#fff;text-decoration:none;padding:12px 24px;border-radius:8px;display:inline-block">
|
|
Définir mon mot de passe
|
|
</a>
|
|
</p>
|
|
<p style="color:#888;font-size:13px">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.</p>
|
|
</div>
|
|
`,
|
|
});
|
|
}
|
|
|
|
// 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 = 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 };
|