import axios, { AxiosError } from 'axios'; import axiosRetry from 'axios-retry'; import type { Family, Category, Expense, RemainingLimit, LoginRequest, LoginResponse, CreateFamilyRequest, CreateMyFamilyRequest, CreateMyFamilyResponse, CreateCategoryRequest, CreateExpenseRequest, VerifyFamilyPasswordRequest, VerifyFamilyPasswordResponse, ShoppingItem, CreateShoppingItemRequest, UpdateShoppingItemRequest, MarkAsPurchasedRequest, BulkOperationResponse, User, OAuthUrlResponse, CreateInviteLinkRequest, InviteLinkResponse, ValidateInviteResponse, JoinFamilyResponse, FamilyMember, LeaveFamilyResponse, } from '../types'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; const apiClient = axios.create({ baseURL: API_BASE_URL, withCredentials: true, timeout: 30000, }); axiosRetry(apiClient, { retries: 3, retryDelay: axiosRetry.exponentialDelay, retryCondition: (error: AxiosError) => { return ( axiosRetry.isNetworkOrIdempotentRequestError(error) || (error.response?.status ? error.response.status >= 500 : false) ); }, onRetry: (retryCount, error, requestConfig) => { console.log(`Retry attempt ${retryCount} for ${requestConfig.url}`, error.message); }, }); apiClient.interceptors.request.use( (config) => { console.log(`[API] ${config.method?.toUpperCase()} ${config.url}`); return config; }, (error) => { console.error('[API] Request error:', error); return Promise.reject(error); } ); apiClient.interceptors.response.use( (response) => { console.log(`[API] ${response.config.method?.toUpperCase()} ${response.config.url} - ${response.status}`); return response; }, (error: AxiosError) => { if (error.response?.status === 401) { console.warn('[API] Unauthorized - redirecting to login'); if (!window.location.pathname.includes('/login')) { window.location.href = '/login'; } } console.error('[API] Response error:', error.response?.status, error.message); return Promise.reject(error); } ); export const authApi = { login: (data: LoginRequest) => apiClient.post('/login', data), logout: () => apiClient.post('/logout'), me: () => apiClient.get('/me'), getGoogleAuthUrl: (redirectUrl?: string) => apiClient.get('/auth/google', { params: redirectUrl ? { redirect_url: redirectUrl } : undefined, }), }; export const familyApi = { getAll: () => apiClient.get('/families'), getById: (id: number) => apiClient.get(`/families/${id}`), create: (data: CreateFamilyRequest) => apiClient.post('/families', data), createMyFamily: (data: CreateMyFamilyRequest) => apiClient.post('/my-family', data), update: (id: number, data: { name: string }) => apiClient.put(`/families/${id}`, data), delete: (id: number) => apiClient.delete(`/families/${id}`), verifyPassword: (id: number, data: VerifyFamilyPasswordRequest) => apiClient.post(`/families/${id}/verify`, data), getMembers: (familyId: number) => apiClient.get(`/families/${familyId}/members`), }; export const userApi = { leaveFamily: () => apiClient.post('/me/leave-family'), }; export const categoryApi = { getAllByFamily: (familyId: number) => apiClient.get(`/families/${familyId}/categories`), getById: (familyId: number, categoryId: number) => apiClient.get(`/families/${familyId}/categories/${categoryId}`), create: (familyId: number, data: CreateCategoryRequest) => apiClient.post(`/families/${familyId}/categories`, data), update: (familyId: number, categoryId: number, data: Partial) => apiClient.put(`/families/${familyId}/categories/${categoryId}`, data), delete: (familyId: number, categoryId: number) => apiClient.delete(`/families/${familyId}/categories/${categoryId}`), resetLimit: (familyId: number, categoryId: number, newLimit: number) => apiClient.put(`/families/${familyId}/categories/${categoryId}`, { limit_amount: newLimit }), }; export const expenseApi = { getAllByCategory: (familyId: number, categoryId: number) => apiClient.get(`/families/${familyId}/categories/${categoryId}/expenses`), getById: (familyId: number, categoryId: number, expenseId: number) => apiClient.get(`/families/${familyId}/categories/${categoryId}/expenses/${expenseId}`), create: (familyId: number, categoryId: number, data: CreateExpenseRequest) => apiClient.post(`/families/${familyId}/categories/${categoryId}/expenses`, data), update: (familyId: number, categoryId: number, expenseId: number, data: Partial) => apiClient.put(`/families/${familyId}/categories/${categoryId}/expenses/${expenseId}`, data), delete: (familyId: number, categoryId: number, expenseId: number) => apiClient.delete(`/families/${familyId}/categories/${categoryId}/expenses/${expenseId}`), getRemainingLimit: (familyId: number, categoryId: number) => apiClient.get(`/families/${familyId}/categories/${categoryId}/remaining`), }; export const shoppingItemApi = { getAllByFamily: (familyId: number) => apiClient.get(`/families/${familyId}/shopping-items`), getById: (familyId: number, itemId: number) => apiClient.get(`/families/${familyId}/shopping-items/${itemId}`), create: (familyId: number, data: CreateShoppingItemRequest) => apiClient.post(`/families/${familyId}/shopping-items`, data), update: (familyId: number, itemId: number, data: UpdateShoppingItemRequest) => apiClient.put(`/families/${familyId}/shopping-items/${itemId}`, data), delete: (familyId: number, itemId: number) => apiClient.delete(`/families/${familyId}/shopping-items/${itemId}`), markAsPurchased: (familyId: number, itemId: number, data: MarkAsPurchasedRequest) => apiClient.patch(`/families/${familyId}/shopping-items/${itemId}/purchased`, data), markAllAsPurchased: (familyId: number) => apiClient.post(`/families/${familyId}/shopping-items/mark-all-purchased`), clearAll: (familyId: number) => apiClient.delete(`/families/${familyId}/shopping-items/clear-all`), }; export const inviteLinkApi = { create: (data: CreateInviteLinkRequest) => apiClient.post('/my-family/invite-links', data), getMyLinks: () => apiClient.get('/my-family/invite-links'), delete: (token: string) => apiClient.delete(`/my-family/invite-links/${token}`), validate: (token: string) => apiClient.get(`/invite/${token}`), join: (token: string) => apiClient.post(`/invite/${token}/join`), };