import { auditLog, checkRateLimit, clearFailedLogins, createAuthToken, createSession, createUser, generateCsrfToken, getUserByEmail, isSetupMode, isUserLocked, recordLogin, registerFailedLogin, setPassword, updateUser, validateAuthToken, validatePasswordStrength, verifyPassword, validateSession, destroySession, } from '../../core/identity/identity-store.mjs'; import { PlatformError } from '../../kernel/errors.mjs'; function fail(message, status = 400, code = 'AUTH_FAILED', details = null) { throw new PlatformError(message, { status, code, expose: true, details }); } export function createAuthService({ setupTokenMatches }) { return Object.freeze({ setupStatus({ mailConfigured, setupTokenRequired }) { return { setupMode: isSetupMode(), mailConfigured: Boolean(mailConfigured), setupTokenRequired: Boolean(setupTokenRequired) }; }, setup({ email, name, password, setupToken, ip, userAgent }) { if (!isSetupMode()) fail('Setup bereits abgeschlossen', 400, 'SETUP_COMPLETED'); if (!setupTokenMatches(setupToken)) fail('Ungültiger Einrichtungs-Code.', 403, 'SETUP_TOKEN_INVALID'); const passwordCheck = validatePasswordStrength(password); if (!passwordCheck.valid) fail(`Passwort benötigt ${passwordCheck.problems.join(', ')}.`, 400, 'PASSWORD_WEAK'); try { const user = createUser(email, name || email.split('@')[0], 'admin', null, password); auditLog(user.id, email, 'user.created', 'user', String(user.id), 'Erster Admin (Setup)', ip); const session = createSession(user.id, ip, userAgent); recordLogin(user.id, ip, userAgent, true); auditLog(user.id, email, 'login.success', null, null, 'via setup', ip); return { user, session, csrf: generateCsrfToken() }; } catch (error) { fail(error?.message || 'Setup fehlgeschlagen.', 500, 'SETUP_FAILED'); } }, login({ email, password, ip, userAgent }) { const rl = checkRateLimit(`login:${ip}`, 5, 15 * 60 * 1000); if (!rl.allowed) fail(`Zu viele Versuche. Bitte warte ${rl.retryAfter} Sekunden.`, 429, 'LOGIN_RATE_LIMIT', { retry_after: rl.retryAfter }); const user = getUserByEmail(email); if (user && isUserLocked(user)) { auditLog(user.id, email, 'login.blocked_locked', null, null, user.locked_until, ip); fail('Account nach mehreren Fehlversuchen vorübergehend gesperrt.', 423, 'ACCOUNT_LOCKED', { locked_until: user.locked_until }); } if (!user || !verifyPassword(password, user.password_hash)) { if (user) { recordLogin(user.id, ip, userAgent, false); const lockResult = registerFailedLogin(user.id); if (lockResult?.locked) { auditLog(user.id, email, 'login.account_locked', 'user', String(user.id), lockResult.lockedUntil, ip); fail('Zu viele Fehlversuche. Account für 15 Minuten gesperrt.', 423, 'ACCOUNT_LOCKED', { locked_until: lockResult.lockedUntil }); } } auditLog(user?.id || null, email, 'login.failed', null, null, null, ip); fail('E-Mail oder Passwort falsch', 401, 'LOGIN_INVALID'); } if (!user.active) fail('Account deaktiviert', 403, 'ACCOUNT_DISABLED'); clearFailedLogins(user.id); const session = createSession(user.id, ip, userAgent); recordLogin(user.id, ip, userAgent, true); auditLog(user.id, email, 'login.success', null, null, 'via password', ip); return { user, session, csrf: generateCsrfToken() }; }, createInvite({ email, role, createdBy }) { return createAuthToken(email, 'invite', role || 'editor', createdBy); }, acceptInvite({ token, password, name, ip, userAgent }) { const passwordCheck = validatePasswordStrength(password); if (!passwordCheck.valid) fail(`Passwort benötigt ${passwordCheck.problems.join(', ')}.`, 400, 'PASSWORD_WEAK'); const authToken = validateAuthToken(token); if (!authToken) fail('Link ungültig oder abgelaufen', 400, 'INVITE_INVALID'); let user = getUserByEmail(authToken.email); if (!user) { user = createUser(authToken.email, name || authToken.email.split('@')[0], authToken.role, authToken.created_by, password); auditLog(user.id, user.email, 'user.activated', 'user', String(user.id), null, ip); } else { setPassword(user.id, password); if (name) user = updateUser(user.id, { name }); } if (!user.active) fail('Account deaktiviert', 403, 'ACCOUNT_DISABLED'); const session = createSession(user.id, ip, userAgent); recordLogin(user.id, ip, userAgent, true); auditLog(user.id, user.email, 'login.success', null, null, 'via invite', ip); return { user, session, csrf: generateCsrfToken() }; }, logout({ token, ip }) { if (!token) return { ok: true }; const session = validateSession(token); if (session) auditLog(session.user_id, session.email, 'logout', null, null, null, ip); destroySession(token); return { ok: true }; }, health() { return { ok: true, setup_mode: isSetupMode() }; }, }); }