Files
ColaFlow-Web/lib/api/client.ts
Yaojia Wang ea67d90880 fix(frontend): Fix critical type safety issues from code review
Address all Critical and High Priority issues identified in frontend code review report:

Critical Issues Fixed:
- Created unified logger utility (lib/utils/logger.ts) to replace all console.log statements
- Consolidated User type definitions - removed duplicate from authStore, using single source from types/user.ts
- Eliminated 'any' types in API client - added proper generic types with AxiosRequestConfig
- Fixed SignalR ConnectionManager - replaced 'any' with generic types <T>
- Created API error types (lib/types/errors.ts) with ApiError and getErrorMessage helper
- Fixed IssueCard component - removed all type assertions, created discriminated union types for Kanban items
- Added React.memo to IssueCard for performance optimization
- Added proper ARIA labels and accessibility attributes to IssueCard

High Priority Issues Fixed:
- Fixed hardcoded user ID in CreateProjectDialog - now uses actual user from authStore
- Added useCallback to CreateProjectDialog onSubmit handler
- Fixed error handlers in use-epics.ts - replaced 'any' with ApiError type
- Updated all error handling to use logger and getErrorMessage

Type Safety Improvements:
- Created KanbanItem discriminated union (KanbanEpic | KanbanStory | KanbanTask) with proper type guards
- Added 'never' types to prevent invalid property access
- Fixed User interface to include all required fields (createdAt, updatedAt)
- Maintained backward compatibility with LegacyKanbanBoard for existing code

Files Changed:
- lib/utils/logger.ts - New centralized logging utility
- lib/types/errors.ts - New API error types and helpers
- types/user.ts - Consolidated User type with TenantRole
- types/kanban.ts - New discriminated union types for type-safe Kanban items
- components/features/kanban/IssueCard.tsx - Type-safe with React.memo
- components/features/projects/CreateProjectDialog.tsx - Fixed hardcoded user ID, added useCallback
- lib/api/client.ts - Eliminated 'any', added proper generics
- lib/signalr/ConnectionManager.ts - Replaced console.log, added generics
- lib/hooks/use-epics.ts - Fixed error handler types
- stores/authStore.ts - Removed duplicate User type
- lib/hooks/useAuth.ts - Added createdAt field to User

TypeScript compilation:  All type checks passing (0 errors)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 19:11:48 +01:00

177 lines
4.7 KiB
TypeScript

import axios, { AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
import { API_BASE_URL } from './config';
import { logger } from '@/lib/utils/logger';
// Create axios instance
export const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Token management
const TOKEN_KEY = 'colaflow_access_token';
const REFRESH_TOKEN_KEY = 'colaflow_refresh_token';
export const tokenManager = {
getAccessToken: () => {
if (typeof window === 'undefined') return null;
return localStorage.getItem(TOKEN_KEY);
},
setAccessToken: (token: string) => {
if (typeof window === 'undefined') return;
localStorage.setItem(TOKEN_KEY, token);
},
getRefreshToken: () => {
if (typeof window === 'undefined') return null;
return localStorage.getItem(REFRESH_TOKEN_KEY);
},
setRefreshToken: (token: string) => {
if (typeof window === 'undefined') return;
localStorage.setItem(REFRESH_TOKEN_KEY, token);
},
clearTokens: () => {
if (typeof window === 'undefined') return;
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
},
};
// Request interceptor: automatically add Access Token
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = tokenManager.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor: automatically refresh Token
let isRefreshing = false;
let failedQueue: Array<{
resolve: (value?: unknown) => void;
reject: (reason?: unknown) => void;
}> = [];
const processQueue = (error: unknown, token: string | null = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean;
};
// If 401 and not a refresh token request, try to refresh token
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// If already refreshing, queue this request
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = tokenManager.getRefreshToken();
if (!refreshToken) {
tokenManager.clearTokens();
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
return Promise.reject(error);
}
try {
const { data } = await axios.post(`${API_BASE_URL}/api/auth/refresh`, {
refreshToken,
});
tokenManager.setAccessToken(data.accessToken);
tokenManager.setRefreshToken(data.refreshToken);
apiClient.defaults.headers.common.Authorization = `Bearer ${data.accessToken}`;
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
processQueue(null, data.accessToken);
return apiClient(originalRequest);
} catch (refreshError) {
processQueue(refreshError, null);
tokenManager.clearTokens();
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
// API helper functions with proper typing
export const api = {
get: async <T>(url: string, config?: AxiosRequestConfig): Promise<T> => {
const response = await apiClient.get<T>(url, config);
return response.data;
},
post: async <T, D = unknown>(
url: string,
data?: D,
config?: AxiosRequestConfig
): Promise<T> => {
const response = await apiClient.post<T>(url, data, config);
return response.data;
},
put: async <T, D = unknown>(
url: string,
data?: D,
config?: AxiosRequestConfig
): Promise<T> => {
const response = await apiClient.put<T>(url, data, config);
return response.data;
},
patch: async <T, D = unknown>(
url: string,
data?: D,
config?: AxiosRequestConfig
): Promise<T> => {
const response = await apiClient.patch<T>(url, data, config);
return response.data;
},
delete: async <T>(url: string, config?: AxiosRequestConfig): Promise<T> => {
const response = await apiClient.delete<T>(url, config);
return response.data;
},
};