Files
TODO_list_system/frontend/src/lib/api.ts
beabigegg b0c86302ff 1ST
2025-08-29 16:25:46 +08:00

316 lines
8.8 KiB
TypeScript

import axios, { AxiosResponse, AxiosError } from 'axios';
import { toast } from 'react-hot-toast';
import {
Todo,
TodoCreate,
TodoUpdate,
TodoFilter,
TodosResponse,
User,
UserPreferences,
LdapUser,
LoginRequest,
LoginResponse,
FireEmailRequest,
FireEmailQuota,
ImportJob,
} from '@/types';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000';
// Create axios instance
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
});
// Request interceptor to add auth token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for error handling
api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as any;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refresh_token');
if (refreshToken) {
const response = await api.post('/api/auth/refresh', {}, {
headers: { Authorization: `Bearer ${refreshToken}` },
});
const { access_token } = response.data;
localStorage.setItem('access_token', access_token);
// Retry original request (mark it to skip toast on failure)
originalRequest._isRetry = true;
return api(originalRequest);
}
} catch (refreshError) {
// Refresh failed, redirect to login
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
// Show error toast (skip for retry requests to avoid duplicates)
if (!originalRequest._isRetry) {
const errorData = (error as any).response?.data;
const status = (error as any).response?.status;
let errorMessage = 'An error occurred';
if (errorData?.message) {
errorMessage = errorData.message;
} else if (errorData?.error) {
errorMessage = errorData.error;
} else if ((error as any).message) {
errorMessage = (error as any).message;
}
// Special handling for database connection errors
if (status === 503) {
toast.error(errorMessage, {
duration: 5000,
style: {
backgroundColor: '#fef3c7',
color: '#92400e',
},
});
} else if (status === 504) {
toast.error(errorMessage, {
duration: 4000,
style: {
backgroundColor: '#fee2e2',
color: '#991b1b',
},
});
} else if (status !== 401) {
toast.error(errorMessage);
}
}
return Promise.reject(error);
}
);
// Auth API
export const authApi = {
login: async (credentials: LoginRequest): Promise<LoginResponse> => {
const response = await api.post('/api/auth/login', credentials);
return response.data;
},
logout: async (): Promise<void> => {
await api.post('/api/auth/logout');
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user');
},
getCurrentUser: async (): Promise<User> => {
const response = await api.get('/api/auth/me');
return response.data;
},
validateToken: async (): Promise<boolean> => {
try {
await api.get('/api/auth/validate');
return true;
} catch {
return false;
}
},
};
// Todos API
export const todosApi = {
getTodos: async (filter: TodoFilter & { page?: number; per_page?: number }): Promise<TodosResponse> => {
const params = new URLSearchParams();
Object.entries(filter).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
params.append(key, value.toString());
}
});
const response = await api.get(`/api/todos?${params.toString()}`);
return response.data;
},
getTodo: async (id: string): Promise<Todo> => {
const response = await api.get(`/api/todos/${id}`);
return response.data;
},
createTodo: async (todo: TodoCreate): Promise<Todo> => {
const response = await api.post('/api/todos', todo);
return response.data;
},
updateTodo: async (id: string, updates: Partial<TodoUpdate>): Promise<Todo> => {
const response = await api.patch(`/api/todos/${id}`, updates);
return response.data;
},
deleteTodo: async (id: string): Promise<void> => {
await api.delete(`/api/todos/${id}`);
},
batchUpdateTodos: async (todoIds: string[], updates: Partial<TodoUpdate>): Promise<{ updated: number; errors: any[] }> => {
const response = await api.patch('/api/todos/batch', {
todo_ids: todoIds,
updates,
});
return response.data;
},
fireEmail: async (request: FireEmailRequest): Promise<void> => {
await api.post('/api/notifications/fire-email', {
todo_id: request.todo_id,
message: request.note,
});
},
};
// Users API
export const usersApi = {
searchUsers: async (query: string): Promise<LdapUser[]> => {
const response = await api.get(`/api/users/search?q=${encodeURIComponent(query)}`);
return response.data.users;
},
getPreferences: async (): Promise<UserPreferences> => {
const response = await api.get('/api/users/preferences');
return response.data;
},
updatePreferences: async (preferences: Partial<UserPreferences>): Promise<UserPreferences> => {
const response = await api.patch('/api/users/preferences', preferences);
return response.data;
},
getFireEmailQuota: async (): Promise<FireEmailQuota> => {
const response = await api.get('/api/users/fire-email-quota');
return response.data;
},
};
// Import API
export const importApi = {
downloadTemplate: async (): Promise<Blob> => {
const response = await api.get('/api/imports/template', {
responseType: 'blob',
});
return response.data;
},
uploadFile: async (file: File): Promise<ImportJob> => {
const formData = new FormData();
formData.append('file', file);
const response = await api.post('/api/imports', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
},
getImportJob: async (jobId: string): Promise<ImportJob> => {
const response = await api.get(`/api/imports/${jobId}`);
return response.data;
},
downloadErrors: async (jobId: string): Promise<Blob> => {
const response = await api.get(`/api/imports/${jobId}/errors`, {
responseType: 'blob',
});
return response.data;
},
};
// Admin API (if needed)
export const adminApi = {
getStats: async (days: number = 30): Promise<any> => {
const response = await api.get(`/api/admin/stats?days=${days}`);
return response.data;
},
getAuditLogs: async (params: any): Promise<any> => {
const queryParams = new URLSearchParams(params).toString();
const response = await api.get(`/api/admin/audit-logs?${queryParams}`);
return response.data;
},
getMailLogs: async (params: any): Promise<any> => {
const queryParams = new URLSearchParams(params).toString();
const response = await api.get(`/api/admin/mail-logs?${queryParams}`);
return response.data;
},
};
// Notifications API
export const notificationsApi = {
getSettings: async (): Promise<any> => {
const response = await api.get('/api/notifications/settings');
return response.data;
},
updateSettings: async (settings: any): Promise<any> => {
const response = await api.patch('/api/notifications/settings', settings);
return response.data;
},
sendTestEmail: async (recipientEmail?: string): Promise<void> => {
await api.post('/api/notifications/test', recipientEmail ? { recipient_email: recipientEmail } : {});
},
sendDigest: async (type: 'weekly' | 'monthly' = 'weekly'): Promise<void> => {
await api.post('/api/notifications/digest', { type });
},
markNotificationRead: async (notificationId: string): Promise<void> => {
await api.post('/api/notifications/mark-read', { notification_id: notificationId });
},
markAllNotificationsRead: async (): Promise<void> => {
await api.post('/api/notifications/mark-all-read');
},
getNotifications: async (): Promise<any> => {
const response = await api.get('/api/notifications/');
return response.data;
},
};
// Health API
export const healthApi = {
check: async (): Promise<any> => {
const response = await api.get('/api/health/healthz');
return response.data;
},
readiness: async (): Promise<any> => {
const response = await api.get('/api/health/readiness');
return response.data;
},
};
export default api;