diff --git a/VEZA_INTEGRATION_PERFECTION_TODOLIST_TEMPLATE.json b/VEZA_INTEGRATION_PERFECTION_TODOLIST_TEMPLATE.json index 2b7f36132..6d71112fc 100644 --- a/VEZA_INTEGRATION_PERFECTION_TODOLIST_TEMPLATE.json +++ b/VEZA_INTEGRATION_PERFECTION_TODOLIST_TEMPLATE.json @@ -680,7 +680,8 @@ "description": "Identifier et supprimer les fichiers de services API non utilisés.", "priority": "P2", "priority_rank": 20, - "status": "todo", + "status": "completed", + "completed_at": "2025-01-27T16:30:00Z", "estimated_hours": 1, "side": "frontend_only", "files_to_modify": [ @@ -1101,13 +1102,13 @@ }, "progress_tracking": { "total_tasks": 32, - "completed": 19, + "completed": 20, "in_progress": 0, - "todo": 13, + "todo": 12, "blocked": 0, - "completion_percentage": 59, - "last_updated": "2025-01-27T16:15:00Z", + "completion_percentage": 63, + "last_updated": "2025-01-27T16:30:00Z", "estimated_completion_date": null, - "estimated_hours_remaining": 19.5 + "estimated_hours_remaining": 18.5 } } diff --git a/apps/web/src/services/csrf.ts b/apps/web/src/services/csrf.ts index b94bb20a6..6bbb15b3e 100644 --- a/apps/web/src/services/csrf.ts +++ b/apps/web/src/services/csrf.ts @@ -63,14 +63,16 @@ class CSRFService { } /** - * Alias pour compatibilité avec secure-auth.ts + * Alias pour compatibilité (legacy) + * INT-CLEANUP-001: Kept for backward compatibility */ clearCsrfToken(): void { this.clearToken(); } /** - * Alias pour compatibilité avec secure-auth.ts + * Alias pour compatibilité (legacy) + * INT-CLEANUP-001: Kept for backward compatibility */ async refreshCsrfToken(): Promise { return this.refreshToken(); diff --git a/apps/web/src/services/offline-storage.ts b/apps/web/src/services/offline-storage.ts deleted file mode 100644 index 2345f7943..000000000 --- a/apps/web/src/services/offline-storage.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * IndexedDB wrapper for offline storage - * Provides persistent storage for large objects and file data - */ - -class OfflineStorage { - private dbName = 'veza_offline_db'; - private dbVersion = 1; - private db: IDBDatabase | null = null; - - /** - * Open database connection - */ - async open(): Promise { - if (this.db) { - return this.db; - } - - return new Promise((resolve, reject) => { - const request = indexedDB.open(this.dbName, this.dbVersion); - - request.onerror = () => { - reject(new Error('Failed to open IndexedDB')); - }; - - request.onsuccess = () => { - this.db = request.result; - resolve(this.db); - }; - - request.onupgradeneeded = (event) => { - const db = (event.target as IDBOpenDBRequest).result; - - // Create object stores for different data types - if (!db.objectStoreNames.contains('queued_actions')) { - const actionStore = db.createObjectStore('queued_actions', { - keyPath: 'id', - }); - actionStore.createIndex('timestamp', 'timestamp', { unique: false }); - actionStore.createIndex('type', 'type', { unique: false }); - } - - if (!db.objectStoreNames.contains('uploaded_files')) { - const fileStore = db.createObjectStore('uploaded_files', { - keyPath: 'id', - }); - fileStore.createIndex('filename', 'filename', { unique: false }); - fileStore.createIndex('status', 'status', { unique: false }); - } - - if (!db.objectStoreNames.contains('messages')) { - const messageStore = db.createObjectStore('messages', { - keyPath: 'id', - }); - messageStore.createIndex('conversationId', 'conversationId', { - unique: false, - }); - messageStore.createIndex('timestamp', 'timestamp', { unique: false }); - } - }; - }); - } - - /** - * Save data to a store - */ - async save(storeName: string, data: T): Promise { - const db = await this.open(); - - return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readwrite'); - const store = transaction.objectStore(storeName); - const request = store.add(data); - - request.onsuccess = () => resolve(); - request.onerror = () => - reject(new Error(`Failed to save data to ${storeName}`)); - }); - } - - /** - * Update data in a store - */ - async update(storeName: string, data: T & { id: string }): Promise { - const db = await this.open(); - - return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readwrite'); - const store = transaction.objectStore(storeName); - const request = store.put(data); - - request.onsuccess = () => resolve(); - request.onerror = () => - reject(new Error(`Failed to update data in ${storeName}`)); - }); - } - - /** - * Get data from a store by ID - */ - async get(storeName: string, id: string): Promise { - const db = await this.open(); - - return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readonly'); - const store = transaction.objectStore(storeName); - const request = store.get(id); - - request.onsuccess = () => { - resolve(request.result || null); - }; - request.onerror = () => - reject(new Error(`Failed to get data from ${storeName}`)); - }); - } - - /** - * Get all data from a store - */ - async getAll(storeName: string): Promise { - const db = await this.open(); - - return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readonly'); - const store = transaction.objectStore(storeName); - const request = store.getAll(); - - request.onsuccess = () => { - resolve(request.result || []); - }; - request.onerror = () => - reject(new Error(`Failed to get all data from ${storeName}`)); - }); - } - - /** - * Delete data from a store by ID - */ - async delete(storeName: string, id: string): Promise { - const db = await this.open(); - - return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readwrite'); - const store = transaction.objectStore(storeName); - const request = store.delete(id); - - request.onsuccess = () => resolve(); - request.onerror = () => - reject(new Error(`Failed to delete data from ${storeName}`)); - }); - } - - /** - * Clear all data from a store - */ - async clear(storeName: string): Promise { - const db = await this.open(); - - return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readwrite'); - const store = transaction.objectStore(storeName); - const request = store.clear(); - - request.onsuccess = () => resolve(); - request.onerror = () => reject(new Error(`Failed to clear ${storeName}`)); - }); - } - - /** - * Query data by index - */ - async query( - storeName: string, - indexName: string, - query: IDBKeyRange | string, - ): Promise { - const db = await this.open(); - - return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readonly'); - const store = transaction.objectStore(storeName); - const index = store.index(indexName); - const request = index.getAll(query); - - request.onsuccess = () => { - resolve(request.result || []); - }; - request.onerror = () => reject(new Error(`Failed to query ${storeName}`)); - }); - } - - /** - * Get count of items in a store - */ - async count(storeName: string): Promise { - const db = await this.open(); - - return new Promise((resolve, reject) => { - const transaction = db.transaction(storeName, 'readonly'); - const store = transaction.objectStore(storeName); - const request = store.count(); - - request.onsuccess = () => resolve(request.result); - request.onerror = () => reject(new Error(`Failed to count ${storeName}`)); - }); - } -} - -// Export singleton instance -export const offlineStorage = new OfflineStorage(); - -// Export types -export interface QueuedAction { - id: string; - type: 'send_message' | 'upload_track' | 'update_profile'; - payload: Record; - timestamp: number; - retries: number; - status: 'pending' | 'processing' | 'failed' | 'completed'; -} - -export interface UploadedFile { - id: string; - filename: string; - file: File; - status: 'pending' | 'uploading' | 'completed' | 'failed'; - progress: number; - error?: string; -} - -export interface OfflineMessage { - id: string; - conversationId: string; - content: string; - timestamp: number; - userId: string; -} diff --git a/apps/web/src/services/secure-auth.ts b/apps/web/src/services/secure-auth.ts deleted file mode 100644 index 3ca3baffa..000000000 --- a/apps/web/src/services/secure-auth.ts +++ /dev/null @@ -1,322 +0,0 @@ -//! Service d'authentification sécurisé avec cookies httpOnly -//! -//! Ce service gère: -//! - Authentification via cookies httpOnly -//! - Gestion des tokens CSRF -//! - Protection contre les attaques XSS et CSRF - -import { z } from 'zod'; -import type { AuthTokens, LoginRequest, RegisterRequest, User } from '@/types'; -import { csrfService } from './csrf'; - -// Custom Error Class -export class ApiError extends Error { - code: string; - details?: Record; - - constructor( - message: string, - code: string = 'UNKNOWN_ERROR', - details?: Record, - ) { - super(message); - this.name = 'ApiError'; - this.code = code; - this.details = details; - } -} - -// Schémas de validation -const UserSchema = z.object({ - id: z.string().uuid(), // Fixed: number -> string, now validates UUID format - 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(), - is_admin: z.boolean().default(false), - is_public: z.boolean().default(false), - updated_at: z.string().default(new Date().toISOString()), -}); - -const AuthTokensSchema = z.object({ - access_token: z.string(), - refresh_token: z.string(), - expires_in: z.number(), -}); - -// Configuration -const API_BASE_URL = (() => { - const url = import.meta.env.VITE_API_URL; - if (!url) { - if (import.meta.env.PROD) { - throw new Error('VITE_API_URL must be defined in production'); - } - // Fallback uniquement en développement - return 'http://127.0.0.1:8080/api/v1'; - } - return url; -})(); - -export class SecureAuthService { - private static instance: SecureAuthService; - private isAuthenticatedFlag: boolean = false; - private user: User | null = null; - - private constructor() { - this.checkAuthenticationStatus(); - } - - public static getInstance(): SecureAuthService { - if (!SecureAuthService.instance) { - SecureAuthService.instance = new SecureAuthService(); - } - return SecureAuthService.instance; - } - - /** - * Vérifie le statut d'authentification via les cookies - */ - private async checkAuthenticationStatus(): Promise { - try { - const response = await fetch(`${API_BASE_URL}/auth/me`, { - method: 'GET', - credentials: 'include', // Important pour les cookies httpOnly - headers: { - 'Content-Type': 'application/json', - ...csrfService.getCsrfHeaders(), - }, - }); - - if (response.ok) { - const data = await response.json(); - this.user = UserSchema.parse(data.user) as User; - this.isAuthenticatedFlag = true; - console.debug('Utilisateur authentifié via cookies:', this.user); - } else { - this.clearAuth(); - } - } catch (error) { - console.warn( - "Erreur lors de la vérification de l'authentification:", - error, - ); - this.clearAuth(); - } - } - - /** - * Connexion avec cookies httpOnly - */ - public async login( - credentials: LoginRequest, - ): Promise<{ user: User; tokens: AuthTokens }> { - try { - // Récupérer le token CSRF avant la connexion - await csrfService.refreshCsrfToken(); - - const response = await fetch(`${API_BASE_URL}/auth/login`, { - method: 'POST', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - ...csrfService.getCsrfHeaders(), - }, - body: JSON.stringify(credentials), - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new ApiError( - errorData.message || 'Erreur de connexion', - errorData.code || 'LOGIN_ERROR', - ); - } - - const data = await response.json(); - const user = UserSchema.parse(data.user) as User; - const tokens = AuthTokensSchema.parse(data.tokens); - - // Les tokens sont automatiquement stockés dans les cookies httpOnly - // On ne les stocke pas dans localStorage pour la sécurité - this.user = user; - this.isAuthenticatedFlag = true; - - console.debug('Connexion réussie avec cookies httpOnly:', user); - return { user, tokens }; - } catch (error) { - console.error('Erreur de connexion:', error); - this.clearAuth(); - throw error; - } - } - - /** - * Inscription avec cookies httpOnly - */ - public async register( - userData: RegisterRequest, - ): Promise<{ user: User; tokens: AuthTokens }> { - try { - // Récupérer le token CSRF avant l'inscription - await csrfService.refreshCsrfToken(); - - const response = await fetch(`${API_BASE_URL}/auth/register`, { - method: 'POST', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - ...csrfService.getCsrfHeaders(), - }, - body: JSON.stringify(userData), - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new ApiError( - errorData.message || "Erreur d'inscription", - errorData.code || 'REGISTER_ERROR', - ); - } - - const data = await response.json(); - const user = UserSchema.parse(data.user); - const tokens = AuthTokensSchema.parse(data.tokens); - - this.user = user; - this.isAuthenticatedFlag = true; - - console.debug('Inscription réussie avec cookies httpOnly:', user); - return { user, tokens }; - } catch (error) { - console.error("Erreur d'inscription:", error); - this.clearAuth(); - throw error; - } - } - - /** - * Déconnexion - */ - public async logout(): Promise { - try { - await fetch(`${API_BASE_URL}/auth/logout`, { - method: 'POST', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - ...csrfService.getCsrfHeaders(), - }, - }); - } catch (error) { - console.warn('Erreur lors de la déconnexion:', error); - } finally { - this.clearAuth(); - } - } - - /** - * Rafraîchit les informations utilisateur - */ - public async refreshUser(): Promise { - try { - const response = await fetch(`${API_BASE_URL}/auth/me`, { - method: 'GET', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - ...csrfService.getCsrfHeaders(), - }, - }); - - if (response.ok) { - const data = await response.json(); - this.user = UserSchema.parse(data.user) as User; - this.isAuthenticatedFlag = true; - return this.user; - } else { - this.clearAuth(); - return null; - } - } catch (error) { - console.warn("Erreur lors du rafraîchissement de l'utilisateur:", error); - this.clearAuth(); - return null; - } - } - - /** - * Rafraîchit le token d'accès - */ - public async refreshAccessToken(): Promise { - try { - const response = await fetch(`${API_BASE_URL}/auth/refresh`, { - method: 'POST', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - ...csrfService.getCsrfHeaders(), - }, - }); - - if (!response.ok) { - throw new ApiError( - 'Impossible de rafraîchir le token', - 'REFRESH_ERROR', - ); - } - - const data = await response.json(); - const tokens = AuthTokensSchema.parse(data.tokens); - - // Le nouveau token est automatiquement stocké dans les cookies httpOnly - return tokens.access_token; - } catch (error) { - console.error('Erreur lors du rafraîchissement du token:', error); - this.clearAuth(); - throw error; - } - } - - /** - * Vérifie si l'utilisateur est authentifié - */ - public isAuthenticated(): boolean { - return this.isAuthenticatedFlag && this.user !== null; - } - - /** - * Obtient l'utilisateur actuel - */ - public getCurrentUser(): User | null { - return this.user; - } - - /** - * Nettoie l'état d'authentification - */ - private clearAuth(): void { - this.user = null; - this.isAuthenticatedFlag = false; - csrfService.clearCsrfToken(); - } - - /** - * Obtient les headers d'authentification pour les requêtes - */ - public getAuthHeaders(): Record { - return { - 'Content-Type': 'application/json', - ...csrfService.getCsrfHeaders(), - }; - } -} - -// Instance singleton -export const secureAuthService = SecureAuthService.getInstance();