Harden published curriculum and student flows

This commit is contained in:
Hamza-Ayed
2026-09-30 08:22:17 +03:00
parent e8163fe265
commit ce7b0fcf14
53 changed files with 4723 additions and 3317 deletions
+38 -56
View File
@@ -517,10 +517,9 @@ class AuthController
if ($user) {
$rawGrade = (string)($user['grade_level'] ?? 'grade_10');
$normGrade = \App\Services\StudentAccessControlService::normalizeGrade($rawGrade);
if ($rawGrade === 'tawjihi_2008' || $rawGrade !== $normGrade) {
Database::query("UPDATE students SET grade_level = ?, updated_at = NOW() WHERE id = ?", [$normGrade, $userId]);
$user['grade_level'] = $normGrade;
}
// Profile reads may normalize presentation, but must not rewrite
// a student's academic record as a side effect of GET.
$user['grade_level'] = $normGrade;
$isCompleted = !empty($user['full_name']) && $user['full_name'] !== 'طالب جديد' && $user['full_name'] !== 'الطالب المتميز' && !empty($user['grade_level']);
$userData = [
'id' => $user['id'],
@@ -586,11 +585,6 @@ class AuthController
$student = Database::selectOne("SELECT * FROM students WHERE national_id_hash = ? LIMIT 1", [$nationalIdHash]);
if ($student) {
if (($student['grade_level'] ?? '') === 'tawjihi_2008') {
Database::query("UPDATE students SET grade_level = 'grade_10', updated_at = NOW() WHERE id = ?", [$student['id']]);
$student['grade_level'] = 'grade_10';
}
// Student exists. Verify identity linkage.
if ($student['identity_id'] === null) {
// Pre-registered by Guardian, link to this identity phone now
@@ -736,41 +730,14 @@ class AuthController
null
);
} else {
// Legacy / Fallback for already logged-in students updating profile
$studentId = (int)$request->user_id;
if (!$studentId) {
$authHeader = $request->getHeader('authorization', '');
if ($authHeader && preg_match('/Bearer\s(\S+)/i', $authHeader, $matches)) {
$payload = Security::verifyJWT($matches[1]);
if ($payload && isset($payload['user_id'])) {
$studentId = (int)$payload['user_id'];
}
}
}
if (!$studentId) {
$response->status(401)->json(['status' => 'error', 'message' => 'غير مصرح']);
return;
}
Database::query(
"UPDATE students SET full_name = ?, grade_level = ?, stream = ?, national_id = IF(? != '', ?, national_id), national_id_hash = IF(? != '', ?, national_id_hash), updated_at = NOW() WHERE id = ?",
[$fullName, $gradeLevel, $stream, $nationalId, Security::encrypt($nationalId), $nationalId, Security::blindIndex($nationalId), $studentId]
);
$student = Database::selectOne("SELECT * FROM students WHERE id = ? LIMIT 1", [$studentId]);
$response->json([
'status' => 'success',
'message' => 'تم استكمال ملف الطالب بنجاح! مرحباً بك في منصة صَقِل',
'user' => [
'id' => $student['id'],
'uuid' => $student['uuid'],
'full_name' => $student['full_name'],
'grade_level' => $student['grade_level'],
'stream' => $student['stream'],
'role' => 'student'
]
// This endpoint accepts only a pending student-identity proof.
// A bare JWT here previously bypassed the active-session and role
// checks and allowed changing a national ID without re-verification.
$response->status(401)->json([
'status' => 'identity_proof_required',
'message' => 'إكمال الملف يتطلب رمز التحقق من الهوية. لتغيير صف طالب مستقل استخدم مسار تغيير الصف المصادق.',
]);
return;
}
}
@@ -781,30 +748,45 @@ class AuthController
public function updateStudentGrade(Request $request, Response $response): void
{
$studentId = (int)$request->user_id;
if (!$studentId) {
$authHeader = $request->getHeader('authorization', '');
if ($authHeader && preg_match('/Bearer\s(\S+)/i', $authHeader, $matches)) {
$payload = Security::verifyJWT($matches[1]);
if ($payload && isset($payload['user_id'])) {
$studentId = (int)$payload['user_id'];
}
}
}
if (!$studentId) {
$response->status(401)->json(['status' => 'error', 'message' => 'غير مصرح']);
return;
}
$body = $request->getBody();
$rawGrade = trim((string)($body['grade_level'] ?? 'grade_10'));
$stream = trim((string)($body['stream'] ?? 'general'));
$rawGrade = trim((string)($body['grade_level'] ?? ''));
$stream = trim((string)($body['stream'] ?? ''));
$normalizedGrade = \App\Services\StudentAccessControlService::normalizeGrade($rawGrade);
if (!preg_match('/^grade_(?:[1-9]|1[0-2])$/', $normalizedGrade)
|| !in_array($stream, ['scientific', 'literary', 'vocational', 'general'], true)) {
$response->status(400)->json(['status' => 'error', 'message' => 'الصف أو المسار غير صالح.']);
return;
}
Database::query(
"UPDATE students SET grade_level = ?, stream = ?, updated_at = NOW() WHERE id = ?",
$scope = Database::selectOne('SELECT id, school_id, is_school_sponsored FROM students WHERE id=? LIMIT 1', [$studentId]);
if (!$scope) {
$response->status(404)->json(['status' => 'error', 'message' => 'طالب غير موجود']);
return;
}
if (!empty($scope['school_id']) || !empty($scope['is_school_sponsored'])) {
$response->status(403)->json(['status' => 'forbidden', 'message' => 'صف الطالب المدرسي يحدّثه مسؤول المدرسة من كشف الانتساب.']);
return;
}
$changed = Database::execute(
"UPDATE students SET grade_level = ?, stream = ?, updated_at = NOW()
WHERE id = ? AND school_id IS NULL AND is_school_sponsored = 0",
[$normalizedGrade, $stream, $studentId]
);
if ($changed !== 1) {
$after = Database::selectOne('SELECT grade_level, stream, school_id, is_school_sponsored FROM students WHERE id=? LIMIT 1', [$studentId]);
if (!$after || !empty($after['school_id']) || !empty($after['is_school_sponsored'])
|| (string)$after['grade_level'] !== $normalizedGrade || (string)$after['stream'] !== $stream) {
$response->status(409)->json(['status' => 'conflict', 'message' => 'تغير نطاق حساب الطالب؛ أعد تحميل ملفك الدراسي.']);
return;
}
}
$student = Database::selectOne(
"SELECT s.id, s.uuid, s.full_name, s.national_id, s.grade_level, s.stream, s.readiness_score, s.school_id, ai.phone_number, ai.status, s.created_at