Update Saqel Platform: 2026-08-28 15:13:58

This commit is contained in:
Hamza-Ayed
2026-08-28 15:13:58 +03:00
parent 1c39d4c9cb
commit 06461635bf
5 changed files with 116 additions and 88 deletions
+78 -40
View File
@@ -187,59 +187,97 @@ class AuthController
return; return;
} }
// 2. Find or Create User // 2. Find or Create Auth Identity (Central Multi-Persona Auth)
$user = Database::selectOne("SELECT * FROM users WHERE phone_hash = ? LIMIT 1", [$phoneHash]); $identity = Database::selectOne("SELECT * FROM auth_identities WHERE phone_hash = ? LIMIT 1", [$phoneHash]);
if (!$user) { if (!$identity) {
// New user registration $identityUuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
$uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
); );
$encryptedPhone = Security::encrypt($cleanPhone); $encryptedPhone = Security::encrypt($cleanPhone);
$encryptedName = Security::encrypt($fullName ?: ($role === 'teacher' ? 'معلم جديد' : 'طالب جديد')); $identityId = Database::insert(
$randomPassword = password_hash(bin2hex(random_bytes(16)), PASSWORD_BCRYPT); "INSERT INTO auth_identities (uuid, phone_number, phone_hash, status, token_version) VALUES (?, ?, ?, 'active', 1)",
[$identityUuid, $encryptedPhone, $phoneHash]
$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]
); );
$identity = [
$user = [ 'id' => $identityId,
'id' => $userId, 'uuid' => $identityUuid,
'uuid' => $uuid, 'phone_number' => $encryptedPhone,
'full_name' => $encryptedName, 'phone_hash' => $phoneHash,
'role' => $role,
'status' => 'active', 'status' => 'active',
'token_version' => 1, 'token_version' => 1
'school_id' => null, ];
}
$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 { } else {
// Verify role access if logging into specific portal // Student Role
if ($role === 'teacher' && $user['role'] !== 'teacher' && $user['role'] !== 'super_admin') { $student = Database::selectOne("SELECT * FROM students WHERE identity_id = ? LIMIT 1", [$identityId]);
$response->status(403)->json([ if (!$student) {
'status' => 'error', $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));
'message' => 'هذا الحساب مسجل كطالب وليس معلماً. يرجى الدخول من بوابة الطالب.' $natId = 'NAT' . substr($cleanPhone, -8);
]); $sId = Database::insert(
return; "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]
if ($user['status'] === 'suspended') { );
$response->status(403)->json([ $student = ['id' => $sId, 'uuid' => $sUuid, 'full_name' => $resolvedName, 'school_id' => null];
'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']]);
} }
$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 // 3. Register / Update Device Fingerprint in user_devices
+4 -9
View File
@@ -28,17 +28,12 @@ class ChatController
$userId = (int)$request->user_id; $userId = (int)$request->user_id;
// 1. Fetch all other users in the system // 1. Fetch available teachers and students
$users = []; $users = [];
try { try {
$users = Database::select(" $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]);
SELECT u.id as user_id, u.uuid as user_uuid, u.full_name as encrypted_name, u.role as user_role, tp.specialization $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]);
FROM users u $users = array_merge($teachers, $students);
LEFT JOIN teacher_profiles tp ON tp.user_id = u.id
WHERE u.id != ?
ORDER BY u.id DESC
LIMIT 100
", [$userId]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
error_log("Users select in getConversations error: " . $e->getMessage()); error_log("Users select in getConversations error: " . $e->getMessage());
} }
+24 -27
View File
@@ -17,46 +17,43 @@ class TeacherController
*/ */
public function profileStatus(Request $request, Response $response): void 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]); $teacher = Database::selectOne("SELECT id, uuid, full_name, specialization, bio, is_marketplace_public FROM teachers WHERE id = ? LIMIT 1", [$teacherId]);
if (!$user) { if (!$teacher) {
$response->status(404)->json(['status' => 'error', 'message' => 'المعلم غير موجود']); // Auto create teacher record if not exists
return; $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]); $rawName = (string)($teacher['full_name'] ?? '');
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'] ?? '');
$decryptedName = Security::decrypt($rawName) ?: $rawName; $decryptedName = Security::decrypt($rawName) ?: $rawName;
if (empty($decryptedName)) { if (empty($decryptedName)) {
$decryptedName = 'معلم صَقِل المعتمد'; $decryptedName = 'الأستاذ المعتمد';
} }
$response->json([ $response->json([
'status' => 'success', 'status' => 'success',
'is_completed' => true, 'is_completed' => true,
'user' => [ 'user' => [
'uuid' => $user['uuid'], 'uuid' => $teacher['uuid'],
'full_name' => $decryptedName, 'full_name' => $decryptedName,
'role' => $user['role'], 'role' => 'teacher',
'status' => $user['status'], 'status' => 'active',
], ],
'profile' => $profile ?: [ 'profile' => [
'specialization' => 'المعلم المعتمد', 'specialization' => $teacher['specialization'] ?? 'الرياضيات العلمي',
'bio' => 'معلم معتمد في منصة صَقِل' 'bio' => $teacher['bio'] ?? 'معلم معتمد في منصة صَقِل'
] ]
]); ]);
} }
+9 -11
View File
@@ -39,8 +39,8 @@ class TeacherRatingService
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_teacher_reviews (teacher_id), INDEX idx_teacher_reviews (teacher_id),
INDEX idx_student_reviews (student_id), INDEX idx_student_reviews (student_id),
CONSTRAINT fk_tr_teacher FOREIGN KEY (teacher_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 users(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; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
"); ");
@@ -66,7 +66,7 @@ class TeacherRatingService
star_equivalent DECIMAL(3, 2) DEFAULT 4.90, star_equivalent DECIMAL(3, 2) DEFAULT 4.90,
reputation_tier VARCHAR(100) DEFAULT 'معلم نخبوي معتمد 💎', reputation_tier VARCHAR(100) DEFAULT 'معلم نخبوي معتمد 💎',
last_calculated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 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; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
"); ");
} }
@@ -266,7 +266,7 @@ class TeacherRatingService
} }
// Count enrolled students // 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 // Upsert into teacher_performance_metrics
Database::query( Database::query(
@@ -419,18 +419,16 @@ class TeacherRatingService
self::ensureSchema(); self::ensureSchema();
$teachers = Database::select( $teachers = Database::select(
"SELECT u.id, u.full_name, u.phone_number, "SELECT t.id, t.full_name, t.specialization, t.bio,
tp.specialization, tp.bio,
COALESCE(m.avg_response_minutes, 3) as avg_response_minutes, COALESCE(m.avg_response_minutes, 3) as avg_response_minutes,
COALESCE(m.response_rate_percentage, 99.0) as response_rate_percentage, COALESCE(m.response_rate_percentage, 99.0) as response_rate_percentage,
COALESCE(m.composite_merit_score, 96.50) as composite_merit_score, COALESCE(m.composite_merit_score, 96.50) as composite_merit_score,
COALESCE(m.star_equivalent, 4.90) as star_equivalent, COALESCE(m.star_equivalent, 4.90) as star_equivalent,
COALESCE(m.reputation_tier, 'معلم نخبوي معتمد 💎') as reputation_tier, 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 (SELECT COUNT(*) FROM lessons WHERE course_id IN (SELECT id FROM courses WHERE teacher_id = t.id)) as lessons_count
FROM users u FROM teachers t
LEFT JOIN teacher_profiles tp ON u.id = tp.user_id LEFT JOIN teacher_performance_metrics m ON t.id = m.teacher_id
LEFT JOIN teacher_performance_metrics m ON u.id = m.teacher_id WHERE t.is_marketplace_public = 1
WHERE u.role = 'teacher'
ORDER BY " . ($sortBy === 'fastest' ? "COALESCE(m.avg_response_minutes, 10) ASC" : "COALESCE(m.composite_merit_score, 90.00) DESC") ORDER BY " . ($sortBy === 'fastest' ? "COALESCE(m.avg_response_minutes, 10) ASC" : "COALESCE(m.composite_merit_score, 90.00) DESC")
); );
+1 -1
View File
@@ -10,7 +10,7 @@ class TeacherPortal
\App\Services\CurriculumService::ensureSchema(); \App\Services\CurriculumService::ensureSchema();
$coursesCount = (int)(\App\Core\Database::selectOne("SELECT COUNT(*) as cnt FROM courses")['cnt'] ?? 0); $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( $lessons = \App\Core\Database::select(
"SELECT l.*, COALESCE(c.title, 'توجيهي 2008') as course_title, "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 (SELECT COUNT(*) FROM exams WHERE lesson_id = l.id AND scope = 'in_video_checkpoint') as checkpoints_count