Files
vendoo/app/modules/auth/service.mjs
T
Masterluke77andGitHub 6f48827f4d Vendoo 1.41.2 – Git Ignore Boundary & Module Tracking Hotfix (#18)
* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix

* fix: track native source modules with root-anchored runtime ignores
2026-07-09 17:09:00 +02:00

101 lines
5.0 KiB
JavaScript

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() };
},
});
}