import { getApiUrl } from '../config/runtime-config'; export interface AccessTokenResponse { accessToken: string; } export interface MfaRequiredResponse { mfaRequired: true; mfaToken: string; } export type LoginResponse = AccessTokenResponse | MfaRequiredResponse; export function isMfaRequired(response: LoginResponse): response is MfaRequiredResponse { return 'mfaRequired' in response && response.mfaRequired; } export async function loginRequest(email: string, password: string): Promise { const apiUrl = await getApiUrl(); const response = await fetch(`${apiUrl}/auth/login`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); if (!response.ok) { throw new Error('Login fehlgeschlagen. Bitte E-Mail und Passwort prüfen.'); } return response.json() as Promise; } export async function verifyMfaRequest(mfaToken: string, code: string): Promise { const apiUrl = await getApiUrl(); const response = await fetch(`${apiUrl}/auth/mfa/verify`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mfaToken, code }), }); if (!response.ok) { throw new Error('Code ungültig. Bitte erneut versuchen.'); } return response.json() as Promise; } // The refresh token is single-use and rotates on every call. If two callers // (e.g. React StrictMode's dev double-invoke, or two components hitting a 401 // at once) both call refresh at the same time, the second one always fails // because the first already rotated the cookie. Deduplicating into one // shared in-flight request makes concurrent callers share the same result // instead of racing each other. let inFlightRefresh: Promise | null = null; async function performRefresh(): Promise { const apiUrl = await getApiUrl(); const response = await fetch(`${apiUrl}/auth/refresh`, { method: 'POST', credentials: 'include', }); if (!response.ok) { return null; } return response.json() as Promise; } export function refreshRequest(): Promise { if (!inFlightRefresh) { inFlightRefresh = performRefresh().finally(() => { inFlightRefresh = null; }); } return inFlightRefresh; } export async function ssoStatusRequest(): Promise { const apiUrl = await getApiUrl(); try { const response = await fetch(`${apiUrl}/auth/sso/status`); if (!response.ok) { return false; } const data = (await response.json()) as { enabled: boolean }; return data.enabled; } catch { return false; } } export async function ssoLoginUrl(): Promise { const apiUrl = await getApiUrl(); return `${apiUrl}/auth/sso/login`; } // The one-time completion code lives only in an httpOnly cookie the callback // set (never in the URL or a request body) - the browser sends it automatically. // Returns the same union as loginRequest: SSO establishes identity, not the // second factor, so a user with local MFA enabled still has to complete the // same challenge as a password login before tokens are issued. export async function ssoCompleteRequest(): Promise { const apiUrl = await getApiUrl(); const response = await fetch(`${apiUrl}/auth/sso/complete`, { method: 'POST', credentials: 'include', }); if (!response.ok) { throw new Error('SSO-Anmeldung fehlgeschlagen. Bitte erneut versuchen.'); } return response.json() as Promise; } export async function logoutRequest(): Promise { const apiUrl = await getApiUrl(); await fetch(`${apiUrl}/auth/logout`, { method: 'POST', credentials: 'include' }); } export interface CurrentUserProfile { id: string; email: string; name: string; roleKey: string; roleName: string; permissions: string[]; mfaEnabled: boolean; hasAvatar: boolean; } export async function getCurrentUserRequest(accessToken: string): Promise { const apiUrl = await getApiUrl(); const response = await fetch(`${apiUrl}/auth/me`, { credentials: 'include', headers: { Authorization: `Bearer ${accessToken}` }, }); if (!response.ok) { throw new Error('Profil konnte nicht geladen werden.'); } return response.json() as Promise; }