krishna_sales/src/api/auth.ts
2026-11-06 13:30:59 +05:30

153 lines
5.2 KiB
TypeScript

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<PasswordPolicy> {
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<string, unknown> = {
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;
}