source: Klonkt/scripts/reset-admin.mjs@ 0f09302

main
Last change on this file since 0f09302 was 0f09302, checked in by Robin <roboburr@…>, 3 weeks ago

reset-admin wijst een instantie aan, en maakt nooit een database aan

In de gedeelde opstelling van deploy/klonkt@.service werkte het noodpad niet.
De code staat daar op /opt/klonkt en de configuratie per instantie in
/var/lib/klonkt/<slug>/.env, waar systemd hem leest via EnvironmentFile. Een
script dat je zelf start krijgt die omgeving niet: dotenv zoekt naast de code,
vindt niets, en draait zonder DATABASE_PATH.

Gemeten op een nagebouwde opstelling: 'npm run reset-admin' legde een lege
database in /opt/klonkt/storage/ -- de gedeelde codemap, die volgens de unit
juist read-only is -- en klapte daarna op 'no such table: users'. Een
stacktrace op de plek waar een aanwijzing hoort.

Drie dingen:

  • --instance <slug> leest dezelfde .env als systemd, met override, want wie de instantie aanwijst hoort te winnen van een DATABASE_PATH die nog in de shell hangt van een vorige.
  • De controle staat VOOR de import van src/config/database.js, want die maakt map en bestand aan zodra hij geladen wordt. Ontbreekt de database, dan stopt hij en noemt het pad; hij maakt er geen.
  • deploy/MULTI-INSTANCE.md noemt het commando nu, met de reden erbij. De hulppagina in de app zegt 'vanuit de projectmap', en dat klopt alleen bij een eigen checkout per instantie.

Zes tests, waaronder de geslaagde weg: het getoonde wachtwoord moet echt op de
gebruiker passen. Controleproef gedraaid -- zonder de wacht valt de test om die
eist dat er niets wordt aangemaakt.

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

  • Property mode set to 100644
File size: 6.0 KB
RevLine 
[9e27d64]1#!/usr/bin/env node
[834bcc3]2// Break-glass: reset (or set) the password of an admin account. Always works,
3// no email required — the self-hoster has shell/server access.
[9e27d64]4//
[834bcc3]5// Usage:
[0f09302]6// npm run reset-admin # reset the (first) god user, print new password
7// npm run reset-admin -- <user|email> # reset a specific user, print new password
8// npm run reset-admin -- <user|email> <pw> # set a chosen password
9// npm run reset-admin -- --instance <slug> [...] # pick an instance (shared code, /var/lib/klonkt)
[9e27d64]10//
[0f09302]11// Run from the project root so DATABASE_PATH/.env is loaded correctly, or name
12// the instance with --instance.
[9e27d64]13
[0f09302]14import fs from 'fs';
15import path from 'path';
[9e27d64]16import crypto from 'crypto';
[0f09302]17import { fileURLToPath } from 'url';
18import dotenv from 'dotenv';
[9e27d64]19import bcrypt from 'bcryptjs';
20
[0f09302]21const __dirname = path.dirname(fileURLToPath(import.meta.url));
22const wortel = path.join(__dirname, '..');
[9e27d64]23
[0f09302]24// De argumenten: --instance mag overal staan, de rest is stelling-afhankelijk.
25const argv = process.argv.slice(2);
26let slug = null;
27const rest = [];
28for (let i = 0; i < argv.length; i++) {
29 const a = argv[i];
30 if (a === '--instance' || a === '-i') { slug = argv[++i] || null; continue; }
31 if (a.startsWith('--instance=')) { slug = a.slice('--instance='.length); continue; }
32 rest.push(a);
33}
34const [arg, pwArg] = rest;
35
36if (slug !== null && !slug) {
37 console.error('--instance verwacht een slug, bijvoorbeeld: --instance boiert');
38 process.exit(1);
39}
40
41// WAAR DE CONFIGURATIE VANDAAN KOMT, en waarom dit meer dan één regel is.
42//
43// Bij één-instantie-per-checkout staat de .env naast de code en vindt dotenv
44// hem vanzelf. In de gedeelde opstelling van deploy/klonkt@.service niet: daar
45// is de code gedeeld op /opt/klonkt en staat de configuratie per instantie in
46// /var/lib/klonkt/<slug>/.env, waar SYSTEMD hem leest via EnvironmentFile.
47// Dotenv kijkt in de werkmap en vindt daar niets, dus zonder --instance draait
48// dit script met een lege DATABASE_PATH -- en dan wijst hij de verkeerde kant
49// op. Vandaar deze schakelaar.
50const DATA_ROOT = process.env.KLONKT_DATA_ROOT || '/var/lib/klonkt';
51if (slug) {
52 const envPad = path.join(DATA_ROOT, slug, '.env');
53 if (!fs.existsSync(envPad)) {
54 console.error(`Geen .env voor instantie "${slug}" op ${envPad}.`);
55 console.error(`Bestaande instanties: ${bestaandeInstanties().join(', ') || '(geen gevonden)'}`);
56 console.error('Staat je data ergens anders? Zet KLONKT_DATA_ROOT.');
57 process.exit(1);
58 }
59 // override: dotenv laat een bestaande variabele normaal met rust, maar wie
60 // --instance typt heeft die instantie AANGEWEZEN. Een DATABASE_PATH die nog
61 // in de shell hangt van een vorige instantie zou anders stilletjes winnen,
62 // en dan reset je het wachtwoord van de verkeerde site.
63 dotenv.config({ path: envPad, override: true });
[9e27d64]64} else {
[0f09302]65 dotenv.config();
66}
67
68function bestaandeInstanties() {
69 try {
70 return fs.readdirSync(DATA_ROOT).filter((d) => fs.existsSync(path.join(DATA_ROOT, d, '.env')));
71 } catch { return []; }
72}
73
74// EERST KIJKEN, DAN PAS IMPORTEREN. src/config/database.js maakt zijn map en
75// zijn bestand aan zodra hij geladen wordt -- prima bij de eerste start van een
76// server, funest hier: sta je in de verkeerde map, dan legt hij een lege
77// database neer en klapt daarna op "no such table: users". Dat is een
78// stacktrace op de plek waar een aanwijzing hoort te staan. Dus dezelfde
79// padkeuze als database.js, maar dan alleen berekend.
80const dbPad = process.env.DATABASE_PATH || path.join(wortel, 'storage/database.sqlite');
81if (!fs.existsSync(dbPad)) {
82 console.error(`Geen database op ${dbPad}.`);
83 console.error('');
84 console.error('Dit script maakt er met opzet geen aan: een ontbrekende database betekent');
85 console.error('bijna altijd dat DATABASE_PATH niet geladen is, niet dat de site leeg is.');
86 console.error('');
87 if (!slug) {
88 const gevonden = bestaandeInstanties();
89 if (gevonden.length) {
90 console.error(`Gedeelde opstelling? Noem de instantie: npm run reset-admin -- --instance <slug>`);
91 console.error(`Gevonden in ${DATA_ROOT}: ${gevonden.join(', ')}`);
92 } else {
93 console.error('Draai dit vanuit de projectmap, zodat de .env ernaast geladen wordt,');
94 console.error('of geef het pad mee: DATABASE_PATH=/pad/naar/database.sqlite npm run reset-admin');
95 }
96 }
97 process.exit(1);
98}
99
100const { default: db } = await import('../src/config/database.js');
101
102let user;
103try {
104 if (arg) {
105 user = db.prepare('SELECT * FROM users WHERE username = ? OR LOWER(email) = LOWER(?)').get(arg, arg);
106 } else {
107 // No arg: pick the admin (god or admin role), otherwise the very first user.
108 user =
109 db.prepare("SELECT * FROM users WHERE role IN ('god','admin') ORDER BY created_at LIMIT 1").get() ||
110 db.prepare('SELECT * FROM users ORDER BY created_at LIMIT 1').get();
111 }
112} catch (e) {
113 // Een database zonder users-tabel is geen lege site maar het verkeerde
114 // bestand -- of een checkout die nog nooit gestart is.
115 if (/no such table/i.test(e.message)) {
116 console.error(`De database op ${dbPad} heeft geen users-tabel.`);
117 console.error('Dat is een ander bestand dan je site, of een site die nooit gestart is.');
118 process.exit(1);
119 }
120 throw e;
[9e27d64]121}
122
123if (!user) {
124 console.error(arg ? `Geen user gevonden voor "${arg}".` : 'Geen god-user gevonden.');
[0f09302]125 console.error(`Gezocht in ${dbPad}.`);
[9e27d64]126 process.exit(1);
127}
128
129if (pwArg && pwArg.length < 8) {
130 console.error('Wachtwoord moet minstens 8 tekens zijn.');
131 process.exit(1);
132}
133
134const newPw = pwArg || crypto.randomBytes(9).toString('base64url');
135db.prepare(`
136 UPDATE users SET password_hash = ?, reset_token = NULL, reset_token_expires = NULL,
137 updated_at = CURRENT_TIMESTAMP WHERE id = ?
138`).run(bcrypt.hashSync(newPw, 10), user.id);
139
140console.log(`Wachtwoord gereset voor ${user.username} <${user.email}> (rol: ${user.role}).`);
[0f09302]141console.log(`Database: ${dbPad}`);
[9e27d64]142if (!pwArg) console.log(`Nieuw wachtwoord: ${newPw}`);
143console.log('Log nu in via /auth/login en wijzig het eventueel in je account.');
144process.exit(0);
Note: See TracBrowser for help on using the repository browser.