clientIp($request); $retryAfter = RateLimiter::retryAfter($rateKey, self::MAX_ATTEMPTS); if ($retryAfter > 0) { Logger::warning('Login rate limit exceeded', ['ip' => $this->clientIp($request)]); return Payload::fail('Too many login attempts. Try again later.', [], 'E_RATE_LIMIT', 429) ->withHeader('Retry-After', (string) $retryAfter); } $body = json_decode((string) $request->getBody(), true) ?: []; $email = $body['email'] ?? $body['user_email'] ?? ''; $password = $body['password'] ?? ''; $validator = (new Validator(['email' => $email, 'password' => $password])) ->required('email')->email('email')->maxLength('email', 255) ->required('password'); if ($validator->fails()) { return Payload::fail($validator->firstError(), [], 'E_VALIDATE', 400); } $secret = $_ENV['JWT_SECRET'] ?? ''; if ($secret === '') { Logger::error('JWT_SECRET is not configured; cannot issue tokens'); return Payload::fail('Internal server error', [], 'E_GENERIC', 500); } $userModel = new UserModel(); $user = $userModel->validateLogin($email, $password); if (!$user) { RateLimiter::hit($rateKey, self::WINDOW_SECONDS); return Payload::fail('Invalid credentials', [], 'E_VALIDATE', 401); } RateLimiter::clear($rateKey); $payload = [ 'sub' => $user['user_id'], 'email' => $user['user_email'], 'company_id' => $user['company_id'], 'role' => $user['user_role'], 'iat' => time(), 'exp' => time() + 3600 ]; $jwt = JWT::encode($payload, $secret, 'HS256'); return Payload::ok([ 'token' => $jwt, 'user' => $user, ]); } /** * Resolve o IP de origem da requisição. Usa o atributo "remote_addr" * populado pelo framework-X e cai para REMOTE_ADDR dos server params. * Não confiamos em X-Forwarded-For (falsificável) para evitar bypass. */ private function clientIp(ServerRequestInterface $request): string { $ip = $request->getAttribute('remote_addr') ?? ($request->getServerParams()['REMOTE_ADDR'] ?? ''); return $ip !== '' ? (string) $ip : 'unknown'; } }