update controllers logic
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Models\IpAttempt;
|
||||
use App\Models\LoginActivity;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Mirrors CodeIgniter `AuthController::apiLogin` security behavior:
|
||||
* IP lockout (ip_attempts), failed_attempts / suspension after 3 failures, login_activity logging.
|
||||
*/
|
||||
class ApiLoginSecurityService
|
||||
{
|
||||
public function isIpBlocked(string $ip): bool
|
||||
{
|
||||
$row = IpAttempt::query()->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
|
||||
{
|
||||
LoginActivity::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'login_time' => utc_now(),
|
||||
'ip_address' => $request->ip(),
|
||||
'user_agent' => (string) ($request->userAgent() ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* CI returns roles as an object map {"RoleName": true}.
|
||||
*
|
||||
* @param list<string> $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),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user