Index: src/routes/auth.js
===================================================================
--- src/routes/auth.js	(revision 2165b6d3d57ceaa8b5d1f70491868482149470cf)
+++ src/routes/auth.js	(revision 7bc636b391c66ac399c33e54f7173a022c6a3cbd)
@@ -6,70 +6,18 @@
 import { renderPage } from '../middleware/render.js';
 import { loginLimiter, registerLimiter } from '../middleware/rate-limit.js';
-import { safeNext, requireAuth } from '../middleware/auth.js';
-import { mailerConfigured, sendMail } from '../config/mailer.js';
-import { resolveLang, t } from '../services/i18n.js';
-import { setSetting } from '../services/SettingsService.js';
+import { safeNext } from '../middleware/auth.js';
 
 const router = express.Router();
 
-// Fixed dummy hash: ensures login always runs one bcrypt comparison, even when the
-// user doesn't exist or has no password — no timing oracle for enumeration.
-const DUMMY_HASH = bcrypt.hashSync('constant-time-login-guard', 10);
-
-// Canonical base URL for links in emails (reset). Building it from headers is
-// spoofable (X-Forwarded-Host); a fixed config eliminates that risk.
-function publicBaseUrl(req) {
-  const cfg = (process.env.PUBLIC_BASE_URL || '').replace(/\/$/, '');
-  if (cfg) return cfg;
-  // Fallback (dev): trust-proxy-sanitised protocol + Host header (NOT the raw
-  // X-Forwarded-Host).
-  return `${req.protocol}://${req.get('host')}`;
-}
-
-function hashToken(raw) {
-  return crypto.createHash('sha256').update(String(raw)).digest('hex');
-}
-
-// First-time setup? Only while there are no users yet may /register create an
-// admin account. Afterwards registration is closed (listeners come via Google).
-function isSetupMode() {
-  return db.prepare('SELECT COUNT(*) AS c FROM users').get().c === 0;
-}
-
 // ==================== LOGIN ====================
-// Single login = admin/owner password (no public/listener login anymore; social
-// interaction happens via the fediverse). /login and /auth/admin both show it.
 router.get('/login', (req, res) => {
   const next = safeNext(req.query.next) || '';
   if (req.session.user) return res.redirect(next || '/');
-  if (isSetupMode()) return res.redirect('/auth/register' + (next ? '?next=' + encodeURIComponent(next) : ''));
   renderPage(req, res, 'pages/auth-login', {
-    pageTitle: 'Inloggen',
-    bodyClass: 'on-special on-auth',
-    error: req.query.error || null,
-    gerr: null,
+    pageTitle: 'Login',
+    bodyClass: 'on-special',
+    error: null,
     success: req.query.success || null,
     username: '',
-    adminLogin: true,
-    googleReady: false,
-    next,
-  });
-});
-
-// Hidden admin login (username + password). Not linked anywhere in the UI —
-// the admin navigates here directly (/auth/admin).
-router.get('/admin', (req, res) => {
-  const next = safeNext(req.query.next) || '';
-  if (req.session.user) return res.redirect(next || '/');
-  if (isSetupMode()) return res.redirect('/auth/register' + (next ? '?next=' + encodeURIComponent(next) : ''));
-  renderPage(req, res, 'pages/auth-login', {
-    pageTitle: 'Beheerder inloggen',
-    bodyClass: 'on-special on-auth',
-    error: req.query.error || null,
-    gerr: null,
-    success: req.query.success || null,
-    username: '',
-    adminLogin: true,
-    googleReady: false,
     next,
   });
@@ -80,100 +28,153 @@
   const next = safeNext(req.body.next) || '';
 
-  // Error display on the (hidden) admin login page: re-show the password
-  // form (adminLogin:true), not the Google-only public page.
-  const renderErr = (error, status = 400) => {
-    res.status(status);
+  if (!username || !password) {
+    res.status(400);
     return renderPage(req, res, 'pages/auth-login', {
-      pageTitle: 'Beheerder inloggen', bodyClass: 'on-special on-auth',
-      error, gerr: null, success: null, username: username || '',
-      adminLogin: true, googleReady: false, next,
-    });
+      pageTitle: 'Login',
+      bodyClass: 'on-special',
+      error: 'Username and password required',
+      username: username || '',
+      next,
+    });
+  }
+
+  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,
   };
 
-  if (!username || !password) return renderErr('Gebruikersnaam en wachtwoord vereist');
-
-  const user = db.prepare('SELECT * FROM users WHERE username = ? OR email = ?').get(username, username);
-  // Always one bcrypt comparison (dummy if the user has no usable password)
-  // so response time reveals nothing about whether the account exists.
-  const usable = !!(user && user.password_hash && user.password_hash !== '!google-oauth');
-  const ok = bcrypt.compareSync(password, usable ? user.password_hash : DUMMY_HASH);
-  if (!usable || !ok) return renderErr('Ongeldige inloggegevens', 401);
-
-  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,
-    readonly: !!user.readonly,
-  };
   res.redirect(next || '/');
 });
 
-// ==================== FIRST-TIME SETUP (create admin account) ====================
+// ==================== REGISTER ====================
 router.get('/register', (req, res) => {
   const next = safeNext(req.query.next) || '';
   if (req.session.user) return res.redirect(next || '/');
-  // No public registration: only the very first admin may be created here.
-  if (!isSetupMode()) return res.redirect('/auth/login' + (next ? '?next=' + encodeURIComponent(next) : ''));
   renderPage(req, res, 'pages/auth-register', {
-    pageJs: 'auth-register',
-    pageTitle: t(resolveLang(req), 'setup.title'), bodyClass: 'on-special',
-    error: null, username: '', email: '', siteName: '', next,
+    pageTitle: 'Register',
+    bodyClass: 'on-special',
+    error: null,
+    username: '',
+    email: '',
+    next,
   });
 });
 
 router.post('/register', registerLimiter, (req, res) => {
-  const { username, email, password, siteName } = req.body;
+  const { username, email, password } = req.body;
   const next = safeNext(req.body.next) || '';
-  const renderErr = (error) => renderPage(req, res, 'pages/auth-register', {
-    pageJs: 'auth-register',
-    pageTitle: t(resolveLang(req), 'setup.title'), bodyClass: 'on-special',
-    error, username: username || '', email: email || '', siteName: siteName || '', next,
-  });
-
-  // Hard-closed once a user exists — prevents a second "admin" via this route.
-  if (!isSetupMode()) return res.redirect('/auth/login');
-
-  if (!username || !email || !password) return renderErr('Alle velden zijn verplicht');
+
+  // 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,
+    });
+  }
+
   if (!/^[a-z0-9_-]{3,32}$/i.test(username)) {
-    return renderErr('Gebruikersnaam: 3-32 tekens, letters/cijfers/_/- alleen');
-  }
-  if (password.length < 8) return renderErr('Wachtwoord moet minstens 8 tekens zijn');
-
+    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);
-  // The very first user is the administrator (god).
+
+  // 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 (?, ?, ?, ?, 'god', 'dark', 'sage')
-  `).run(userId, username, email, hash);
-
-  // Auto-create a personal site (single-tenant restructure follows later).
-  // Setup wizard: site name + language come from the form; language = the language
-  // the visitor used to fill in the wizard (resolveLang) and becomes the site default.
-  if (!db.prepare('SELECT 1 FROM sites LIMIT 1').get()) {
+    VALUES (?, ?, ?, ?, ?, 'dark', 'sage')
+  `).run(userId, username, email, hash, role);
+
+  // First user gets a personal site auto-created
+  if (userCount === 0) {
     const siteId = uuid();
-    const lang = resolveLang(req);
-    const title = (siteName || '').trim().slice(0, 80) || (username + "'s Site");
+    const siteSlug = username.toLowerCase();
     db.prepare(`
       INSERT INTO sites (id, slug, title, description, owner_id, palette, accent, language)
-      VALUES (?, ?, ?, ?, ?, 'klonkt', '#e8b04b', ?)
-    `).run(siteId, username.toLowerCase(), title, '', userId, lang);
-    db.prepare(`INSERT INTO site_members (site_id, user_id, role) VALUES (?, ?, 'admin')`).run(siteId, userId);
-    try { setSetting('default_lang', lang); } catch (e) { /* non-fatal */ }
-  }
-
-  req.session.user = { id: userId, username, email, role: 'god', palette: 'klonkt', theme: 'dark' };
+      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 || '/');
 });
 
-// ==================== FORGOT PASSWORD (request) ====================
+// ==================== PASSWORD RESET (request) ====================
 router.get('/reset-request', (req, res) => {
   if (req.session.user) return res.redirect('/');
   renderPage(req, res, 'pages/auth-reset-request', {
-    pageTitle: 'Wachtwoord resetten', bodyClass: 'on-special',
-    error: null, sent: false, devResetUrl: null, mailer: mailerConfigured(),
-  });
-});
-
-router.post('/reset-request', registerLimiter, async (req, res) => {
+    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;
@@ -182,52 +183,52 @@
     const user = db.prepare('SELECT id, email FROM users WHERE LOWER(email) = ?').get(email);
     if (user) {
-      const token = crypto.randomBytes(32).toString('hex'); // raw: only goes into the mail/link
+      // 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
-      // Store only the HASH: so DB read access yields no usable token.
       db.prepare('UPDATE users SET reset_token = ?, reset_token_expires = ? WHERE id = ?')
-        .run(hashToken(token), expires, user.id);
-
-      const url = `${publicBaseUrl(req)}/auth/reset/${token}`;
-
-      if (mailerConfigured()) {
-        try {
-          await sendMail({
-            to: user.email,
-            subject: 'Wachtwoord resetten',
-            text: `Reset je wachtwoord via deze link (30 min geldig):\n\n${url}\n\nNiet aangevraagd? Negeer deze mail.`,
-            html: `<p>Reset je wachtwoord via deze link (30 min geldig):</p><p><a href="${url}">${url}</a></p><p>Niet aangevraagd? Negeer deze mail.</p>`,
-          });
-        } catch (e) {
-          console.error('[reset-request] mail faalde:', e.message);
-        }
-      } else if (process.env.NODE_ENV !== 'production') {
-        // Dev without SMTP: show the link in the log + on the page.
-        console.log(`[password-reset] ${user.email} -> ${url}`);
-        devResetUrl = url;
-      } else {
-        // Production without SMTP: NEVER log the token. Refer to the CLI break-glass.
-        console.log(`[password-reset] requested for ${user.email} (no SMTP — use 'npm run reset-admin')`);
-      }
+        .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 address exists.
+  // Anti-enumeration: same response regardless of whether the email exists.
   renderPage(req, res, 'pages/auth-reset-request', {
-    pageTitle: 'Wachtwoord resetten', bodyClass: 'on-special',
-    error: null, sent: true, devResetUrl, mailer: mailerConfigured(),
-  });
-});
-
-// ==================== RESET PASSWORD (apply) ====================
+    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 datetime(reset_token_expires) > datetime('now')
-  `).get(hashToken(req.params.token));
+    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: 'Wachtwoord resetten', bodyClass: 'on-special',
-    error: row ? null : 'Deze reset-link is ongeldig of verlopen.',
-    token: row ? req.params.token : null,
-    username: row ? row.username : null,
+    pageTitle: 'Reset password',
+    bodyClass: 'on-special',
+    error: null,
+    token: req.params.token,
+    username: row.username,
   });
 });
@@ -237,27 +238,43 @@
   const row = db.prepare(`
     SELECT id, username FROM users
-    WHERE reset_token = ? AND datetime(reset_token_expires) > datetime('now')
-  `).get(hashToken(req.params.token));
+    WHERE reset_token = ? AND reset_token_expires > datetime('now')
+  `).get(req.params.token);
 
   const renderError = (msg) => renderPage(req, res, 'pages/auth-reset', {
-    pageTitle: 'Wachtwoord resetten', bodyClass: 'on-special',
-    error: msg, token: row ? req.params.token : null, username: row ? row.username : null,
-  });
-
-  if (!row) return renderError('Deze reset-link is ongeldig of verlopen.');
-  if (!new_password || new_password.length < 8) return renderError('Wachtwoord moet minstens 8 tekens zijn');
-  if (new_password !== confirm) return renderError('Wachtwoorden komen niet overeen');
+    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 = ?
+    UPDATE users
+    SET password_hash = ?, reset_token = NULL, reset_token_expires = NULL,
+        updated_at = CURRENT_TIMESTAMP
+    WHERE id = ?
   `).run(hash, row.id);
-  res.redirect('/auth/admin?success=' + encodeURIComponent('Wachtwoord gereset — log nu in.'));
+
+  // Force login (don't auto-log them in — let them prove ownership)
+  res.redirect('/auth/login?success=' + encodeURIComponent('Password reset — please log in.'));
 });
 
 // ==================== LOGOUT ====================
-router.get('/logout', (req, res) => { req.session.destroy(() => res.redirect('/')); });
-router.post('/logout', (req, res) => { req.session.destroy(() => res.redirect('/')); });
+router.get('/logout', (req, res) => {
+  req.session.destroy(() => res.redirect('/'));
+});
+
+router.post('/logout', (req, res) => {
+  req.session.destroy(() => res.redirect('/'));
+});
 
 export default router;
