diff --git a/backend/app/Controllers/AuthController.php b/backend/app/Controllers/AuthController.php index 04412f3..fd00ba7 100644 --- a/backend/app/Controllers/AuthController.php +++ b/backend/app/Controllers/AuthController.php @@ -254,27 +254,22 @@ class AuthController 'school_id' => null ]; } else { - // 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 - ]; + // Student Role - Requires National ID Phase (Netflix-style Profile Selection via National ID) + $identityToken = Security::generateJWT([ + 'identity_id' => $identityId, + 'role' => 'student_identity_pending', + 'phone' => $cleanPhone, + ], 3600); // 1 hour validity + + $response->status(200)->json([ + 'status' => 'success', + 'message' => 'تم التحقق من رقم الهاتف بنجاح. يرجى إدخال الرقم الوطني للمتابعة.', + 'data' => [ + 'identity_token' => $identityToken, + 'requires_national_id' => true + ] + ]); + return; } // 3. Register / Update Device Fingerprint in user_devices @@ -461,6 +456,67 @@ class AuthController ]); } + /** + * Verify Student National ID (Profile Selection / Sub-account Login) + * POST /api/auth/student/login-national-id + */ + public function verifyNationalId(Request $request, Response $response): void + { + $body = $request->getJSON(); + $identityToken = $body['identity_token'] ?? ''; + $nationalId = trim((string)($body['national_id'] ?? '')); + + if (!$identityToken || !$nationalId) { + $response->status(400)->json(['status' => 'error', 'message' => 'الرقم الوطني مطلوب']); + return; + } + + $decoded = Security::verifyJWT($identityToken); + if (!$decoded || ($decoded->role ?? '') !== 'student_identity_pending') { + $response->status(401)->json(['status' => 'error', 'message' => 'الجلسة غير صالحة، يرجى إعادة التحقق من رقم الهاتف']); + return; + } + + $identityId = (int)$decoded->identity_id; + + // Check if student exists with this National ID + $student = Database::selectOne("SELECT * FROM students WHERE national_id = ? LIMIT 1", [$nationalId]); + + if ($student) { + // Student exists. Verify identity linkage. + if ($student['identity_id'] === null) { + // Pre-registered by Guardian, link to this identity phone now + Database::query("UPDATE students SET identity_id = ? WHERE id = ?", [$identityId, $student['id']]); + } elseif ((int)$student['identity_id'] !== $identityId) { + $response->status(403)->json(['status' => 'error', 'message' => 'الرقم الوطني مسجل ومربوط برقم هاتف آخر. يرجى مراجعة الدعم الفني.']); + return; + } + + // Generate full student session! + $this->generateSessionAndRespond( + (int)$student['id'], + $student['uuid'], + 'student', + 'browser_default', + $decoded->phone, + $student['full_name'], + $response, + 'تم تسجيل الدخول لملف الطالب بنجاح' + ); + } else { + // New Student! Forward to Onboarding. + $response->status(200)->json([ + 'status' => 'success', + 'message' => 'الرقم الوطني غير مسجل مسبقاً، يرجى استكمال البيانات لفتح ملف جديد.', + 'data' => [ + 'requires_onboarding' => true, + 'identity_token' => $identityToken, + 'national_id' => $nationalId + ] + ]); + } + } + /** * Check if Student Profile is complete * GET /api/student/profile/status @@ -501,38 +557,78 @@ class AuthController */ public function studentProfileSetup(Request $request, Response $response): void { - $studentId = (int)$request->user_id; $body = $request->getBody(); - + $identityToken = $body['identity_token'] ?? ''; + $fullName = trim((string)($body['full_name'] ?? '')); $gradeLevel = trim((string)($body['grade_level'] ?? 'grade_10')); $stream = trim((string)($body['stream'] ?? 'scientific')); $nationalId = trim((string)($body['national_id'] ?? '')); - if (empty($fullName)) { - $response->status(400)->json(['status' => 'error', 'message' => 'الاسم الكامل مطلوب']); + if (empty($fullName) || empty($nationalId)) { + $response->status(400)->json(['status' => 'error', 'message' => 'الاسم الكامل والرقم الوطني مطلوبان']); return; } - Database::query( - "UPDATE students SET full_name = ?, grade_level = ?, stream = ?, national_id = IF(? != '', ?, national_id), updated_at = NOW() WHERE id = ?", - [$fullName, $gradeLevel, $stream, $nationalId, $nationalId, $studentId] - ); + if ($identityToken) { + // New Student Flow + $decoded = Security::verifyJWT($identityToken); + if (!$decoded || ($decoded->role ?? '') !== 'student_identity_pending') { + $response->status(401)->json(['status' => 'error', 'message' => 'الجلسة غير صالحة']); + return; + } + $identityId = (int)$decoded->identity_id; - $student = Database::selectOne("SELECT * FROM students WHERE id = ? LIMIT 1", [$studentId]); + // Ensure National ID doesn't exist + $existing = Database::selectOne("SELECT id FROM students WHERE national_id = ? LIMIT 1", [$nationalId]); + if ($existing) { + $response->status(400)->json(['status' => 'error', 'message' => 'الرقم الوطني مستخدم مسبقاً']); + return; + } - $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' - ] - ]); + $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)); + + $sId = Database::insert( + "INSERT INTO students (uuid, identity_id, national_id, full_name, grade_level, stream, is_school_sponsored) + VALUES (?, ?, ?, ?, ?, ?, 0)", + [$sUuid, $identityId, $nationalId, $fullName, $gradeLevel, $stream] + ); + + // Auto-link to Guardian if exists on this phone + $guardian = Database::selectOne("SELECT id FROM guardians WHERE identity_id = ? LIMIT 1", [$identityId]); + if ($guardian) { + Database::insert("INSERT IGNORE INTO guardian_students (guardian_id, student_id) VALUES (?, ?)", [$guardian['id'], $sId]); + } + + $this->generateSessionAndRespond( + $sId, $sUuid, 'student', 'browser_default', $decoded->phone, $fullName, $response, 'تم استكمال التسجيل بنجاح' + ); + } else { + // Legacy / Fallback for already logged-in students updating profile + $studentId = (int)$request->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), updated_at = NOW() WHERE id = ?", + [$fullName, $gradeLevel, $stream, $nationalId, $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' + ] + ]); + } } /** diff --git a/backend/app/Views/StudentPortal.php b/backend/app/Views/StudentPortal.php index d540e50..8561372 100644 --- a/backend/app/Views/StudentPortal.php +++ b/backend/app/Views/StudentPortal.php @@ -590,6 +590,24 @@ class StudentPortal + + +