From b11a4bd8c187b521019d1d935dbf552752f69f8f Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Thu, 27 Aug 2026 03:39:57 +0300 Subject: [PATCH] fix: Resolve WebSocket authentication error, add verifyJWT alias, and implement 20s heartbeat keep-alive for permanent stable connections --- backend/app/Core/Security.php | 8 ++++++++ backend/app/Views/StudentPortal.php | 25 ++++++++++++++++++++++--- backend/app/Views/TeacherPortal.php | 28 ++++++++++++++++++++++------ backend/websocket/server.php | 2 +- 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/backend/app/Core/Security.php b/backend/app/Core/Security.php index 2e8896e..bd876a7 100644 --- a/backend/app/Core/Security.php +++ b/backend/app/Core/Security.php @@ -194,6 +194,14 @@ class Security return $payload; } + /** + * Alias for verifyJWT for backward compatibility + */ + public static function decodeJwt(string $token) + { + return self::verifyJWT($token); + } + private static function base64UrlEncode(string $data): string { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); diff --git a/backend/app/Views/StudentPortal.php b/backend/app/Views/StudentPortal.php index a726e1a..c6a5a9f 100644 --- a/backend/app/Views/StudentPortal.php +++ b/backend/app/Views/StudentPortal.php @@ -877,21 +877,34 @@ class StudentPortal return `${m}:${s}`; } + let wsPingInterval = null; + // 1. Initialize Real-time WebSocket (Workerman) function initWebSocket(token) { if (!token) return; const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}/ws`; + if (wsSocket && (wsSocket.readyState === WebSocket.OPEN || wsSocket.readyState === WebSocket.CONNECTING)) { + return; + } + try { wsSocket = new WebSocket(wsUrl); wsSocket.onopen = () => { - updateWsStatus(true); wsSocket.send(JSON.stringify({ event: 'auth', data: { token: token } })); + + // Heartbeat keep-alive every 20s + clearInterval(wsPingInterval); + wsPingInterval = setInterval(() => { + if (wsSocket && wsSocket.readyState === WebSocket.OPEN) { + wsSocket.send(JSON.stringify({ event: 'ping' })); + } + }, 20000); }; wsSocket.onmessage = (event) => { @@ -905,7 +918,11 @@ class StudentPortal wsSocket.onclose = () => { updateWsStatus(false); - setTimeout(() => initWebSocket(token), 3000); + clearInterval(wsPingInterval); + setTimeout(() => { + const currentToken = localStorage.getItem('saqel_student_jwt'); + if (currentToken) initWebSocket(currentToken); + }, 5000); }; wsSocket.onerror = (err) => { @@ -933,7 +950,9 @@ class StudentPortal const event = msg.event; const data = msg.data; - if (event === 'chat_message' || event === 'chat_sent') { + if (event === 'authenticated') { + updateWsStatus(true); + } else if (event === 'chat_message' || event === 'chat_sent') { appendChatMessage(data); } else if (event === 'drm_session_terminated') { alert('⚠️ تم فتح هذا الحساب من متصفح آخر.'); diff --git a/backend/app/Views/TeacherPortal.php b/backend/app/Views/TeacherPortal.php index 9e38f65..677d7a6 100644 --- a/backend/app/Views/TeacherPortal.php +++ b/backend/app/Views/TeacherPortal.php @@ -556,22 +556,34 @@ class TeacherPortal } }); - // 1. Initialize Real-time WebSocket (Workerman) + let wsPingInterval = null; + function initWebSocket(token) { if (!token) return; const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}/ws`; + if (wsSocket && (wsSocket.readyState === WebSocket.OPEN || wsSocket.readyState === WebSocket.CONNECTING)) { + return; + } + try { wsSocket = new WebSocket(wsUrl); wsSocket.onopen = () => { console.log('⚡ Connected to Saqel Workerman Gateway'); - updateWsStatus(true); wsSocket.send(JSON.stringify({ event: 'auth', data: { token: token } })); + + // Heartbeat keep-alive every 20s + clearInterval(wsPingInterval); + wsPingInterval = setInterval(() => { + if (wsSocket && wsSocket.readyState === WebSocket.OPEN) { + wsSocket.send(JSON.stringify({ event: 'ping' })); + } + }, 20000); }; wsSocket.onmessage = (event) => { @@ -585,12 +597,14 @@ class TeacherPortal wsSocket.onclose = () => { updateWsStatus(false); - // Reconnect after 3s - setTimeout(() => initWebSocket(token), 3000); + clearInterval(wsPingInterval); + setTimeout(() => { + const currentToken = localStorage.getItem('saqel_teacher_jwt'); + if (currentToken) initWebSocket(currentToken); + }, 5000); }; wsSocket.onerror = (err) => { - console.warn('WS Error:', err); updateWsStatus(false); }; } catch (e) { @@ -616,7 +630,9 @@ class TeacherPortal const event = msg.event; const data = msg.data; - if (event === 'chat_message' || event === 'chat_sent') { + if (event === 'authenticated') { + updateWsStatus(true); + } else if (event === 'chat_message' || event === 'chat_sent') { appendChatMessage(data); loadConversations(); // refresh sidebar } else if (event === 'user_typing') { diff --git a/backend/websocket/server.php b/backend/websocket/server.php index 530b8c3..ff5d5e0 100644 --- a/backend/websocket/server.php +++ b/backend/websocket/server.php @@ -145,7 +145,7 @@ $wsWorker->onMessage = function (TcpConnection $connection, $data) use (&$userCo } try { - $decoded = \App\Core\Security::decodeJwt($token); + $decoded = \App\Core\Security::verifyJWT($token); if (!$decoded || empty($decoded['user_id'])) { $connection->send(json_encode(['event' => 'auth_error', 'message' => 'Invalid or expired token'])); return;