Changeset 9e27d64 in Klonkt for src/routes/account.js


Ignore:
Timestamp:
06/14/2026 07:31:23 AM (3 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
6351545
Parents:
32cc601
Message:

auth: password admin + per-instance Google for listeners (no broker)

Robin's choice: every self-hoster has their own password admin account,
and can optionally let listeners log in to comment using their OWN Google
client. No central broker (that would tie every customer site to Robin's
Google Cloud -> systemic risk on abuse).

  • Admin = username/password (bcrypt). First-time setup via /auth/register (only when there are 0 users); closed afterwards. No public registration.
  • Forgot password: /auth/reset-request -> email (if SMTP configured) with reset link; CLI break-glass npm run reset-admin always works (no email needed).
  • Change password (logged in) restored in /account.
  • Google = per-instance own credentials, OPTIONAL, listeners only -> always role member, never admin (god/admin email is rejected; google_sub mismatch too).
  • config/google.js back to direct Google OAuth; config/mailer.js new (nodemailer).
  • jose removed from deps; nodemailer added.

Security review (workflow) incorporated:

  • Reset token no longer in production logs (dev only).
  • Reset link from PUBLIC_BASE_URL instead of X-Forwarded-Host (host poisoning).
  • Reset tokens stored SHA-256-hashed in the DB.
  • Same-origin check on all state-modifying POSTs (CSRF layer on top of sameSite-lax).
  • Login always runs one bcrypt comparison (no timing enumeration).

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/account.js

    r32cc601 r9e27d64  
    1717import fs from 'fs';
    1818import { fileURLToPath } from 'url';
     19import bcrypt from 'bcryptjs';
    1920import multer from 'multer';
    2021import { v4 as uuid } from 'uuid';
     
    5657router.get('/', requireAuth, (req, res) => {
    5758  const account = db.prepare(`
    58     SELECT id, username, email, role, bio, avatar_url, created_at
     59    SELECT id, username, email, role, bio, avatar_url, created_at, password_hash
    5960    FROM users WHERE id = ?
    6061  `).get(req.session.user.id);
     62  const hasPassword = !!(account && account.password_hash && account.password_hash !== '!google-oauth');
     63  if (account) delete account.password_hash; // niet naar de view lekken
    6164
    6265  renderPage(req, res, 'pages/account', {
     
    6467    bodyClass: 'on-special',
    6568    account,
     69    hasPassword,
    6670    success: req.query.success || null,
    6771    error: req.query.error || null,
     
    8387// longer read or written.
    8488
    85 // Wachtwoord-wijzigen is verwijderd: inloggen gaat sinds de Google-OAuth-migratie
    86 // volledig via Google, er is geen wachtwoord meer om te wijzigen.
     89// ==================== CHANGE PASSWORD ====================
     90router.post('/password', requireAuth, (req, res) => {
     91  const { current, new_password, confirm } = req.body;
     92  if (!current || !new_password || !confirm) {
     93    return res.redirect('/account?error=' + encodeURIComponent('Alle wachtwoordvelden zijn verplicht'));
     94  }
     95  if (new_password.length < 8) {
     96    return res.redirect('/account?error=' + encodeURIComponent('Nieuw wachtwoord moet minstens 8 tekens zijn'));
     97  }
     98  if (new_password !== confirm) {
     99    return res.redirect('/account?error=' + encodeURIComponent('Nieuwe wachtwoorden komen niet overeen'));
     100  }
     101
     102  const row = db.prepare('SELECT password_hash FROM users WHERE id = ?').get(req.session.user.id);
     103  // Google-only accounts (luisteraars) hebben geen echt wachtwoord.
     104  if (!row || !row.password_hash || row.password_hash === '!google-oauth') {
     105    return res.redirect('/account?error=' + encodeURIComponent('Dit account heeft geen wachtwoord (Google-login)'));
     106  }
     107  if (!bcrypt.compareSync(current, row.password_hash)) {
     108    return res.redirect('/account?error=' + encodeURIComponent('Huidig wachtwoord is onjuist'));
     109  }
     110
     111  const newHash = bcrypt.hashSync(new_password, 10);
     112  db.prepare('UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
     113    .run(newHash, req.session.user.id);
     114
     115  res.redirect('/account?success=' + encodeURIComponent('Wachtwoord gewijzigd'));
     116});
    87117
    88118// ==================== UPLOAD AVATAR ====================
Note: See TracChangeset for help on using the changeset viewer.