import { BASE_URL } from './config'; import { currentToken } from './clients'; import { AUTH_CREDENTIALS } from './session'; import { passwordPolicyMessage, type PasswordPolicy } from '../utils/passwordPolicy'; export class AuthApiError extends Error { status: number; code?: string; violations?: string[]; constructor(message: string, status: number, code?: string, violations?: string[]) { super(message); this.name = 'AuthApiError'; this.status = status; this.code = code; this.violations = violations; } } /** * GET /usr/password-policy — public endpoint to fetch the app-user password rules. */ export async function fetchPasswordPolicy(): Promise { const res = await fetch(`${BASE_URL}/usr/password-policy`); const body = await res.json().catch(() => ({})); if (!res.ok) { throw new AuthApiError(body.error || `Failed to load password policy: ${res.status}`, res.status); } return body; } /** * POST /usr/forgot-password — request a one-time 6-digit reset code to email. * The backend ALWAYS responds 200 with a neutral message to prevent account enumeration. */ export async function forgotPassword(email: string): Promise<{ message: string }> { const res = await fetch(`${BASE_URL}/usr/forgot-password`, { method: 'POST', credentials: AUTH_CREDENTIALS, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email.trim() }), }); const body = await res.json().catch(() => ({})); if (!res.ok) { throw new AuthApiError(body.error || `Failed to request password reset: ${res.status}`, res.status); } return body; } /** * POST /usr/verify-reset-code — exchange the emailed OTP code for a short-lived reset token (JWT). */ export async function verifyResetCode(email: string, code: string): Promise<{ reset_token: string }> { const res = await fetch(`${BASE_URL}/usr/verify-reset-code`, { method: 'POST', credentials: AUTH_CREDENTIALS, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email.trim(), code: code.trim() }), }); const body = await res.json().catch(() => ({})); if (!res.ok || !body?.reset_token) { throw new AuthApiError(body.error || 'The code is invalid or has expired.', res.status); } return body; } /** * POST /usr/reset-password — set the new password using the reset token from verify-reset-code. */ export async function resetPassword(resetToken: string, newPassword: string): Promise<{ message: string }> { const res = await fetch(`${BASE_URL}/usr/reset-password`, { method: 'POST', credentials: AUTH_CREDENTIALS, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reset_token: resetToken, new_password: newPassword }), }); const body = await res.json().catch(() => ({})); if (!res.ok) { const msg = passwordPolicyMessage(body) || body.message || `Failed to reset password: ${res.status}`; throw new AuthApiError(msg, res.status, body?.code, body?.violations); } return body; } /** * POST /usr/me/password — authenticated self-service password change. * Wrong current password returns 403 (inline error, not 401). * On 200 success, the backend bumps password_changed_at and invalidates the session JWT. */ export async function changePassword( currentPassword: string, newPassword: string, token?: string | null ): Promise<{ message: string }> { const authToken = token ?? currentToken(); if (!authToken) { throw new AuthApiError('You must be signed in to change your password', 401); } const res = await fetch(`${BASE_URL}/usr/me/password`, { method: 'POST', credentials: AUTH_CREDENTIALS, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authToken}`, }, body: JSON.stringify({ current_password: currentPassword, new_password: newPassword, }), }); const body = await res.json().catch(() => ({})); if (!res.ok) { const msg = passwordPolicyMessage(body) || body.error || `Failed to change password: ${res.status}`; throw new AuthApiError(msg, res.status, body?.code, body?.violations); } return body; } /** * POST /usr/change-expired-password — completes a login that answered 403 password_change_required. * It re-proves the credentials the user just typed and sets the new password. * A 200 is the SAME body as /usr/login ({ token, user }). */ export async function changeExpiredPassword( email: string, currentPassword: string, newPassword: string, orgId?: string | number ): Promise<{ token: string; user: any }> { const body: Record = { email: email.trim(), current_password: currentPassword, new_password: newPassword, }; if (orgId) body.org_id = Number(orgId); const res = await fetch(`${BASE_URL}/usr/change-expired-password`, { method: 'POST', credentials: AUTH_CREDENTIALS, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json().catch(() => ({})); if (!res.ok) { const msg = passwordPolicyMessage(data) || data.error || data.message || `Failed to set new password: ${res.status}`; throw new AuthApiError(msg, res.status, data.code, data.violations); } return data; }