import { randomBytes } from 'node:crypto'; import { auditLog, createAuthToken, createUser, deleteUser, getLoginHistory, getUser, getUserByEmail, getUsers, setPassword, updateUser, validatePasswordStrength, listInvitations, getInvitation, revokeInvitation, } from '../../core/identity/identity-store.mjs'; import { permissionsForRole, getRoleDefinitions } from '../../../lib/security.mjs'; import { PlatformError } from '../../kernel/errors.mjs'; const ROLES = new Set(['admin', 'manager', 'editor', 'publisher', 'viewer']); function fail(message, status = 400, code = 'USER_OPERATION_FAILED') { throw new PlatformError(message, { status, code, expose: true }); } function publicUser(user) { if (!user) return null; const { password_hash: _passwordHash, ...safe } = user; return safe; } export function createUsersService() { return Object.freeze({ list() { return getUsers().map(publicUser); }, find(id) { return publicUser(getUser(id)); }, findByEmail(email) { return publicUser(getUserByEmail(email)); }, currentIdentity(userId, csrf) { const user = getUser(userId); if (!user) fail('Benutzer nicht gefunden.', 404, 'USER_NOT_FOUND'); const initials = (user.name || user.email).split(/[\s@]/).map(word => word[0]?.toUpperCase()).filter(Boolean).slice(0, 2).join(''); return { id: user.id, email: user.email, name: user.name, role: user.role, avatar_color: user.avatar_color, initials, permissions: [...permissionsForRole(user.role)], role_definition: getRoleDefinitions().find(role => role.key === user.role) || null, csrf, }; }, updateProfile(userId, data) { const updated = updateUser(userId, { name: data.name }); if (!updated) fail('Benutzer nicht gefunden.', 404, 'USER_NOT_FOUND'); return publicUser(updated); }, prepareInvite({ email, name, role, method, createdBy }) { const normalizedRole = ROLES.has(role) ? role : 'viewer'; if (getUserByEmail(email)) fail('User existiert bereits', 400, 'USER_EXISTS'); if (method === 'auto_password') { const password = `Vd!${randomBytes(9).toString('base64url')}9a`; const user = createUser(email, name || email.split('@')[0], normalizedRole, createdBy, password); return { mode: 'auto_password', user: publicUser(user), password, role: normalizedRole }; } const invite = createAuthToken(email, 'invite', normalizedRole, createdBy); return { mode: 'magic_link', invite, email, name: name || '', role: normalizedRole }; }, resendInvite({ userId, createdBy }) { const user = getUser(userId); if (!user) fail('User nicht gefunden', 404, 'USER_NOT_FOUND'); return { user: publicUser(user), invite: createAuthToken(user.email, 'invite', user.role, createdBy) }; }, invitations(limit = 100) { return listInvitations(limit); }, resendPendingInvitation({ invitationId, createdBy }) { const invitation = getInvitation(invitationId); if (!invitation) fail('Einladung nicht gefunden', 404, 'INVITATION_NOT_FOUND'); if (invitation.status !== 'pending' && invitation.status !== 'expired') fail('Diese Einladung kann nicht erneut versendet werden.', 409, 'INVITATION_NOT_RESENDABLE'); const role = ROLES.has(invitation.role) ? invitation.role : 'viewer'; return { invitation, invite: createAuthToken(invitation.email, 'invite', role, createdBy), role }; }, revokePendingInvitation(invitationId) { const invitation = revokeInvitation(invitationId); if (!invitation) fail('Einladung nicht gefunden', 404, 'INVITATION_NOT_FOUND'); return invitation; }, updateManagedUser({ actor, userId, patch }) { const existing = getUser(userId); if (!existing) fail('User nicht gefunden', 404, 'USER_NOT_FOUND'); if (Number(actor.id) === Number(userId) && patch.role && patch.role !== 'admin') fail('Du kannst dir selbst nicht die Admin-Rolle entziehen'); const activeAdmins = getUsers().filter(user => user.role === 'admin' && user.active).length; const removesLastAdmin = existing.role === 'admin' && existing.active && activeAdmins <= 1 && ((patch.role && patch.role !== 'admin') || Number(patch.active) === 0); if (removesLastAdmin) fail('Der letzte aktive Administrator darf nicht deaktiviert oder herabgestuft werden.'); const normalized = {}; if (patch.name !== undefined) normalized.name = patch.name; if (patch.role !== undefined) normalized.role = ROLES.has(patch.role) ? patch.role : 'viewer'; if (patch.active !== undefined) normalized.active = Number(patch.active) === 0 ? 0 : 1; if (patch.avatar_color !== undefined) normalized.avatar_color = patch.avatar_color; return publicUser(updateUser(userId, normalized)); }, deleteManagedUser({ actor, userId }) { if (Number(actor.id) === Number(userId)) fail('Du kannst dich nicht selbst löschen'); const user = getUser(userId); if (!user) fail('User nicht gefunden', 404, 'USER_NOT_FOUND'); if (user.role === 'admin' && user.active && getUsers().filter(entry => entry.role === 'admin' && entry.active).length <= 1) { fail('Der letzte aktive Administrator darf nicht gelöscht werden.'); } deleteUser(userId); return publicUser(user); }, resetPassword({ userId, password }) { const check = validatePasswordStrength(password); if (!check.valid) fail(`Passwort benötigt ${check.problems.join(', ')}.`, 400, 'PASSWORD_WEAK'); const user = getUser(userId); if (!user) fail('User nicht gefunden', 404, 'USER_NOT_FOUND'); setPassword(userId, password); return publicUser(user); }, loginHistory(userId, limit = 30) { return getLoginHistory(userId, limit); }, health() { return { ok: true, users: getUsers().length }; }, auditLog, }); }