Changeset 075185a in Klonkt


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
Files:
2 deleted
7 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('/')); });
  • src/views/pages/account.ejs

    r5b47619 r075185a  
    163163  </section>
    164164
    165   <% if (typeof googleAvailable !== 'undefined' && googleAvailable) { %>
    166   <section class="ax-card">
    167     <div class="ax-card-title"><%= t('acct.google_login') %></div>
    168     <% if (typeof googleLinked !== 'undefined' && googleLinked) { %>
    169       <p class="ax-tagline" style="margin:0 0 1rem"><%= t('acct.google_linked') %></p>
    170       <form action="/account/google/unlink" method="post">
    171         <button type="submit" class="ax-btn"><%= t('acct.google_unlink') %></button>
    172       </form>
    173     <% } else { %>
    174       <p class="ax-tagline" style="margin:0 0 1rem"><%= t('acct.google_link_hint') %></p>
    175       <a href="/auth/google/link" class="ax-btn ax-btn-primary" data-full-load><%= t('acct.google_link') %></a>
    176     <% } %>
    177   </section>
    178   <% } %>
    179165  <% } else { %>
    180166  <section class="ax-card">
  • src/views/pages/admin-settings.ejs

    r5b47619 r075185a  
    161161  </section>
    162162  <% } %>
    163 
    164   <%# Google login has its own admin page (compact link shown here). %>
    165   <section class="set-card" style="margin-top:1rem">
    166     <h2><%= t('aset.google_login') %></h2>
    167     <p class="set-help"><%= t('aset.google_login_help') %>
    168       <% if (google && google.configured) { %><strong><%= t('aset.status_set') %>&nbsp;✓</strong><% } else { %><%= t('aset.not_set_yet') %><% } %></p>
    169     <div class="set-actions"><a href="/admin/settings/google" class="btn btn-primary"><%= t('aset.google_login_setup') %> &rarr;</a></div>
    170   </section>
    171163
    172164  <%# Newsletter signup in the footer (premium). %>
  • src/views/pages/admin.ejs

    r5b47619 r075185a  
    5959      <a href="/admin/shows" class="btn"><%= t('admin.b_agenda') %></a>
    6060    <% } %>
    61     <a href="/admin/settings/google" class="btn"><%= t('admin.b_google') %></a>
    6261    <a href="/admin/updates" class="btn"><%= t('admin.b_updates') %></a>
    6362    <a href="/admin/handleiding" class="btn"><%= t('admin.b_help') %></a>
  • src/views/pages/auth-login.ejs

    r5b47619 r075185a  
    4848      <p class="auth-link auth-link-muted"><a href="/auth/reset-request"><%= t('auth.forgot') %></a></p>
    4949    </form>
    50   <% } else if (googleReady) { %>
    51     <%# Public login: Google only, for listeners/fans. %>
    52     <p class="auth-sub"><%= t('auth.public_sub') %></p>
    53     <a class="btn btn-google" href="/auth/google<%= (typeof next !== 'undefined' && next) ? '?next=' + encodeURIComponent(next) : '' %>">
    54       <svg class="g-icon" width="18" height="18" viewBox="0 0 18 18" aria-hidden="true">
    55         <path fill="#4285F4" d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.72v2.26h2.92c1.7-1.57 2.68-3.88 2.68-6.62z"/>
    56         <path fill="#34A853" d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.8.54-1.84.86-3.04.86-2.34 0-4.32-1.58-5.03-3.7H.96v2.33A9 9 0 0 0 9 18z"/>
    57         <path fill="#FBBC05" d="M3.97 10.72a5.4 5.4 0 0 1 0-3.44V4.95H.96a9 9 0 0 0 0 8.1l3.01-2.33z"/>
    58         <path fill="#EA4335" d="M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.58A9 9 0 0 0 .96 4.95l3.01 2.33C4.68 5.16 6.66 3.58 9 3.58z"/>
    59       </svg>
    60       <span><%= t('auth.google_btn') %></span>
    61     </a>
    62   <% } else { %>
    63     <div class="alert"><%= t('auth.google_unavail') %></div>
    64   <% } %>
    65 
    66   <% if (!_admin) { %>
    67     <%# Clickable box for the admin → navigates to the hidden password login. %>
    68     <a class="auth-admin-box" href="/auth/admin<%= (typeof next !== 'undefined' && next) ? '?next=' + encodeURIComponent(next) : '' %>">
    69       <span class="auth-admin-box-icon" aria-hidden="true">🔑</span>
    70       <span class="auth-admin-box-text">
    71         <strong><%= t('auth.admin_box_q') %></strong>
    72         <small><%= t('auth.admin_box_sub') %></small>
    73       </span>
    74       <span class="auth-admin-box-arrow" aria-hidden="true">&rarr;</span>
    75     </a>
    7650  <% } %>
    7751</div>
Note: See TracChangeset for help on using the changeset viewer.