request->getJSON(true) ?? []; $nisn = trim((string) ($payload['nisn'] ?? '')); $pin = (string) ($payload['pin'] ?? ''); if ($nisn === '' || $pin === '') { return $this->errorResponse('NISN dan PIN wajib diisi', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY); } $studentModel = new StudentModel(); $student = $studentModel->findByNisn($nisn); if (! $student) { return $this->errorResponse('Siswa dengan NISN tersebut tidak ditemukan', null, null, ResponseInterface::HTTP_NOT_FOUND); } if ((int) ($student->is_active ?? 1) === 0) { return $this->errorResponse('Siswa ini tidak aktif', null, null, ResponseInterface::HTTP_FORBIDDEN); } $accountModel = new StudentMobileAccountModel(); $accountRow = $accountModel->where('student_id', (int) $student->id)->first(); if (! $accountRow || empty($accountRow['pin_hash']) || ! password_verify($pin, $accountRow['pin_hash'])) { return $this->errorResponse('PIN salah', null, null, ResponseInterface::HTTP_UNAUTHORIZED); } $classModel = new ClassModel(); /** @var \App\Modules\Academic\Entities\ClassEntity|null $class */ $class = null; if ($student->class_id) { $class = $classModel->find($student->class_id); } $classLabel = '-'; if ($class !== null) { $parts = array_filter([ trim((string) ($class->grade ?? '')), trim((string) ($class->major ?? '')), trim((string) ($class->name ?? '')), ]); $label = implode(' ', $parts); $classLabel = $label !== '' ? $label : ('Kelas #' . (int) $class->id); } $data = [ 'student_id' => (int) $student->id, 'name' => (string) ($student->name ?? ''), 'nisn' => (string) ($student->nisn ?? ''), 'class_id' => $student->class_id ? (int) $student->class_id : null, 'class_label' => $classLabel, ]; return $this->successResponse($data, 'Login berhasil'); } /** * POST /api/mobile/register/nisn * * Body: { "nisn": "..." } */ public function checkNisn(): ResponseInterface { $payload = $this->request->getJSON(true) ?? []; $nisn = trim((string) ($payload['nisn'] ?? '')); if ($nisn === '') { return $this->errorResponse('NISN wajib diisi', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY); } $studentModel = new StudentModel(); $student = $studentModel->findByNisn($nisn); if (! $student) { return $this->errorResponse('Siswa dengan NISN tersebut tidak ditemukan', null, null, ResponseInterface::HTTP_NOT_FOUND); } if ((int) ($student->is_active ?? 1) === 0) { return $this->errorResponse('Siswa ini tidak aktif', null, null, ResponseInterface::HTTP_FORBIDDEN); } // Sudah punya akun mobile (PIN) → arahkan ke login $accountModel = new StudentMobileAccountModel(); $accountRow = $accountModel->where('student_id', (int) $student->id)->first(); if ($accountRow && ! empty($accountRow['pin_hash'])) { return $this->errorResponse('NISN ini sudah terdaftar. Silakan masuk.', null, null, ResponseInterface::HTTP_CONFLICT); } $classModel = new ClassModel(); $classes = $classModel->orderBy('grade', 'ASC') ->orderBy('major', 'ASC') ->orderBy('name', 'ASC') ->findAll(); $availableClasses = []; foreach ($classes as $c) { $parts = array_filter([ trim((string) ($c->grade ?? '')), trim((string) ($c->major ?? '')), trim((string) ($c->name ?? '')), ]); $label = implode(' ', $parts); $availableClasses[] = [ 'id' => (int) $c->id, 'label' => $label !== '' ? $label : ('Kelas #' . (int) $c->id), ]; } $currentClassId = $student->class_id ? (int) $student->class_id : null; $currentClassLabel = null; if ($currentClassId !== null) { foreach ($availableClasses as $cls) { if ($cls['id'] === $currentClassId) { $currentClassLabel = $cls['label']; break; } } } $data = [ 'student_id' => (int) $student->id, 'name' => (string) ($student->name ?? ''), 'nisn' => (string) ($student->nisn ?? ''), 'current_class_id' => $currentClassId, 'current_class_label' => $currentClassLabel, 'available_classes' => $availableClasses, ]; return $this->successResponse($data, 'NISN valid'); } /** * POST /api/mobile/register/complete * * Body: { "student_id": 0, "class_id": 0, "pin": "123456" } */ public function complete(): ResponseInterface { $payload = $this->request->getJSON(true) ?? []; $studentId = (int) ($payload['student_id'] ?? 0); $classId = (int) ($payload['class_id'] ?? 0); $pin = (string) ($payload['pin'] ?? ''); if ($studentId <= 0) { return $this->errorResponse('student_id wajib diisi', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY); } if ($classId <= 0) { return $this->errorResponse('Kelas wajib dipilih', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY); } $pin = trim($pin); if ($pin === '' || strlen($pin) < 4) { return $this->errorResponse('PIN minimal 4 karakter', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY); } $studentModel = new StudentModel(); $classModel = new ClassModel(); $student = $studentModel->find($studentId); if (! $student) { return $this->errorResponse('Siswa tidak ditemukan', null, null, ResponseInterface::HTTP_NOT_FOUND); } $class = $classModel->find($classId); if (! $class) { return $this->errorResponse('Kelas tidak ditemukan', null, null, ResponseInterface::HTTP_NOT_FOUND); } // Update class_id siswa (mapping pertama kali / koreksi) $studentModel->update($studentId, ['class_id' => $classId]); // Simpan / update akun mobile siswa $accountModel = new StudentMobileAccountModel(); $pinHash = password_hash($pin, PASSWORD_BCRYPT); $exists = $accountModel->where('student_id', $studentId)->first(); if ($exists) { $accountModel->update((int) $exists['id'], [ 'pin_hash' => $pinHash, ]); } else { $accountModel->insert([ 'student_id' => $studentId, 'pin_hash' => $pinHash, ]); } $classLabelParts = array_filter([ trim((string) ($class->grade ?? '')), trim((string) ($class->major ?? '')), trim((string) ($class->name ?? '')), ]); $classLabel = implode(' ', $classLabelParts); $data = [ 'student_id' => $studentId, 'name' => (string) ($student->name ?? ''), 'nisn' => (string) ($student->nisn ?? ''), 'class_id' => (int) $class->id, 'class_label' => $classLabel !== '' ? $classLabel : ('Kelas #' . (int) $class->id), ]; return $this->successResponse($data, 'Registrasi mobile berhasil'); } /** * POST /api/mobile/forgot-pin * * Body: { "nisn": "...", "new_pin": "1234", "new_pin_confirm": "1234" } */ public function forgotPin(): ResponseInterface { $payload = $this->request->getJSON(true) ?? []; $nisn = trim((string) ($payload['nisn'] ?? '')); $newPin = (string) ($payload['new_pin'] ?? ''); $newPinConfirm = (string) ($payload['new_pin_confirm'] ?? ''); if ($nisn === '') { return $this->errorResponse('NISN wajib diisi', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY); } $newPin = trim($newPin); $newPinConfirm = trim($newPinConfirm); if ($newPin === '' || strlen($newPin) < 4) { return $this->errorResponse('PIN baru minimal 4 karakter', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY); } if ($newPin !== $newPinConfirm) { return $this->errorResponse('PIN baru dan konfirmasi tidak sama', null, null, ResponseInterface::HTTP_UNPROCESSABLE_ENTITY); } $studentModel = new StudentModel(); $student = $studentModel->findByNisn($nisn); if (! $student) { return $this->errorResponse('Siswa dengan NISN tersebut tidak ditemukan', null, null, ResponseInterface::HTTP_NOT_FOUND); } if ((int) ($student->is_active ?? 1) === 0) { return $this->errorResponse('Siswa ini tidak aktif', null, null, ResponseInterface::HTTP_FORBIDDEN); } $accountModel = new StudentMobileAccountModel(); $accountRow = $accountModel->where('student_id', (int) $student->id)->first(); if (! $accountRow || empty($accountRow['pin_hash'])) { return $this->errorResponse('NISN ini belum terdaftar. Silakan daftar dulu.', null, null, ResponseInterface::HTTP_CONFLICT); } $pinHash = password_hash($newPin, PASSWORD_BCRYPT); $accountModel->update((int) $accountRow['id'], ['pin_hash' => $pinHash]); return $this->successResponse( ['student_id' => (int) $student->id], 'PIN berhasil direset. Silakan masuk dengan PIN baru.' ); } }