* docs: prepare Vendoo 1.41.2 git-ignore boundary hotfix * fix: track native source modules with root-anchored runtime ignores
101 lines
5.0 KiB
JavaScript
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() };
|
|
},
|
|
});
|
|
}
|