From 07f1bccff06bf0378c21e2f9d6a5dcda43961e21 Mon Sep 17 00:00:00 2001 From: Kenearos Date: Tue, 7 Jul 2026 22:34:30 +0200 Subject: [PATCH] =?UTF-8?q?feat(auth):=20Story=201.5=20=E2=80=94=20atomare?= =?UTF-8?q?,=20idempotente=20Migration=20Alt-Daten=20=E2=86=92=20Admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/auth.js | 31 ++++++++++++++++++++++++++++++- server/migrate.test.js | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 server/migrate.test.js 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); +});