source: Klonkt/src/routes/account.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: 4.8 KB
RevLine 
[7bc636b]1/**
2 * Account routes — profile, password, avatar.
3 *
4 * Sections:
5 * GET / -> render full account page (profile + password + avatar + danger)
6 * POST /profile -> update bio
7 * POST /password -> change password (verify current, hash new)
8 * POST /avatar -> upload avatar image (multer)
9 * POST /avatar/remove -> clear avatar_url
10 *
11 * Each form has its own POST handler. After success, redirects back to
12 * /account?success=... so the page picks it up via query string.
13 */
14
15import express from 'express';
16import path from 'path';
17import fs from 'fs';
18import { fileURLToPath } from 'url';
19import multer from 'multer';
20import { v4 as uuid } from 'uuid';
21import db from '../config/database.js';
22import { renderPage } from '../middleware/render.js';
23import { requireAuth } from '../middleware/auth.js';
24
25const __dirname = path.dirname(fileURLToPath(import.meta.url));
26const AVATAR_DIR = path.resolve(
27 process.env.AVATAR_PATH || path.join(__dirname, '..', '..', 'storage', 'media', 'avatars')
28);
29fs.mkdirSync(AVATAR_DIR, { recursive: true });
30
31const ALLOWED_AVATAR_EXT = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']);
32const MAX_AVATAR_BYTES = 5 * 1024 * 1024;
33
34const avatarStorage = multer.diskStorage({
35 destination: (req, file, cb) => cb(null, AVATAR_DIR),
36 filename: (req, file, cb) => {
37 const ext = path.extname(file.originalname).toLowerCase();
38 cb(null, `${uuid()}${ext}`);
39 },
40});
41const avatarUpload = multer({
42 storage: avatarStorage,
43 limits: { fileSize: MAX_AVATAR_BYTES },
44 fileFilter: (req, file, cb) => {
45 const ext = path.extname(file.originalname).toLowerCase();
46 if (!ALLOWED_AVATAR_EXT.has(ext)) {
47 return cb(new Error('Avatar must be jpg/png/webp/gif'));
48 }
49 cb(null, true);
50 },
51});
52
53const router = express.Router();
54
55// ==================== GET account page ====================
56router.get('/', requireAuth, (req, res) => {
57 const account = db.prepare(`
58 SELECT id, username, email, role, bio, avatar_url, created_at
59 FROM users WHERE id = ?
60 `).get(req.session.user.id);
61
62 renderPage(req, res, 'pages/account', {
63 pageTitle: 'Account',
64 bodyClass: 'on-special',
65 account,
66 success: req.query.success || null,
67 error: req.query.error || null,
68 });
69});
70
71// ==================== UPDATE BIO ====================
72router.post('/profile', requireAuth, (req, res) => {
73 const bio = (req.body.bio || '').toString().slice(0, 500).trim();
74 db.prepare('UPDATE users SET bio = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
75 .run(bio || null, req.session.user.id);
76 res.redirect('/account?success=' + encodeURIComponent('Profile updated'));
77});
78
79// P57 — /preferences route removed. Per-user theme/palette was a multi-tenant
80// holdover that conflicts with the site-default model: visitors should see
81// the site's appearance, not whatever a user once picked. The users.theme and
82// users.palette columns stay in the schema (no migration needed) but are no
83// longer read or written.
84
[c80e78b]85// Wachtwoord-wijzigen is verwijderd: inloggen gaat sinds de Google-OAuth-migratie
86// volledig via Google, er is geen wachtwoord meer om te wijzigen.
[7bc636b]87
88// ==================== UPLOAD AVATAR ====================
89router.post('/avatar', requireAuth, (req, res) => {
90 avatarUpload.single('avatar')(req, res, (err) => {
91 if (err) {
92 return res.redirect('/account?error=' + encodeURIComponent(err.message));
93 }
94 if (!req.file) {
95 return res.redirect('/account?error=' + encodeURIComponent('No file uploaded'));
96 }
97
98 const url = `/media/avatars/${req.file.filename}`;
99
100 // Remove the old avatar file (if it lives in our avatar dir)
101 const old = db.prepare('SELECT avatar_url FROM users WHERE id = ?').get(req.session.user.id)?.avatar_url;
102 if (old && old.startsWith('/media/avatars/')) {
103 const oldPath = path.join(AVATAR_DIR, path.basename(old));
104 try { fs.unlinkSync(oldPath); } catch {}
105 }
106
107 db.prepare('UPDATE users SET avatar_url = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
108 .run(url, req.session.user.id);
109 req.session.user.avatar_url = url;
110
111 res.redirect('/account?success=' + encodeURIComponent('Avatar updated'));
112 });
113});
114
115// ==================== REMOVE AVATAR ====================
116router.post('/avatar/remove', requireAuth, (req, res) => {
117 const old = db.prepare('SELECT avatar_url FROM users WHERE id = ?').get(req.session.user.id)?.avatar_url;
118 if (old && old.startsWith('/media/avatars/')) {
119 const oldPath = path.join(AVATAR_DIR, path.basename(old));
120 try { fs.unlinkSync(oldPath); } catch {}
121 }
122 db.prepare('UPDATE users SET avatar_url = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
123 .run(req.session.user.id);
124 req.session.user.avatar_url = null;
125 res.redirect('/account?success=' + encodeURIComponent('Avatar removed'));
126});
127
128export default router;
Note: See TracBrowser for help on using the repository browser.