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
Build & Deploy / deploy (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
/* =====================================================================
|
||||
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`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user