source: Klonkt/src/routes/auth.js@ c80e78b

main
Last change on this file since c80e78b was c80e78b, checked in by roboburr <roboburr@…>, 3 months ago

auth: replace username/password with Google login

Listeners (and the owner) now log in via Google instead of a local
username/password account. This lowers the barrier to commenting and
removes the home-built password/registration system.

  • src/config/google.js: raw OAuth2 helpers (authorize/token/userinfo) via the built-in fetch, config-driven. Boot keeps working without credentials.
  • src/routes/auth.js: /auth/google + /auth/google/callback (find-or-create user on email, ADMIN_EMAIL -> god, set session). login/register/reset POST handlers removed; /login now shows the Google button.
  • database.js: idempotent column users.google_sub.
  • account.js + account.ejs: change-password removed.
  • auth-login.ejs / welcome.ejs: Google button instead of password form.
  • shared-styles.ejs: .btn-google styling.
  • .env.example: GOOGLE_CLIENT_ID/SECRET/REDIRECT_URI + ADMIN_EMAIL.

Existing users are matched on email (owner retains their site).
DO NOT deploy to roboburr until the Google credentials are in .env, otherwise
the owner locks themselves out.

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

  • Property mode set to 100644
File size: 5.3 KB
Line 
1import express from 'express';
2import crypto from 'crypto';
3import { v4 as uuid } from 'uuid';
4import db from '../config/database.js';
5import { renderPage } from '../middleware/render.js';
6import { safeNext } from '../middleware/auth.js';
7import { googleConfigured, authorizeUrl, exchangeCode, fetchUserinfo } from '../config/google.js';
8
9const router = express.Router();
10const ADMIN_EMAIL = (process.env.ADMIN_EMAIL || '').trim().toLowerCase();
11
12// ==================== LOGIN (alleen Google) ====================
13// Het oude username/wachtwoord-systeem is verwijderd; inloggen gaat via Google.
14router.get('/login', (req, res) => {
15 const next = safeNext(req.query.next) || '';
16 if (req.session.user) return res.redirect(next || '/');
17 renderPage(req, res, 'pages/auth-login', {
18 pageTitle: 'Inloggen',
19 bodyClass: 'on-special',
20 googleReady: googleConfigured(),
21 error: req.query.error || null,
22 next,
23 });
24});
25
26// Start de Google OAuth-flow.
27router.get('/google', (req, res) => {
28 if (!googleConfigured()) {
29 return res.redirect('/auth/login?error=' + encodeURIComponent('Google-login is op deze site nog niet geconfigureerd.'));
30 }
31 const next = safeNext(req.query.next) || '';
32 const state = crypto.randomBytes(16).toString('hex');
33 req.session.oauthState = state;
34 req.session.oauthNext = next;
35 res.redirect(authorizeUrl(state));
36});
37
38// Leid een geldige, unieke username af uit naam/e-mail (schema vereist username).
39function uniqueUsername(base) {
40 let u = String(base || 'luisteraar').toLowerCase().replace(/[^a-z0-9_-]/g, '').slice(0, 28);
41 if (u.length < 3) u = 'luisteraar';
42 let candidate = u, n = 1;
43 while (db.prepare('SELECT 1 FROM users WHERE username = ?').get(candidate)) {
44 candidate = (u.slice(0, 26) + n).slice(0, 32);
45 n++;
46 }
47 return candidate;
48}
49
50// Google callback: wissel code in, haal profiel, vind-of-maak user (op e-mail),
51// zet de sessie. ADMIN_EMAIL bepaalt wie owner/admin (god) is.
52router.get('/google/callback', async (req, res) => {
53 const fail = (msg) => res.redirect('/auth/login?error=' + encodeURIComponent(msg));
54 try {
55 const { code, state } = req.query;
56 if (!code || !state || state !== req.session.oauthState) {
57 return fail('Login afgebroken of ongeldige sessie.');
58 }
59 const next = safeNext(req.session.oauthNext) || '';
60 delete req.session.oauthState;
61 delete req.session.oauthNext;
62
63 const tok = await exchangeCode(String(code));
64 const info = await fetchUserinfo(tok.access_token);
65 const email = (info.email || '').trim().toLowerCase();
66 if (!email || info.email_verified === false) {
67 return fail('Geen geverifieerd Google-e-mailadres ontvangen.');
68 }
69
70 const isAdmin = !!ADMIN_EMAIL && email === ADMIN_EMAIL;
71 let user = db.prepare('SELECT * FROM users WHERE LOWER(email) = ?').get(email);
72
73 if (!user) {
74 const userCount = db.prepare('SELECT COUNT(*) AS c FROM users').get().c;
75 const role = (isAdmin || userCount === 0) ? 'god' : 'member';
76 const userId = uuid();
77 const username = uniqueUsername(info.name || email.split('@')[0]);
78 // password_hash is NOT NULL in het schema; we zetten een onbruikbare
79 // sentinel (er is geen wachtwoord-login meer).
80 db.prepare(`
81 INSERT INTO users (id, username, email, password_hash, role, avatar_url, theme, palette, google_sub)
82 VALUES (?, ?, ?, '!google-oauth', ?, ?, 'dark', 'sage', ?)
83 `).run(userId, username, info.email || email, role, info.picture || null, info.sub || null);
84
85 // Eerste/admin-user krijgt een persoonlijke site (zoals de oude flow), maar
86 // alleen als er nog geen site bestaat.
87 if (role === 'god' && !db.prepare('SELECT 1 FROM sites LIMIT 1').get()) {
88 const siteId = uuid();
89 db.prepare(`
90 INSERT INTO sites (id, slug, title, description, owner_id, palette, accent, language)
91 VALUES (?, ?, ?, ?, ?, 'sage', '#c2410c', 'nl')
92 `).run(siteId, username.toLowerCase(), username + "'s Site", 'Welkom', userId);
93 db.prepare(`INSERT INTO site_members (site_id, user_id, role) VALUES (?, ?, 'admin')`).run(siteId, userId);
94 }
95 user = db.prepare('SELECT * FROM users WHERE id = ?').get(userId);
96 } else {
97 // Bestaande user (gekoppeld op e-mail): koppel Google-id + avatar als die
98 // nog ontbreken, en promoveer naar god als dit ADMIN_EMAIL is.
99 db.prepare(`
100 UPDATE users
101 SET google_sub = COALESCE(google_sub, ?),
102 avatar_url = COALESCE(avatar_url, ?),
103 updated_at = CURRENT_TIMESTAMP
104 WHERE id = ?
105 `).run(info.sub || null, info.picture || null, user.id);
106 if (isAdmin && user.role !== 'god') {
107 db.prepare("UPDATE users SET role = 'god' WHERE id = ?").run(user.id);
108 user.role = 'god';
109 }
110 }
111
112 req.session.user = {
113 id: user.id,
114 username: user.username,
115 email: user.email,
116 role: user.role,
117 avatar_url: user.avatar_url,
118 palette: user.palette,
119 theme: user.theme,
120 };
121 res.redirect(next || '/');
122 } catch (e) {
123 console.error('[auth/google/callback]', e.message);
124 fail('Google-login mislukt. Probeer opnieuw.');
125 }
126});
127
128// ==================== LOGOUT ====================
129router.get('/logout', (req, res) => {
130 req.session.destroy(() => res.redirect('/'));
131});
132router.post('/logout', (req, res) => {
133 req.session.destroy(() => res.redirect('/'));
134});
135
136export default router;
Note: See TracBrowser for help on using the repository browser.