where('ip_address', $ip)->first(); if (!$row || !$row->blocked_until) { return false; } return $row->blocked_until->isFuture(); } public function logIpAttempt(string $ip): void { $now = utc_now(); $attempt = IpAttempt::query()->where('ip_address', $ip)->first(); if ($attempt) { $attempts = ((int) $attempt->attempts) + 1; $blockedUntil = null; if ($attempts >= 10) { $blockedUntil = date('Y-m-d H:i:s', strtotime('+24 hours')); } $attempt->update([ 'attempts' => $attempts, 'last_attempt_at' => $now, 'blocked_until' => $blockedUntil, ]); return; } IpAttempt::query()->create([ 'ip_address' => $ip, 'attempts' => 1, 'last_attempt_at' => $now, ]); } public function handleFailedLogin(User $user, string $email, string $ip): void { $user->refresh(); $failedAttempts = ((int) ($user->failed_attempts ?? 0)) + 1; $data = [ 'failed_attempts' => $failedAttempts, 'last_failed_at' => utc_now(), ]; if ($failedAttempts >= 3) { $data['is_suspended'] = true; // CI sends reset email via View\UserController::sendResetEmail — wire Laravel mail here when parity is needed. Log::notice('api_login: account suspended after failed attempts', ['email' => $email, 'user_id' => $user->id]); } $user->fill($data); $user->save(); $this->logIpAttempt($ip); } public function resetFailedAttempts(User $user): void { $user->failed_attempts = 0; $user->last_failed_at = null; $user->save(); } public function logSuccessfulLogin(User $user, Request $request): void { $schoolYear = trim((string) (Configuration::getConfig('school_year') ?? date('Y'))); $semester = trim((string) (Configuration::getConfig('semester') ?? 'Fall')); LoginActivity::query()->create([ 'user_id' => $user->id, 'email' => $user->email, 'login_time' => utc_now(), 'ip_address' => $request->ip(), 'user_agent' => (string) ($request->userAgent() ?? ''), 'school_year' => $schoolYear, 'semester' => $semester !== '' ? $semester : 'Fall', ]); } /** * CI returns roles as an object map {"RoleName": true}. * * @param list $roleNames */ public function rolesToObjectMap(array $roleNames): object { $rolesMap = []; foreach ($roleNames as $r) { $rolesMap[$r] = true; } return (object) $rolesMap; } /** * Top-level JSON body aligned with the CodeIgniter 4 `AuthController::apiLogin` success payload. * * @return array{status: true, token: string, user: array{id: int, name: string, roles: object}} */ public function buildLoginResponse(User $user, string $jwtToken): array { $roleNames = $user->roleNamesLikeCodeIgniter(); $fullName = trim(($user->firstname ?? '').' '.($user->lastname ?? '')); return [ 'status' => true, 'token' => $jwtToken, 'user' => [ 'id' => (int) $user->id, 'name' => $fullName, 'roles' => $this->rolesToObjectMap($roleNames), ], ]; } }