/* ===================================================================== lib/oauth-handlers.js — gemeinsame Start-/Callback-Logik für alle OAuth-Anbieter. 1:1 portiert aus functions/_shared/oauth-handlers.js, auf Express (req/res) statt Cloudflare Request/Response umgestellt. ===================================================================== */ import { erzeugeZufallswert, codeChallengeFuer, baueAutorisierungsUrl, istProviderKonfiguriert, tauscheCodeGegenToken, holeNutzerprofil, } from "./oauth.js"; import { signCustomerSession, setCustomerCookie, CUSTOMER_SESSION_TAGE } from "./customer-auth.js"; import { db } from "../db.js"; const STATE_COOKIE = "vandiy_oauth_state"; const VERIFIER_COOKIE = "vandiy_oauth_verifier"; const LANG_COOKIE = "vandiy_oauth_lang"; const OAUTH_TEMP_MAXAGE = 600; // Sekunden const ERLAUBTE_SPRACHEN = ["", "en", "ch", "fr"]; function langPrefix(lang) { return ERLAUBTE_SPRACHEN.includes(lang) ? lang : ""; } function originVon(req) { return `${req.protocol}://${req.get("host")}`; } /** GET /api/auth/:provider/start */ export async function starteOAuth(req, res, providerName) { const origin = originVon(req); const lang = langPrefix(req.query.lang || ""); if (!istProviderKonfiguriert(providerName)) { return res.redirect(302, `${origin}/${lang ? lang + "/" : ""}konto/?login_error=${providerName}_unconfigured`); } const state = erzeugeZufallswert(24); const codeVerifier = erzeugeZufallswert(48); const codeChallenge = await codeChallengeFuer(codeVerifier); const redirectUri = `${origin}/api/auth/${providerName}/callback`; const authUrl = baueAutorisierungsUrl(providerName, { redirectUri, state, codeChallenge }); const cookiePath = `/api/auth/${providerName}`; const cookieOpts = { path: cookiePath, maxAge: OAUTH_TEMP_MAXAGE * 1000, httpOnly: true, secure: true, sameSite: "lax" }; res.cookie(STATE_COOKIE, state, cookieOpts); res.cookie(VERIFIER_COOKIE, codeVerifier, cookieOpts); res.cookie(LANG_COOKIE, lang, cookieOpts); return res.redirect(302, authUrl); } /** GET /api/auth/:provider/callback */ export async function verarbeiteOAuthCallback(req, res, providerName) { const origin = originVon(req); const code = req.query.code; const state = req.query.state; const errorParam = req.query.error; const lang = langPrefix(req.cookies?.[LANG_COOKIE] || ""); const kontoPfad = `${origin}/${lang ? lang + "/" : ""}konto/`; const cookiePath = `/api/auth/${providerName}`; function loescheTempCookies() { res.clearCookie(STATE_COOKIE, { path: cookiePath }); res.clearCookie(VERIFIER_COOKIE, { path: cookiePath }); res.clearCookie(LANG_COOKIE, { path: cookiePath }); } function fehlerRedirect(grund) { loescheTempCookies(); return res.redirect(302, `${kontoPfad}?login_error=${grund}`); } if (errorParam) return fehlerRedirect(`${providerName}_denied`); if (!code || !state) return fehlerRedirect(`${providerName}_invalid`); const stateCookie = req.cookies?.[STATE_COOKIE]; const verifierCookie = req.cookies?.[VERIFIER_COOKIE]; if (!stateCookie || stateCookie !== state || !verifierCookie) { return fehlerRedirect(`${providerName}_state_mismatch`); } if (!process.env.CUSTOMER_SESSION_SECRET) { return fehlerRedirect(`${providerName}_backend_unconfigured`); } try { const redirectUri = `${origin}/api/auth/${providerName}/callback`; const accessToken = await tauscheCodeGegenToken(providerName, { code, redirectUri, codeVerifier: verifierCookie }); const profil = await holeNutzerprofil(providerName, accessToken); const now = new Date().toISOString(); const bestehend = db .prepare(`SELECT id FROM customers WHERE provider = ? AND provider_user_id = ?`) .get(providerName, profil.providerUserId); let customerId; if (bestehend) { customerId = bestehend.id; db.prepare(`UPDATE customers SET email = ?, name = ?, updated_at = ?, last_login_at = ? WHERE id = ?`) .run(profil.email, profil.name, now, now, customerId); } else { const insert = db .prepare( `INSERT INTO customers (created_at, updated_at, last_login_at, provider, provider_user_id, email, name) VALUES (?,?,?,?,?,?,?)` ) .run(now, now, now, providerName, profil.providerUserId, profil.email, profil.name); customerId = insert.lastInsertRowid; } const sessionToken = await signCustomerSession(customerId, process.env.CUSTOMER_SESSION_SECRET); setCustomerCookie(res, sessionToken, CUSTOMER_SESSION_TAGE * 24 * 60 * 60); loescheTempCookies(); return res.redirect(302, `${origin}/${lang ? lang + "/" : ""}konto/angemeldet/`); } catch (err) { return fehlerRedirect(`${providerName}_failed`); } }