fix(auth): reset-admin.js ends the account's sessions and matches its email in any case

Resetting a locked-out or compromised admin left every session on that
account valid, because the script did not move password_version on the
way every password change in the app does. It also looked the address
up case-sensitively and created a second account when the stored one
had capitals.
This commit is contained in:
Maurice
2026-10-07 16:36:39 +02:00
parent 8923918eb4
commit 8c3fad5e82
2 changed files with 25 additions and 3 deletions
+6 -3
View File
@@ -58,11 +58,14 @@ db.exec(`PRAGMA journal_mode = ${journalMode}`);
db.exec(`PRAGMA synchronous = ${synchronous}`);
const hash = bcrypt.hashSync(password, BCRYPT_COST);
const existing = db.prepare('SELECT id FROM users WHERE email = ?').get(email);
// Case-insensitive like login, so Admin@example.com is found as admin@example.com.
const existing = db.prepare('SELECT id FROM users WHERE LOWER(email) = LOWER(?)').get(email);
if (existing) {
db.prepare('UPDATE users SET password_hash = ?, role = ?, must_change_password = 1 WHERE email = ?')
.run(hash, 'admin', email);
// password_version moves on, as on every password change in the app: it is
// what ends the sessions still signed in to an account that needed resetting.
db.prepare('UPDATE users SET password_hash = ?, role = ?, must_change_password = 1, password_version = COALESCE(password_version, 0) + 1 WHERE id = ?')
.run(hash, 'admin', existing.id);
console.log(`\n✓ Admin password reset: ${email}`);
} else {
// 'admin' is usually taken by the first-run seed — pick the first free username
+19
View File
@@ -184,6 +184,7 @@ describe('standalone scripts honour the same configuration', () => {
password_hash TEXT,
role TEXT,
must_change_password INTEGER DEFAULT 0,
password_version INTEGER NOT NULL DEFAULT 0,
mfa_secret TEXT,
maps_api_key TEXT,
openweather_api_key TEXT,
@@ -226,6 +227,24 @@ describe('standalone scripts honour the same configuration', () => {
expect(admin?.role).toBe('admin');
}, 60000);
it('reset-admin.js finds the account whatever the case, and ends its sessions', () => {
seedWalDb();
const seed = new Database(dbPath);
seed.prepare("INSERT INTO users (username, email, password_hash, role, password_version) VALUES ('boss', 'Boss@Example.com', 'x', 'user', 3)").run();
seed.close();
execFileSync(process.execPath, ['reset-admin.js'], {
cwd: SERVER_ROOT,
env: { ...process.env, TREK_DB_FILE: dbPath, RESET_ADMIN_EMAIL: 'boss@example.com', RESET_ADMIN_PASSWORD: 'Recovery12345!' },
stdio: 'pipe',
});
const db = new Database(dbPath);
const rows = db.prepare('SELECT email, role, password_version, must_change_password FROM users').all();
db.close();
expect(rows).toEqual([{ email: 'Boss@Example.com', role: 'admin', password_version: 4, must_change_password: 1 }]);
}, 60000);
it('migrate-encryption.ts no longer forces the database back to WAL', () => {
seedWalDb();