diff --git a/server/auth.js b/server/auth.js index 2596ce1..3d8739b 100644 --- a/server/auth.js +++ b/server/auth.js @@ -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, }; diff --git a/server/migrate.test.js b/server/migrate.test.js new file mode 100644 index 0000000..ab860ef --- /dev/null +++ b/server/migrate.test.js @@ -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); +});