Changeset 075185a in Klonkt for src/routes


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@…>

Location:
src/routes
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/routes/account.js

    r5b47619 r075185a  
    2424import { renderPage } from '../middleware/render.js';
    2525import { requireAuth } from '../middleware/auth.js';
    26 import { googleConfigured } from '../config/google.js';
    2726import { toWebp } from '../services/ImageWebpService.js';
    2827import { SUPPORTED } from '../services/i18n.js';
     
    6564  `).get(req.session.user.id);
    6665  const hasPassword = !!(account && account.password_hash && account.password_hash !== '!google-oauth');
    67   const googleLinked = !!(account && account.google_sub);
    6866  if (account) { delete account.password_hash; delete account.google_sub; } // don't leak to the view
    6967
     
    7472    account,
    7573    hasPassword,
    76     googleLinked,
    77     googleAvailable: googleConfigured(),
    7874    editableSite,
    7975    // Display fallback: when you have no own account avatar, show your site's photo.
     
    204200});
    205201
    206 // Unlink Google account. Only allowed if a password is set,
    207 // otherwise the user would lock themselves out (no login method left).
    208 router.post('/google/unlink', requireAuth, (req, res) => {
    209   const row = db.prepare('SELECT password_hash, google_sub FROM users WHERE id = ?').get(req.session.user.id);
    210   if (!row || !row.google_sub) {
    211     return res.redirect('/account?error=' + encodeURIComponent('Er is geen Google-account gekoppeld'));
    212   }
    213   if (!row.password_hash || row.password_hash === '!google-oauth') {
    214     return res.redirect('/account?error=' + encodeURIComponent('Stel eerst een wachtwoord in — anders kun je niet meer inloggen.'));
    215   }
    216   db.prepare('UPDATE users SET google_sub = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(req.session.user.id);
    217   res.redirect('/account?success=' + encodeURIComponent('Google-account ontkoppeld'));
    218 });
    219 
    220202// ==================== UPLOAD AVATAR ====================
    221203router.post('/avatar', requireAuth, (req, res) => {
  • src/routes/admin-settings.js

    r5b47619 r075185a  
    2525import { mailerStatus, sendMail } from '../config/mailer.js';
    2626import { entitlementStatus, premiumUnlocked } from '../services/PatreonService.js';
    27 import { googleConfigured, redirectUri, currentClientId, clientSecretSet } from '../config/google.js';
    2827import { toWebp } from '../services/ImageWebpService.js';
    2928
     
    8180    defaultLang: getSetting('default_lang') || '',
    8281    premium: entitlementStatus(),
    83     google: {
    84       configured: googleConfigured(),
    85       redirectUri: redirectUri(),
    86       clientId: currentClientId(),
    87       secretSet: clientSecretSet(),
    88     },
    8982    smtp: mailerStatus(),
    9083    footerNewsletter: getSetting('footer_newsletter') === '1',
     
    155148});
    156149
    157 // Google login on its own admin page (separate from the general settings).
    158 router.get('/google', requireGod, (req, res) => {
    159   renderPage(req, res, 'pages/admin-google', {
    160     pageTitle: 'Google-login',
    161     bodyClass: 'on-admin',
    162     google: {
    163       configured: googleConfigured(),
    164       redirectUri: redirectUri(),
    165       clientId: currentClientId(),
    166       secretSet: clientSecretSet(),
    167     },
    168     success: req.query.success || null,
    169     error: req.query.error || null,
    170   });
    171 });
    172 
    173 // Configure Google login (listeners) — Client ID + Secret in app_settings.
    174 // The redirect URI is derived from PUBLIC_BASE_URL (see config/google.js).
    175 router.post('/google', requireGod, (req, res) => {
    176   if (req.body.clear === '1') {
    177     setSetting('google_client_id', '');
    178     setSetting('google_client_secret', '');
    179     return res.redirect('/admin/settings?success=' + encodeURIComponent('Google-login losgekoppeld'));
    180   }
    181   setSetting('google_client_id', (req.body.google_client_id || '').toString().trim());
    182   // Only overwrite the secret if a new value was entered (empty = leave as-is).
    183   const secret = (req.body.google_client_secret || '').toString().trim();
    184   if (secret) setSetting('google_client_secret', secret);
    185   res.redirect('/admin/settings?success=' + encodeURIComponent('Google-login opgeslagen'));
    186 });
    187 
    188150// ── SMTP / e-mail-instellingen ────────────────────────────────────
    189151router.post('/smtp', requireGod, (req, res) => {
  • 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.