Index: src/config/google.js
===================================================================
--- src/config/google.js	(revision 32cc601dd3bba9d9131f32ae282712f54feb838c)
+++ src/config/google.js	(revision 9e27d64c70dff768f7a07cb5f10586ad65f56c42)
@@ -1,75 +1,60 @@
-// Google-login via de centrale Klonkt-broker (license.klonkt.com).
+// Google OAuth2 voor LUISTERAARS (reageren). Per-instance: de self-hoster zet
+// z'n EIGEN Google-client in .env. Zo hangt elke site aan z'n eigen Google Cloud
+// project — geen centrale afhankelijkheid, geen gedeelde aansprakelijkheid.
 //
-// Deze instance praat NOOIT zelf met Google. De broker doet de OAuth-dans met één
-// centrale Google-client en stuurt een kortlevend, gesigneerd identity-token terug;
-// dat verifiëren we offline tegen de broker-pubkey. Zo hoeft geen enkele self-host
-// een eigen Google-client aan te maken.
+// Optioneel: staat dit niet ingesteld, dan is er simpelweg geen "Login met
+// Google"-knop en blijft de rest van de site werken. Google-login geeft NOOIT
+// beheerrechten — beheer gaat via het wachtwoord-account.
 //
 // Config via env:
-//   KLONKT_BROKER_URL = https://license.klonkt.com
-//   SITE_ORIGIN       = het eigen publieke origin (bv https://roboburr.com) —
-//                       bepaalt de callback + de audience die we eisen.
-//   ADMIN_EMAIL       = de Google-mail die owner/admin (god) is op deze instance.
+//   GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
+//   GOOGLE_REDIRECT_URI = https://<dit-domein>/auth/google/callback
 
-import { importSPKI, jwtVerify } from 'jose';
+const CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '';
+const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || '';
+const REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI || '';
 
-const ALG = 'EdDSA';
-const ISSUER = 'klonkt-license';
+const AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
+const TOKEN_URL = 'https://oauth2.googleapis.com/token';
+const USERINFO_URL = 'https://openidconnect.googleapis.com/v1/userinfo';
 
-const BROKER_URL = (process.env.KLONKT_BROKER_URL || '').replace(/\/$/, '');
-const SITE_ORIGIN = (process.env.SITE_ORIGIN || '').replace(/\/$/, '');
-
-export function brokerConfigured() {
-  return !!(BROKER_URL && SITE_ORIGIN);
+export function googleConfigured() {
+  return !!(CLIENT_ID && CLIENT_SECRET && REDIRECT_URI);
 }
 
-// Waar de broker naartoe terugstuurt (moet in de broker-allowlist staan).
-export function callbackUrl() {
-  return `${SITE_ORIGIN}/auth/google/callback`;
+export function authorizeUrl(state) {
+  const p = new URLSearchParams({
+    client_id: CLIENT_ID,
+    redirect_uri: REDIRECT_URI,
+    response_type: 'code',
+    scope: 'openid email profile',
+    state,
+    access_type: 'online',
+    prompt: 'select_account',
+  });
+  return `${AUTH_URL}?${p.toString()}`;
 }
 
-export function brokerStartUrl(istate) {
-  const p = new URLSearchParams({ return: callbackUrl(), istate });
-  return `${BROKER_URL}/auth/google/start?${p.toString()}`;
+export async function exchangeCode(code) {
+  const body = new URLSearchParams({
+    code,
+    client_id: CLIENT_ID,
+    client_secret: CLIENT_SECRET,
+    redirect_uri: REDIRECT_URI,
+    grant_type: 'authorization_code',
+  });
+  const r = await fetch(TOKEN_URL, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+    body,
+  });
+  if (!r.ok) throw new Error(`Google token-exchange faalde: ${r.status}`);
+  return r.json(); // { access_token, id_token, ... }
 }
 
-// Broker-pubkey ophalen + cachen (bij fout NIET permanent cachen).
-let _pubkeyPromise = null;
-function getPublicKey() {
-  if (!_pubkeyPromise) {
-    _pubkeyPromise = (async () => {
-      const r = await fetch(`${BROKER_URL}/pubkey`);
-      if (!r.ok) throw new Error(`broker /pubkey faalde: ${r.status}`);
-      return importSPKI(await r.text(), ALG);
-    })().catch((e) => {
-      _pubkeyPromise = null;
-      throw e;
-    });
-  }
-  return _pubkeyPromise;
+// Returns { sub, email, email_verified, name, picture }.
+export async function fetchUserinfo(accessToken) {
+  const r = await fetch(USERINFO_URL, { headers: { Authorization: `Bearer ${accessToken}` } });
+  if (!r.ok) throw new Error(`Google userinfo faalde: ${r.status}`);
+  return r.json();
 }
-
-// Verifieer het identity-token van de broker. Returnt de payload
-// { typ:'identity', sub, email, name, picture, jti, exp, ... }.
-export async function verifyIdentityToken(token) {
-  const key = await getPublicKey();
-  const { payload } = await jwtVerify(token, key, {
-    issuer: ISSUER,
-    algorithms: [ALG],
-    audience: SITE_ORIGIN, // token moet voor ÓNZE site bedoeld zijn
-  });
-  if (payload.typ !== 'identity') throw new Error('verkeerd tokentype');
-  return payload;
-}
-
-// Kleine in-memory jti-cache tegen replay binnen de (korte) geldigheidsduur.
-// Returnt false als de jti al gebruikt is.
-const _usedJti = new Map(); // jti -> exp (epoch seconds)
-export function consumeJti(jti, expEpoch) {
-  if (!jti) return true; // geen jti = niets te dedupen
-  const now = Math.floor(Date.now() / 1000);
-  for (const [k, e] of _usedJti) if (e < now) _usedJti.delete(k);
-  if (_usedJti.has(jti)) return false;
-  _usedJti.set(jti, expEpoch || now + 600);
-  return true;
-}
Index: src/config/mailer.js
===================================================================
--- src/config/mailer.js	(revision 9e27d64c70dff768f7a07cb5f10586ad65f56c42)
+++ src/config/mailer.js	(revision 9e27d64c70dff768f7a07cb5f10586ad65f56c42)
@@ -0,0 +1,37 @@
+// E-mail versturen (wachtwoord-reset). Optioneel: alleen actief als de self-hoster
+// SMTP heeft ingesteld. Niet ingesteld? Dan valt de reset terug op de CLI
+// (`npm run reset-admin`) — zie README.
+//
+// Config via env:
+//   SMTP_HOST, SMTP_PORT (default 587), SMTP_USER, SMTP_PASS
+//   SMTP_FROM (afzender; default = SMTP_USER)
+
+import nodemailer from 'nodemailer';
+
+const HOST = process.env.SMTP_HOST || '';
+const PORT = parseInt(process.env.SMTP_PORT || '587', 10);
+const USER = process.env.SMTP_USER || '';
+const PASS = process.env.SMTP_PASS || '';
+const FROM = process.env.SMTP_FROM || USER;
+
+export function mailerConfigured() {
+  return !!(HOST && USER && PASS);
+}
+
+let _transport = null;
+function transport() {
+  if (!_transport) {
+    _transport = nodemailer.createTransport({
+      host: HOST,
+      port: PORT,
+      secure: PORT === 465, // 465 = impliciete TLS; 587 = STARTTLS
+      auth: { user: USER, pass: PASS },
+    });
+  }
+  return _transport;
+}
+
+export async function sendMail({ to, subject, text, html }) {
+  if (!mailerConfigured()) throw new Error('SMTP niet geconfigureerd');
+  return transport().sendMail({ from: FROM, to, subject, text, html });
+}
