316 lines
8.8 KiB
TypeScript
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; |