Index: src/routes/auth.js
===================================================================
--- src/routes/auth.js	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
+++ src/routes/auth.js	(revision ae924a2babb279ac76f99438ade75ab50968359c)
@@ -1,270 +1,127 @@
 import express from 'express';
 import crypto from 'crypto';
-import bcrypt from 'bcryptjs';
 import { v4 as uuid } from 'uuid';
 import db from '../config/database.js';
 import { renderPage } from '../middleware/render.js';
-import { loginLimiter, registerLimiter } from '../middleware/rate-limit.js';
 import { safeNext } from '../middleware/auth.js';
+import { googleConfigured, authorizeUrl, exchangeCode, fetchUserinfo } from '../config/google.js';
 
 const router = express.Router();
+const ADMIN_EMAIL = (process.env.ADMIN_EMAIL || '').trim().toLowerCase();
 
-// ==================== LOGIN ====================
+// ==================== LOGIN (alleen Google) ====================
+// Het oude username/wachtwoord-systeem is verwijderd; inloggen gaat via Google.
 router.get('/login', (req, res) => {
   const next = safeNext(req.query.next) || '';
   if (req.session.user) return res.redirect(next || '/');
   renderPage(req, res, 'pages/auth-login', {
-    pageTitle: 'Login',
+    pageTitle: 'Inloggen',
     bodyClass: 'on-special',
-    error: null,
-    success: req.query.success || null,
-    username: '',
+    googleReady: googleConfigured(),
+    error: req.query.error || null,
     next,
   });
 });
 
-router.post('/login', loginLimiter, (req, res) => {
-  const { username, password } = req.body;
-  const next = safeNext(req.body.next) || '';
-
-  if (!username || !password) {
-    res.status(400);
-    return renderPage(req, res, 'pages/auth-login', {
-      pageTitle: 'Login',
-      bodyClass: 'on-special',
-      error: 'Username and password required',
-      username: username || '',
-      next,
-    });
+// Start de Google OAuth-flow.
+router.get('/google', (req, res) => {
+  if (!googleConfigured()) {
+    return res.redirect('/auth/login?error=' + encodeURIComponent('Google-login is op deze site nog niet geconfigureerd.'));
   }
-
-  const user = db.prepare(
-    'SELECT * FROM users WHERE username = ? OR email = ?'
-  ).get(username, username);
-
-  if (!user || !bcrypt.compareSync(password, user.password_hash)) {
-    res.status(401);
-    return renderPage(req, res, 'pages/auth-login', {
-      pageTitle: 'Login',
-      bodyClass: 'on-special',
-      error: 'Invalid credentials',
-      username: username,
-      next,
-    });
-  }
-
-  req.session.user = {
-    id: user.id,
-    username: user.username,
-    email: user.email,
-    role: user.role,
-    avatar_url: user.avatar_url,
-    palette: user.palette,
-    theme: user.theme,
-  };
-
-  res.redirect(next || '/');
+  const next = safeNext(req.query.next) || '';
+  const state = crypto.randomBytes(16).toString('hex');
+  req.session.oauthState = state;
+  req.session.oauthNext = next;
+  res.redirect(authorizeUrl(state));
 });
 
-// ==================== REGISTER ====================
-router.get('/register', (req, res) => {
-  const next = safeNext(req.query.next) || '';
-  if (req.session.user) return res.redirect(next || '/');
-  renderPage(req, res, 'pages/auth-register', {
-    pageTitle: 'Register',
-    bodyClass: 'on-special',
-    error: null,
-    username: '',
-    email: '',
-    next,
-  });
-});
+// Leid een geldige, unieke username af uit naam/e-mail (schema vereist username).
+function uniqueUsername(base) {
+  let u = String(base || 'luisteraar').toLowerCase().replace(/[^a-z0-9_-]/g, '').slice(0, 28);
+  if (u.length < 3) u = 'luisteraar';
+  let candidate = u, n = 1;
+  while (db.prepare('SELECT 1 FROM users WHERE username = ?').get(candidate)) {
+    candidate = (u.slice(0, 26) + n).slice(0, 32);
+    n++;
+  }
+  return candidate;
+}
 
-router.post('/register', registerLimiter, (req, res) => {
-  const { username, email, password } = req.body;
-  const next = safeNext(req.body.next) || '';
+// Google callback: wissel code in, haal profiel, vind-of-maak user (op e-mail),
+// zet de sessie. ADMIN_EMAIL bepaalt wie owner/admin (god) is.
+router.get('/google/callback', async (req, res) => {
+  const fail = (msg) => res.redirect('/auth/login?error=' + encodeURIComponent(msg));
+  try {
+    const { code, state } = req.query;
+    if (!code || !state || state !== req.session.oauthState) {
+      return fail('Login afgebroken of ongeldige sessie.');
+    }
+    const next = safeNext(req.session.oauthNext) || '';
+    delete req.session.oauthState;
+    delete req.session.oauthNext;
 
-  // Validation
-  if (!username || !email || !password) {
-    return renderPage(req, res, 'pages/auth-register', {
-      pageTitle: 'Register',
-      bodyClass: 'on-special',
-      error: 'All fields required',
-      username: username || '',
-      email: email || '',
-      next,
-    });
+    const tok = await exchangeCode(String(code));
+    const info = await fetchUserinfo(tok.access_token);
+    const email = (info.email || '').trim().toLowerCase();
+    if (!email || info.email_verified === false) {
+      return fail('Geen geverifieerd Google-e-mailadres ontvangen.');
+    }
+
+    const isAdmin = !!ADMIN_EMAIL && email === ADMIN_EMAIL;
+    let user = db.prepare('SELECT * FROM users WHERE LOWER(email) = ?').get(email);
+
+    if (!user) {
+      const userCount = db.prepare('SELECT COUNT(*) AS c FROM users').get().c;
+      const role = (isAdmin || userCount === 0) ? 'god' : 'member';
+      const userId = uuid();
+      const username = uniqueUsername(info.name || email.split('@')[0]);
+      // password_hash is NOT NULL in het schema; we zetten een onbruikbare
+      // sentinel (er is geen wachtwoord-login meer).
+      db.prepare(`
+        INSERT INTO users (id, username, email, password_hash, role, avatar_url, theme, palette, google_sub)
+        VALUES (?, ?, ?, '!google-oauth', ?, ?, 'dark', 'sage', ?)
+      `).run(userId, username, info.email || email, role, info.picture || null, info.sub || null);
+
+      // Eerste/admin-user krijgt een persoonlijke site (zoals de oude flow), maar
+      // alleen als er nog geen site bestaat.
+      if (role === 'god' && !db.prepare('SELECT 1 FROM sites LIMIT 1').get()) {
+        const siteId = uuid();
+        db.prepare(`
+          INSERT INTO sites (id, slug, title, description, owner_id, palette, accent, language)
+          VALUES (?, ?, ?, ?, ?, 'sage', '#c2410c', 'nl')
+        `).run(siteId, username.toLowerCase(), username + "'s Site", 'Welkom', userId);
+        db.prepare(`INSERT INTO site_members (site_id, user_id, role) VALUES (?, ?, 'admin')`).run(siteId, userId);
+      }
+      user = db.prepare('SELECT * FROM users WHERE id = ?').get(userId);
+    } else {
+      // Bestaande user (gekoppeld op e-mail): koppel Google-id + avatar als die
+      // nog ontbreken, en promoveer naar god als dit ADMIN_EMAIL is.
+      db.prepare(`
+        UPDATE users
+        SET google_sub = COALESCE(google_sub, ?),
+            avatar_url = COALESCE(avatar_url, ?),
+            updated_at = CURRENT_TIMESTAMP
+        WHERE id = ?
+      `).run(info.sub || null, info.picture || null, user.id);
+      if (isAdmin && user.role !== 'god') {
+        db.prepare("UPDATE users SET role = 'god' WHERE id = ?").run(user.id);
+        user.role = 'god';
+      }
+    }
+
+    req.session.user = {
+      id: user.id,
+      username: user.username,
+      email: user.email,
+      role: user.role,
+      avatar_url: user.avatar_url,
+      palette: user.palette,
+      theme: user.theme,
+    };
+    res.redirect(next || '/');
+  } catch (e) {
+    console.error('[auth/google/callback]', e.message);
+    fail('Google-login mislukt. Probeer opnieuw.');
   }
-
-  if (!/^[a-z0-9_-]{3,32}$/i.test(username)) {
-    return renderPage(req, res, 'pages/auth-register', {
-      pageTitle: 'Register',
-      bodyClass: 'on-special',
-      error: 'Username: 3-32 characters, letters/numbers/underscore/dash only',
-      username, email, next,
-    });
-  }
-
-  if (password.length < 8) {
-    return renderPage(req, res, 'pages/auth-register', {
-      pageTitle: 'Register',
-      bodyClass: 'on-special',
-      error: 'Password must be at least 8 characters',
-      username, email, next,
-    });
-  }
-
-  // Check uniqueness
-  const existing = db.prepare(
-    'SELECT id FROM users WHERE username = ? OR email = ?'
-  ).get(username, email);
-
-  if (existing) {
-    return renderPage(req, res, 'pages/auth-register', {
-      pageTitle: 'Register',
-      bodyClass: 'on-special',
-      error: 'Username or email already taken',
-      username, email, next,
-    });
-  }
-
-  // Create user
-  const userId = uuid();
-  const hash = bcrypt.hashSync(password, 10);
-
-  // First user becomes god
-  const userCount = db.prepare('SELECT COUNT(*) as c FROM users').get().c;
-  const role = userCount === 0 ? 'god' : 'member';
-
-  db.prepare(`
-    INSERT INTO users (id, username, email, password_hash, role, theme, palette)
-    VALUES (?, ?, ?, ?, ?, 'dark', 'sage')
-  `).run(userId, username, email, hash, role);
-
-  // First user gets a personal site auto-created
-  if (userCount === 0) {
-    const siteId = uuid();
-    const siteSlug = username.toLowerCase();
-    db.prepare(`
-      INSERT INTO sites (id, slug, title, description, owner_id, palette, accent, language)
-      VALUES (?, ?, ?, ?, ?, 'sage', '#c2410c', 'nl')
-    `).run(siteId, siteSlug, username + "'s Site", 'Welcome to my site', userId);
-    
-    db.prepare(`
-      INSERT INTO site_members (site_id, user_id, role) VALUES (?, ?, 'admin')
-    `).run(siteId, userId);
-  }
-
-  req.session.user = {
-    id: userId, username, email, role,
-    palette: 'sage', theme: 'dark',
-  };
-
-  res.redirect(next || '/');
-});
-
-// ==================== PASSWORD RESET (request) ====================
-router.get('/reset-request', (req, res) => {
-  if (req.session.user) return res.redirect('/');
-  renderPage(req, res, 'pages/auth-reset-request', {
-    pageTitle: 'Reset password',
-    bodyClass: 'on-special',
-    error: null,
-    sent: false,
-    devResetUrl: null,
-  });
-});
-
-router.post('/reset-request', registerLimiter, (req, res) => {
-  const email = (req.body.email || '').trim().toLowerCase();
-  let devResetUrl = null;
-
-  if (email) {
-    const user = db.prepare('SELECT id, email FROM users WHERE LOWER(email) = ?').get(email);
-    if (user) {
-      // Generate token (32-byte hex, ~10 min expiry)
-      const token = crypto.randomBytes(32).toString('hex');
-      const expires = new Date(Date.now() + 30 * 60 * 1000).toISOString(); // 30 min
-      db.prepare('UPDATE users SET reset_token = ?, reset_token_expires = ? WHERE id = ?')
-        .run(token, expires, user.id);
-
-      // No email sender yet — log the reset URL so dev can use it.
-      const proto = req.headers['x-forwarded-proto'] || req.protocol || 'http';
-      const host = req.headers['x-forwarded-host'] || req.get('host');
-      const url = `${proto}://${host}/auth/reset/${token}`;
-      console.log(`[password-reset] ${email} -> ${url}`);
-      if (process.env.NODE_ENV !== 'production') devResetUrl = url;
-    }
-  }
-
-  // Anti-enumeration: same response regardless of whether the email exists.
-  renderPage(req, res, 'pages/auth-reset-request', {
-    pageTitle: 'Reset password',
-    bodyClass: 'on-special',
-    error: null,
-    sent: true,
-    devResetUrl,
-  });
-});
-
-// ==================== PASSWORD RESET (apply) ====================
-router.get('/reset/:token', (req, res) => {
-  const row = db.prepare(`
-    SELECT id, username FROM users
-    WHERE reset_token = ? AND reset_token_expires > datetime('now')
-  `).get(req.params.token);
-
-  if (!row) {
-    return renderPage(req, res, 'pages/auth-reset', {
-      pageTitle: 'Reset password',
-      bodyClass: 'on-special',
-      error: 'This reset link is invalid or has expired.',
-      token: null,
-      username: null,
-    });
-  }
-
-  renderPage(req, res, 'pages/auth-reset', {
-    pageTitle: 'Reset password',
-    bodyClass: 'on-special',
-    error: null,
-    token: req.params.token,
-    username: row.username,
-  });
-});
-
-router.post('/reset/:token', (req, res) => {
-  const { new_password, confirm } = req.body;
-  const row = db.prepare(`
-    SELECT id, username FROM users
-    WHERE reset_token = ? AND reset_token_expires > datetime('now')
-  `).get(req.params.token);
-
-  const renderError = (msg) => renderPage(req, res, 'pages/auth-reset', {
-    pageTitle: 'Reset password',
-    bodyClass: 'on-special',
-    error: msg,
-    token: row ? req.params.token : null,
-    username: row ? row.username : null,
-  });
-
-  if (!row) return renderError('This reset link is invalid or has expired.');
-  if (!new_password || new_password.length < 8) {
-    return renderError('Password must be at least 8 characters');
-  }
-  if (new_password !== confirm) {
-    return renderError('Passwords do not match');
-  }
-
-  const hash = bcrypt.hashSync(new_password, 10);
-  db.prepare(`
-    UPDATE users
-    SET password_hash = ?, reset_token = NULL, reset_token_expires = NULL,
-        updated_at = CURRENT_TIMESTAMP
-    WHERE id = ?
-  `).run(hash, row.id);
-
-  // Force login (don't auto-log them in — let them prove ownership)
-  res.redirect('/auth/login?success=' + encodeURIComponent('Password reset — please log in.'));
 });
 
@@ -273,5 +130,4 @@
   req.session.destroy(() => res.redirect('/'));
 });
-
 router.post('/logout', (req, res) => {
   req.session.destroy(() => res.redirect('/'));
