all(); if ($request->isJson() && ($payload === [] || $payload === null)) { $decoded = json_decode((string) $request->getContent(), true); $payload = is_array($decoded) ? $decoded : []; } $email = strtolower(trim((string) ($payload['email'] ?? ''))); $password = (string) ($payload['password'] ?? ''); if ($email === '' || $password === '') { return response()->json([ 'status' => false, 'message' => 'Email and password are required.', ], 400); } $ip = $request->ip(); if ($security->isIpBlocked((string) $ip)) { return response()->json([ 'status' => false, 'message' => 'Too many failed attempts from your IP. Please try again later.', ], 429); } $user = User::query()->whereRaw('LOWER(email) = ?', [$email])->first(); if (!$user) { $security->logIpAttempt((string) $ip); return response()->json([ 'status' => false, 'message' => 'Invalid email or password.', ], 401); } if (! ci_password_verify($password, (string) $user->password)) { $security->handleFailedLogin($user, $email, (string) $ip); return response()->json([ 'status' => false, 'message' => 'Invalid email or password.', ], 401); } // Suspension is checked AFTER credentials are verified so that the // response shape cannot be used to enumerate which emails exist. if ($user->is_suspended) { return response()->json([ 'status' => false, 'message' => 'Account suspended. Please reset your password.', ], 403); } $security->resetFailedAttempts($user); $security->logSuccessfulLogin($user->fresh(), $request); $fresh = $user->fresh(); if (!$fresh) { return response()->json([ 'status' => false, 'message' => 'Invalid email or password.', ], 401); } $jwtToken = JWTAuth::fromUser($fresh); return response()->json($security->buildLoginResponse($fresh, $jwtToken)); } public function refresh(Request $request): JsonResponse { try { $newToken = JWTAuth::parseToken()->refresh(); } catch (\Throwable $e) { return $this->respondError('Token refresh failed.', 401); } return $this->respondSuccess([ 'access_token' => $newToken, 'token_type' => 'bearer', 'expires_in' => (int) config('jwt.ttl') * 60, ], 'Token refreshed.'); } public function me(): JsonResponse { $user = Auth::user(); if (!$user) { return $this->respondError('Unauthorized.', 401); } $teacherContext = $user->teacherSessionContext(); return $this->respondSuccess([ 'id' => (int) $user->id, 'firstname' => $user->firstname, 'lastname' => $user->lastname, 'email' => $user->email, 'class_section_id' => $teacherContext['class_section_id'], 'class_section_name' => $teacherContext['class_section_name'], ], 'OK'); } public function logout(Request $request): JsonResponse { $token = $request->bearerToken(); if ($token && substr_count($token, '.') === 2) { try { JWTAuth::setToken($token)->invalidate(); } catch (\Throwable $e) { // ignore invalidation errors } } $user = $request->user(); if ($user && method_exists($user, 'currentAccessToken')) { $current = $user->currentAccessToken(); if ($current) { $current->delete(); } } return $this->respondSuccess(null, 'Logged out.'); } }