| [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] | 14 | import fs from 'fs';
|
|---|
| 15 | import path from 'path';
|
|---|
| [9e27d64] | 16 | import crypto from 'crypto';
|
|---|
| [0f09302] | 17 | import { fileURLToPath } from 'url';
|
|---|
| 18 | import dotenv from 'dotenv';
|
|---|
| [9e27d64] | 19 | import bcrypt from 'bcryptjs';
|
|---|
| 20 |
|
|---|
| [0f09302] | 21 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|---|
| 22 | const wortel = path.join(__dirname, '..');
|
|---|
| [9e27d64] | 23 |
|
|---|
| [0f09302] | 24 | // De argumenten: --instance mag overal staan, de rest is stelling-afhankelijk.
|
|---|
| 25 | const argv = process.argv.slice(2);
|
|---|
| 26 | let slug = null;
|
|---|
| 27 | const rest = [];
|
|---|
| 28 | for (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 | }
|
|---|
| 34 | const [arg, pwArg] = rest;
|
|---|
| 35 |
|
|---|
| 36 | if (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.
|
|---|
| 50 | const DATA_ROOT = process.env.KLONKT_DATA_ROOT || '/var/lib/klonkt';
|
|---|
| 51 | if (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 |
|
|---|
| 68 | function 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.
|
|---|
| 80 | const dbPad = process.env.DATABASE_PATH || path.join(wortel, 'storage/database.sqlite');
|
|---|
| 81 | if (!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 |
|
|---|
| 100 | const { default: db } = await import('../src/config/database.js');
|
|---|
| 101 |
|
|---|
| 102 | let user;
|
|---|
| 103 | try {
|
|---|
| 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 |
|
|---|
| 123 | if (!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 |
|
|---|
| 129 | if (pwArg && pwArg.length < 8) {
|
|---|
| 130 | console.error('Wachtwoord moet minstens 8 tekens zijn.');
|
|---|
| 131 | process.exit(1);
|
|---|
| 132 | }
|
|---|
| 133 |
|
|---|
| 134 | const newPw = pwArg || crypto.randomBytes(9).toString('base64url');
|
|---|
| 135 | db.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 |
|
|---|
| 140 | console.log(`Wachtwoord gereset voor ${user.username} <${user.email}> (rol: ${user.role}).`);
|
|---|
| [0f09302] | 141 | console.log(`Database: ${dbPad}`);
|
|---|
| [9e27d64] | 142 | if (!pwArg) console.log(`Nieuw wachtwoord: ${newPw}`);
|
|---|
| 143 | console.log('Log nu in via /auth/login en wijzig het eventueel in je account.');
|
|---|
| 144 | process.exit(0);
|
|---|