interface LoginResponse { access_token: string; refresh_token: string; user: { id: string; username: string; email?: string; }; } interface RefreshResponse { access_token: string; } class UserService { private baseUrl = "/api/user"; async login(username: string, password: string): Promise { const response = await fetch(`${this.baseUrl}/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); if (!response.ok) { throw new Error("Invalid credentials"); } return await response.json(); } async refreshToken(): Promise { const refreshToken = localStorage.getItem("refresh_token"); if (!refreshToken) { throw new Error("No refresh token available"); } const response = await fetch(`${this.baseUrl}/refresh`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${refreshToken}`, }, }); if (!response.ok) { // Refresh token is invalid or expired, clear storage localStorage.removeItem("access_token"); localStorage.removeItem("refresh_token"); throw new Error("Failed to refresh token"); } const data: RefreshResponse = await response.json(); localStorage.setItem("access_token", data.access_token); return data.access_token; } async validateToken(): Promise { const refreshToken = localStorage.getItem("refresh_token"); if (!refreshToken) { return false; } try { await this.refreshToken(); return true; } catch (error) { return false; } } async fetchWithAuth( url: string, options: RequestInit = {}, ): Promise { const accessToken = localStorage.getItem("access_token"); // Add authorization header const headers = { "Content-Type": "application/json", ...(options.headers || {}), ...(accessToken && { Authorization: `Bearer ${accessToken}` }), }; let response = await fetch(url, { ...options, headers }); // If unauthorized, try refreshing the token if (response.status === 401) { try { const newAccessToken = await this.refreshToken(); // Retry the request with new token headers.Authorization = `Bearer ${newAccessToken}`; response = await fetch(url, { ...options, headers }); } catch (error) { // Refresh failed, redirect to login or throw error throw new Error("Session expired. Please log in again."); } } return response; } async fetchWithRefreshToken( url: string, options: RequestInit = {}, { skipContentType = false }: { skipContentType?: boolean } = {}, ): Promise { const refreshToken = localStorage.getItem("refresh_token"); // Add authorization header const headers: Record = { ...(skipContentType ? {} : { "Content-Type": "application/json" }), ...((options.headers as Record) || {}), ...(refreshToken ? { Authorization: `Bearer ${refreshToken}` } : {}), }; let response = await fetch(url, { ...options, headers }); // If unauthorized, try refreshing the token if (response.status === 401) { try { const newAccessToken = await this.refreshToken(); // Retry the request with new token headers.Authorization = `Bearer ${newAccessToken}`; response = await fetch(url, { ...options, headers }); } catch (error) { // Refresh failed, redirect to login or throw error throw new Error("Session expired. Please log in again."); } } return response; } async getMe(): Promise<{ id: string; username: string; email: string; is_admin: boolean }> { const response = await this.fetchWithRefreshToken(`${this.baseUrl}/me`); if (!response.ok) throw new Error("Failed to fetch user profile"); return response.json(); } async adminListUsers(): Promise { const response = await this.fetchWithRefreshToken(`${this.baseUrl}/admin/users`); if (!response.ok) throw new Error("Failed to list users"); return response.json(); } async adminSetWhatsapp(userId: string, number: string): Promise { const response = await this.fetchWithRefreshToken( `${this.baseUrl}/admin/users/${userId}/whatsapp`, { method: "PUT", body: JSON.stringify({ whatsapp_number: number }) }, ); if (response.status === 409) { const data = await response.json(); throw new Error(data.error ?? "WhatsApp number already in use"); } if (!response.ok) throw new Error("Failed to set WhatsApp number"); return response.json(); } async adminUnlinkWhatsapp(userId: string): Promise { const response = await this.fetchWithRefreshToken( `${this.baseUrl}/admin/users/${userId}/whatsapp`, { method: "DELETE" }, ); if (!response.ok) throw new Error("Failed to unlink WhatsApp number"); } async adminToggleEmail(userId: string): Promise { const response = await this.fetchWithRefreshToken( `${this.baseUrl}/admin/users/${userId}/email`, { method: "PUT" }, ); if (!response.ok) throw new Error("Failed to enable email"); return response.json(); } async adminDisableEmail(userId: string): Promise { const response = await this.fetchWithRefreshToken( `${this.baseUrl}/admin/users/${userId}/email`, { method: "DELETE" }, ); if (!response.ok) throw new Error("Failed to disable email"); } } export interface AdminUserRecord { id: string; username: string; email: string; whatsapp_number: string | null; auth_provider: string; email_enabled: boolean; email_address: string | null; } export { UserService }; export const userService = new UserService();