Changeset 05665bc in Klonkt
- Timestamp:
- 07/25/2026 06:25:15 AM (7 weeks ago)
- Branches:
- main
- Children:
- 28267519
- Parents:
- c1f5c23
- Location:
- src
- Files:
-
- 3 edited
-
config/database.js (modified) (1 diff)
-
routes/guardian2.js (modified) (2 diffs)
-
views/pages/guardian2.ejs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
rc1f5c23 r05665bc 38 38 // SQLite throws if the column already exists; we swallow that. 39 39 ensureColumn('sites', 'enable_audio_player', 'INTEGER DEFAULT 1'); 40 // Guardian 2: losse guardians. Een guardian-only account is user + minimale 41 // site (alleen de actor telt); de vlag houdt CMS/listings erbuiten. 42 ensureColumn('sites', 'guardian_only', 'INTEGER DEFAULT 0'); 43 db.exec(`CREATE TABLE IF NOT EXISTS ap_guardian_invites ( 44 token TEXT PRIMARY KEY, 45 created_by TEXT NOT NULL, 46 created_at TEXT DEFAULT CURRENT_TIMESTAMP, 47 used_by TEXT, 48 used_at TEXT 49 )`); 40 50 ensureColumn('sites', 'profile_photo', 'TEXT'); 41 51 ensureColumn('audio_tracks', 'cover_url', 'TEXT'); -
src/routes/guardian2.js
rc1f5c23 r05665bc 10 10 */ 11 11 import express from 'express'; 12 import crypto from 'crypto'; 13 import bcrypt from 'bcryptjs'; 12 14 import path from 'path'; 13 15 import { fileURLToPath } from 'url'; … … 181 183 }); 182 184 185 // ── Losse guardians (Guardian 2): uitnodigen en aansluiten ─────────────── 186 // De familie nodigt oma uit; zij kiest naam + wachtwoord en heeft daarmee een 187 // guardian-only account: user + minimale site (guardian_only=1). Alles wat al 188 // per slug werkt (actor, inbox, offers, push, deze PWA) werkt dan meteen. 189 190 router.post('/invite', requireAuth, (req, res) => { 191 const token = crypto.randomBytes(16).toString('base64url'); 192 db.prepare('INSERT INTO ap_guardian_invites (token, created_by) VALUES (?,?)') 193 .run(token, req.session.user.id); 194 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 195 const url = `${base}/guardian2/join/${token}`; 196 res.send(`<!doctype html><meta charset="utf-8"><body style="font-family:sans-serif;max-width:480px;margin:40px auto"> 197 <h2>Invite a guardian</h2> 198 <p>Share this link. It lets one person create a guardian account here:</p> 199 <p><a href="${url}">${url}</a></p> 200 <p><a href="/guardian2">Back</a></p></body>`); 201 }); 202 203 function joinForm(token, error) { 204 return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"> 205 <body style="font-family:sans-serif;max-width:420px;margin:40px auto"> 206 <h2>Become a guardian</h2> 207 <p>Watch over someone you care about. Pick a name and a password; that is all.</p> 208 ${error ? `<p style="color:#b00">${error}</p>` : ''} 209 <form method="post" action="/guardian2/join/${token}"> 210 <p><input name="name" placeholder="your name (grandma)" required pattern="[a-z0-9_-]{1,32}" 211 style="width:100%;padding:10px" autocapitalize="none"></p> 212 <p><input name="password" type="password" placeholder="password" required minlength="8" 213 style="width:100%;padding:10px"></p> 214 <p><button style="width:100%;padding:12px">Create my guardian account</button></p> 215 </form></body>`; 216 } 217 218 router.get('/join/:token', (req, res) => { 219 const inv = db.prepare('SELECT * FROM ap_guardian_invites WHERE token = ? AND used_at IS NULL') 220 .get(req.params.token); 221 if (!inv) return res.status(404).send('This invite is no longer valid.'); 222 res.send(joinForm(req.params.token)); 223 }); 224 225 router.post('/join/:token', express.urlencoded({ extended: false }), (req, res) => { 226 const inv = db.prepare('SELECT * FROM ap_guardian_invites WHERE token = ? AND used_at IS NULL') 227 .get(req.params.token); 228 if (!inv) return res.status(404).send('This invite is no longer valid.'); 229 const name = String(req.body.name || '').trim().toLowerCase(); 230 const password = String(req.body.password || ''); 231 if (!/^[a-z0-9_-]{1,32}$/.test(name)) return res.status(400).send(joinForm(req.params.token, 'Only lowercase letters, digits, - and _.')); 232 if (password.length < 8) return res.status(400).send(joinForm(req.params.token, 'Password: at least 8 characters.')); 233 if (db.prepare('SELECT 1 FROM sites WHERE slug = ?').get(name) || db.prepare('SELECT 1 FROM users WHERE username = ?').get(name)) { 234 return res.status(409).send(joinForm(req.params.token, 'That name is taken, pick another.')); 235 } 236 const userId = crypto.randomUUID(); 237 db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)') 238 .run(userId, name, `${name}@guardian.invalid`, bcrypt.hashSync(password, 10), 'member'); 239 db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary, guardian_only) VALUES (?,?,?,?,0,1)') 240 .run(crypto.randomUUID(), name, name, userId); 241 db.prepare('UPDATE ap_guardian_invites SET used_by = ?, used_at = CURRENT_TIMESTAMP WHERE token = ?') 242 .run(userId, req.params.token); 243 req.session.user = { id: userId, username: name, role: 'member' }; 244 res.redirect('/guardian2'); 245 }); 246 183 247 export default router; -
src/views/pages/guardian2.ejs
rc1f5c23 r05665bc 26 26 <span class="g-me" title="<%= t('guardian.acting_as') %>">@<%= state.site %></span> 27 27 <% } %> 28 </header> 28 <form method="post" action="/guardian2/invite" style="display:inline"> 29 <button class="quiet small" type="submit">Invite a guardian</button> 30 </form> 31 </header> 29 32 30 33 <main>
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)