import axios, { type AxiosInstance } from 'axios'; import { z } from 'zod'; import type { ApiError, AuthTokens, LoginRequest, RegisterRequest, User, PaginatedResponse, Track, LibraryItem, Conversation, } from '@/types'; export type { Track }; // Configuration de base const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1'; const WS_BASE_URL = import.meta.env.VITE_WS_BASE_URL || 'ws://localhost:8081'; // Schémas de validation Zod const UserSchema = z.object({ id: z.string(), username: z.string(), email: z.string().email(), first_name: z.string().optional(), last_name: z.string().optional(), role: z.enum(['user', 'admin', 'super_admin']), is_active: z.boolean(), is_verified: z.boolean(), created_at: z.string(), last_login_at: z.string().optional(), avatar_url: z.string().optional(), bio: z.string().optional(), }); const AuthTokensSchema = z.object({ access_token: z.string(), refresh_token: z.string(), expires_in: z.number(), }); const ApiErrorSchema = z.object({ message: z.string(), code: z.string().optional(), details: z.record(z.string(), z.unknown()).optional(), }); // Classe principale du service API export class ApiService { private client: AxiosInstance; private refreshPromise: Promise | null = null; constructor() { this.client = axios.create({ baseURL: API_BASE_URL, timeout: 10000, headers: { 'Content-Type': 'application/json', }, }); this.setupInterceptors(); } private setupInterceptors() { // Intercepteur de requête pour ajouter le token this.client.interceptors.request.use( (config) => { const token = this.getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error), ); // Intercepteur de réponse pour gérer les erreurs 401 this.client.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { const newToken = await this.refreshAccessToken(); originalRequest.headers.Authorization = `Bearer ${newToken}`; return this.client(originalRequest); } catch (refreshError) { this.clearTokens(); window.location.href = '/login'; return Promise.reject(refreshError); } } return Promise.reject(this.handleError(error)); }, ); } private getAccessToken(): string | null { return localStorage.getItem('access_token'); } private getRefreshToken(): string | null { return localStorage.getItem('refresh_token'); } private setTokens(tokens: AuthTokens): void { localStorage.setItem('access_token', tokens.access_token); localStorage.setItem('refresh_token', tokens.refresh_token); } private clearTokens(): void { localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); } private async refreshAccessToken(): Promise { if (this.refreshPromise) { return this.refreshPromise; } this.refreshPromise = this.performRefresh(); try { const token = await this.refreshPromise; return token; } finally { this.refreshPromise = null; } } private async performRefresh(): Promise { const refreshToken = this.getRefreshToken(); if (!refreshToken) { throw new Error('No refresh token available'); } try { const response = await axios.post(`${API_BASE_URL}/auth/refresh`, { refresh_token: refreshToken, }); const tokens = AuthTokensSchema.parse(response.data); this.setTokens(tokens); return tokens.access_token; } catch (error) { this.clearTokens(); throw error; } } private handleError(error: any): ApiError { if (error.response?.data) { return ApiErrorSchema.parse(error.response.data); } return { message: error.message || 'An unexpected error occurred', code: 'UNKNOWN_ERROR', }; } // Méthodes d'authentification async login( credentials: LoginRequest, ): Promise<{ user: User; tokens: AuthTokens }> { const response = await this.client.post('/auth/login', credentials); // Backend returns { success: true, data: { user, token } } const { user, token } = response.data.data; const validatedUser = UserSchema.parse(user); const validatedTokens = AuthTokensSchema.parse(token); this.setTokens(validatedTokens); return { user: validatedUser, tokens: validatedTokens }; } async register( userData: RegisterRequest, ): Promise<{ user: User; tokens: AuthTokens }> { const response = await this.client.post('/auth/register', userData); // Backend returns { success: true, data: { user, token } } const { user, token } = response.data.data; const validatedUser = UserSchema.parse(user); const validatedTokens = AuthTokensSchema.parse(token); this.setTokens(validatedTokens); return { user: validatedUser, tokens: validatedTokens }; } async logout(): Promise { try { await this.client.post('/auth/logout'); } finally { this.clearTokens(); } } async getCurrentUser(): Promise { const response = await this.client.get('/auth/me'); return UserSchema.parse(response.data); } // Méthodes pour les utilisateurs async getUsers(params?: { page?: number; limit?: number; search?: string; }): Promise> { const response = await this.client.get('/users', { params }); return response.data; } async getUser(id: string): Promise { const response = await this.client.get(`/users/${id}`); return UserSchema.parse(response.data); } async updateUser(id: string, data: Partial): Promise { const response = await this.client.put(`/users/${id}`, data); return UserSchema.parse(response.data); } // Méthodes pour les tracks async getTracks(params?: { page?: number; limit?: number; search?: string; artist?: string; }): Promise> { const response = await this.client.get('/tracks', { params }); // Ensure response.data maps to PaginatedResponse // If backend returns { tracks: [], total: ... }, we might need mapping // But let's assume standard response for now or fix if types mismatch return response.data; } async getTrack(id: string): Promise { const response = await this.client.get(`/tracks/${id}`); return response.data; } async uploadTrack( file: File, metadata: { title: string; artist: string; album?: string }, ): Promise { const formData = new FormData(); formData.append('file', file); formData.append('title', metadata.title); formData.append('artist', metadata.artist); if (metadata.album) { formData.append('album', metadata.album); } const response = await this.client.post('/tracks', formData, { headers: { 'Content-Type': 'multipart/form-data', }, }); return response.data; } async deleteTrack(id: string): Promise { await this.client.delete(`/tracks/${id}`); } // Méthodes pour la bibliothèque async getLibraryItems(params?: { page?: number; limit?: number; type?: string; }): Promise> { const response = await this.client.get('/library', { params }); return response.data; } async uploadFile( file: File, metadata: { title: string; description?: string }, ): Promise { const formData = new FormData(); formData.append('file', file); formData.append('title', metadata.title); if (metadata.description) { formData.append('description', metadata.description); } const response = await this.client.post('/library', formData, { headers: { 'Content-Type': 'multipart/form-data', }, }); return response.data; } async toggleFavorite(itemId: string): Promise { const response = await this.client.post(`/library/${itemId}/favorite`); return response.data; } // Méthodes pour les messages async getMessages( conversationId: string, params?: { page?: number; limit?: number }, ): Promise> { const response = await this.client.get(`/messages`, { params: { conversation_id: conversationId, ...params }, }); return response.data; } async sendMessage( conversationId: string, content: string, parentMessageId?: string, ): Promise { const response = await this.client.post('/messages', { conversation_id: conversationId, content, parent_message_id: parentMessageId, }); return response.data; } // Méthodes pour les conversations async getConversations(): Promise { const response = await this.client.get('/conversations'); // Backend retourne { conversations: [...], total: X } const conversations = response.data.conversations || []; // Convertir les IDs de int64 à string pour le frontend return conversations.map((conv: any) => ({ ...conv, id: String(conv.id), // Backend retourne int64, frontend attend string name: conv.name || `Conversation ${conv.id}`, participants: conv.participants || [], })); } async createConversation(params: { name: string; description?: string; type?: 'public' | 'private' | 'direct'; is_private?: boolean; }): Promise { const response = await this.client.post('/conversations', { name: params.name, description: params.description || '', type: params.type || 'public', is_private: params.is_private || false, }); // Convertir la réponse backend en format frontend const data = response.data; return { id: String(data.id), // Backend retourne int64, frontend attend string name: data.name, description: data.description, type: data.type === 'public' || data.type === 'private' || data.type === 'direct' ? data.type : 'public', is_private: data.is_private || false, created_by: data.created_by, participants: data.participants || [], created_at: data.created_at, updated_at: data.updated_at, }; } // Méthodes utilitaires getWebSocketUrl(): string { const token = this.getAccessToken(); return `${WS_BASE_URL}?token=${token}`; } isAuthenticated(): boolean { return !!this.getAccessToken(); } // Chat methods alias/helpers async getChatStats(): Promise<{ active_users: number; total_messages: number; rooms_active: number; }> { const response = await this.client.get('/chat/stats'); return response.data; } async getChatMessages(params: { room: string; limit?: number; }): Promise<{ success: boolean; data: any[] }> { // Assuming room name maps to conversation ID or backend handles it // Using existing getMessages logic or creating specific endpoint call try { // If room is 'general', we might need to look it up or use a specific ID. // For now, assuming room IS the conversationId or name that backend resolves. // But typically getMessages expects conversation_id. // Let's call /messages directly with query params const response = await this.client.get('/messages', { params: { conversation_id: params.room, limit: params.limit }, }); return { success: true, data: response.data.data }; } catch (error) { console.error('Failed to get chat messages', error); return { success: false, data: [] }; } } async sendChatMessage(data: { content: string; author: string; room: string; is_direct: boolean; }): Promise { return this.sendMessage(data.room, data.content); } } // Instance singleton export const apiService = new ApiService();