fix: Resolve WebSocket authentication error, add verifyJWT alias, and implement 20s heartbeat keep-alive for permanent stable connections
This commit is contained in:
@@ -194,6 +194,14 @@ class Security
|
|||||||
return $payload;
|
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
|
private static function base64UrlEncode(string $data): string
|
||||||
{
|
{
|
||||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||||
|
|||||||
@@ -877,21 +877,34 @@ class StudentPortal
|
|||||||
return `${m}:${s}`;
|
return `${m}:${s}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let wsPingInterval = null;
|
||||||
|
|
||||||
// 1. Initialize Real-time WebSocket (Workerman)
|
// 1. Initialize Real-time WebSocket (Workerman)
|
||||||
function initWebSocket(token) {
|
function initWebSocket(token) {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
const wsUrl = `${wsProtocol}//${window.location.host}/ws`;
|
const wsUrl = `${wsProtocol}//${window.location.host}/ws`;
|
||||||
|
|
||||||
|
if (wsSocket && (wsSocket.readyState === WebSocket.OPEN || wsSocket.readyState === WebSocket.CONNECTING)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
wsSocket = new WebSocket(wsUrl);
|
wsSocket = new WebSocket(wsUrl);
|
||||||
|
|
||||||
wsSocket.onopen = () => {
|
wsSocket.onopen = () => {
|
||||||
updateWsStatus(true);
|
|
||||||
wsSocket.send(JSON.stringify({
|
wsSocket.send(JSON.stringify({
|
||||||
event: 'auth',
|
event: 'auth',
|
||||||
data: { token: token }
|
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) => {
|
wsSocket.onmessage = (event) => {
|
||||||
@@ -905,7 +918,11 @@ class StudentPortal
|
|||||||
|
|
||||||
wsSocket.onclose = () => {
|
wsSocket.onclose = () => {
|
||||||
updateWsStatus(false);
|
updateWsStatus(false);
|
||||||
setTimeout(() => initWebSocket(token), 3000);
|
clearInterval(wsPingInterval);
|
||||||
|
setTimeout(() => {
|
||||||
|
const currentToken = localStorage.getItem('saqel_student_jwt');
|
||||||
|
if (currentToken) initWebSocket(currentToken);
|
||||||
|
}, 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
wsSocket.onerror = (err) => {
|
wsSocket.onerror = (err) => {
|
||||||
@@ -933,7 +950,9 @@ class StudentPortal
|
|||||||
const event = msg.event;
|
const event = msg.event;
|
||||||
const data = msg.data;
|
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);
|
appendChatMessage(data);
|
||||||
} else if (event === 'drm_session_terminated') {
|
} else if (event === 'drm_session_terminated') {
|
||||||
alert('⚠️ تم فتح هذا الحساب من متصفح آخر.');
|
alert('⚠️ تم فتح هذا الحساب من متصفح آخر.');
|
||||||
|
|||||||
@@ -556,22 +556,34 @@ class TeacherPortal
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1. Initialize Real-time WebSocket (Workerman)
|
let wsPingInterval = null;
|
||||||
|
|
||||||
function initWebSocket(token) {
|
function initWebSocket(token) {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
const wsUrl = `${wsProtocol}//${window.location.host}/ws`;
|
const wsUrl = `${wsProtocol}//${window.location.host}/ws`;
|
||||||
|
|
||||||
|
if (wsSocket && (wsSocket.readyState === WebSocket.OPEN || wsSocket.readyState === WebSocket.CONNECTING)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
wsSocket = new WebSocket(wsUrl);
|
wsSocket = new WebSocket(wsUrl);
|
||||||
|
|
||||||
wsSocket.onopen = () => {
|
wsSocket.onopen = () => {
|
||||||
console.log('⚡ Connected to Saqel Workerman Gateway');
|
console.log('⚡ Connected to Saqel Workerman Gateway');
|
||||||
updateWsStatus(true);
|
|
||||||
wsSocket.send(JSON.stringify({
|
wsSocket.send(JSON.stringify({
|
||||||
event: 'auth',
|
event: 'auth',
|
||||||
data: { token: token }
|
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) => {
|
wsSocket.onmessage = (event) => {
|
||||||
@@ -585,12 +597,14 @@ class TeacherPortal
|
|||||||
|
|
||||||
wsSocket.onclose = () => {
|
wsSocket.onclose = () => {
|
||||||
updateWsStatus(false);
|
updateWsStatus(false);
|
||||||
// Reconnect after 3s
|
clearInterval(wsPingInterval);
|
||||||
setTimeout(() => initWebSocket(token), 3000);
|
setTimeout(() => {
|
||||||
|
const currentToken = localStorage.getItem('saqel_teacher_jwt');
|
||||||
|
if (currentToken) initWebSocket(currentToken);
|
||||||
|
}, 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
wsSocket.onerror = (err) => {
|
wsSocket.onerror = (err) => {
|
||||||
console.warn('WS Error:', err);
|
|
||||||
updateWsStatus(false);
|
updateWsStatus(false);
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -616,7 +630,9 @@ class TeacherPortal
|
|||||||
const event = msg.event;
|
const event = msg.event;
|
||||||
const data = msg.data;
|
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);
|
appendChatMessage(data);
|
||||||
loadConversations(); // refresh sidebar
|
loadConversations(); // refresh sidebar
|
||||||
} else if (event === 'user_typing') {
|
} else if (event === 'user_typing') {
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ $wsWorker->onMessage = function (TcpConnection $connection, $data) use (&$userCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$decoded = \App\Core\Security::decodeJwt($token);
|
$decoded = \App\Core\Security::verifyJWT($token);
|
||||||
if (!$decoded || empty($decoded['user_id'])) {
|
if (!$decoded || empty($decoded['user_id'])) {
|
||||||
$connection->send(json_encode(['event' => 'auth_error', 'message' => 'Invalid or expired token']));
|
$connection->send(json_encode(['event' => 'auth_error', 'message' => 'Invalid or expired token']));
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user