Changeset 075185a in Klonkt for src/routes/auth.js


Ignore:
Timestamp:
06/24/2026 02:16:57 PM (3 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
2e9773f
Parents:
5b47619
Message:

refactor(auth): remove Google login (listeners now interact via the fediverse)

Removes config/google.js, the /auth/google* routes, the admin Google config page
+ links, the account link/unlink, and the public Google login button. /login and
/auth/admin both show the admin password form. SEO/Search-Console Google is
untouched. (Native comments removed next.)

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/auth.js

    r5b47619 r075185a  
    77import { loginLimiter, registerLimiter } from '../middleware/rate-limit.js';
    88import { safeNext, requireAuth } from '../middleware/auth.js';
    9 import { googleConfigured, authorizeUrl, exchangeCode, fetchUserinfo } from '../config/google.js';
    10 import { premiumUnlocked } from '../services/PatreonService.js';
    11 
    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).
    15 function fanLoginReady() {
    16   return googleConfigured() && premiumUnlocked();
    17 }
    189import { mailerConfigured, sendMail } from '../config/mailer.js';
    1910import { resolveLang, t } from '../services/i18n.js';
     
    4738
    4839// ==================== LOGIN ====================
    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.
     40// Single login = admin/owner password (no public/listener login anymore; social
     41// interaction happens via the fediverse). /login and /auth/admin both show it.
    5242router.get('/login', (req, res) => {
    5343  const next = safeNext(req.query.next) || '';
     
    5848    bodyClass: 'on-special on-auth',
    5949    error: req.query.error || null,
    60     gerr: req.query.gerr || null, // foutcode voor een rijkere uitleg (bv. 'admin')
     50    gerr: null,
    6151    success: req.query.success || null,
    6252    username: '',
    63     adminLogin: false,
    64     googleReady: fanLoginReady(),
     53    adminLogin: true,
     54    googleReady: false,
    6555    next,
    6656  });
     
    265255});
    266256
    267 // ==================== GOOGLE LOGIN (listeners/commenters) ====================
    268 // Per-instance, own Google client. ALWAYS grants role member — never admin.
    269 router.get('/google', (req, res) => {
    270   if (!fanLoginReady()) {
    271     return res.redirect('/auth/login?gerr=unavailable');
    272   }
    273   const state = crypto.randomBytes(16).toString('hex');
    274   req.session.oauthState = state;
    275   req.session.oauthNext = safeNext(req.query.next) || '';
    276   delete req.session.oauthLink;
    277   res.redirect(authorizeUrl(state));
    278 });
    279 
    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).
    284 router.get('/google/link', requireAuth, (req, res) => {
    285   if (!googleConfigured()) {
    286     return res.redirect('/account?error=' + encodeURIComponent('Google-login is op deze site niet ingesteld.'));
    287   }
    288   const state = crypto.randomBytes(16).toString('hex');
    289   req.session.oauthState = state;
    290   req.session.oauthLink = true; // link mode instead of login mode
    291   res.redirect(authorizeUrl(state));
    292 });
    293 
    294 function uniqueUsername(base) {
    295   let u = String(base || 'luisteraar').toLowerCase().replace(/[^a-z0-9_-]/g, '').slice(0, 28);
    296   if (u.length < 3) u = 'luisteraar';
    297   let candidate = u, n = 1;
    298   while (db.prepare('SELECT 1 FROM users WHERE username = ?').get(candidate)) {
    299     candidate = (u.slice(0, 26) + n).slice(0, 32);
    300     n++;
    301   }
    302   return candidate;
    303 }
    304 
    305 router.get('/google/callback', async (req, res) => {
    306   const linking = !!req.session.oauthLink;
    307   const failLogin = (code) => res.redirect('/auth/login?gerr=' + code);
    308   const failLink = (msg) => res.redirect('/account?error=' + encodeURIComponent(msg));
    309 
    310   try {
    311     const { code, state } = req.query;
    312     if (!code || !state || state !== req.session.oauthState) {
    313       delete req.session.oauthState; delete req.session.oauthLink; delete req.session.oauthNext;
    314       return linking ? failLink('Google-koppeling afgebroken of sessie verlopen. Probeer opnieuw.') : failLogin('session');
    315     }
    316 
    317     const tok = await exchangeCode(String(code));
    318     const info = await fetchUserinfo(tok.access_token);
    319     const email = (info.email || '').trim().toLowerCase();
    320 
    321     // ── LINK MODE: attach Google to the current (logged-in) account ──
    322     if (linking) {
    323       delete req.session.oauthState; delete req.session.oauthLink;
    324       if (!req.session.user) return failLogin('session');
    325       if (!info.sub) return failLink('Google gaf geen account-id terug. Probeer opnieuw.');
    326       if (info.email && info.email_verified === false) return failLink('Je Google-adres is niet geverifieerd.');
    327       // This Google account must not already be linked to a DIFFERENT account.
    328       const other = db.prepare('SELECT id FROM users WHERE google_sub = ? AND id != ?').get(info.sub, req.session.user.id);
    329       if (other) return failLink('Dit Google-account is al aan een andere gebruiker gekoppeld.');
    330       db.prepare(`
    331         UPDATE users SET google_sub = ?, avatar_url = COALESCE(avatar_url, ?),
    332           updated_at = CURRENT_TIMESTAMP WHERE id = ?
    333       `).run(info.sub, info.picture || null, req.session.user.id);
    334       return res.redirect('/account?success=' + encodeURIComponent('Google-account gekoppeld — je kunt nu ook met Google inloggen.'));
    335     }
    336 
    337     // ── LOGIN MODE (listeners/fans + linked admin) ──
    338     if (!fanLoginReady()) return failLogin('unavailable');
    339     const next = safeNext(req.session.oauthNext) || '';
    340     delete req.session.oauthState; delete req.session.oauthNext;
    341     if (!email || info.email_verified === false) return failLogin('email');
    342 
    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.
    347     let user = info.sub ? db.prepare('SELECT * FROM users WHERE google_sub = ?').get(info.sub) : null;
    348 
    349     if (user) {
    350       // Linked account found → keep their own role. Update avatar if empty.
    351       db.prepare(`
    352         UPDATE users SET avatar_url = COALESCE(avatar_url, ?), updated_at = CURRENT_TIMESTAMP WHERE id = ?
    353       `).run(info.picture || null, user.id);
    354     } else {
    355       user = db.prepare('SELECT * FROM users WHERE LOWER(email) = ?').get(email);
    356       if (user && (user.role === 'god' || user.role === 'admin')) {
    357         // Admin found by email but WITHOUT a linked sub → Google never grants admin.
    358         // Must first link via Account → Sign in with Google.
    359         return failLogin('admin');
    360       } else if (user) {
    361         // Existing listener: link google_sub/avatar if missing.
    362         db.prepare(`
    363           UPDATE users SET google_sub = COALESCE(google_sub, ?), avatar_url = COALESCE(avatar_url, ?),
    364             updated_at = CURRENT_TIMESTAMP WHERE id = ?
    365         `).run(info.sub || null, info.picture || null, user.id);
    366       } else {
    367         // New listener — always member.
    368         const userId = uuid();
    369         const username = uniqueUsername(info.name || email.split('@')[0]);
    370         db.prepare(`
    371           INSERT INTO users (id, username, email, password_hash, role, avatar_url, theme, palette, google_sub)
    372           VALUES (?, ?, ?, '!google-oauth', 'member', ?, 'dark', 'sage', ?)
    373         `).run(userId, username, info.email || email, info.picture || null, info.sub || null);
    374         user = db.prepare('SELECT * FROM users WHERE id = ?').get(userId);
    375       }
    376     }
    377 
    378     req.session.user = {
    379       id: user.id, username: user.username, email: user.email, role: user.role,
    380       avatar_url: user.avatar_url, palette: user.palette, theme: user.theme,
    381       readonly: !!user.readonly,
    382     };
    383     res.redirect(next || '/');
    384   } catch (e) {
    385     console.error('[auth/google/callback]', e.message);
    386     delete req.session.oauthState; delete req.session.oauthLink; delete req.session.oauthNext;
    387     return linking
    388       ? res.redirect('/account?error=' + encodeURIComponent('Google koppelen mislukt — probeer opnieuw.'))
    389       : failLogin('failed');
    390   }
    391 });
    392 
    393257// ==================== LOGOUT ====================
    394258router.get('/logout', (req, res) => { req.session.destroy(() => res.redirect('/')); });
Note: See TracChangeset for help on using the changeset viewer.