Echtes Google-/PayPal-Login: OAuth 2.0 + PKCE, eigene Kundendatenbank, DSGVO-Selbstbedienung

Kundenkonten sind jetzt genauso echt wie die PayPal-Zahlung: server-geprüftes OAuth 2.0 mit
PKCE für Google und "Log in with PayPal" (functions/_shared/oauth.js, oauth-handlers.js),
neue D1-Tabelle "customers" (bewusst ohne Passwort-Feld), eigene von der Zugangscode-Schranke
getrennte Sitzungs-Logik (customer-auth.js). Echte DSGVO-Rechte direkt im Kontobereich:
Daten herunterladen (Art. 15/20) und Konto unwiderruflich löschen (Art. 17), Bestellungen
bleiben aus gesetzlichen Gründen erhalten. Datenschutzerklärung entsprechend ergänzt.

Ohne echte Google-/PayPal-Zugangsdaten zeigt der Login-Button ehrlich einen
"noch nicht eingerichtet"-Hinweis statt eine Anmeldung vorzutäuschen.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
qcigano
2026-08-04 01:45:55 +02:00
co-authored by Claude Sonnet 5
parent e80aba5ca6
commit bf0d169015
24 changed files with 985 additions and 127 deletions
+79
View File
@@ -0,0 +1,79 @@
/* =====================================================================
functions/_shared/customer-auth.js — Sitzungs-Logik für echte Kundenkonten (Google-/PayPal-
Login), bewusst GETRENNT von functions/_shared/auth.js (das ist die Zugangscode-Schranke für
qciga/VanVan) — unterschiedliche Geheimnisse, unterschiedliche Cookies, unterschiedliche
Nutzer:innengruppe. Ein Leck bei der einen Sitzungsart darf nie die andere gefährden.
Gleiches HMAC-Signaturprinzip wie bei der Zugangscode-Schranke (siehe dort für die
Begründung), hier bewusst als eigene, kleine Kopie statt geteiltem Code — weniger Kopplung
zwischen zwei sicherheitskritischen, aber konzeptionell getrennten Systemen. ===================================================================== */
export const CUSTOMER_COOKIE_NAME = "vandiy_customer_session";
export const CUSTOMER_SESSION_TAGE = 30;
function b64urlEncode(bytes) {
let bin = "";
bytes.forEach((b) => (bin += String.fromCharCode(b)));
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function b64urlDecodeToBytes(str) {
str = str.replace(/-/g, "+").replace(/_/g, "/");
while (str.length % 4) str += "=";
const bin = atob(str);
return Uint8Array.from(bin, (c) => c.charCodeAt(0));
}
async function hmacKey(secret) {
return crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
}
/** Signiert eine Kunden-Sitzung für die Datenbank-ID des customers-Eintrags. */
export async function signCustomerSession(customerId, secret) {
const payload = JSON.stringify({ cid: customerId, exp: Date.now() + CUSTOMER_SESSION_TAGE * 24 * 60 * 60 * 1000 });
const payloadB64 = b64urlEncode(new TextEncoder().encode(payload));
const key = await hmacKey(secret);
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payloadB64));
return `${payloadB64}.${b64urlEncode(new Uint8Array(sig))}`;
}
export async function verifyCustomerSession(token, secret) {
if (!token || !token.includes(".")) return null;
const [payloadB64, sigB64] = token.split(".");
try {
const key = await hmacKey(secret);
const valid = await crypto.subtle.verify("HMAC", key, b64urlDecodeToBytes(sigB64), new TextEncoder().encode(payloadB64));
if (!valid) return null;
const payload = JSON.parse(new TextDecoder().decode(b64urlDecodeToBytes(payloadB64)));
if (!payload.exp || payload.exp < Date.now()) return null;
return payload; // { cid, exp }
} catch {
return null;
}
}
export function getCookie(request, name) {
const header = request.headers.get("Cookie") || "";
const match = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
return match ? decodeURIComponent(match[1]) : null;
}
/** Liefert den eingeloggten customers-Datensatz (aus D1) für die aktuelle Anfrage, oder null.
* Zentrale Stelle, die jede Kunden-API-Route nutzt — Kunden-ID kommt NIE direkt vom Client,
* immer nur aus der signierten, serverseitig geprüften Sitzung. */
export async function getCustomer(request, env) {
if (!env.CUSTOMER_SESSION_SECRET || !env.DB) return null;
const token = getCookie(request, CUSTOMER_COOKIE_NAME);
const payload = token ? await verifyCustomerSession(token, env.CUSTOMER_SESSION_SECRET) : null;
if (!payload?.cid) return null;
const row = await env.DB.prepare(
`SELECT id, email, name, provider, created_at FROM customers WHERE id = ?`
).bind(payload.cid).first();
return row || null;
}
export function customerCookieHeader(token, maxAgeSekunden) {
return `${CUSTOMER_COOKIE_NAME}=${encodeURIComponent(token)}; Path=/; Max-Age=${maxAgeSekunden}; HttpOnly; Secure; SameSite=Lax`;
}
export function clearCustomerCookieHeader() {
return `${CUSTOMER_COOKIE_NAME}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax`;
}
+134
View File
@@ -0,0 +1,134 @@
/* =====================================================================
functions/_shared/oauth-handlers.js — gemeinsame Start-/Callback-Logik für alle OAuth-
Anbieter (Google, PayPal), von den dünnen Routen unter functions/api/auth/<provider>/start.js
bzw. callback.js aufgerufen. So gibt es die sicherheitskritische Logik (State-Prüfung, PKCE,
Sitzungserstellung) nur EINMAL, nicht einmal pro Anbieter dupliziert. ===================================================================== */
import {
erzeugeZufallswert,
codeChallengeFuer,
baueAutorisierungsUrl,
istProviderKonfiguriert,
tauscheCodeGegenToken,
holeNutzerprofil,
} from "./oauth.js";
import { signCustomerSession, customerCookieHeader, CUSTOMER_SESSION_TAGE } from "./customer-auth.js";
const STATE_COOKIE = "vandiy_oauth_state";
const VERIFIER_COOKIE = "vandiy_oauth_verifier";
const LANG_COOKIE = "vandiy_oauth_lang";
const OAUTH_TEMP_MAXAGE = 600; // 10 Minuten — reicht locker für den Login-Vorgang, hält aber die
// Angriffsfläche klein, falls die Cookies je "hängen bleiben".
const ERLAUBTE_SPRACHEN = ["", "en", "ch", "fr"]; // "" = Deutsch (kein Präfix)
function getCookieRaw(request, name) {
const header = request.headers.get("Cookie") || "";
const m = header.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
return m ? decodeURIComponent(m[1]) : null;
}
function langPrefix(lang) {
return ERLAUBTE_SPRACHEN.includes(lang) ? lang : "";
}
/** GET /api/auth/:provider/start — leitet zum Anbieter weiter, nachdem State + PKCE-Verifier in
* kurzlebigen, signierten Cookies zwischengespeichert wurden. */
export async function starteOAuth(context, providerName) {
const { request, env } = context;
const url = new URL(request.url);
const lang = langPrefix(url.searchParams.get("lang") || "");
if (!istProviderKonfiguriert(env, providerName)) {
return Response.redirect(`${url.origin}/${lang ? lang + "/" : ""}konto/?login_error=${providerName}_unconfigured`, 302);
}
const state = erzeugeZufallswert(24);
const codeVerifier = erzeugeZufallswert(48);
const codeChallenge = await codeChallengeFuer(codeVerifier);
const redirectUri = `${url.origin}/api/auth/${providerName}/callback`;
const authUrl = baueAutorisierungsUrl(env, providerName, { redirectUri, state, codeChallenge });
const cookiePath = `/api/auth/${providerName}`;
const headers = new Headers({ Location: authUrl });
headers.append("Set-Cookie", `${STATE_COOKIE}=${state}; Path=${cookiePath}; Max-Age=${OAUTH_TEMP_MAXAGE}; HttpOnly; Secure; SameSite=Lax`);
headers.append("Set-Cookie", `${VERIFIER_COOKIE}=${codeVerifier}; Path=${cookiePath}; Max-Age=${OAUTH_TEMP_MAXAGE}; HttpOnly; Secure; SameSite=Lax`);
headers.append("Set-Cookie", `${LANG_COOKIE}=${lang}; Path=${cookiePath}; Max-Age=${OAUTH_TEMP_MAXAGE}; HttpOnly; Secure; SameSite=Lax`);
return new Response(null, { status: 302, headers });
}
/** GET /api/auth/:provider/callback — prüft State (CSRF-Schutz), tauscht den Code gegen ein
* Zugangstoken, holt das echte Profil direkt vom Anbieter, legt bei Bedarf einen neuen
* Kunden-Datensatz an (oder aktualisiert den bestehenden) und setzt die echte Sitzung. */
export async function verarbeiteOAuthCallback(context, providerName) {
const { request, env } = context;
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const errorParam = url.searchParams.get("error");
const lang = langPrefix(getCookieRaw(request, LANG_COOKIE) || "");
const kontoPfad = `${url.origin}/${lang ? lang + "/" : ""}konto/`;
const cookiePath = `/api/auth/${providerName}`;
function loescheTempCookies(headers) {
headers.append("Set-Cookie", `${STATE_COOKIE}=; Path=${cookiePath}; Max-Age=0`);
headers.append("Set-Cookie", `${VERIFIER_COOKIE}=; Path=${cookiePath}; Max-Age=0`);
headers.append("Set-Cookie", `${LANG_COOKIE}=; Path=${cookiePath}; Max-Age=0`);
}
function fehlerRedirect(grund) {
const headers = new Headers({ Location: `${kontoPfad}?login_error=${grund}` });
loescheTempCookies(headers);
return new Response(null, { status: 302, headers });
}
// Nutzer:in hat bei Google/PayPal abgebrochen/abgelehnt — kein Fehler, nur kein Login.
if (errorParam) return fehlerRedirect(`${providerName}_denied`);
if (!code || !state) return fehlerRedirect(`${providerName}_invalid`);
// ── CSRF-Schutz: der State aus der Weiterleitungs-URL MUSS exakt zum eigenen, kurz zuvor
// gesetzten Cookie passen — sonst könnte jemand einen fremden Autorisierungs-Code
// "unterschieben" (klassischer OAuth-CSRF-Angriff). ──
const stateCookie = getCookieRaw(request, STATE_COOKIE);
const verifierCookie = getCookieRaw(request, VERIFIER_COOKIE);
if (!stateCookie || stateCookie !== state || !verifierCookie) {
return fehlerRedirect(`${providerName}_state_mismatch`);
}
if (!env.DB || !env.CUSTOMER_SESSION_SECRET) {
return fehlerRedirect(`${providerName}_backend_unconfigured`);
}
try {
const redirectUri = `${url.origin}/api/auth/${providerName}/callback`;
const accessToken = await tauscheCodeGegenToken(env, providerName, { code, redirectUri, codeVerifier: verifierCookie });
// Einzige Quelle für die Identität: die direkte, server-zu-server-Antwort vom Anbieter.
const profil = await holeNutzerprofil(env, providerName, accessToken);
const now = new Date().toISOString();
const bestehend = await env.DB.prepare(
`SELECT id FROM customers WHERE provider = ? AND provider_user_id = ?`
).bind(providerName, profil.providerUserId).first();
let customerId;
if (bestehend) {
customerId = bestehend.id;
await env.DB.prepare(`UPDATE customers SET email = ?, name = ?, updated_at = ?, last_login_at = ? WHERE id = ?`)
.bind(profil.email, profil.name, now, now, customerId)
.run();
} else {
const insert = await env.DB.prepare(
`INSERT INTO customers (created_at, updated_at, last_login_at, provider, provider_user_id, email, name) VALUES (?,?,?,?,?,?,?)`
)
.bind(now, now, now, providerName, profil.providerUserId, profil.email, profil.name)
.run();
customerId = insert.meta.last_row_id;
}
const sessionToken = await signCustomerSession(customerId, env.CUSTOMER_SESSION_SECRET);
const headers = new Headers({ Location: `${url.origin}/${lang ? lang + "/" : ""}konto/angemeldet/` });
headers.append("Set-Cookie", customerCookieHeader(sessionToken, CUSTOMER_SESSION_TAGE * 24 * 60 * 60));
loescheTempCookies(headers);
return new Response(null, { status: 302, headers });
} catch (err) {
return fehlerRedirect(`${providerName}_failed`);
}
}
+141
View File
@@ -0,0 +1,141 @@
/* =====================================================================
functions/_shared/oauth.js — echte OAuth-/OpenID-Connect-Anmeldung für Google und "Log in
with PayPal". Standard-Authorization-Code-Flow MIT PKCE (Proof Key for Code Exchange) — auch
wenn der Code-Austausch server-seitig läuft (also ein "confidential client" ist, der das
Client-Secret kennt), macht PKCE einen abgefangenen Autorisierungs-Code für Angreifer wertlos,
ohne den echten, zufällig erzeugten code_verifier — zusätzliche Verteidigungsebene, siehe
functions/api/auth/<provider>/start.js für die Erzeugung.
WICHTIG: Der Server vertraut NIE einer vom Client mitgeschickten Identität — E-Mail/Name/
Nutzer-ID kommen ausschließlich aus der direkten, server-zu-server-Antwort von Google/PayPal
selbst (holeNutzerprofil()), nachdem WIR den Autorisierungs-Code gegen ein Zugangstoken
getauscht haben. Genau dasselbe Sicherheitsprinzip wie bei der PayPal-Zahlungsprüfung (siehe
functions/_shared/paypal.js). ===================================================================== */
function paypalBasis(env) {
return env.PAYPAL_ENV === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com";
}
function paypalSigninBasis(env) {
return 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",
// Google akzeptiert den Token-Exchange als klassisches Formular (application/x-www-form-urlencoded).
tokenAuthStyle: "body",
parseUserinfo: (data) => ({ providerUserId: data.sub, email: data.email, name: data.name || null }),
},
paypal: {
authorizeUrl: (env) => `${paypalSigninBasis(env)}/signin/authorize`,
tokenUrl: (env) => `${paypalBasis(env)}/v1/oauth2/token`,
userinfoUrl: (env) => `${paypalBasis(env)}/v1/identity/openidconnect/userinfo/?schema=openid`,
scope: "openid email profile",
// "Log in with PayPal" nutzt bewusst DIESELBEN App-Zugangsdaten wie die PayPal-Zahlungs-
// anbindung (functions/_shared/paypal.js) — VanVan muss in ihrer einen PayPal-App im
// Entwickler-Dashboard nur zusätzlich die Funktion "Log in with PayPal" aktivieren, statt
// eine zweite App anzulegen (siehe Vault-Doku).
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(env, name) {
const p = provider(name);
return !!(env[p.clientIdEnvKey] && env[p.clientSecretEnvKey]);
}
/** Baut die Weiterleitungs-URL zum Anbieter — state (CSRF-Schutz) und code_challenge (PKCE)
* wurden vorher vom Aufrufer erzeugt und in signierten Cookies zwischengespeichert. */
export function baueAutorisierungsUrl(env, name, { redirectUri, state, codeChallenge }) {
const p = provider(name);
const clientId = 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(env)}?${params.toString()}`;
}
/** Tauscht den Autorisierungs-Code server-seitig gegen ein Zugangstoken — das Client-Secret
* verlässt den Server dabei nie, es fließt nur in diesen einen serverseitigen Aufruf ein. */
export async function tauscheCodeGegenToken(env, name, { code, redirectUri, codeVerifier }) {
const p = provider(name);
const clientId = env[p.clientIdEnvKey];
const clientSecret = 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 ${btoa(`${clientId}:${clientSecret}`)}`;
} else {
body.set("client_id", clientId);
body.set("client_secret", clientSecret);
}
const res = await fetch(p.tokenUrl(env), { 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;
}
/** Holt Name/E-Mail/Nutzer-ID DIREKT vom Anbieter (server-zu-server, mit dem eben erhaltenen
* Zugangstoken) — das ist die einzige Quelle, der wir für die Identität vertrauen. */
export async function holeNutzerprofil(env, name, accessToken) {
const p = provider(name);
const res = await fetch(p.userinfoUrl(env), { 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;
}
// ── PKCE-Hilfsfunktionen ──
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));
}
+29
View File
@@ -0,0 +1,29 @@
/* =====================================================================
POST /api/account/delete — löscht das echte Kundenkonto unwiderruflich (DSGVO Art. 17,
"Recht auf Löschung"). Bewusst als echtes Selbstbedienungs-Werkzeug gebaut, nicht nur als
"schreib uns eine E-Mail"-Hinweis — genau das, was rechtlich am saubersten ist und was
VanVan vor Anfragen/Beschwerden schützt, die sonst manuell bearbeitet werden müssten.
Löscht NUR den customers-Datensatz (Login-Identität) — bereits aufgegebene Bestellungen
(orders-Tabelle) bleiben bestehen, weil dafür eine gesetzliche Aufbewahrungspflicht gilt
(handels-/steuerrechtliche Aufbewahrungsfristen, § 257 HGB / § 147 AO) — das Konto ist davon
rechtlich unabhängig. ===================================================================== */
import { json } from "../../_shared/http.js";
import { getCustomer, clearCustomerCookieHeader } from "../../_shared/customer-auth.js";
export async function onRequestPost(context) {
const { request, env } = context;
const customer = await getCustomer(request, env);
if (!customer) return json(401, { ok: false, error: "Nicht angemeldet." });
try {
await env.DB.prepare(`DELETE FROM customers WHERE id = ?`).bind(customer.id).run();
} catch {
return json(500, { ok: false, error: "Konto konnte nicht gelöscht werden. Bitte versuche es erneut." });
}
const headers = new Headers({ "Content-Type": "application/json" });
headers.append("Set-Cookie", clearCustomerCookieHeader());
return new Response(JSON.stringify({ ok: true }), { status: 200, headers });
}
+38
View File
@@ -0,0 +1,38 @@
/* =====================================================================
GET /api/account/export — liefert alle über dieses Konto gespeicherten Daten als JSON-Datei
zum Download (DSGVO Art. 15 "Auskunftsrecht" + Art. 20 "Recht auf Datenübertragbarkeit").
Echtes Selbstbedienungs-Werkzeug statt eines manuellen Prozesses. ===================================================================== */
import { getCustomer } from "../../_shared/customer-auth.js";
export async function onRequestGet(context) {
const { request, env } = context;
const customer = await getCustomer(request, env);
if (!customer) {
return new Response(JSON.stringify({ ok: false, error: "Nicht angemeldet." }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const daten = {
konto: {
email: customer.email,
name: customer.name,
anmeldeart: customer.provider,
erstelltAm: customer.created_at,
},
hinweis:
"Dies sind alle Daten, die Van's DIY & Bastelbedarf zu deinem Kundenkonto speichert. " +
"Bereits aufgegebene Bestellungen werden getrennt davon aus gesetzlichen Aufbewahrungsgründen " +
"weiter gespeichert (siehe Datenschutzerklärung, Abschnitt Bestellabwicklung).",
};
return new Response(JSON.stringify(daten, null, 2), {
status: 200,
headers: {
"Content-Type": "application/json",
"Content-Disposition": 'attachment; filename="meine-daten.json"',
},
});
}
+13
View File
@@ -0,0 +1,13 @@
/* =====================================================================
POST /api/account/logout — löscht die echte Kunden-Sitzung (Cookie). Die lokale Demo-
Kontoabmeldung (localStorage, siehe scripts/account.ts logout()) läuft weiterhin zusätzlich
im Frontend, damit die Kopfzeile sofort reagiert. ===================================================================== */
import { json } from "../../_shared/http.js";
import { clearCustomerCookieHeader } from "../../_shared/customer-auth.js";
export async function onRequestPost() {
const headers = new Headers({ "Content-Type": "application/json" });
headers.append("Set-Cookie", clearCustomerCookieHeader());
return new Response(JSON.stringify({ ok: true }), { status: 200, headers });
}
+20
View File
@@ -0,0 +1,20 @@
/* =====================================================================
GET /api/account/me — liefert den eingeloggten Kundenkonto-Datensatz (oder ok:false, wenn
keine gültige Sitzung vorliegt). Wird vom Frontend (siehe scripts/account.ts-Brücke in
konto/index.astro + konto/angemeldet.astro) genutzt, um die echte Server-Sitzung mit dem
bisherigen, rein lokalen Demo-Kontosystem abzugleichen — bewusst ok:false statt 401, weil
diese Route aktiv zum "unauffällig nachschauen, ob schon eine Sitzung besteht" gedacht ist,
nicht zur Absicherung einer geschützten Aktion. ===================================================================== */
import { json } from "../../_shared/http.js";
import { getCustomer } from "../../_shared/customer-auth.js";
export async function onRequestGet(context) {
const { request, env } = context;
const customer = await getCustomer(request, env);
if (!customer) return json(200, { ok: false });
return json(200, {
ok: true,
customer: { email: customer.email, name: customer.name, provider: customer.provider, seit: customer.created_at },
});
}
+5
View File
@@ -0,0 +1,5 @@
import { verarbeiteOAuthCallback } from "../../../_shared/oauth-handlers.js";
export function onRequestGet(context) {
return verarbeiteOAuthCallback(context, "google");
}
+5
View File
@@ -0,0 +1,5 @@
import { starteOAuth } from "../../../_shared/oauth-handlers.js";
export function onRequestGet(context) {
return starteOAuth(context, "google");
}
+5
View File
@@ -0,0 +1,5 @@
import { verarbeiteOAuthCallback } from "../../../_shared/oauth-handlers.js";
export function onRequestGet(context) {
return verarbeiteOAuthCallback(context, "paypal");
}
+5
View File
@@ -0,0 +1,5 @@
import { starteOAuth } from "../../../_shared/oauth-handlers.js";
export function onRequestGet(context) {
return starteOAuth(context, "paypal");
}