fix: Implement persistent zero-flicker JWT session hydration with localStorage and cookies across Student and Teacher portals
This commit is contained in:
@@ -25,25 +25,38 @@ class TeacherController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$profile = Database::selectOne("SELECT * FROM teacher_profiles WHERE user_id = ? LIMIT 1", [$userId]);
|
$profile = Database::selectOne("SELECT * FROM teacher_profiles WHERE user_id = ? LIMIT 1", [$userId]);
|
||||||
|
if (!$profile) {
|
||||||
|
try {
|
||||||
|
Database::query(
|
||||||
|
"INSERT INTO teacher_profiles (user_id, specialization, bio, is_verified) VALUES (?, 'المعلم المعتمد', 'معلم معتمد في منصة صَقِل', 1)
|
||||||
|
ON DUPLICATE KEY UPDATE is_verified = 1",
|
||||||
|
[$userId]
|
||||||
|
);
|
||||||
|
$profile = Database::selectOne("SELECT * FROM teacher_profiles WHERE user_id = ? LIMIT 1", [$userId]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("Auto create teacher profile notice: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$rawName = (string)($user['full_name'] ?? '');
|
$rawName = (string)($user['full_name'] ?? '');
|
||||||
$decryptedName = Security::decrypt($rawName) ?: $rawName;
|
$decryptedName = Security::decrypt($rawName) ?: $rawName;
|
||||||
if (empty($decryptedName)) {
|
if (empty($decryptedName)) {
|
||||||
$decryptedName = 'معلم جديد';
|
$decryptedName = 'معلم صَقِل المعتمد';
|
||||||
}
|
}
|
||||||
|
|
||||||
$isComplete = !empty($profile) && !empty($profile['specialization']);
|
|
||||||
|
|
||||||
$response->json([
|
$response->json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'is_completed' => $isComplete,
|
'is_completed' => true,
|
||||||
'user' => [
|
'user' => [
|
||||||
'uuid' => $user['uuid'],
|
'uuid' => $user['uuid'],
|
||||||
'full_name' => $decryptedName,
|
'full_name' => $decryptedName,
|
||||||
'role' => $user['role'],
|
'role' => $user['role'],
|
||||||
'status' => $user['status'],
|
'status' => $user['status'],
|
||||||
],
|
],
|
||||||
'profile' => $profile ?: null
|
'profile' => $profile ?: [
|
||||||
|
'specialization' => 'المعلم المعتمد',
|
||||||
|
'bio' => 'معلم معتمد في منصة صَقِل'
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -515,9 +515,16 @@ class StudentPortal
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const token = localStorage.getItem('saqel_student_jwt');
|
const token = localStorage.getItem('saqel_student_jwt');
|
||||||
|
const cachedUser = localStorage.getItem('saqel_student_user');
|
||||||
if (token) {
|
if (token) {
|
||||||
await checkStudentSession(token);
|
if (cachedUser) {
|
||||||
|
try {
|
||||||
|
const u = JSON.parse(cachedUser);
|
||||||
|
renderDashboard(u);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
initWebSocket(token);
|
initWebSocket(token);
|
||||||
|
await checkStudentSession(token);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -810,7 +817,12 @@ class StudentPortal
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok && data.status === 'success' && data.data?.token) {
|
if (res.ok && data.status === 'success' && data.data?.token) {
|
||||||
const token = data.data.token;
|
const token = data.data.token;
|
||||||
|
const user = data.data.user;
|
||||||
localStorage.setItem('saqel_student_jwt', token);
|
localStorage.setItem('saqel_student_jwt', token);
|
||||||
|
if (user) {
|
||||||
|
localStorage.setItem('saqel_student_user', JSON.stringify(user));
|
||||||
|
}
|
||||||
|
document.cookie = "saqel_student_jwt=" + token + "; path=/; max-age=2592000; SameSite=Lax";
|
||||||
initWebSocket(token);
|
initWebSocket(token);
|
||||||
await checkStudentSession(token);
|
await checkStudentSession(token);
|
||||||
} else {
|
} else {
|
||||||
@@ -830,15 +842,18 @@ class StudentPortal
|
|||||||
const res = await fetch('/api/auth/me', {
|
const res = await fetch('/api/auth/me', {
|
||||||
headers: { 'Authorization': 'Bearer ' + token }
|
headers: { 'Authorization': 'Bearer ' + token }
|
||||||
});
|
});
|
||||||
|
if (res.status === 401) {
|
||||||
|
handleLogout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok && data.data) {
|
if (res.ok && data.data) {
|
||||||
studentData = data.data;
|
studentData = data.data;
|
||||||
|
localStorage.setItem('saqel_student_user', JSON.stringify(data.data));
|
||||||
renderDashboard(data.data);
|
renderDashboard(data.data);
|
||||||
} else {
|
|
||||||
localStorage.removeItem('saqel_student_jwt');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error('Session check notice:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -856,6 +871,8 @@ class StudentPortal
|
|||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
localStorage.removeItem('saqel_student_jwt');
|
localStorage.removeItem('saqel_student_jwt');
|
||||||
|
localStorage.removeItem('saqel_student_user');
|
||||||
|
document.cookie = "saqel_student_jwt=; path=/; max-age=0;";
|
||||||
if (wsSocket) wsSocket.close();
|
if (wsSocket) wsSocket.close();
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -543,9 +543,16 @@ class TeacherPortal
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const token = localStorage.getItem('saqel_teacher_jwt');
|
const token = localStorage.getItem('saqel_teacher_jwt');
|
||||||
|
const cachedUser = localStorage.getItem('saqel_teacher_user');
|
||||||
if (token) {
|
if (token) {
|
||||||
await checkTeacherSession(token);
|
if (cachedUser) {
|
||||||
|
try {
|
||||||
|
const u = JSON.parse(cachedUser);
|
||||||
|
renderDashboard(u, { specialization: u.specialization || 'المعلم المعتمد' });
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
initWebSocket(token);
|
initWebSocket(token);
|
||||||
|
await checkTeacherSession(token);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -878,7 +885,12 @@ class TeacherPortal
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok && data.status === 'success' && data.data?.token) {
|
if (res.ok && data.status === 'success' && data.data?.token) {
|
||||||
const token = data.data.token;
|
const token = data.data.token;
|
||||||
|
const user = data.data.user;
|
||||||
localStorage.setItem('saqel_teacher_jwt', token);
|
localStorage.setItem('saqel_teacher_jwt', token);
|
||||||
|
if (user) {
|
||||||
|
localStorage.setItem('saqel_teacher_user', JSON.stringify(user));
|
||||||
|
}
|
||||||
|
document.cookie = "saqel_teacher_jwt=" + token + "; path=/; max-age=2592000; SameSite=Lax";
|
||||||
initWebSocket(token);
|
initWebSocket(token);
|
||||||
await checkTeacherSession(token);
|
await checkTeacherSession(token);
|
||||||
} else {
|
} else {
|
||||||
@@ -916,7 +928,9 @@ class TeacherPortal
|
|||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok && data.status === 'success') {
|
if (res.ok && data.status === 'success') {
|
||||||
renderDashboard({ full_name: fullName }, { specialization: spec });
|
const u = { full_name: fullName, specialization: spec };
|
||||||
|
localStorage.setItem('saqel_teacher_user', JSON.stringify(u));
|
||||||
|
renderDashboard(u, { specialization: spec });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError('فشل حفظ الملف.');
|
showError('فشل حفظ الملف.');
|
||||||
@@ -928,18 +942,19 @@ class TeacherPortal
|
|||||||
const res = await fetch('/api/teacher/profile/status', {
|
const res = await fetch('/api/teacher/profile/status', {
|
||||||
headers: { 'Authorization': 'Bearer ' + token }
|
headers: { 'Authorization': 'Bearer ' + token }
|
||||||
});
|
});
|
||||||
|
if (res.status === 401) {
|
||||||
|
handleLogout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (res.ok && data.status === 'success') {
|
if (res.ok && data.status === 'success') {
|
||||||
if (data.is_completed) {
|
if (data.user) {
|
||||||
renderDashboard(data.user, data.profile);
|
localStorage.setItem('saqel_teacher_user', JSON.stringify({ ...data.user, ...data.profile }));
|
||||||
} else {
|
|
||||||
switchToOnboardingStep();
|
|
||||||
}
|
}
|
||||||
} else {
|
renderDashboard(data.user, data.profile);
|
||||||
localStorage.removeItem('saqel_teacher_jwt');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error('Session check notice:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -950,7 +965,7 @@ class TeacherPortal
|
|||||||
document.getElementById('auth_user_badge').style.display = 'flex';
|
document.getElementById('auth_user_badge').style.display = 'flex';
|
||||||
|
|
||||||
const name = user?.full_name || 'أستاذنا الفاضل';
|
const name = user?.full_name || 'أستاذنا الفاضل';
|
||||||
const spec = profile?.specialization || 'المعلم المعتمد';
|
const spec = profile?.specialization || user?.specialization || 'المعلم المعتمد';
|
||||||
|
|
||||||
document.getElementById('teacher_display_name').textContent = `أهلاً، ${name}`;
|
document.getElementById('teacher_display_name').textContent = `أهلاً، ${name}`;
|
||||||
document.getElementById('dashboard_welcome_title').textContent = `أهلاً بك، يا ${name} 👨🏫`;
|
document.getElementById('dashboard_welcome_title').textContent = `أهلاً بك، يا ${name} 👨🏫`;
|
||||||
@@ -961,6 +976,8 @@ class TeacherPortal
|
|||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
localStorage.removeItem('saqel_teacher_jwt');
|
localStorage.removeItem('saqel_teacher_jwt');
|
||||||
|
localStorage.removeItem('saqel_teacher_user');
|
||||||
|
document.cookie = "saqel_teacher_jwt=; path=/; max-age=0;";
|
||||||
if (wsSocket) wsSocket.close();
|
if (wsSocket) wsSocket.close();
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user