import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'; import type { AuthResponse, Category, PaginatedResponse, Tenant, User, Workflow, WorkflowStep, } from '../types'; const api = axios.create({ baseURL: '/api', headers: { 'Content-Type': 'application/json' }, }); let isRefreshing = false; let failedQueue: Array<{ resolve: (token: string) => void; reject: (err: unknown) => void; }> = []; function processQueue(error: unknown, token: string | null = null) { failedQueue.forEach((p) => { if (error) { p.reject(error); } else { p.resolve(token!); } }); failedQueue = []; } api.interceptors.request.use((config: InternalAxiosRequestConfig) => { const token = localStorage.getItem('accessToken'); if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; } return config; }); api.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean; }; if (error.response?.status === 401 && !originalRequest._retry) { if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve: (token: string) => { if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${token}`; } resolve(api(originalRequest)); }, reject, }); }); } originalRequest._retry = true; isRefreshing = true; const refreshToken = localStorage.getItem('refreshToken'); if (!refreshToken) { localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); window.location.href = '/login'; return Promise.reject(error); } try { const { data } = await axios.post<{ accessToken: string; refreshToken: string }>( '/api/auth/refresh', { refreshToken }, ); localStorage.setItem('accessToken', data.accessToken); localStorage.setItem('refreshToken', data.refreshToken); processQueue(null, data.accessToken); if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${data.accessToken}`; } return api(originalRequest); } catch (refreshError) { processQueue(refreshError, null); localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); window.location.href = '/login'; return Promise.reject(refreshError); } finally { isRefreshing = false; } } return Promise.reject(error); } ); // --- Auth --- // Login returns { accessToken, refreshToken, user } directly // /me returns user object directly // /refresh returns { accessToken, refreshToken } (no user) export const authApi = { login: (email: string, password: string) => api.post('/auth/login', { email, password }).then((r) => r.data), refresh: (refreshToken: string) => api .post<{ accessToken: string; refreshToken: string }>('/auth/refresh', { refreshToken }) .then((r) => r.data), me: () => api.get('/auth/me').then((r) => r.data), logout: (refreshToken?: string | null) => api.post('/auth/logout', { refreshToken }).then((r) => r.data), }; // --- Tenants --- // Backend returns { data: Tenant[] } (no pagination) export const tenantsApi = { list: (params?: { page?: number; pageSize?: number; search?: string }) => api .get<{ data: Tenant[] }>('/tenants', { params }) .then((r) => { const tenants = r.data.data; // Client-side search if the backend doesn't support it let filtered = tenants; if (params?.search) { const s = params.search.toLowerCase(); filtered = tenants.filter( (t) => t.name.toLowerCase().includes(s) || t.slug.toLowerCase().includes(s) ); } // Client-side pagination const page = params?.page || 1; const pageSize = params?.pageSize || 20; const total = filtered.length; const totalPages = Math.ceil(total / pageSize) || 1; const start = (page - 1) * pageSize; const data = filtered.slice(start, start + pageSize); return { data, total, page, pageSize, totalPages } as PaginatedResponse; }), get: (id: string) => api.get<{ data: Tenant }>(`/tenants/${id}`).then((r) => r.data.data), create: (data: Partial) => api.post<{ data: Tenant }>('/tenants', data).then((r) => r.data.data), update: (id: string, data: Partial) => api.put<{ data: Tenant }>(`/tenants/${id}`, data).then((r) => r.data.data), delete: (id: string) => api.delete(`/tenants/${id}`).then((r) => r.data), }; // --- Users --- // Backend returns { data: User[] } (no pagination) export const usersApi = { list: (params?: { page?: number; pageSize?: number; search?: string; tenantId?: string; role?: string; }) => api .get<{ data: User[] }>('/users', { params: { tenantId: params?.tenantId, }, }) .then((r) => { let users = r.data.data; // Client-side search if (params?.search) { const s = params.search.toLowerCase(); users = users.filter( (u) => u.firstName.toLowerCase().includes(s) || u.lastName.toLowerCase().includes(s) || u.email.toLowerCase().includes(s) ); } // Client-side role filter if (params?.role) { users = users.filter((u) => u.role === params.role); } // Client-side pagination const page = params?.page || 1; const pageSize = params?.pageSize || 20; const total = users.length; const totalPages = Math.ceil(total / pageSize) || 1; const start = (page - 1) * pageSize; const data = users.slice(start, start + pageSize); return { data, total, page, pageSize, totalPages } as PaginatedResponse; }), get: (id: string) => api.get<{ data: User }>(`/users/${id}`).then((r) => r.data.data), create: (data: Partial & { password?: string }) => api.post<{ data: User }>('/users', data).then((r) => r.data.data), update: (id: string, data: Partial) => api.put<{ data: User }>(`/users/${id}`, data).then((r) => r.data.data), delete: (id: string) => api.delete(`/users/${id}`).then((r) => r.data), }; // --- Workflows --- // Backend list returns { data: Workflow[], pagination: { page, limit, total, totalPages } } // Backend uses "limit" query param (not "pageSize") // Single-item endpoints return { data: Workflow } export const workflowsApi = { list: (params?: { page?: number; pageSize?: number; search?: string; status?: string; categoryId?: string; tenantId?: string; isTemplate?: boolean; tags?: string; }) => api .get<{ data: Workflow[]; pagination: { page: number; limit: number; total: number; totalPages: number }; }>('/workflows', { params: { page: params?.page, limit: params?.pageSize, search: params?.search, status: params?.status, categoryId: params?.categoryId, tenantId: params?.tenantId, isTemplate: params?.isTemplate, tags: params?.tags, }, }) .then((r) => ({ data: r.data.data, total: r.data.pagination.total, page: r.data.pagination.page, pageSize: r.data.pagination.limit, totalPages: r.data.pagination.totalPages, })), get: (id: string) => api.get<{ data: Workflow }>(`/workflows/${id}`).then((r) => r.data.data), create: (data: Partial) => api.post<{ data: Workflow }>('/workflows', data).then((r) => r.data.data), update: (id: string, data: Partial) => api.put<{ data: Workflow }>(`/workflows/${id}`, data).then((r) => r.data.data), delete: (id: string) => api.delete(`/workflows/${id}`).then((r) => r.data), duplicate: (id: string) => api.post<{ data: Workflow }>(`/workflows/${id}/duplicate`).then((r) => r.data.data), // Publish and archive use the PATCH /:id/status endpoint publish: (id: string) => api .patch<{ data: Workflow }>(`/workflows/${id}/status`, { status: 'PUBLISHED' }) .then((r) => r.data.data), archive: (id: string) => api .patch<{ data: Workflow }>(`/workflows/${id}/status`, { status: 'ARCHIVED' }) .then((r) => r.data.data), changeStatus: (id: string, status: Workflow['status']) => api .patch<{ data: Workflow }>(`/workflows/${id}/status`, { status }) .then((r) => r.data.data), }; // --- Workflow Steps --- export const stepsApi = { create: (workflowId: string, data: Partial) => api .post<{ data: WorkflowStep }>(`/workflows/${workflowId}/steps`, data) .then((r) => r.data.data), update: (workflowId: string, stepId: string, data: Partial) => api .put<{ data: WorkflowStep }>(`/workflows/${workflowId}/steps/${stepId}`, data) .then((r) => r.data.data), delete: (workflowId: string, stepId: string) => api.delete(`/workflows/${workflowId}/steps/${stepId}`).then((r) => r.data), reorder: (workflowId: string, steps: { id: string; order: number }[]) => api .patch(`/workflows/${workflowId}/steps/reorder`, { steps }) .then((r) => r.data), }; // --- Categories --- export const categoriesApi = { list: () => api.get<{ data: Category[] }>('/categories').then((r) => r.data.data), create: (data: Partial) => api.post<{ data: Category }>('/categories', data).then((r) => r.data.data), update: (id: string, data: Partial) => api.put<{ data: Category }>(`/categories/${id}`, data).then((r) => r.data.data), delete: (id: string) => api.delete(`/categories/${id}`).then((r) => r.data), }; export default api;