import type { Request, Response } from "express"; import { changePasswordSchema, loginSchema } from "../../shared/validation/auth.schemas.js"; import { InvalidCredentialsError, PasswordUnchangedError, WrongPasswordError, } from "../../domain/services/user.service.js"; import { authConfig, loginThrottle, sessionStore, userService } from "../composition/auth.js"; import { cookieOptions, identityFromRequest, sessionIdFromRequest, setSessionCookie, } from "../auth/auth.middleware.js"; import { SESSION_COOKIE } from "../auth/auth-config.js"; import { userRepository } from "../composition/application-repositories.js"; export async function getAuthStatus(req: Request, res: Response) { const identity = await identityFromRequest(req, sessionStore, userRepository); const sessionId = sessionIdFromRequest(req); if (identity && sessionId) setSessionCookie(res, authConfig, sessionStore, sessionId); res.json({ signedIn: Boolean(identity), mustChangePassword: identity?.mustChangePassword ?? false, }); } export async function login(req: Request, res: Response) { const parsed = loginSchema.safeParse(req.body); if (!parsed.success) { res.status(401).json({ error: "Anmeldung fehlgeschlagen." }); return; } const { username, password } = parsed.data; const retryAfterMs = loginThrottle.retryAfterMs(username); if (retryAfterMs > 0) { res.setHeader("Retry-After", Math.ceil(retryAfterMs / 1000).toString()); res.status(429).json({ error: "Zu viele Fehlversuche. Bitte in einigen Minuten erneut versuchen.", }); return; } try { const user = await userService.authenticate(username, password); loginThrottle.recordSuccess(username); // A fresh session id on every login: an id planted before login // (session fixation) never becomes authenticated. sessionStore.destroy(sessionIdFromRequest(req)); const session = sessionStore.create(user.id); setSessionCookie(res, authConfig, sessionStore, session.id); res.json({ signedIn: true, mustChangePassword: user.mustChangePassword }); } catch (error) { if (error instanceof InvalidCredentialsError) { loginThrottle.recordFailure(username); res.status(401).json({ error: error.message }); return; } throw error; } } export function getMe(req: Request, res: Response) { const identity = req.authUser; if (!identity) { res.status(401).json({ error: "Nicht angemeldet." }); return; } res.json(identity); } export async function changePassword(req: Request, res: Response) { const identity = req.authUser; if (!identity) { res.status(401).json({ error: "Nicht angemeldet." }); return; } const parsed = changePasswordSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ error: "Das neue Passwort muss 8 bis 200 Zeichen lang sein." }); return; } try { await userService.changeOwnPassword( identity.id, parsed.data.currentPassword, parsed.data.newPassword ); } catch (error) { if (error instanceof WrongPasswordError || error instanceof PasswordUnchangedError) { res.status(400).json({ error: error.message }); return; } throw error; } // Every session of this account ends - including the current one, which is // replaced by a fresh id so the user stays signed in on this device only. sessionStore.destroyAllForUser(identity.id); const session = sessionStore.create(identity.id); setSessionCookie(res, authConfig, sessionStore, session.id); res.json({ changed: true }); } export function logout(req: Request, res: Response) { sessionStore.destroy(sessionIdFromRequest(req)); res.clearCookie(SESSION_COOKIE, { ...cookieOptions(authConfig, 0), maxAge: undefined }); res.json({ signedOut: true }); }