Node.js/Express-Server als Ersatz für Cloudflare Pages Functions + D1 (läuft auf eigenem Server statt Cloudflare)
Build & Deploy / deploy (push) Waiting to run

This commit is contained in:
qcigano
2026-08-05 15:06:17 +02:00
parent 53e7bb158a
commit b8b8ec7b7a
19 changed files with 1322 additions and 0 deletions
+120
View File
@@ -0,0 +1,120 @@
/* =====================================================================
lib/oauth.js — echte OAuth-/OpenID-Connect-Anmeldung für Google und "Log in with PayPal".
1:1 portiert aus functions/_shared/oauth.js, Zugangsdaten aus process.env statt env.*.
===================================================================== */
function paypalBasis() {
return process.env.PAYPAL_ENV === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com";
}
function paypalSigninBasis() {
return process.env.PAYPAL_ENV === "live" ? "https://www.paypal.com" : "https://www.sandbox.paypal.com";
}
const PROVIDERS = {
google: {
authorizeUrl: () => "https://accounts.google.com/o/oauth2/v2/auth",
tokenUrl: () => "https://oauth2.googleapis.com/token",
userinfoUrl: () => "https://www.googleapis.com/oauth2/v3/userinfo",
scope: "openid email profile",
clientIdEnvKey: "GOOGLE_CLIENT_ID",
clientSecretEnvKey: "GOOGLE_CLIENT_SECRET",
tokenAuthStyle: "body",
parseUserinfo: (data) => ({ providerUserId: data.sub, email: data.email, name: data.name || null }),
},
paypal: {
authorizeUrl: () => `${paypalSigninBasis()}/signin/authorize`,
tokenUrl: () => `${paypalBasis()}/v1/oauth2/token`,
userinfoUrl: () => `${paypalBasis()}/v1/identity/openidconnect/userinfo/?schema=openid`,
scope: "openid email profile",
clientIdEnvKey: "PAYPAL_CLIENT_ID",
clientSecretEnvKey: "PAYPAL_CLIENT_SECRET",
tokenAuthStyle: "basic",
parseUserinfo: (data) => ({ providerUserId: data.user_id || data.payer_id, email: data.email, name: data.name || null }),
},
};
export function provider(name) {
const p = PROVIDERS[name];
if (!p) throw new Error(`Unbekannter OAuth-Anbieter: ${name}`);
return p;
}
export function istProviderKonfiguriert(name) {
const p = provider(name);
return !!(process.env[p.clientIdEnvKey] && process.env[p.clientSecretEnvKey]);
}
export function baueAutorisierungsUrl(name, { redirectUri, state, codeChallenge }) {
const p = provider(name);
const clientId = process.env[p.clientIdEnvKey];
const params = new URLSearchParams({
client_id: clientId,
response_type: "code",
scope: p.scope,
redirect_uri: redirectUri,
state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
});
if (name === "google") params.set("prompt", "select_account");
return `${p.authorizeUrl()}?${params.toString()}`;
}
export async function tauscheCodeGegenToken(name, { code, redirectUri, codeVerifier }) {
const p = provider(name);
const clientId = process.env[p.clientIdEnvKey];
const clientSecret = process.env[p.clientSecretEnvKey];
const body = new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
});
const headers = { "Content-Type": "application/x-www-form-urlencoded" };
if (p.tokenAuthStyle === "basic") {
headers.Authorization = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
} else {
body.set("client_id", clientId);
body.set("client_secret", clientSecret);
}
const res = await fetch(p.tokenUrl(), { method: "POST", headers, body: body.toString() });
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Token-Austausch bei ${name} fehlgeschlagen (Status ${res.status}): ${text}`);
}
const data = await res.json();
if (!data.access_token) throw new Error(`${name} hat kein Zugangstoken geliefert.`);
return data.access_token;
}
export async function holeNutzerprofil(name, accessToken) {
const p = provider(name);
const res = await fetch(p.userinfoUrl(), { headers: { Authorization: `Bearer ${accessToken}` } });
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Profilabruf bei ${name} fehlgeschlagen (Status ${res.status}): ${text}`);
}
const data = await res.json();
const profil = p.parseUserinfo(data);
if (!profil.providerUserId || !profil.email) {
throw new Error(`${name} hat kein vollständiges Profil geliefert (E-Mail/ID fehlt).`);
}
return profil;
}
function b64url(bytes) {
let bin = "";
bytes.forEach((b) => (bin += String.fromCharCode(b)));
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
export function erzeugeZufallswert(laenge = 32) {
const bytes = new Uint8Array(laenge);
crypto.getRandomValues(bytes);
return b64url(bytes);
}
export async function codeChallengeFuer(codeVerifier) {
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
return b64url(new Uint8Array(hash));
}