Fuegt users/login_tokens/sessions/audit_log Tabellen + FK-Indizes zu server/db.js hinzu (foreign_keys=ON fuer CASCADE), plus server/audit.js (append-only, keine PII). Bestehende documents/history-Tabellen und exports unveraendert. TDD: server/auth-schema.test.js zuerst rot, dann gruen; volle Suite (24 Tests) regressionsfrei.
77 lines
3.1 KiB
JavaScript
77 lines
3.1 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
// DATA_DIR auf Temp umbiegen, BEVOR db.js geladen wird
|
|
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'dp-auth-schema-'));
|
|
const { db } = require('./db');
|
|
const { audit } = require('./audit');
|
|
|
|
function tableNames() {
|
|
return db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((r) => r.name);
|
|
}
|
|
|
|
function indexNames() {
|
|
return db.prepare("SELECT name FROM sqlite_master WHERE type='index'").all().map((r) => r.name);
|
|
}
|
|
|
|
test('Auth-Tabellen existieren', () => {
|
|
const names = tableNames();
|
|
assert.ok(names.includes('users'), 'users fehlt');
|
|
assert.ok(names.includes('login_tokens'), 'login_tokens fehlt');
|
|
assert.ok(names.includes('sessions'), 'sessions fehlt');
|
|
assert.ok(names.includes('audit_log'), 'audit_log fehlt');
|
|
});
|
|
|
|
test('foreign_keys ist aktiv', () => {
|
|
assert.strictEqual(db.pragma('foreign_keys', { simple: true }), 1);
|
|
});
|
|
|
|
test('Indizes auf login_tokens.user_id und sessions.user_id existieren', () => {
|
|
const names = indexNames();
|
|
assert.ok(names.includes('idx_login_tokens_user'), 'idx_login_tokens_user fehlt');
|
|
assert.ok(names.includes('idx_sessions_user'), 'idx_sessions_user fehlt');
|
|
});
|
|
|
|
test('foreign_keys CASCADE: Nutzer löschen räumt tokens+sessions', () => {
|
|
const now = '2026-07-07T10:00:00.000Z';
|
|
const userId = db.prepare('INSERT INTO users (email, is_admin, created_at) VALUES (?, 0, ?)')
|
|
.run('cascade-test@example.com', now).lastInsertRowid;
|
|
db.prepare('INSERT INTO login_tokens (token_hash, user_id, expires_at) VALUES (?, ?, ?)')
|
|
.run('hash-cascade', userId, now);
|
|
db.prepare('INSERT INTO sessions (id_hash, user_id, expires_at, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?)')
|
|
.run('sess-cascade', userId, now, now, now);
|
|
|
|
db.prepare('DELETE FROM users WHERE id = ?').run(userId);
|
|
|
|
assert.strictEqual(db.prepare('SELECT * FROM login_tokens WHERE user_id = ?').get(userId), undefined);
|
|
assert.strictEqual(db.prepare('SELECT * FROM sessions WHERE user_id = ?').get(userId), undefined);
|
|
});
|
|
|
|
test('audit() schreibt append-only Zeile ohne PII', () => {
|
|
const before = db.prepare('SELECT COUNT(*) AS n FROM audit_log').get().n;
|
|
audit('login_ok', 1, 'abc');
|
|
const after = db.prepare('SELECT COUNT(*) AS n FROM audit_log').get().n;
|
|
assert.strictEqual(after, before + 1);
|
|
|
|
const row = db.prepare('SELECT * FROM audit_log ORDER BY id DESC LIMIT 1').get();
|
|
assert.strictEqual(row.event, 'login_ok');
|
|
assert.strictEqual(row.user_id, 1);
|
|
assert.strictEqual(row.ip_hash, 'abc');
|
|
assert.ok(row.ts, 'ts fehlt');
|
|
// keine PII-Spalten (email/name) vorhanden
|
|
assert.deepStrictEqual(
|
|
Object.keys(row).sort(),
|
|
['event', 'id', 'ip_hash', 'ts', 'user_id'].sort()
|
|
);
|
|
});
|
|
|
|
test('audit() ohne userId/ipHash (defaults null)', () => {
|
|
audit('server_start');
|
|
const row = db.prepare('SELECT * FROM audit_log ORDER BY id DESC LIMIT 1').get();
|
|
assert.strictEqual(row.event, 'server_start');
|
|
assert.strictEqual(row.user_id, null);
|
|
assert.strictEqual(row.ip_hash, null);
|
|
});
|