v0.72.0: fix admin login — re-seed scrypt digest computed by verifyPassword() call
CI / build + unit + e2e (pull_request) Successful in 14m7s
CI / build + unit + e2e (pull_request) Successful in 14m7s
This commit is contained in:
+36
-1
@@ -223,7 +223,10 @@ describe('openGameDb', () => {
|
||||
});
|
||||
|
||||
// v0.71.0: the seeded admin row — hash-only, idempotent across every open().
|
||||
const ADMIN_HASH = '8dbed630b49a15073e8be571367c1858$0e5207dae2e3c813a7172a5f610fd47a90e8ff17513180ab1e0a7779c3766ef0';
|
||||
const ADMIN_HASH = '339149a14282ff3a2ed94534f9722aa4$abaf94cf073c5aafc892b4a39dce97aaf53ab6f7c662a588815bda1bc1771caf';
|
||||
// The v0.71.0 digest — computed with different scrypt parameters than verifyPassword(),
|
||||
// so it never verified; production DBs seeded by that version carry exactly this value.
|
||||
const LEGACY_HASH = '8dbed630b49a15073e8be571367c1858$0e5207dae2e3c813a7172a5f610fd47a90e8ff17513180ab1e0a7779c3766ef0';
|
||||
|
||||
it('seeds exactly one admin user on first open and never duplicates on reopen', async () => {
|
||||
const file = path.join(dir, 'admin-seed.db');
|
||||
@@ -258,6 +261,38 @@ describe('openGameDb', () => {
|
||||
expect((await inst.db.select().from(schema.account))[0]?.cash).toBe(20000);
|
||||
});
|
||||
|
||||
it('rotates a v0.71.0 legacy admin digest on open (idempotent)', async () => {
|
||||
// A file exactly as v0.71.0 left production: an admin row carrying the old, incompatible digest.
|
||||
const SQL = await initSqlJs();
|
||||
const raw = new SQL.Database();
|
||||
raw.exec(`CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
admin INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL
|
||||
)`);
|
||||
raw.run('INSERT INTO users VALUES (?, ?, ?, 1, ?)', [
|
||||
'admin-kamilkrzywda',
|
||||
'kamilkrzywda',
|
||||
LEGACY_HASH,
|
||||
Date.now(),
|
||||
]);
|
||||
const file = path.join(dir, 'admin-rotate.db');
|
||||
await fs.writeFile(file, Buffer.from(raw.export()));
|
||||
|
||||
// First open heals the row to the current digest…
|
||||
const first = await openGameDb(file);
|
||||
let rows = first.raw.exec("SELECT password_hash FROM users WHERE id = 'admin-kamilkrzywda'");
|
||||
expect(rows[0].values).toHaveLength(1);
|
||||
expect(rows[0].values[0][0]).toBe(ADMIN_HASH);
|
||||
|
||||
// …and a second open is a no-op (idempotent rotation).
|
||||
const second = await openGameDb(file);
|
||||
rows = second.raw.exec("SELECT password_hash FROM users WHERE id = 'admin-kamilkrzywda'");
|
||||
expect(rows[0].values[0][0]).toBe(ADMIN_HASH);
|
||||
});
|
||||
|
||||
it('does not re-run the v0.12 plane reset on later opens (migration is one-shot)', async () => {
|
||||
const file = path.join(dir, 'pre-v12-reopen.db');
|
||||
const SQL = await initSqlJs();
|
||||
|
||||
+11
-1
@@ -79,7 +79,10 @@ CREATE TABLE IF NOT EXISTS sessions (
|
||||
|
||||
// v0.71.0: the seeded admin's scrypt hash (salt$hex). The plaintext exists only in the
|
||||
// operator's head — never in code, tests or comments; login verifies against this string.
|
||||
const ADMIN_SEED_HASH = '8dbed630b49a15073e8be571367c1858$0e5207dae2e3c813a7172a5f610fd47a90e8ff17513180ab1e0a7779c3766ef0';
|
||||
// v0.72.0: recomputed with exactly the same call as verifyPassword() — salt passed as a
|
||||
// plain string, scryptSync(pw, saltHex, 32) — because the v0.71.0 digest used different
|
||||
// parameters and real logins failed with 401.
|
||||
const ADMIN_SEED_HASH = '339149a14282ff3a2ed94534f9722aa4$abaf94cf073c5aafc892b4a39dce97aaf53ab6f7c662a588815bda1bc1771caf';
|
||||
|
||||
// v0.71.0: idempotent admin seed — runs on every open(), no-ops when the row exists, so
|
||||
// pre-existing production DBs gain exactly one admin row and never a duplicate.
|
||||
@@ -90,6 +93,13 @@ function seedAdminUser(raw: RawSqlJsDatabase): void {
|
||||
WHERE NOT EXISTS (SELECT 1 FROM users WHERE username = 'kamilkrzywda')`,
|
||||
[ADMIN_SEED_HASH, Date.now()],
|
||||
);
|
||||
// v0.72.0: v0.71.0 seeded an incompatible digest into production DBs and the INSERT above
|
||||
// can't overwrite them (WHERE NOT EXISTS), so rotate any legacy row to the current digest
|
||||
// at boot — a no-op once healed, durable on the next persist().
|
||||
raw.run(
|
||||
`UPDATE users SET password_hash = ? WHERE id = 'admin-kamilkrzywda' AND password_hash != ?`,
|
||||
[ADMIN_SEED_HASH, ADMIN_SEED_HASH],
|
||||
);
|
||||
}
|
||||
|
||||
// v0.7.0: DBs created by earlier versions lack the new columns; CREATE TABLE IF
|
||||
|
||||
Reference in New Issue
Block a user