import crypto from 'crypto'; import { db } from '../db/index.js'; export const SUPERUSER_GROUP_NAME = 'superuser'; const selectGroupByName = db.prepare('SELECT * FROM user_groups WHERE name = ?'); const insertGroup = db.prepare('INSERT INTO user_groups (name, description) VALUES (?, ?)'); const selectSuperuser = db.prepare(` SELECT u.* FROM users u INNER JOIN user_group_memberships m ON m.user_id = u.id INNER JOIN user_groups g ON g.id = m.group_id WHERE g.name = ? LIMIT 1 `); const selectUserByUsername = db.prepare('SELECT * FROM users WHERE username = ?'); const selectUserByEmail = db.prepare('SELECT * FROM users WHERE email = ?'); const selectUserById = db.prepare('SELECT * FROM users WHERE id = ?'); const insertUser = db.prepare(` INSERT INTO users (username, email, password_hash, password_salt, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) `); const updateUser = db.prepare(` UPDATE users SET username = ?, email = ?, password_hash = ?, password_salt = ?, is_active = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); const insertMembership = db.prepare(` INSERT OR IGNORE INTO user_group_memberships (user_id, group_id, role) VALUES (?, ?, ?) `); const removeMembershipsForGroup = db.prepare(` DELETE FROM user_group_memberships WHERE group_id = ? `); const selectUserIdsForGroup = db.prepare(` SELECT u.id FROM users u INNER JOIN user_group_memberships m ON m.user_id = u.id WHERE m.group_id = ? `); const deleteUserById = db.prepare('DELETE FROM users WHERE id = ?'); const deleteGroupById = db.prepare('DELETE FROM user_groups WHERE id = ?'); const countSuperusers = db.prepare(` SELECT COUNT(*) as count FROM user_group_memberships m INNER JOIN user_groups g ON g.id = m.group_id WHERE g.name = ? `); const creationTransaction = db.transaction(({ username, email, passwordHash, passwordSalt, groupId }) => { const existingUser = selectUserByUsername.get(username) || selectUserByEmail.get(email); let userId; if (existingUser) { updateUser.run(username, email, passwordHash, passwordSalt, existingUser.id); userId = existingUser.id; } else { const result = insertUser.run(username, email, passwordHash, passwordSalt); userId = result.lastInsertRowid; } removeMembershipsForGroup.run(groupId); insertMembership.run(userId, groupId, SUPERUSER_GROUP_NAME); return selectUserById.get(userId); }); function hashPassword(password) { if (!password || typeof password !== 'string') { throw new Error('INVALID_PASSWORD'); } const trimmed = password.trim(); if (trimmed.length < 8) { throw new Error('PASSWORD_TOO_SHORT'); } const salt = crypto.randomBytes(16).toString('hex'); const hash = crypto.pbkdf2Sync(trimmed, salt, 120000, 64, 'sha512').toString('hex'); return { salt, hash }; } function ensureGroup(name, description = null) { let group = selectGroupByName.get(name); if (group) return group; insertGroup.run(name, description); group = selectGroupByName.get(name); return group; } export function hasSuperuser() { const { count } = countSuperusers.get(SUPERUSER_GROUP_NAME); return count > 0; } function createOrUpdateSuperuser({ username, email, password }) { const normalizedUsername = String(username || '').trim(); const normalizedEmail = String(email || '').trim().toLowerCase(); if (!normalizedUsername) { throw new Error('USERNAME_REQUIRED'); } if (!normalizedEmail || !normalizedEmail.includes('@')) { throw new Error('EMAIL_INVALID'); } const { hash, salt } = hashPassword(password); const group = ensureGroup(SUPERUSER_GROUP_NAME, 'System Superuser'); const user = creationTransaction({ username: normalizedUsername, email: normalizedEmail, passwordHash: hash, passwordSalt: salt, groupId: group.id }); return { id: user.id, username: user.username, email: user.email }; } const deletionTransaction = db.transaction((groupId) => { const members = selectUserIdsForGroup.all(groupId); members.forEach(({ id }) => { deleteUserById.run(id); }); const { changes: groupChanges } = deleteGroupById.run(groupId); return { usersRemoved: members.length, groupRemoved: groupChanges > 0 }; }); export function ensureSuperuserFromEnv() { const username = process.env.SUPERUSER_USERNAME; const email = process.env.SUPERUSER_EMAIL; const password = process.env.SUPERUSER_PASSWORD; if (hasSuperuser()) { if (username || email || password) { console.log('ℹ️ Superuser existiert bereits - Umgebungsvariablen werden ignoriert.'); } return false; } if (!username || !email || !password) { return false; } try { createOrUpdateSuperuser({ username, email, password }); console.log('✅ Superuser aus Umgebungsvariablen angelegt.'); return true; } catch (error) { console.error('⚠️ Superuser konnte nicht aus Umgebungsvariablen erzeugt werden:', error.message); return false; } } export function registerSuperuser({ username, email, password }) { if (hasSuperuser()) { const err = new Error('SUPERUSER_ALREADY_EXISTS'); err.code = 'SUPERUSER_ALREADY_EXISTS'; throw err; } try { return createOrUpdateSuperuser({ username, email, password }); } catch (error) { if (error.message === 'USERNAME_REQUIRED' || error.message === 'EMAIL_INVALID' || error.message === 'PASSWORD_TOO_SHORT') { error.code = error.message; } throw error; } } export function removeSuperuser() { const group = selectGroupByName.get(SUPERUSER_GROUP_NAME); if (!group) { const error = new Error('SUPERUSER_NOT_FOUND'); error.code = 'SUPERUSER_NOT_FOUND'; throw error; } return deletionTransaction(group.id); } export function getSuperuserSummary() { const user = selectSuperuser.get(SUPERUSER_GROUP_NAME); if (!user) return null; return { id: user.id, username: user.username, email: user.email }; }