Update Saqel Platform: 2026-08-28 15:13:58
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'] ?? 'معلم معتمد في منصة صَقِل'
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user