feat(auth): Story 1.5 — atomare, idempotente Migration Alt-Daten → Admin

This commit is contained in:
Kenearos 2026-07-07 22:34:30 +02:00
parent c994dbf3b5
commit 07f1bccff0
2 changed files with 69 additions and 1 deletions

View file

@ -117,9 +117,38 @@ function seedAdmin(adminEmail) {
return Number(existing.id);
}
/**
* Migriert die (globale) Single-User-Datenbasis auf Mehrbenutzer, indem alle
* Alt-Zeilen dem Admin zugeordnet werden. Atomar (eine Transaktion), idempotent
* (Guard über die documents.user_id-Spalte). Muss NACH seedAdmin laufen.
* @returns {boolean} true wenn migriert, false wenn bereits migriert (No-Op)
*/
function migrateToMultiUser(adminUserId) {
const cols = db.prepare("PRAGMA table_info('documents')").all().map(c => c.name);
if (cols.includes('user_id')) return false; // schon migriert
const tx = db.transaction(() => {
db.exec(`CREATE TABLE documents_v2 (
user_id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (user_id, key)
)`);
db.prepare('INSERT INTO documents_v2 (user_id, key, value, updated_at) SELECT ?, key, value, updated_at FROM documents')
.run(adminUserId);
db.exec('DROP TABLE documents');
db.exec('ALTER TABLE documents_v2 RENAME TO documents');
const hcols = db.prepare("PRAGMA table_info('history')").all().map(c => c.name);
if (!hcols.includes('user_id')) db.exec('ALTER TABLE history ADD COLUMN user_id INTEGER');
db.prepare('UPDATE history SET user_id = ? WHERE user_id IS NULL').run(adminUserId);
});
tx();
return true;
}
module.exports = {
normalizeEmail, hashToken, createLoginToken, consumeLoginToken,
createSession, validateSession, deleteSession, deleteUserSessions,
seedAdmin,
seedAdmin, migrateToMultiUser,
TOKEN_TTL_MIN, SESSION_TTL_DAYS, SESSION_IDLE_HOURS,
};

39
server/migrate.test.js Normal file
View file

@ -0,0 +1,39 @@
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'dp-migrate-'));
const { db } = require('./db');
const { migrateToMultiUser, seedAdmin } = require('./auth');
test('Migration ordnet Alt-Daten dem Admin zu, setzt neues PK-Schema, ist idempotent', () => {
// Alt-Schema (Single-User) erzwingen — unabhängig von der aktuellen db.js-Definition
db.exec("DROP TABLE IF EXISTS documents; CREATE TABLE documents (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL)");
db.exec("DROP TABLE IF EXISTS history; CREATE TABLE history (id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, value TEXT NOT NULL, replaced_at TEXT NOT NULL)");
const now = new Date().toISOString();
db.prepare('INSERT INTO documents (key,value,updated_at) VALUES (?,?,?)').run('employees', '["Max"]', now);
db.prepare('INSERT INTO documents (key,value,updated_at) VALUES (?,?,?)').run('duties', '{}', now);
db.prepare('INSERT INTO history (key,value,replaced_at) VALUES (?,?,?)').run('employees', '[]', now);
const adminId = seedAdmin('admin@x.de');
assert.strictEqual(migrateToMultiUser(adminId), true);
const cols = db.prepare("PRAGMA table_info('documents')").all().map(c => c.name);
assert.ok(cols.includes('user_id'), 'documents hat user_id');
const rows = db.prepare('SELECT user_id, key FROM documents ORDER BY key').all();
assert.deepStrictEqual(rows, [
{ user_id: adminId, key: 'duties' },
{ user_id: adminId, key: 'employees' },
]);
assert.strictEqual(db.prepare('SELECT user_id FROM history').get().user_id, adminId);
// PK ist (user_id,key): dieselbe Kombi doppelt schlägt fehl
assert.throws(() => db.prepare('INSERT INTO documents (user_id,key,value,updated_at) VALUES (?,?,?,?)')
.run(adminId, 'duties', '{}', now));
// Idempotent
assert.strictEqual(migrateToMultiUser(adminId), false);
});