Changeset 0f09302 in Klonkt


Ignore:
Timestamp:
08/21/2026 08:00:05 AM (3 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
bb9e54c
Parents:
1294c36
Message:

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@…>

Files:
1 added
2 edited

Legend:

Unmodified
Added
Removed
  • deploy/MULTI-INSTANCE.md

    r1294c36 r0f09302  
    210210| `klonkt-update` | update the code, restart all instances |
    211211| `ls /var/lib/klonkt` | which instances exist |
     212| `cd /opt/klonkt && npm run reset-admin -- --instance <slug>` | reset a forgotten admin password |
     213
     214### Why the password reset needs `--instance`
     215
     216Every other command here names its instance, and this one is no different — but
     217the reason is easy to miss, because the app's own help page says only "run
     218`npm run reset-admin` from the project folder". That advice is written for the
     219one-instance layout, where the `.env` sits next to the code.
     220
     221Here it does not. The code is shared and read-only at `/opt/klonkt`; the
     222configuration lives at `/var/lib/klonkt/<slug>/.env`, and **systemd** reads it
     223through `EnvironmentFile=`. A script started by hand gets none of that: it looks
     224for an `.env` beside the code, finds nothing, and is left without
     225`DATABASE_PATH`. `--instance` reads the same file systemd does.
     226
     227Without it the script now stops and says which path it wanted, instead of
     228creating an empty database inside the shared code directory. If your data lives
     229somewhere other than `/var/lib/klonkt`, set `KLONKT_DATA_ROOT`.
  • scripts/reset-admin.mjs

    r1294c36 r0f09302  
    44//
    55// Usage:
    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
     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)
    910//
    10 // Run from the project root so DATABASE_PATH/.env is loaded correctly.
     11// Run from the project root so DATABASE_PATH/.env is loaded correctly, or name
     12// the instance with --instance.
    1113
    12 import 'dotenv/config';
     14import fs from 'fs';
     15import path from 'path';
    1316import crypto from 'crypto';
     17import { fileURLToPath } from 'url';
     18import dotenv from 'dotenv';
    1419import bcrypt from 'bcryptjs';
    15 import db from '../src/config/database.js';
    1620
    17 const arg = process.argv[2];
    18 const pwArg = process.argv[3];
     21const __dirname = path.dirname(fileURLToPath(import.meta.url));
     22const wortel = path.join(__dirname, '..');
     23
     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 });
     64} else {
     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');
    19101
    20102let user;
    21 if (arg) {
    22   user = db.prepare('SELECT * FROM users WHERE username = ? OR LOWER(email) = LOWER(?)').get(arg, arg);
    23 } else {
    24   // No arg: pick the admin (god or admin role), otherwise the very first user.
    25   user =
    26     db.prepare("SELECT * FROM users WHERE role IN ('god','admin') ORDER BY created_at LIMIT 1").get() ||
    27     db.prepare('SELECT * FROM users ORDER BY created_at LIMIT 1').get();
     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;
    28121}
    29122
    30123if (!user) {
    31124  console.error(arg ? `Geen user gevonden voor "${arg}".` : 'Geen god-user gevonden.');
     125  console.error(`Gezocht in ${dbPad}.`);
    32126  process.exit(1);
    33127}
     
    45139
    46140console.log(`Wachtwoord gereset voor ${user.username} <${user.email}> (rol: ${user.role}).`);
     141console.log(`Database: ${dbPad}`);
    47142if (!pwArg) console.log(`Nieuw wachtwoord: ${newPw}`);
    48143console.log('Log nu in via /auth/login en wijzig het eventueel in je account.');
Note: See TracChangeset for help on using the changeset viewer.