Changeset 834bcc3 in Klonkt for src/routes/auth.js


Ignore:
Timestamp:
06/23/2026 06:14:27 PM (3 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
d774679
Parents:
bb42dfb
Message:

i18n: translate Dutch code comments to English across src/

Comments in routes/services/views/config/middleware/assets translated to
English for the public repo. A few dev-facing throw/console message strings
were Englished too. No user-facing UI strings or i18n dictionary values changed
(src/services/i18n.js untouched). Logic unchanged.

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/auth.js

    rbb42dfb r834bcc3  
    1010import { premiumUnlocked } from '../services/PatreonService.js';
    1111
    12 // Fan-login (luisteraars inloggen met Google om te reageren) is een premium-
    13 // feature: beschikbaar als Google is ingesteld ÉN de premium-laag ontgrendeld is
    14 // (premium uit = vrij; aan = Patreon vereist).
     12// Fan login (listeners signing in with Google to comment) is a premium feature:
     13// available when Google is configured AND the premium layer is unlocked
     14// (premium off = open to all; on = Patreon required).
    1515function fanLoginReady() {
    1616  return googleConfigured() && premiumUnlocked();
     
    2222const router = express.Router();
    2323
    24 // Vaste dummy-hash: zo draait login altijd één bcrypt-vergelijking, ook als de
    25 // user niet bestaat of geen wachtwoord heeft — geen timing-oracle voor enumeratie.
     24// Fixed dummy hash: ensures login always runs one bcrypt comparison, even when the
     25// user doesn't exist or has no password — no timing oracle for enumeration.
    2626const DUMMY_HASH = bcrypt.hashSync('constant-time-login-guard', 10);
    2727
    28 // Canonieke basis-URL voor links in e-mails (reset). Uit headers bouwen is
    29 // spoofbaar (X-Forwarded-Host); een vaste config sluit dat uit.
     28// Canonical base URL for links in emails (reset). Building it from headers is
     29// spoofable (X-Forwarded-Host); a fixed config eliminates that risk.
    3030function publicBaseUrl(req) {
    3131  const cfg = (process.env.PUBLIC_BASE_URL || '').replace(/\/$/, '');
    3232  if (cfg) return cfg;
    33   // Fallback (dev): trust-proxy-gesaneerde protocol + Host-header (NIET de rauwe
     33  // Fallback (dev): trust-proxy-sanitised protocol + Host header (NOT the raw
    3434  // X-Forwarded-Host).
    3535  return `${req.protocol}://${req.get('host')}`;
     
    4040}
    4141
    42 // Eerste-keer-setup? Pas zolang er nog geen enkele gebruiker is mag /register een
    43 // beheerder aanmaken. Daarna is registratie dicht (luisteraars komen via Google).
     42// First-time setup? Only while there are no users yet may /register create an
     43// admin account. Afterwards registration is closed (listeners come via Google).
    4444function isSetupMode() {
    4545  return db.prepare('SELECT COUNT(*) AS c FROM users').get().c === 0;
     
    4747
    4848// ==================== LOGIN ====================
    49 // Publieke loginpagina: voor BEZOEKERS alleen Google-login (luisteraars/fans).
    50 // De beheerders-login (wachtwoord) staat hier bewust NIET — die zit verborgen op
    51 // /auth/admin (zie hieronder), zodat de admin-login niet zichtbaar is op de plek
    52 // waar bezoekers heen worden gestuurd.
     49// Public login page: for VISITORS only Google-login (listeners/fans).
     50// The admin login (password) is intentionally NOT here — it lives hidden at
     51// /auth/admin (see below), so the admin login is not visible where visitors land.
    5352router.get('/login', (req, res) => {
    5453  const next = safeNext(req.query.next) || '';
     
    6867});
    6968
    70 // Verborgen beheerders-login (gebruikersnaam + wachtwoord). Nergens in de UI
    71 // gelinkt — de beheerder navigeert hier rechtstreeks naartoe (/auth/admin).
     69// Hidden admin login (username + password). Not linked anywhere in the UI —
     70// the admin navigates here directly (/auth/admin).
    7271router.get('/admin', (req, res) => {
    7372  const next = safeNext(req.query.next) || '';
     
    9190  const next = safeNext(req.body.next) || '';
    9291
    93   // Foutweergave op de (verborgen) beheerders-loginpagina: toon het wachtwoord-
    94   // formulier opnieuw (adminLogin:true), niet de Google-only publieke pagina.
     92  // Error display on the (hidden) admin login page: re-show the password
     93  // form (adminLogin:true), not the Google-only public page.
    9594  const renderErr = (error, status = 400) => {
    9695    res.status(status);
     
    105104
    106105  const user = db.prepare('SELECT * FROM users WHERE username = ? OR email = ?').get(username, username);
    107   // Altijd één bcrypt-vergelijking (dummy als de user geen bruikbaar wachtwoord
    108   // heeft) zodat de responstijd niets over het bestaan van een account verraadt.
     106  // Always one bcrypt comparison (dummy if the user has no usable password)
     107  // so response time reveals nothing about whether the account exists.
    109108  const usable = !!(user && user.password_hash && user.password_hash !== '!google-oauth');
    110109  const ok = bcrypt.compareSync(password, usable ? user.password_hash : DUMMY_HASH);
     
    119118});
    120119
    121 // ==================== EERSTE-KEER-SETUP (beheerder aanmaken) ====================
     120// ==================== FIRST-TIME SETUP (create admin account) ====================
    122121router.get('/register', (req, res) => {
    123122  const next = safeNext(req.query.next) || '';
    124123  if (req.session.user) return res.redirect(next || '/');
    125   // Geen publieke registratie: alleen de allereerste beheerder mag hier aangemaakt.
     124  // No public registration: only the very first admin may be created here.
    126125  if (!isSetupMode()) return res.redirect('/auth/login' + (next ? '?next=' + encodeURIComponent(next) : ''));
    127126  renderPage(req, res, 'pages/auth-register', {
     
    139138  });
    140139
    141   // Hard gesloten zodra er een gebruiker is — voorkomt een tweede "admin" via deze route.
     140  // Hard-closed once a user exists — prevents a second "admin" via this route.
    142141  if (!isSetupMode()) return res.redirect('/auth/login');
    143142
     
    150149  const userId = uuid();
    151150  const hash = bcrypt.hashSync(password, 10);
    152   // De allereerste gebruiker is de beheerder (god).
     151  // The very first user is the administrator (god).
    153152  db.prepare(`
    154153    INSERT INTO users (id, username, email, password_hash, role, theme, palette)
     
    156155  `).run(userId, username, email, hash);
    157156
    158   // Persoonlijke site auto-aanmaken (single-tenant-ombouw volgt later).
    159   // Setup-wizard: sitenaam + taal komen uit het formulier; taal = de taal waarin
    160   // de bezoeker de wizard invulde (resolveLang) en wordt meteen de site-standaard.
     157  // Auto-create a personal site (single-tenant restructure follows later).
     158  // Setup wizard: site name + language come from the form; language = the language
     159  // the visitor used to fill in the wizard (resolveLang) and becomes the site default.
    161160  if (!db.prepare('SELECT 1 FROM sites LIMIT 1').get()) {
    162161    const siteId = uuid();
     
    168167    `).run(siteId, username.toLowerCase(), title, '', userId, lang);
    169168    db.prepare(`INSERT INTO site_members (site_id, user_id, role) VALUES (?, ?, 'admin')`).run(siteId, userId);
    170     try { setSetting('default_lang', lang); } catch (e) { /* niet fataal */ }
     169    try { setSetting('default_lang', lang); } catch (e) { /* non-fatal */ }
    171170  }
    172171
     
    175174});
    176175
    177 // ==================== WACHTWOORD VERGETEN (aanvraag) ====================
     176// ==================== FORGOT PASSWORD (request) ====================
    178177router.get('/reset-request', (req, res) => {
    179178  if (req.session.user) return res.redirect('/');
     
    191190    const user = db.prepare('SELECT id, email FROM users WHERE LOWER(email) = ?').get(email);
    192191    if (user) {
    193       const token = crypto.randomBytes(32).toString('hex'); // ruw: gaat alleen de mail/link in
     192      const token = crypto.randomBytes(32).toString('hex'); // raw: only goes into the mail/link
    194193      const expires = new Date(Date.now() + 30 * 60 * 1000).toISOString(); // 30 min
    195       // Alleen de HASH opslaan: DB-leestoegang levert zo geen bruikbaar token op.
     194      // Store only the HASH: so DB read access yields no usable token.
    196195      db.prepare('UPDATE users SET reset_token = ?, reset_token_expires = ? WHERE id = ?')
    197196        .run(hashToken(token), expires, user.id);
     
    211210        }
    212211      } else if (process.env.NODE_ENV !== 'production') {
    213         // Dev zonder SMTP: link in log + op de pagina tonen.
     212        // Dev without SMTP: show the link in the log + on the page.
    214213        console.log(`[password-reset] ${user.email} -> ${url}`);
    215214        devResetUrl = url;
    216215      } else {
    217         // Productie zonder SMTP: NOOIT het token loggen. Verwijs naar de CLI break-glass.
     216        // Production without SMTP: NEVER log the token. Refer to the CLI break-glass.
    218217        console.log(`[password-reset] aangevraagd voor ${user.email} (geen SMTP — gebruik 'npm run reset-admin')`);
    219218      }
     
    221220  }
    222221
    223   // Anti-enumeratie: zelfde antwoord ongeacht of het adres bestaat.
     222  // Anti-enumeration: same response regardless of whether the address exists.
    224223  renderPage(req, res, 'pages/auth-reset-request', {
    225224    pageTitle: 'Wachtwoord resetten', bodyClass: 'on-special',
     
    228227});
    229228
    230 // ==================== WACHTWOORD RESETTEN (toepassen) ====================
     229// ==================== RESET PASSWORD (apply) ====================
    231230router.get('/reset/:token', (req, res) => {
    232231  const row = db.prepare(`
     
    266265});
    267266
    268 // ==================== GOOGLE-LOGIN (luisteraars/reageerders) ====================
    269 // Per-instance, eigen Google-client. Geeft ALTIJD rol member — nooit beheer.
     267// ==================== GOOGLE LOGIN (listeners/commenters) ====================
     268// Per-instance, own Google client. ALWAYS grants role member — never admin.
    270269router.get('/google', (req, res) => {
    271270  if (!fanLoginReady()) {
     
    279278});
    280279
    281 // Google KOPPELEN aan het huidige (ingelogde) account — bv. een beheerder die
    282 // voortaan óók met Google wil inloggen. Vereist dat je al ingelogd bent (met
    283 // wachtwoord); de koppeling slaat de google_sub op het eigen account op.
    284 // Alleen googleConfigured() nodig (geen premium-gate — dit is geen fan-login).
     280// LINK Google to the current (logged-in) account — e.g. an admin who also wants
     281// to log in with Google. Requires being already logged in (with password); the
     282// link stores the google_sub on their own account.
     283// Only googleConfigured() needed (no premium gate — this is not fan login).
    285284router.get('/google/link', requireAuth, (req, res) => {
    286285  if (!googleConfigured()) {
     
    289288  const state = crypto.randomBytes(16).toString('hex');
    290289  req.session.oauthState = state;
    291   req.session.oauthLink = true; // koppel-modus i.p.v. login-modus
     290  req.session.oauthLink = true; // link mode instead of login mode
    292291  res.redirect(authorizeUrl(state));
    293292});
     
    320319    const email = (info.email || '').trim().toLowerCase();
    321320
    322     // ── KOPPEL-MODUS: Google aan het huidige (ingelogde) account hangen ──
     321    // ── LINK MODE: attach Google to the current (logged-in) account ──
    323322    if (linking) {
    324323      delete req.session.oauthState; delete req.session.oauthLink;
     
    326325      if (!info.sub) return failLink('Google gaf geen account-id terug. Probeer opnieuw.');
    327326      if (info.email && info.email_verified === false) return failLink('Je Google-adres is niet geverifieerd.');
    328       // Dit Google-account mag niet al aan een ANDER account hangen.
     327      // This Google account must not already be linked to a DIFFERENT account.
    329328      const other = db.prepare('SELECT id FROM users WHERE google_sub = ? AND id != ?').get(info.sub, req.session.user.id);
    330329      if (other) return failLink('Dit Google-account is al aan een andere gebruiker gekoppeld.');
     
    336335    }
    337336
    338     // ── LOGIN-MODUS (luisteraars/fans + gekoppelde beheerder) ──
     337    // ── LOGIN MODE (listeners/fans + linked admin) ──
    339338    if (!fanLoginReady()) return failLogin('unavailable');
    340339    const next = safeNext(req.session.oauthNext) || '';
     
    342341    if (!email || info.email_verified === false) return failLogin('email');
    343342
    344     // Zoek EERST op de gekoppelde Google-account (google_sub). Een sub-match is het
    345     // expliciete koppel-bewijs → log in met de eigen rol, OOK als het Google-
    346     // mailadres afwijkt van het account-mailadres (bv. een beheerder die een ander
    347     // Gmail koppelt). Daarna pas op e-mail.
     343    // Look FIRST by linked Google account (google_sub). A sub-match is explicit
     344    // proof of the link → log in with their own role, EVEN IF the Google email
     345    // differs from the account email (e.g. an admin who linked a different Gmail).
     346    // Only then fall back to email lookup.
    348347    let user = info.sub ? db.prepare('SELECT * FROM users WHERE google_sub = ?').get(info.sub) : null;
    349348
    350349    if (user) {
    351       // Gekoppeld account gevonden → eigen rol behouden. Avatar bijwerken indien leeg.
     350      // Linked account found → keep their own role. Update avatar if empty.
    352351      db.prepare(`
    353352        UPDATE users SET avatar_url = COALESCE(avatar_url, ?), updated_at = CURRENT_TIMESTAMP WHERE id = ?
     
    356355      user = db.prepare('SELECT * FROM users WHERE LOWER(email) = ?').get(email);
    357356      if (user && (user.role === 'god' || user.role === 'admin')) {
    358         // Beheerder gevonden op e-mail maar ZONDER gekoppelde sub → Google geeft
    359         // nooit beheer. Eerst koppelen via Account → Inloggen met Google.
     357        // Admin found by email but WITHOUT a linked sub → Google never grants admin.
     358        // Must first link via Account → Sign in with Google.
    360359        return failLogin('admin');
    361360      } else if (user) {
    362         // Bestaande luisteraar: koppel google_sub/avatar als die ontbreken.
     361        // Existing listener: link google_sub/avatar if missing.
    363362        db.prepare(`
    364363          UPDATE users SET google_sub = COALESCE(google_sub, ?), avatar_url = COALESCE(avatar_url, ?),
     
    366365        `).run(info.sub || null, info.picture || null, user.id);
    367366      } else {
    368         // Nieuwe luisteraar — altijd member.
     367        // New listener — always member.
    369368        const userId = uuid();
    370369        const username = uniqueUsername(info.name || email.split('@')[0]);
Note: See TracChangeset for help on using the changeset viewer.