Changeset 247988e in Klonkt
- Timestamp:
- 06/19/2026 09:55:26 AM (3 months ago)
- Branches:
- main
- Children:
- f33b24a
- Parents:
- 535f955
- Files:
-
- 5 edited
-
CHANGELOG.md (modified) (1 diff)
-
src/routes/account.js (modified) (4 diffs)
-
src/routes/auth.js (modified) (4 diffs)
-
src/views/pages/account.ejs (modified) (1 diff)
-
src/views/pages/auth-login.ejs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
CHANGELOG.md
r535f955 r247988e 15 15 16 16 ### Toegevoegd 17 - Beheerder kan z'n **Google-account koppelen** (Account → Inloggen met Google) en 18 daarna óók met Google inloggen. Veilig: koppelen kan alleen terwijl je met 19 wachtwoord bent ingelogd, en Google-login geeft alleen beheer als de exacte 20 gekoppelde Google-account (`google_sub`) matcht. Ontkoppelen kan zolang er een 21 wachtwoord is. 17 22 - Posts liken / favorieten: ingelogde gebruikers kunnen een post liken met een ♥-knop 18 23 (toont het aantal likes). Je gelikete posts staan op de nieuwe **Favorieten**-pagina -
src/routes/account.js
r535f955 r247988e 24 24 import { renderPage } from '../middleware/render.js'; 25 25 import { requireAuth } from '../middleware/auth.js'; 26 import { googleConfigured } from '../config/google.js'; 26 27 27 28 const __dirname = path.dirname(fileURLToPath(import.meta.url)); … … 58 59 router.get('/', requireAuth, (req, res) => { 59 60 const account = db.prepare(` 60 SELECT id, username, email, role, bio, avatar_url, created_at, password_hash 61 SELECT id, username, email, role, bio, avatar_url, created_at, password_hash, google_sub 61 62 FROM users WHERE id = ? 62 63 `).get(req.session.user.id); 63 64 const hasPassword = !!(account && account.password_hash && account.password_hash !== '!google-oauth'); 64 if (account) delete account.password_hash; // niet naar de view lekken 65 const googleLinked = !!(account && account.google_sub); 66 if (account) { delete account.password_hash; delete account.google_sub; } // niet naar de view lekken 65 67 66 68 renderPage(req, res, 'pages/account', { … … 69 71 account, 70 72 hasPassword, 73 googleLinked, 74 googleAvailable: googleConfigured(), 71 75 editableSite: ownedSite(req.session.user), 72 76 success: req.query.success || null, … … 144 148 }); 145 149 150 // Google-account ontkoppelen. Alleen toegestaan als er nog een wachtwoord is, 151 // anders zou je jezelf buitensluiten (geen login-methode meer over). 152 router.post('/google/unlink', requireAuth, (req, res) => { 153 const row = db.prepare('SELECT password_hash, google_sub FROM users WHERE id = ?').get(req.session.user.id); 154 if (!row || !row.google_sub) { 155 return res.redirect('/account?error=' + encodeURIComponent('Er is geen Google-account gekoppeld')); 156 } 157 if (!row.password_hash || row.password_hash === '!google-oauth') { 158 return res.redirect('/account?error=' + encodeURIComponent('Stel eerst een wachtwoord in — anders kun je niet meer inloggen.')); 159 } 160 db.prepare('UPDATE users SET google_sub = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(req.session.user.id); 161 res.redirect('/account?success=' + encodeURIComponent('Google-account ontkoppeld')); 162 }); 163 146 164 // ==================== UPLOAD AVATAR ==================== 147 165 router.post('/avatar', requireAuth, (req, res) => { -
src/routes/auth.js
r535f955 r247988e 6 6 import { renderPage } from '../middleware/render.js'; 7 7 import { loginLimiter, registerLimiter } from '../middleware/rate-limit.js'; 8 import { safeNext } from '../middleware/auth.js';8 import { safeNext, requireAuth } from '../middleware/auth.js'; 9 9 import { googleConfigured, authorizeUrl, exchangeCode, fetchUserinfo } from '../config/google.js'; 10 10 import { premiumUnlocked } from '../services/PatreonService.js'; … … 268 268 req.session.oauthState = state; 269 269 req.session.oauthNext = safeNext(req.query.next) || ''; 270 delete req.session.oauthLink; 271 res.redirect(authorizeUrl(state)); 272 }); 273 274 // Google KOPPELEN aan het huidige (ingelogde) account — bv. een beheerder die 275 // voortaan óók met Google wil inloggen. Vereist dat je al ingelogd bent (met 276 // wachtwoord); de koppeling slaat de google_sub op het eigen account op. 277 // Alleen googleConfigured() nodig (geen premium-gate — dit is geen fan-login). 278 router.get('/google/link', requireAuth, (req, res) => { 279 if (!googleConfigured()) { 280 return res.redirect('/account?error=' + encodeURIComponent('Google-login is op deze site niet ingesteld.')); 281 } 282 const state = crypto.randomBytes(16).toString('hex'); 283 req.session.oauthState = state; 284 req.session.oauthLink = true; // koppel-modus i.p.v. login-modus 270 285 res.redirect(authorizeUrl(state)); 271 286 }); … … 283 298 284 299 router.get('/google/callback', async (req, res) => { 285 const fail = (code) => res.redirect('/auth/login?gerr=' + code); 286 if (!fanLoginReady()) return fail('unavailable'); 300 const linking = !!req.session.oauthLink; 301 const failLogin = (code) => res.redirect('/auth/login?gerr=' + code); 302 const failLink = (msg) => res.redirect('/account?error=' + encodeURIComponent(msg)); 303 287 304 try { 288 305 const { code, state } = req.query; 289 if (!code || !state || state !== req.session.oauthState) return fail('session');290 const next = safeNext(req.session.oauthNext) || '';291 delete req.session.oauthState;292 delete req.session.oauthNext;306 if (!code || !state || state !== req.session.oauthState) { 307 delete req.session.oauthState; delete req.session.oauthLink; delete req.session.oauthNext; 308 return linking ? failLink('Google-koppeling afgebroken of sessie verlopen. Probeer opnieuw.') : failLogin('session'); 309 } 293 310 294 311 const tok = await exchangeCode(String(code)); 295 312 const info = await fetchUserinfo(tok.access_token); 296 313 const email = (info.email || '').trim().toLowerCase(); 297 if (!email || info.email_verified === false) return fail('email'); 314 315 // ── KOPPEL-MODUS: Google aan het huidige (ingelogde) account hangen ── 316 if (linking) { 317 delete req.session.oauthState; delete req.session.oauthLink; 318 if (!req.session.user) return failLogin('session'); 319 if (!info.sub) return failLink('Google gaf geen account-id terug. Probeer opnieuw.'); 320 if (info.email && info.email_verified === false) return failLink('Je Google-adres is niet geverifieerd.'); 321 // Dit Google-account mag niet al aan een ANDER account hangen. 322 const other = db.prepare('SELECT id FROM users WHERE google_sub = ? AND id != ?').get(info.sub, req.session.user.id); 323 if (other) return failLink('Dit Google-account is al aan een andere gebruiker gekoppeld.'); 324 db.prepare(` 325 UPDATE users SET google_sub = ?, avatar_url = COALESCE(avatar_url, ?), 326 updated_at = CURRENT_TIMESTAMP WHERE id = ? 327 `).run(info.sub, info.picture || null, req.session.user.id); 328 return res.redirect('/account?success=' + encodeURIComponent('Google-account gekoppeld — je kunt nu ook met Google inloggen.')); 329 } 330 331 // ── LOGIN-MODUS (luisteraars/fans + gekoppelde beheerder) ── 332 if (!fanLoginReady()) return failLogin('unavailable'); 333 const next = safeNext(req.session.oauthNext) || ''; 334 delete req.session.oauthState; delete req.session.oauthNext; 335 if (!email || info.email_verified === false) return failLogin('email'); 298 336 299 337 let user = db.prepare('SELECT * FROM users WHERE LOWER(email) = ?').get(email); 300 338 301 if (user) { 302 // Strikte scheiding: Google geeft nooit beheer. Hoort dit adres bij een 303 // beheerder, dan moet diegene met wachtwoord inloggen (geen Google-bypass). 304 if (user.role === 'god' || user.role === 'admin') { 305 // Eigen, duidelijke uitleg-pagina (zie auth-login.ejs) i.p.v. een kale melding. 306 return fail('admin'); 339 if (user && (user.role === 'god' || user.role === 'admin')) { 340 // Beheerder mag ALLEEN met Google in als 'ie z'n Google expliciet gekoppeld 341 // heeft (matchende google_sub). Anders blijft gelden: Google = nooit beheer. 342 if (!(user.google_sub && info.sub && user.google_sub === info.sub)) { 343 return failLogin('admin'); 307 344 } 345 // gekoppeld + match → doorgaan met de eigen (beheer)rol. 346 } else if (user) { 308 347 // Bestaande luisteraar: koppel google_sub/avatar als die ontbreken; weiger 309 348 // als al aan een ander Google-account gekoppeld. 310 if (user.google_sub && info.sub && user.google_sub !== info.sub) { 311 return fail('linked'); 312 } 349 if (user.google_sub && info.sub && user.google_sub !== info.sub) return failLogin('linked'); 313 350 db.prepare(` 314 351 UPDATE users SET google_sub = COALESCE(google_sub, ?), avatar_url = COALESCE(avatar_url, ?), … … 334 371 } catch (e) { 335 372 console.error('[auth/google/callback]', e.message); 336 fail('failed'); 373 delete req.session.oauthState; delete req.session.oauthLink; delete req.session.oauthNext; 374 return linking 375 ? res.redirect('/account?error=' + encodeURIComponent('Google koppelen mislukt — probeer opnieuw.')) 376 : failLogin('failed'); 337 377 } 338 378 }); -
src/views/pages/account.ejs
r535f955 r247988e 132 132 </form> 133 133 </section> 134 135 <% if (typeof googleAvailable !== 'undefined' && googleAvailable) { %> 136 <section class="ax-card"> 137 <div class="ax-card-title">Inloggen met Google</div> 138 <% if (typeof googleLinked !== 'undefined' && googleLinked) { %> 139 <p class="ax-tagline" style="margin:0 0 1rem">✓ Je Google-account is gekoppeld — je kunt ook met Google inloggen.</p> 140 <form action="/account/google/unlink" method="post"> 141 <button type="submit" class="ax-btn">Google ontkoppelen</button> 142 </form> 143 <% } else { %> 144 <p class="ax-tagline" style="margin:0 0 1rem">Koppel je Google-account zodat je voortaan ook met één klik via Google kunt inloggen (naast je wachtwoord).</p> 145 <a href="/auth/google/link" class="ax-btn ax-btn-primary" data-full-load>Koppel Google-account</a> 146 <% } %> 147 </section> 148 <% } %> 134 149 <% } else { %> 135 150 <section class="ax-card"> -
src/views/pages/auth-login.ejs
r535f955 r247988e 13 13 admin: { icon: '🔐', title: 'Dit is een beheerders-account', 14 14 body: 'Je logde in met Google, maar dit e-mailadres hoort bij de beheerder van deze site. Inloggen met Google is alleen voor luisteraars/fans — beheerders gebruiken altijd hun wachtwoord (zo kan een Google-login nooit per ongeluk beheerrechten geven).', 15 list: ['<strong>Ben je de beheerder?</strong> <a href="/auth/admin">Log in met je gebruikersnaam + wachtwoord</a>.', '<strong>Wil je als fan inloggen?</strong> Gebruik een <strong>ander Google-account</strong> (niet je beheerdersadres).'] },15 list: ['<strong>Ben je de beheerder?</strong> <a href="/auth/admin">Log in met je gebruikersnaam + wachtwoord</a>.', '<strong>Wil je voortaan met Google inloggen?</strong> Log eerst in met wachtwoord en <strong>koppel je Google-account</strong> onder Account → daarna werkt Google-login ook voor jou.', '<strong>Wil je als fan inloggen?</strong> Gebruik een <strong>ander Google-account</strong> (niet je beheerdersadres).'] }, 16 16 session: { icon: '⏱️', title: 'Login afgebroken', body: 'De Google-login is afgebroken of de sessie was verlopen. Probeer het gewoon opnieuw.' }, 17 17 email: { icon: '✉️', title: 'Geen geverifieerd e-mailadres', body: 'Je Google-account heeft geen geverifieerd e-mailadres. Verifieer je adres bij Google.' },
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)