Files
family_budget/frontend/src/api/client.ts
2026-01-29 15:17:54 +03:00

211 lines
6.9 KiB
TypeScript

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<LoginResponse>('/login', data),
logout: () =>
apiClient.post('/logout'),
me: () =>
apiClient.get<User>('/me'),
getGoogleAuthUrl: (redirectUrl?: string) =>
apiClient.get<OAuthUrlResponse>('/auth/google', {
params: redirectUrl ? { redirect_url: redirectUrl } : undefined,
}),
};
export const familyApi = {
getAll: () =>
apiClient.get<Family[]>('/families'),
getById: (id: number) =>
apiClient.get<Family>(`/families/${id}`),
create: (data: CreateFamilyRequest) =>
apiClient.post<Family>('/families', data),
createMyFamily: (data: CreateMyFamilyRequest) =>
apiClient.post<CreateMyFamilyResponse>('/my-family', data),
update: (id: number, data: { name: string }) =>
apiClient.put<Family>(`/families/${id}`, data),
delete: (id: number) =>
apiClient.delete(`/families/${id}`),
verifyPassword: (id: number, data: VerifyFamilyPasswordRequest) =>
apiClient.post<VerifyFamilyPasswordResponse>(`/families/${id}/verify`, data),
getMembers: (familyId: number) =>
apiClient.get<FamilyMember[]>(`/families/${familyId}/members`),
};
export const userApi = {
leaveFamily: () =>
apiClient.post<LeaveFamilyResponse>('/me/leave-family'),
};
export const categoryApi = {
getAllByFamily: (familyId: number) =>
apiClient.get<Category[]>(`/families/${familyId}/categories`),
getById: (familyId: number, categoryId: number) =>
apiClient.get<Category>(`/families/${familyId}/categories/${categoryId}`),
create: (familyId: number, data: CreateCategoryRequest) =>
apiClient.post<Category>(`/families/${familyId}/categories`, data),
update: (familyId: number, categoryId: number, data: Partial<CreateCategoryRequest>) =>
apiClient.put<Category>(`/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<Category>(`/families/${familyId}/categories/${categoryId}`, { limit_amount: newLimit }),
};
export const expenseApi = {
getAllByCategory: (familyId: number, categoryId: number) =>
apiClient.get<Expense[]>(`/families/${familyId}/categories/${categoryId}/expenses`),
getById: (familyId: number, categoryId: number, expenseId: number) =>
apiClient.get<Expense>(`/families/${familyId}/categories/${categoryId}/expenses/${expenseId}`),
create: (familyId: number, categoryId: number, data: CreateExpenseRequest) =>
apiClient.post<Expense>(`/families/${familyId}/categories/${categoryId}/expenses`, data),
update: (familyId: number, categoryId: number, expenseId: number, data: Partial<CreateExpenseRequest>) =>
apiClient.put<Expense>(`/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<RemainingLimit>(`/families/${familyId}/categories/${categoryId}/remaining`),
};
export const shoppingItemApi = {
getAllByFamily: (familyId: number) =>
apiClient.get<ShoppingItem[]>(`/families/${familyId}/shopping-items`),
getById: (familyId: number, itemId: number) =>
apiClient.get<ShoppingItem>(`/families/${familyId}/shopping-items/${itemId}`),
create: (familyId: number, data: CreateShoppingItemRequest) =>
apiClient.post<ShoppingItem>(`/families/${familyId}/shopping-items`, data),
update: (familyId: number, itemId: number, data: UpdateShoppingItemRequest) =>
apiClient.put<ShoppingItem>(`/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<ShoppingItem>(`/families/${familyId}/shopping-items/${itemId}/purchased`, data),
markAllAsPurchased: (familyId: number) =>
apiClient.post<BulkOperationResponse>(`/families/${familyId}/shopping-items/mark-all-purchased`),
clearAll: (familyId: number) =>
apiClient.delete<BulkOperationResponse>(`/families/${familyId}/shopping-items/clear-all`),
};
export const inviteLinkApi = {
create: (data: CreateInviteLinkRequest) =>
apiClient.post<InviteLinkResponse>('/my-family/invite-links', data),
getMyLinks: () =>
apiClient.get<InviteLinkResponse[]>('/my-family/invite-links'),
delete: (token: string) =>
apiClient.delete(`/my-family/invite-links/${token}`),
validate: (token: string) =>
apiClient.get<ValidateInviteResponse>(`/invite/${token}`),
join: (token: string) =>
apiClient.post<JoinFamilyResponse>(`/invite/${token}/join`),
};