import crypto from 'crypto'; import { db } from '../db/index.js'; export const SUPERUSER_GROUP_NAME = 'superuser'; export const AVATAR_COLORS = [ 'bg-arcticBlue-600', 'bg-copperRust-500', 'bg-sunsetCoral-600', 'bg-mintTea-400', 'bg-lavenderSmoke-500', 'bg-emeraldMist-500', 'bg-roseQuartz-500', 'bg-auroraTeal-500', 'bg-citrusPunch-500', 'bg-mossGreen-400' ]; export const DEFAULT_AVATAR_COLOR = 'bg-mossGreen-500'; const AVATAR_COLOR_SET = new Set([...AVATAR_COLORS, DEFAULT_AVATAR_COLOR]); export const pickRandomAvatarColor = () => { if (!Array.isArray(AVATAR_COLORS) || AVATAR_COLORS.length === 0) { return DEFAULT_AVATAR_COLOR; } const index = Math.floor(Math.random() * AVATAR_COLORS.length); return AVATAR_COLORS[index] ?? DEFAULT_AVATAR_COLOR; }; export const normalizeAvatarColor = (value) => { if (!value) return null; const candidate = String(value).trim(); return AVATAR_COLOR_SET.has(candidate) ? candidate : null; }; 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 updateLastLogin = db.prepare(` UPDATE users SET last_login = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); const insertUser = db.prepare(` INSERT INTO users (username, email, password_hash, password_salt, avatar_color, 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, avatar_color = COALESCE(?, avatar_color), updated_at = CURRENT_TIMESTAMP WHERE id = ? `); const insertMembership = db.prepare(` INSERT OR IGNORE INTO user_group_memberships (user_id, group_id) 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, avatarColor }) => { const existingUser = selectUserByUsername.get(username) || selectUserByEmail.get(email); let userId; if (existingUser) { const existingColor = normalizeAvatarColor(existingUser.avatar_color); const normalizedNewColor = normalizeAvatarColor(avatarColor); const colorToPersist = existingColor || normalizedNewColor || DEFAULT_AVATAR_COLOR; updateUser.run(username, email, passwordHash, passwordSalt, colorToPersist, existingUser.id); userId = existingUser.id; } else { const colorToPersist = normalizeAvatarColor(avatarColor) || DEFAULT_AVATAR_COLOR; const result = insertUser.run(username, email, passwordHash, passwordSalt, colorToPersist); userId = result.lastInsertRowid; } removeMembershipsForGroup.run(groupId); insertMembership.run(userId, groupId); return selectUserById.get(userId); }); export function hashPassword(password) { if (!password || typeof password !== 'string') { const err = new Error('INVALID_PASSWORD'); err.code = 'INVALID_PASSWORD'; throw err; } const trimmed = password.trim(); if (!trimmed) { const err = new Error('INVALID_PASSWORD'); err.code = 'INVALID_PASSWORD'; throw err; } if (trimmed.length < 8) { const err = new Error('PASSWORD_TOO_SHORT'); err.code = 'PASSWORD_TOO_SHORT'; throw err; } const salt = crypto.randomBytes(16).toString('hex'); const hash = crypto.pbkdf2Sync(trimmed, salt, 120000, 64, 'sha512').toString('hex'); return { salt, hash }; } export function verifyPassword(password, hash, salt) { if (!hash || !salt) return false; if (!password || typeof password !== 'string') return false; const trimmed = password.trim(); if (!trimmed) return false; const derived = crypto.pbkdf2Sync(trimmed, salt, 120000, 64, 'sha512').toString('hex'); const storedBuffer = Buffer.from(hash, 'hex'); const derivedBuffer = Buffer.from(derived, 'hex'); if (storedBuffer.length !== derivedBuffer.length) { return false; } return crypto.timingSafeEqual(storedBuffer, derivedBuffer); } 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; } export function findUserByIdentifier(identifier) { const value = String(identifier || '').trim(); if (!value) return null; if (value.includes('@')) { return selectUserByEmail.get(value.toLowerCase()); } return selectUserByUsername.get(value); } export function findUserById(id) { if (!id) return null; return selectUserById.get(id); } export function markUserLogin(userId) { if (!userId) return; updateLastLogin.run(userId); } 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 avatarColor = pickRandomAvatarColor(); const user = creationTransaction({ username: normalizedUsername, email: normalizedEmail, passwordHash: hash, passwordSalt: salt, groupId: group.id, avatarColor }); const persistedColor = normalizeAvatarColor(user.avatar_color) || avatarColor || DEFAULT_AVATAR_COLOR; return { id: user.id, username: user.username, email: user.email, avatarColor: persistedColor }; } 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 }; }