feat: Implement National ID sub-account login flow for students
- Updates verifyOtp to return identity_token for students instead of auto-login - Adds verifyNationalId API endpoint to select or create a student profile under a single phone number - Allows multiple students per guardian phone number
This commit is contained in:
@@ -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'
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user