diff --git a/apps/web/src/services/websocket.ts b/apps/web/src/services/websocket.ts index 7a47a0be9..818d8bf0f 100644 --- a/apps/web/src/services/websocket.ts +++ b/apps/web/src/services/websocket.ts @@ -29,6 +29,7 @@ interface WebSocketService { } import { logger } from '@/utils/logger'; +import { TokenStorage } from '@/services/tokenStorage'; class WebSocketServiceImpl implements WebSocketService { private ws: WebSocket | null = null; @@ -45,26 +46,26 @@ class WebSocketServiceImpl implements WebSocketService { return; } - // CORRECTION: Le serveur Rust expose le WebSocket sur /ws - const wsUrl = (() => { + // CORRECTION: Le serveur Rust expose le WebSocket sur /ws et requiert token en query (v0.101) + const baseUrl = (() => { const url = import.meta.env.VITE_WS_URL; if (!url) { if (import.meta.env.PROD) { throw new Error('VITE_WS_URL must be defined in production'); } - // Fallback uniquement en développement - utiliser chemin relatif par défaut - // SECURITY: Ne jamais utiliser localhost hardcodé, utiliser chemin relatif - // Convertir chemin relatif en URL WebSocket const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${protocol}//${window.location.host}/ws`; } - // Convertir URL relative en URL absolue pour WebSocket if (url.startsWith('/')) { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${protocol}//${window.location.host}${url}`; } return url; })(); + const token = TokenStorage.getAccessToken(); + const wsUrl = token + ? `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}` + : baseUrl; return new Promise((resolve, reject) => { try { diff --git a/docs/ENV_CONFIG.md b/docs/ENV_CONFIG.md index 6a64e0970..17616a147 100644 --- a/docs/ENV_CONFIG.md +++ b/docs/ENV_CONFIG.md @@ -43,6 +43,13 @@ REDIS_ENABLE=true RABBITMQ_URL=amqp://veza:password@localhost:5672/%2f RABBITMQ_ENABLE=true +# ClamAV (v0.101 - virus scanning for uploads) +# Dev (Docker): ENABLE_CLAMAV=true, CLAMAV_REQUIRED=false (accepts uploads if ClamAV slow/down) +# Prod: CLAMAV_REQUIRED=true (startup fails if ClamAV unavailable) +ENABLE_CLAMAV=true +CLAMAV_REQUIRED=false +CLAMAV_ADDRESS=clamav:3310 + # Security JWT_SECRET=your-secret-key-here-change-in-production CORS_ALLOWED_ORIGINS=http://localhost:3000 diff --git a/veza-chat-server/src/main.rs b/veza-chat-server/src/main.rs index 481ed6f76..a23a90fc0 100644 --- a/veza-chat-server/src/main.rs +++ b/veza-chat-server/src/main.rs @@ -322,9 +322,10 @@ async fn main() -> Result<(), ChatError> { let ws_state_clone = ws_state.clone(); move |ws: WebSocketUpgrade, query: Query>, + headers: axum::http::HeaderMap, request_id: Option>| async move { - websocket_handler(ws, query, State(ws_state_clone), request_id).await + websocket_handler(ws, query, headers, State(ws_state_clone), request_id).await } }), ) diff --git a/veza-chat-server/src/websocket/handler.rs b/veza-chat-server/src/websocket/handler.rs index f8c476771..153636541 100644 --- a/veza-chat-server/src/websocket/handler.rs +++ b/veza-chat-server/src/websocket/handler.rs @@ -5,6 +5,7 @@ use axum::extract::ws::{Message, WebSocket}; use axum::extract::{Query, State, WebSocketUpgrade}; +use axum::http::HeaderMap; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use futures_util::StreamExt; @@ -45,6 +46,27 @@ pub struct WebSocketState { pub rate_limiter: Arc, } +/// Extract access token from query param (?token=) or Cookie header (access_token) +fn extract_token(params: &HashMap, headers: &HeaderMap) -> Option { + if let Some(t) = params.get("token") { + return Some(t.clone()); + } + if let Some(cookie_header) = headers.get(axum::http::header::COOKIE) { + if let Ok(cookie_str) = cookie_header.to_str() { + for part in cookie_str.split(';') { + let part = part.trim(); + if part.starts_with("access_token=") { + let value = part.trim_start_matches("access_token=").trim(); + if !value.is_empty() { + return Some(value.to_string()); + } + } + } + } + } + None +} + /// Handler principal pour les connexions WebSocket /// /// Cette fonction gère la mise à niveau de la connexion HTTP vers WebSocket @@ -53,8 +75,8 @@ pub struct WebSocketState { pub async fn websocket_handler( ws: WebSocketUpgrade, Query(params): Query>, + headers: HeaderMap, State(state): State, - // FIX #13: Extraire le request_id depuis les extensions (ajouté par request_id_middleware) request_id: Option>, ) -> Response { // FIX #13: Extraire le request_id pour la corrélation @@ -69,16 +91,15 @@ pub async fn websocket_handler( async move { info!(request_id = %request_id, "🔌 Nouvelle connexion WebSocket demandée"); - let token = match params.get("token") { + let token = match extract_token(¶ms, &headers) { Some(t) => t, None => { - error!(request_id = %request_id, "❌ Token manquant"); + error!(request_id = %request_id, "❌ Token manquant (query ?token= ou cookie access_token)"); return (StatusCode::UNAUTHORIZED, "Missing token").into_response(); } }; - // Validate token - match state.jwt_manager.validate_access_token(token).await { + match state.jwt_manager.validate_access_token(&token).await { Ok(claims) => { info!( request_id = %request_id,