LoginController.php 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. <?php
  2. namespace Controllers;
  3. use Firebase\JWT\JWT;
  4. use Libs\Logger;
  5. use Libs\Payload;
  6. use Libs\RateLimiter;
  7. use Libs\Validator;
  8. use Models\UserModel;
  9. use Psr\Http\Message\ServerRequestInterface;
  10. class LoginController
  11. {
  12. /** Máximo de tentativas falhas por IP antes do bloqueio temporário. */
  13. private const MAX_ATTEMPTS = 5;
  14. /** Duração da janela de bloqueio, em segundos (15 minutos). */
  15. private const WINDOW_SECONDS = 900;
  16. public function __invoke(ServerRequestInterface $request)
  17. {
  18. // Anti brute-force: chaveado por IP de origem. Conta apenas tentativas
  19. // falhas; uma autenticação bem-sucedida zera o contador.
  20. $rateKey = 'login:' . $this->clientIp($request);
  21. $retryAfter = RateLimiter::retryAfter($rateKey, self::MAX_ATTEMPTS);
  22. if ($retryAfter > 0) {
  23. Logger::warning('Login rate limit exceeded', ['ip' => $this->clientIp($request)]);
  24. return Payload::fail('Too many login attempts. Try again later.', [], 'E_RATE_LIMIT', 429)
  25. ->withHeader('Retry-After', (string) $retryAfter);
  26. }
  27. $body = json_decode((string) $request->getBody(), true) ?: [];
  28. $email = $body['email'] ?? $body['user_email'] ?? '';
  29. $password = $body['password'] ?? '';
  30. $validator = (new Validator(['email' => $email, 'password' => $password]))
  31. ->required('email')->email('email')->maxLength('email', 255)
  32. ->required('password');
  33. if ($validator->fails()) {
  34. return Payload::fail($validator->firstError(), [], 'E_VALIDATE', 400);
  35. }
  36. $secret = $_ENV['JWT_SECRET'] ?? '';
  37. if ($secret === '') {
  38. Logger::error('JWT_SECRET is not configured; cannot issue tokens');
  39. return Payload::fail('Internal server error', [], 'E_GENERIC', 500);
  40. }
  41. $userModel = new UserModel();
  42. $user = $userModel->validateLogin($email, $password);
  43. if (!$user) {
  44. RateLimiter::hit($rateKey, self::WINDOW_SECONDS);
  45. return Payload::fail('Invalid credentials', [], 'E_VALIDATE', 401);
  46. }
  47. RateLimiter::clear($rateKey);
  48. $payload = [
  49. 'sub' => $user['user_id'],
  50. 'email' => $user['user_email'],
  51. 'company_id' => $user['company_id'],
  52. 'role' => $user['user_role'],
  53. 'iat' => time(),
  54. 'exp' => time() + 3600
  55. ];
  56. $jwt = JWT::encode($payload, $secret, 'HS256');
  57. return Payload::ok([
  58. 'token' => $jwt,
  59. 'user' => $user,
  60. ]);
  61. }
  62. /**
  63. * Resolve o IP de origem da requisição. Usa o atributo "remote_addr"
  64. * populado pelo framework-X e cai para REMOTE_ADDR dos server params.
  65. * Não confiamos em X-Forwarded-For (falsificável) para evitar bypass.
  66. */
  67. private function clientIp(ServerRequestInterface $request): string
  68. {
  69. $ip = $request->getAttribute('remote_addr')
  70. ?? ($request->getServerParams()['REMOTE_ADDR'] ?? '');
  71. return $ip !== '' ? (string) $ip : 'unknown';
  72. }
  73. }