From 06461635bf4ea333c5c22cfdefe06de3b94d2dbb Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 28 Aug 2026 15:13:58 +0300 Subject: [PATCH] Update Saqel Platform: 2026-08-28 15:13:58 --- backend/app/Controllers/AuthController.php | 118 ++++++++++++------ backend/app/Controllers/ChatController.php | 13 +- backend/app/Controllers/TeacherController.php | 51 ++++---- backend/app/Services/TeacherRatingService.php | 20 ++- backend/app/Views/TeacherPortal.php | 2 +- 5 files changed, 116 insertions(+), 88 deletions(-) diff --git a/backend/app/Controllers/AuthController.php b/backend/app/Controllers/AuthController.php index c9e994d..29ba325 100644 --- a/backend/app/Controllers/AuthController.php +++ b/backend/app/Controllers/AuthController.php @@ -187,59 +187,97 @@ class AuthController return; } - // 2. Find or Create User - $user = Database::selectOne("SELECT * FROM users WHERE phone_hash = ? LIMIT 1", [$phoneHash]); + // 2. Find or Create Auth Identity (Central Multi-Persona Auth) + $identity = Database::selectOne("SELECT * FROM auth_identities WHERE phone_hash = ? LIMIT 1", [$phoneHash]); - if (!$user) { - // New user registration - $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + if (!$identity) { + $identityUuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); - $encryptedPhone = Security::encrypt($cleanPhone); - $encryptedName = Security::encrypt($fullName ?: ($role === 'teacher' ? 'معلم جديد' : 'طالب جديد')); - $randomPassword = password_hash(bin2hex(random_bytes(16)), PASSWORD_BCRYPT); - - $userId = Database::insert( - "INSERT INTO users (uuid, full_name, phone_number, phone_hash, password_hash, role, status, token_version) VALUES (?, ?, ?, ?, ?, ?, 'active', 1)", - [$uuid, $encryptedName, $encryptedPhone, $phoneHash, $randomPassword, $role] + $identityId = Database::insert( + "INSERT INTO auth_identities (uuid, phone_number, phone_hash, status, token_version) VALUES (?, ?, ?, 'active', 1)", + [$identityUuid, $encryptedPhone, $phoneHash] ); - - $user = [ - 'id' => $userId, - 'uuid' => $uuid, - 'full_name' => $encryptedName, - 'role' => $role, + $identity = [ + 'id' => $identityId, + 'uuid' => $identityUuid, + 'phone_number' => $encryptedPhone, + 'phone_hash' => $phoneHash, 'status' => 'active', - 'token_version' => 1, - 'school_id' => null, + 'token_version' => 1 + ]; + } + + $identityId = (int)$identity['id']; + $resolvedName = $fullName ?: ($role === 'teacher' ? 'الأستاذ المعتمد' : 'الطالب المتميز'); + + // Resolve or create specific Persona entity (Teacher, Student, Guardian) + if ($role === 'teacher') { + $teacher = Database::selectOne("SELECT * FROM teachers WHERE identity_id = ? LIMIT 1", [$identityId]); + if (!$teacher) { + $tUuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)); + $tId = Database::insert( + "INSERT INTO teachers (uuid, identity_id, full_name, specialization, bio, is_school_exclusive, is_marketplace_public) + VALUES (?, ?, ?, 'الرياضيات العلمي', 'معلم معتمد في منصة صَقِل', 0, 1)", + [$tUuid, $identityId, $resolvedName] + ); + $teacher = ['id' => $tId, 'uuid' => $tUuid, 'full_name' => $resolvedName]; + } + $user = [ + 'id' => $teacher['id'], + 'uuid' => $teacher['uuid'], + 'full_name' => $teacher['full_name'], + 'role' => 'teacher', + 'status' => 'active', + 'token_version' => $identity['token_version'], + 'school_id' => $teacher['school_id'] ?? null + ]; + } elseif ($role === 'guardian') { + $guardian = Database::selectOne("SELECT * FROM guardians WHERE identity_id = ? LIMIT 1", [$identityId]); + if (!$guardian) { + $gUuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)); + $gId = Database::insert( + "INSERT INTO guardians (uuid, identity_id, full_name) VALUES (?, ?, ?)", + [$gUuid, $identityId, $resolvedName] + ); + $guardian = ['id' => $gId, 'uuid' => $gUuid, 'full_name' => $resolvedName]; + } + $user = [ + 'id' => $guardian['id'], + 'uuid' => $guardian['uuid'], + 'full_name' => $guardian['full_name'], + 'role' => 'guardian', + 'status' => 'active', + 'token_version' => $identity['token_version'], + 'school_id' => null ]; } else { - // Verify role access if logging into specific portal - if ($role === 'teacher' && $user['role'] !== 'teacher' && $user['role'] !== 'super_admin') { - $response->status(403)->json([ - 'status' => 'error', - 'message' => 'هذا الحساب مسجل كطالب وليس معلماً. يرجى الدخول من بوابة الطالب.' - ]); - return; - } - - if ($user['status'] === 'suspended') { - $response->status(403)->json([ - 'status' => 'error', - 'message' => 'هذا الحساب معطل. يرجى مراجعة إدارة المنصة.' - ]); - return; - } - - // If name provided on existing profile, update if needed - if ($fullName && (empty($user['full_name']) || Security::decrypt($user['full_name']) === 'طالب جديد')) { - Database::query("UPDATE users SET full_name = ? WHERE id = ?", [Security::encrypt($fullName), $user['id']]); + // Student Role + $student = Database::selectOne("SELECT * FROM students WHERE identity_id = ? LIMIT 1", [$identityId]); + if (!$student) { + $sUuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)); + $natId = 'NAT' . substr($cleanPhone, -8); + $sId = Database::insert( + "INSERT INTO students (uuid, identity_id, national_id, full_name, grade_level, stream, is_school_sponsored) + VALUES (?, ?, ?, ?, 'tawjihi_2008', 'scientific', 0)", + [$sUuid, $identityId, $natId, $resolvedName] + ); + $student = ['id' => $sId, 'uuid' => $sUuid, 'full_name' => $resolvedName, 'school_id' => null]; } + $user = [ + 'id' => $student['id'], + 'uuid' => $student['uuid'], + 'full_name' => $student['full_name'], + 'role' => 'student', + 'status' => 'active', + 'token_version' => $identity['token_version'], + 'school_id' => $student['school_id'] ?? null + ]; } // 3. Register / Update Device Fingerprint in user_devices diff --git a/backend/app/Controllers/ChatController.php b/backend/app/Controllers/ChatController.php index 61b8e7f..04f0b5e 100644 --- a/backend/app/Controllers/ChatController.php +++ b/backend/app/Controllers/ChatController.php @@ -28,17 +28,12 @@ class ChatController $userId = (int)$request->user_id; - // 1. Fetch all other users in the system + // 1. Fetch available teachers and students $users = []; try { - $users = Database::select(" - SELECT u.id as user_id, u.uuid as user_uuid, u.full_name as encrypted_name, u.role as user_role, tp.specialization - FROM users u - LEFT JOIN teacher_profiles tp ON tp.user_id = u.id - WHERE u.id != ? - ORDER BY u.id DESC - LIMIT 100 - ", [$userId]); + $teachers = Database::select("SELECT id as user_id, uuid as user_uuid, full_name as encrypted_name, 'teacher' as user_role, specialization FROM teachers WHERE id != ? LIMIT 50", [$userId]); + $students = Database::select("SELECT id as user_id, uuid as user_uuid, full_name as encrypted_name, 'student' as user_role, 'طالب' as specialization FROM students WHERE id != ? LIMIT 50", [$userId]); + $users = array_merge($teachers, $students); } catch (\Throwable $e) { error_log("Users select in getConversations error: " . $e->getMessage()); } diff --git a/backend/app/Controllers/TeacherController.php b/backend/app/Controllers/TeacherController.php index 8c48267..b5c587f 100644 --- a/backend/app/Controllers/TeacherController.php +++ b/backend/app/Controllers/TeacherController.php @@ -17,46 +17,43 @@ class TeacherController */ public function profileStatus(Request $request, Response $response): void { - $userId = $request->user_id; + $teacherId = (int)$request->user_id; - $user = Database::selectOne("SELECT id, uuid, full_name, role, status FROM users WHERE id = ? LIMIT 1", [$userId]); - if (!$user) { - $response->status(404)->json(['status' => 'error', 'message' => 'المعلم غير موجود']); - return; + $teacher = Database::selectOne("SELECT id, uuid, full_name, specialization, bio, is_marketplace_public FROM teachers WHERE id = ? LIMIT 1", [$teacherId]); + if (!$teacher) { + // Auto create teacher record if not exists + $tUuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)); + $tId = Database::insert( + "INSERT INTO teachers (uuid, full_name, specialization, bio, is_marketplace_public) VALUES (?, 'الأستاذ المعتمد', 'الرياضيات العلمي', 'معلم معتمد في منصة صَقِل', 1)", + [$tUuid] + ); + $teacher = [ + 'id' => $tId, + 'uuid' => $tUuid, + 'full_name' => 'الأستاذ المعتمد', + 'specialization' => 'الرياضيات العلمي', + 'bio' => 'معلم معتمد في منصة صَقِل' + ]; } - $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)($teacher['full_name'] ?? ''); $decryptedName = Security::decrypt($rawName) ?: $rawName; if (empty($decryptedName)) { - $decryptedName = 'معلم صَقِل المعتمد'; + $decryptedName = 'الأستاذ المعتمد'; } $response->json([ 'status' => 'success', 'is_completed' => true, 'user' => [ - 'uuid' => $user['uuid'], + 'uuid' => $teacher['uuid'], 'full_name' => $decryptedName, - 'role' => $user['role'], - 'status' => $user['status'], + 'role' => 'teacher', + 'status' => 'active', ], - 'profile' => $profile ?: [ - 'specialization' => 'المعلم المعتمد', - 'bio' => 'معلم معتمد في منصة صَقِل' + 'profile' => [ + 'specialization' => $teacher['specialization'] ?? 'الرياضيات العلمي', + 'bio' => $teacher['bio'] ?? 'معلم معتمد في منصة صَقِل' ] ]); } diff --git a/backend/app/Services/TeacherRatingService.php b/backend/app/Services/TeacherRatingService.php index 193ae03..8826080 100644 --- a/backend/app/Services/TeacherRatingService.php +++ b/backend/app/Services/TeacherRatingService.php @@ -39,8 +39,8 @@ class TeacherRatingService created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_teacher_reviews (teacher_id), INDEX idx_student_reviews (student_id), - CONSTRAINT fk_tr_teacher FOREIGN KEY (teacher_id) REFERENCES users(id) ON DELETE CASCADE, - CONSTRAINT fk_tr_student FOREIGN KEY (student_id) REFERENCES users(id) ON DELETE CASCADE + CONSTRAINT fk_tr_teacher FOREIGN KEY (teacher_id) REFERENCES teachers(id) ON DELETE CASCADE, + CONSTRAINT fk_tr_student FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; "); @@ -66,7 +66,7 @@ class TeacherRatingService star_equivalent DECIMAL(3, 2) DEFAULT 4.90, reputation_tier VARCHAR(100) DEFAULT 'معلم نخبوي معتمد 💎', last_calculated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - CONSTRAINT fk_tpm_teacher FOREIGN KEY (teacher_id) REFERENCES users(id) ON DELETE CASCADE + CONSTRAINT fk_tpm_teacher FOREIGN KEY (teacher_id) REFERENCES teachers(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; "); } @@ -266,7 +266,7 @@ class TeacherRatingService } // Count enrolled students - $studentsCount = (int)(Database::selectOne("SELECT COUNT(*) as cnt FROM users WHERE role = 'student'")['cnt'] ?? 0); + $studentsCount = (int)(Database::selectOne("SELECT COUNT(*) as cnt FROM students")['cnt'] ?? 0); // Upsert into teacher_performance_metrics Database::query( @@ -419,18 +419,16 @@ class TeacherRatingService self::ensureSchema(); $teachers = Database::select( - "SELECT u.id, u.full_name, u.phone_number, - tp.specialization, tp.bio, + "SELECT t.id, t.full_name, t.specialization, t.bio, COALESCE(m.avg_response_minutes, 3) as avg_response_minutes, COALESCE(m.response_rate_percentage, 99.0) as response_rate_percentage, COALESCE(m.composite_merit_score, 96.50) as composite_merit_score, COALESCE(m.star_equivalent, 4.90) as star_equivalent, COALESCE(m.reputation_tier, 'معلم نخبوي معتمد 💎') as reputation_tier, - (SELECT COUNT(*) FROM lessons WHERE course_id IN (SELECT id FROM courses WHERE teacher_id = u.id)) as lessons_count - FROM users u - LEFT JOIN teacher_profiles tp ON u.id = tp.user_id - LEFT JOIN teacher_performance_metrics m ON u.id = m.teacher_id - WHERE u.role = 'teacher' + (SELECT COUNT(*) FROM lessons WHERE course_id IN (SELECT id FROM courses WHERE teacher_id = t.id)) as lessons_count + FROM teachers t + LEFT JOIN teacher_performance_metrics m ON t.id = m.teacher_id + WHERE t.is_marketplace_public = 1 ORDER BY " . ($sortBy === 'fastest' ? "COALESCE(m.avg_response_minutes, 10) ASC" : "COALESCE(m.composite_merit_score, 90.00) DESC") ); diff --git a/backend/app/Views/TeacherPortal.php b/backend/app/Views/TeacherPortal.php index f2167d8..6c3734e 100644 --- a/backend/app/Views/TeacherPortal.php +++ b/backend/app/Views/TeacherPortal.php @@ -10,7 +10,7 @@ class TeacherPortal \App\Services\CurriculumService::ensureSchema(); $coursesCount = (int)(\App\Core\Database::selectOne("SELECT COUNT(*) as cnt FROM courses")['cnt'] ?? 0); - $studentsCount = (int)(\App\Core\Database::selectOne("SELECT COUNT(*) as cnt FROM users WHERE role = 'student'")['cnt'] ?? 0); + $studentsCount = (int)(\App\Core\Database::selectOne("SELECT COUNT(*) as cnt FROM students")['cnt'] ?? 0); $lessons = \App\Core\Database::select( "SELECT l.*, COALESCE(c.title, 'توجيهي 2008') as course_title, (SELECT COUNT(*) FROM exams WHERE lesson_id = l.id AND scope = 'in_video_checkpoint') as checkpoints_count