hashan-7's picture
Handle API rate limit responses
bd4991c
Raw
History Blame Contribute Delete
6.22 kB
import type { ChatMessage, FullProfile, PublicProfile } from '../types';
const API_BASE_URL =
import.meta.env.VITE_API_BASE_URL ?? (import.meta.env.DEV ? 'http://localhost:7860' : '');
const ADMIN_TOKEN_KEY = 'admin_token';
const ADMIN_TOKEN_EXPIRES_AT_KEY = 'admin_token_expires_at';
interface AdminLoginResponse {
token?: string;
expires_at?: number;
expires_in_seconds?: number;
}
interface ApiErrorResponse {
error?: string;
message?: string;
}
function getStoredAdminExpiryMs(): number | null {
const value = localStorage.getItem(ADMIN_TOKEN_EXPIRES_AT_KEY);
if (!value) {
return null;
}
const parsedValue = Number(value);
if (!Number.isFinite(parsedValue) || parsedValue <= 0) {
return null;
}
return parsedValue;
}
function clearAdminToken(): void {
localStorage.removeItem(ADMIN_TOKEN_KEY);
localStorage.removeItem(ADMIN_TOKEN_EXPIRES_AT_KEY);
}
function getAdminToken(): string | null {
const token = localStorage.getItem(ADMIN_TOKEN_KEY);
if (!token) {
return null;
}
const expiresAtMs = getStoredAdminExpiryMs();
if (expiresAtMs !== null && Date.now() >= expiresAtMs) {
clearAdminToken();
return null;
}
return token;
}
function getAuthHeaders() {
const token = getAdminToken();
return {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
}
async function getApiErrorMessage(response: Response, fallbackMessage: string): Promise<string> {
if (response.status === 429) {
return 'Too many requests. Please wait a moment and try again.';
}
try {
const data = (await response.json()) as ApiErrorResponse;
const message = data.error?.trim() || data.message?.trim();
if (message) {
return message;
}
} catch {
return fallbackMessage;
}
return fallbackMessage;
}
export function logoutAdmin(): void {
clearAdminToken();
}
export function getAdminSessionRemainingMs(): number | null {
const token = localStorage.getItem(ADMIN_TOKEN_KEY);
const expiresAtMs = getStoredAdminExpiryMs();
if (!token || expiresAtMs === null) {
return null;
}
return expiresAtMs - Date.now();
}
export async function getProfile(): Promise<PublicProfile> {
const response = await fetch(`${API_BASE_URL}/api/profile`);
if (!response.ok) {
throw new Error(await getApiErrorMessage(response, `Failed to load profile data. Status: ${response.status}`));
}
return response.json();
}
export async function sendChatMessage(history: ChatMessage[], scope = 'all'): Promise<string> {
const response = await fetch(`${API_BASE_URL}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ history, scope }),
});
if (!response.ok) {
throw new Error(await getApiErrorMessage(response, `Failed to send chat message. Status: ${response.status}`));
}
const data = (await response.json()) as { reply?: string };
return data.reply ?? 'No reply received from H7 Assistant.';
}
export async function loginAdmin(email: string, password: string): Promise<string> {
const response = await fetch(`${API_BASE_URL}/api/admin/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
clearAdminToken();
if (response.status === 429) {
throw new Error(await getApiErrorMessage(response, 'Too many login attempts. Please wait and try again.'));
}
throw new Error('Invalid admin email or password.');
}
const data = (await response.json()) as AdminLoginResponse;
if (!data.token) {
clearAdminToken();
throw new Error('Admin token was not received.');
}
const fallbackExpiresAtMs = Date.now() + 60 * 60 * 1000;
const expiresAtMs = data.expires_at ? data.expires_at * 1000 : fallbackExpiresAtMs;
localStorage.setItem(ADMIN_TOKEN_KEY, data.token);
localStorage.setItem(ADMIN_TOKEN_EXPIRES_AT_KEY, String(expiresAtMs));
return data.token;
}
export async function verifyAdminSession(): Promise<boolean> {
const token = getAdminToken();
if (!token) {
clearAdminToken();
return false;
}
try {
const response = await fetch(`${API_BASE_URL}/api/admin/verify`, {
headers: getAuthHeaders(),
});
if (!response.ok) {
clearAdminToken();
return false;
}
return true;
} catch {
clearAdminToken();
return false;
}
}
export async function getAdminProfile(): Promise<FullProfile> {
const response = await fetch(`${API_BASE_URL}/api/admin/profile`, {
headers: getAuthHeaders(),
});
if (response.status === 401) {
clearAdminToken();
throw new Error('Admin session expired. Please sign in again.');
}
if (!response.ok) {
throw new Error(await getApiErrorMessage(response, `Failed to load admin profile. Status: ${response.status}`));
}
return response.json();
}
export async function updateAdminProfile(profile: FullProfile): Promise<void> {
const response = await fetch(`${API_BASE_URL}/api/admin/profile`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify(profile),
});
if (response.status === 401) {
clearAdminToken();
throw new Error('Admin session expired. Please sign in again.');
}
if (!response.ok) {
throw new Error(await getApiErrorMessage(response, `Failed to update profile. Status: ${response.status}`));
}
}
export async function uploadMedia(file: File): Promise<string[]> {
const token = getAdminToken();
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${API_BASE_URL}/api/admin/media/upload`, {
method: 'POST',
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: formData,
});
if (response.status === 401) {
clearAdminToken();
throw new Error('Admin session expired. Please sign in again.');
}
if (!response.ok) {
throw new Error(await getApiErrorMessage(response, `Failed to upload media. Status: ${response.status}`));
}
const data = (await response.json()) as { files?: string[] };
return data.files ?? [];
}