[INT-CLEANUP-001] Remove all unused API service files (offline-storage.ts, secure-auth.ts)
This commit is contained in:
parent
c171d66b0c
commit
6d1e3cea3d
4 changed files with 11 additions and 567 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string> {
|
||||
return this.refreshToken();
|
||||
|
|
|
|||
|
|
@ -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<IDBDatabase> {
|
||||
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<T>(storeName: string, data: T): Promise<void> {
|
||||
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<T>(storeName: string, data: T & { id: string }): Promise<void> {
|
||||
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<T>(storeName: string, id: string): Promise<T | null> {
|
||||
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<T>(storeName: string): Promise<T[]> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<T>(
|
||||
storeName: string,
|
||||
indexName: string,
|
||||
query: IDBKeyRange | string,
|
||||
): Promise<T[]> {
|
||||
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<number> {
|
||||
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<string, any>;
|
||||
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;
|
||||
}
|
||||
|
|
@ -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<string, unknown>;
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
code: string = 'UNKNOWN_ERROR',
|
||||
details?: Record<string, unknown>,
|
||||
) {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<User | null> {
|
||||
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<string> {
|
||||
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<string, string> {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
...csrfService.getCsrfHeaders(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Instance singleton
|
||||
export const secureAuthService = SecureAuthService.getInstance();
|
||||
Loading…
Reference in a new issue