Initial local backup snapshot
This commit is contained in:
commit
acd9e14ba5
367 changed files with 118038 additions and 0 deletions
281
server/socialAuth.ts
Normal file
281
server/socialAuth.ts
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
import { randomBytes } from "node:crypto";
|
||||
import type { Express, Request, Response } from "express";
|
||||
import { parse as parseCookieHeader } from "cookie";
|
||||
import { createSessionToken, loginOAuthUser, setSessionCookie } from "./_core/auth";
|
||||
import { getSessionCookieOptions } from "./_core/cookies";
|
||||
import { ENV } from "./_core/env";
|
||||
|
||||
const GOOGLE_STATE_COOKIE = "oauth_state_google";
|
||||
const FACEBOOK_STATE_COOKIE = "oauth_state_facebook";
|
||||
const OAUTH_STATE_MAX_AGE_MS = 10 * 60 * 1000;
|
||||
|
||||
function parseCookies(cookieHeader: string | undefined) {
|
||||
if (!cookieHeader) return new Map<string, string>();
|
||||
return new Map(Object.entries(parseCookieHeader(cookieHeader)));
|
||||
}
|
||||
|
||||
function buildBaseUrl(req: Request) {
|
||||
if (ENV.appBaseUrl) {
|
||||
return ENV.appBaseUrl.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
const forwardedProto = req.headers["x-forwarded-proto"];
|
||||
const forwardedHost = req.headers["x-forwarded-host"];
|
||||
const proto = typeof forwardedProto === "string"
|
||||
? forwardedProto.split(",")[0]?.trim()
|
||||
: req.protocol;
|
||||
const host = typeof forwardedHost === "string"
|
||||
? forwardedHost.split(",")[0]?.trim()
|
||||
: req.get("host");
|
||||
|
||||
return `${proto || "http"}://${host}`;
|
||||
}
|
||||
|
||||
function getCallbackUrl(req: Request, provider: "google" | "facebook") {
|
||||
return `${buildBaseUrl(req)}/auth/${provider}/callback`;
|
||||
}
|
||||
|
||||
function redirectToLogin(res: Response, message: string) {
|
||||
res.redirect(`/login?authError=${encodeURIComponent(message)}`);
|
||||
}
|
||||
|
||||
function setOAuthStateCookie(req: Request, res: Response, name: string, value: string) {
|
||||
res.cookie(name, value, {
|
||||
...getSessionCookieOptions(req),
|
||||
maxAge: OAUTH_STATE_MAX_AGE_MS,
|
||||
});
|
||||
}
|
||||
|
||||
function clearOAuthStateCookie(req: Request, res: Response, name: string) {
|
||||
res.clearCookie(name, {
|
||||
...getSessionCookieOptions(req),
|
||||
maxAge: -1,
|
||||
});
|
||||
}
|
||||
|
||||
function readAndValidateState(req: Request, res: Response, cookieName: string, receivedState: string | null) {
|
||||
const cookies = parseCookies(req.headers.cookie);
|
||||
const expectedState = cookies.get(cookieName) ?? "";
|
||||
clearOAuthStateCookie(req, res, cookieName);
|
||||
return Boolean(receivedState && expectedState && receivedState === expectedState);
|
||||
}
|
||||
|
||||
function createOAuthState() {
|
||||
return randomBytes(24).toString("hex");
|
||||
}
|
||||
|
||||
function isGoogleConfigured() {
|
||||
return Boolean(ENV.googleClientId && ENV.googleClientSecret);
|
||||
}
|
||||
|
||||
function isFacebookConfigured() {
|
||||
return Boolean(ENV.facebookAppId && ENV.facebookAppSecret);
|
||||
}
|
||||
|
||||
async function finishLogin(req: Request, res: Response, userData: {
|
||||
provider: "google" | "facebook";
|
||||
providerUserId: string;
|
||||
email: string;
|
||||
name?: string | null;
|
||||
}) {
|
||||
const user = await loginOAuthUser(userData);
|
||||
const token = await createSessionToken(user);
|
||||
setSessionCookie(req, res, token);
|
||||
res.redirect(user.role === "service_terrain" ? "/terrain" : "/dashboard");
|
||||
}
|
||||
|
||||
export function getOAuthProviderStatus() {
|
||||
return {
|
||||
google: isGoogleConfigured(),
|
||||
facebook: isFacebookConfigured(),
|
||||
};
|
||||
}
|
||||
|
||||
export function registerSocialAuthRoutes(app: Express) {
|
||||
app.get("/auth/google/start", (req, res) => {
|
||||
if (!isGoogleConfigured()) {
|
||||
redirectToLogin(res, "Connexion Google non configurée");
|
||||
return;
|
||||
}
|
||||
|
||||
const state = createOAuthState();
|
||||
setOAuthStateCookie(req, res, GOOGLE_STATE_COOKIE, state);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: ENV.googleClientId,
|
||||
redirect_uri: getCallbackUrl(req, "google"),
|
||||
response_type: "code",
|
||||
scope: "openid email profile",
|
||||
state,
|
||||
access_type: "online",
|
||||
include_granted_scopes: "true",
|
||||
prompt: "select_account",
|
||||
});
|
||||
|
||||
res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`);
|
||||
});
|
||||
|
||||
app.get("/auth/google/callback", async (req, res) => {
|
||||
try {
|
||||
if (!isGoogleConfigured()) {
|
||||
redirectToLogin(res, "Connexion Google non configurée");
|
||||
return;
|
||||
}
|
||||
|
||||
const code = typeof req.query.code === "string" ? req.query.code : "";
|
||||
const state = typeof req.query.state === "string" ? req.query.state : null;
|
||||
|
||||
if (!code || !readAndValidateState(req, res, GOOGLE_STATE_COOKIE, state)) {
|
||||
redirectToLogin(res, "Connexion Google invalide ou expirée");
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
code,
|
||||
client_id: ENV.googleClientId,
|
||||
client_secret: ENV.googleClientSecret,
|
||||
redirect_uri: getCallbackUrl(req, "google"),
|
||||
grant_type: "authorization_code",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
redirectToLogin(res, "Google a refusé la connexion");
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json() as { access_token?: string };
|
||||
if (!tokenData.access_token) {
|
||||
redirectToLogin(res, "Jeton Google manquant");
|
||||
return;
|
||||
}
|
||||
|
||||
const userInfoResponse = await fetch("https://openidconnect.googleapis.com/v1/userinfo", {
|
||||
headers: {
|
||||
authorization: `Bearer ${tokenData.access_token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userInfoResponse.ok) {
|
||||
redirectToLogin(res, "Impossible de récupérer le profil Google");
|
||||
return;
|
||||
}
|
||||
|
||||
const userInfo = await userInfoResponse.json() as {
|
||||
sub?: string;
|
||||
email?: string;
|
||||
email_verified?: boolean;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
if (!userInfo.sub || !userInfo.email || userInfo.email_verified === false) {
|
||||
redirectToLogin(res, "Le compte Google doit fournir une adresse email vérifiée");
|
||||
return;
|
||||
}
|
||||
|
||||
await finishLogin(req, res, {
|
||||
provider: "google",
|
||||
providerUserId: userInfo.sub,
|
||||
email: userInfo.email,
|
||||
name: userInfo.name,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[OAuth] Google callback failed:", error);
|
||||
redirectToLogin(res, "Connexion Google impossible pour le moment");
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/auth/facebook/start", (req, res) => {
|
||||
if (!isFacebookConfigured()) {
|
||||
redirectToLogin(res, "Connexion Facebook non configurée");
|
||||
return;
|
||||
}
|
||||
|
||||
const state = createOAuthState();
|
||||
setOAuthStateCookie(req, res, FACEBOOK_STATE_COOKIE, state);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
client_id: ENV.facebookAppId,
|
||||
redirect_uri: getCallbackUrl(req, "facebook"),
|
||||
state,
|
||||
scope: "email,public_profile",
|
||||
});
|
||||
|
||||
res.redirect(`https://www.facebook.com/dialog/oauth?${params.toString()}`);
|
||||
});
|
||||
|
||||
app.get("/auth/facebook/callback", async (req, res) => {
|
||||
try {
|
||||
if (!isFacebookConfigured()) {
|
||||
redirectToLogin(res, "Connexion Facebook non configurée");
|
||||
return;
|
||||
}
|
||||
|
||||
const code = typeof req.query.code === "string" ? req.query.code : "";
|
||||
const state = typeof req.query.state === "string" ? req.query.state : null;
|
||||
|
||||
if (!code || !readAndValidateState(req, res, FACEBOOK_STATE_COOKIE, state)) {
|
||||
redirectToLogin(res, "Connexion Facebook invalide ou expirée");
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenUrl = new URL("https://graph.facebook.com/oauth/access_token");
|
||||
tokenUrl.search = new URLSearchParams({
|
||||
client_id: ENV.facebookAppId,
|
||||
client_secret: ENV.facebookAppSecret,
|
||||
redirect_uri: getCallbackUrl(req, "facebook"),
|
||||
code,
|
||||
}).toString();
|
||||
|
||||
const tokenResponse = await fetch(tokenUrl);
|
||||
if (!tokenResponse.ok) {
|
||||
redirectToLogin(res, "Facebook a refusé la connexion");
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json() as { access_token?: string };
|
||||
if (!tokenData.access_token) {
|
||||
redirectToLogin(res, "Jeton Facebook manquant");
|
||||
return;
|
||||
}
|
||||
|
||||
const profileUrl = new URL("https://graph.facebook.com/me");
|
||||
profileUrl.search = new URLSearchParams({
|
||||
fields: "id,name,email",
|
||||
access_token: tokenData.access_token,
|
||||
}).toString();
|
||||
|
||||
const profileResponse = await fetch(profileUrl);
|
||||
if (!profileResponse.ok) {
|
||||
redirectToLogin(res, "Impossible de récupérer le profil Facebook");
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await profileResponse.json() as {
|
||||
id?: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
if (!profile.id || !profile.email) {
|
||||
redirectToLogin(res, "Le compte Facebook doit partager une adresse email");
|
||||
return;
|
||||
}
|
||||
|
||||
await finishLogin(req, res, {
|
||||
provider: "facebook",
|
||||
providerUserId: profile.id,
|
||||
email: profile.email,
|
||||
name: profile.name,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[OAuth] Facebook callback failed:", error);
|
||||
redirectToLogin(res, "Connexion Facebook impossible pour le moment");
|
||||
}
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue