funcional a parte de token

This commit is contained in:
2026-05-19 14:48:58 -05:00
parent abb1fae70d
commit cd38287503
12 changed files with 863 additions and 10 deletions

View File

@@ -58,6 +58,7 @@ JWT_ISSUER=
JWT_AUDIENCE=
JWT_PUBLIC_KEY_PEM=
JWT_TOKEN=
JWT_ALLOW_ANY_TOKEN=false
# Railway production example:
# APP_ENV=production

View File

@@ -141,6 +141,76 @@ endpoints:
controller: null
method: null
route: null
-
custom: []
httpMethods:
- GET
uri: api/v1/rankings/history
metadata:
custom: []
groupName: Rankings
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: 'Histórico de ranking por query string'
description: 'Retorna a evolução de um jogo específico usando o parâmetro `id` na query string.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters:
id:
custom: []
name: id
description: 'O ID do jogo.'
required: true
example: 1
type: integer
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanQueryParameters:
id: 1
bodyParameters:
id:
custom: []
name: id
description: 'The <code>id</code> of an existing record in the games table.'
required: true
example: 16
type: integer
enumValues: []
exampleWasSpecified: false
nullable: false
deprecated: false
cleanBodyParameters:
id: 16
fileParameters: []
responses:
-
custom: []
status: 422
content: '{"message":"The selected id is invalid.","errors":{"id":["The selected id is invalid."]}}'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '56'
access-control-allow-origin: '*'
description: null
responseFields: []
auth:
- headers
- Authorization
- 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnYW1ldmVyc2UtYXV0aCIsImF1ZCI6InJhbmtpbmctYXBpIiwic3ViIjoiZGVtby11c2VyIiwiaWF0IjoxNzc5MTQxNTcxLCJleHAiOjE4MTA2Nzc1NzF9.aiCMcNXMs1GxvGqY5Ln87D1VJG-J2CzQ2lktqJstEzm2ogcj9M4WxI1ye2Ps3p4IHExr5IQ9KwoNn3hTgnDI5C8LiMmRa6yqdB8ZlrkZZ_eSlNxFhuAhGiCIqLsQwHony4UpxFjS1MpSuJKPyY1Z4VSulOzUExcTt0Y-G1ynq8IYnsfjqoCTP20oQGP2pb2TTbZFf4jACxctnz2oIijvgWEMAiqn72G4DJ-8nWFXZ9Yf6Of2S76MDLtWjysgFoQQYriye_Ns9ynoPjIo9igUCFyzc_AgIjh_VE0IrGW9ifkx5kOISf0b95bh7rhMuDzyvBQbFay7lIUyKMRKi_i-qw'
controller: null
method: null
route: null
-
custom: []
httpMethods:
@@ -188,7 +258,7 @@ endpoints:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '56'
x-ratelimit-remaining: '55'
access-control-allow-origin: '*'
description: null
responseFields: []
@@ -246,7 +316,53 @@ endpoints:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '55'
x-ratelimit-remaining: '54'
access-control-allow-origin: '*'
description: null
responseFields: []
auth:
- headers
- Authorization
- 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnYW1ldmVyc2UtYXV0aCIsImF1ZCI6InJhbmtpbmctYXBpIiwic3ViIjoiZGVtby11c2VyIiwiaWF0IjoxNzc5MTQxNTcxLCJleHAiOjE4MTA2Nzc1NzF9.aiCMcNXMs1GxvGqY5Ln87D1VJG-J2CzQ2lktqJstEzm2ogcj9M4WxI1ye2Ps3p4IHExr5IQ9KwoNn3hTgnDI5C8LiMmRa6yqdB8ZlrkZZ_eSlNxFhuAhGiCIqLsQwHony4UpxFjS1MpSuJKPyY1Z4VSulOzUExcTt0Y-G1ynq8IYnsfjqoCTP20oQGP2pb2TTbZFf4jACxctnz2oIijvgWEMAiqn72G4DJ-8nWFXZ9Yf6Of2S76MDLtWjysgFoQQYriye_Ns9ynoPjIo9igUCFyzc_AgIjh_VE0IrGW9ifkx5kOISf0b95bh7rhMuDzyvBQbFay7lIUyKMRKi_i-qw'
controller: null
method: null
route: null
-
custom: []
httpMethods:
- GET
uri: api/v1/games
metadata:
custom: []
groupName: Rankings
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: 'Listar jogos'
description: 'Retorna os jogos cadastrados com seus IDs para o frontend escolher qual histórico consultar.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: '[{"id":11,"name":"Apex Legends","platform":"Steam","active_players":218457,"weekly_points":945,"monthly_points":8776,"yearly_points":56526,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":5,"name":"Baldur''s Gate 3","platform":"Steam","active_players":296988,"weekly_points":352,"monthly_points":3595,"yearly_points":62260,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":12,"name":"Call of Duty: Warzone","platform":"Battle.net","active_players":243114,"weekly_points":877,"monthly_points":2426,"yearly_points":36655,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":1,"name":"Counter-Strike 2","platform":"Steam","active_players":1086549,"weekly_points":729,"monthly_points":1215,"yearly_points":71182,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":14,"name":"Cyberpunk 2077","platform":"Steam","active_players":1161973,"weekly_points":874,"monthly_points":4853,"yearly_points":27988,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":8,"name":"EA SPORTS FC 24","platform":"Steam","active_players":398998,"weekly_points":872,"monthly_points":5333,"yearly_points":81468,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":2,"name":"Elden Ring","platform":"Steam","active_players":715531,"weekly_points":697,"monthly_points":7369,"yearly_points":44291,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":6,"name":"Fortnite","platform":"Epic Games","active_players":1091171,"weekly_points":611,"monthly_points":5678,"yearly_points":96832,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":7,"name":"Grand Theft Auto V","platform":"Steam","active_players":262363,"weekly_points":199,"monthly_points":2257,"yearly_points":62350,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":4,"name":"Helldivers 2","platform":"Steam","active_players":217823,"weekly_points":617,"monthly_points":5232,"yearly_points":24531,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":10,"name":"League of Legends","platform":"Riot Launcher","active_players":1166370,"weekly_points":786,"monthly_points":4506,"yearly_points":21445,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":13,"name":"Minecraft","platform":"Multiplataforma","active_players":242066,"weekly_points":184,"monthly_points":9278,"yearly_points":33053,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":9,"name":"Roblox","platform":"Multiplataforma","active_players":991415,"weekly_points":770,"monthly_points":2080,"yearly_points":22209,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":15,"name":"Stardew Valley","platform":"Steam","active_players":1117483,"weekly_points":702,"monthly_points":7545,"yearly_points":42912,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":3,"name":"Valorant","platform":"Riot Launcher","active_players":821498,"weekly_points":241,"monthly_points":1030,"yearly_points":57266,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"}]'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '53'
access-control-allow-origin: '*'
description: null
responseFields: []
@@ -292,7 +408,7 @@ endpoints:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '54'
x-ratelimit-remaining: '52'
access-control-allow-origin: '*'
description: null
responseFields: []

View File

@@ -139,6 +139,76 @@ endpoints:
controller: null
method: null
route: null
-
custom: []
httpMethods:
- GET
uri: api/v1/rankings/history
metadata:
custom: []
groupName: Rankings
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: 'Histórico de ranking por query string'
description: 'Retorna a evolução de um jogo específico usando o parâmetro `id` na query string.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters:
id:
custom: []
name: id
description: 'O ID do jogo.'
required: true
example: 1
type: integer
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanQueryParameters:
id: 1
bodyParameters:
id:
custom: []
name: id
description: 'The <code>id</code> of an existing record in the games table.'
required: true
example: 16
type: integer
enumValues: []
exampleWasSpecified: false
nullable: false
deprecated: false
cleanBodyParameters:
id: 16
fileParameters: []
responses:
-
custom: []
status: 422
content: '{"message":"The selected id is invalid.","errors":{"id":["The selected id is invalid."]}}'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '56'
access-control-allow-origin: '*'
description: null
responseFields: []
auth:
- headers
- Authorization
- 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnYW1ldmVyc2UtYXV0aCIsImF1ZCI6InJhbmtpbmctYXBpIiwic3ViIjoiZGVtby11c2VyIiwiaWF0IjoxNzc5MTQxNTcxLCJleHAiOjE4MTA2Nzc1NzF9.aiCMcNXMs1GxvGqY5Ln87D1VJG-J2CzQ2lktqJstEzm2ogcj9M4WxI1ye2Ps3p4IHExr5IQ9KwoNn3hTgnDI5C8LiMmRa6yqdB8ZlrkZZ_eSlNxFhuAhGiCIqLsQwHony4UpxFjS1MpSuJKPyY1Z4VSulOzUExcTt0Y-G1ynq8IYnsfjqoCTP20oQGP2pb2TTbZFf4jACxctnz2oIijvgWEMAiqn72G4DJ-8nWFXZ9Yf6Of2S76MDLtWjysgFoQQYriye_Ns9ynoPjIo9igUCFyzc_AgIjh_VE0IrGW9ifkx5kOISf0b95bh7rhMuDzyvBQbFay7lIUyKMRKi_i-qw'
controller: null
method: null
route: null
-
custom: []
httpMethods:
@@ -186,7 +256,7 @@ endpoints:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '56'
x-ratelimit-remaining: '55'
access-control-allow-origin: '*'
description: null
responseFields: []
@@ -244,7 +314,53 @@ endpoints:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '55'
x-ratelimit-remaining: '54'
access-control-allow-origin: '*'
description: null
responseFields: []
auth:
- headers
- Authorization
- 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnYW1ldmVyc2UtYXV0aCIsImF1ZCI6InJhbmtpbmctYXBpIiwic3ViIjoiZGVtby11c2VyIiwiaWF0IjoxNzc5MTQxNTcxLCJleHAiOjE4MTA2Nzc1NzF9.aiCMcNXMs1GxvGqY5Ln87D1VJG-J2CzQ2lktqJstEzm2ogcj9M4WxI1ye2Ps3p4IHExr5IQ9KwoNn3hTgnDI5C8LiMmRa6yqdB8ZlrkZZ_eSlNxFhuAhGiCIqLsQwHony4UpxFjS1MpSuJKPyY1Z4VSulOzUExcTt0Y-G1ynq8IYnsfjqoCTP20oQGP2pb2TTbZFf4jACxctnz2oIijvgWEMAiqn72G4DJ-8nWFXZ9Yf6Of2S76MDLtWjysgFoQQYriye_Ns9ynoPjIo9igUCFyzc_AgIjh_VE0IrGW9ifkx5kOISf0b95bh7rhMuDzyvBQbFay7lIUyKMRKi_i-qw'
controller: null
method: null
route: null
-
custom: []
httpMethods:
- GET
uri: api/v1/games
metadata:
custom: []
groupName: Rankings
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: 'Listar jogos'
description: 'Retorna os jogos cadastrados com seus IDs para o frontend escolher qual histórico consultar.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: '[{"id":11,"name":"Apex Legends","platform":"Steam","active_players":218457,"weekly_points":945,"monthly_points":8776,"yearly_points":56526,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":5,"name":"Baldur''s Gate 3","platform":"Steam","active_players":296988,"weekly_points":352,"monthly_points":3595,"yearly_points":62260,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":12,"name":"Call of Duty: Warzone","platform":"Battle.net","active_players":243114,"weekly_points":877,"monthly_points":2426,"yearly_points":36655,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":1,"name":"Counter-Strike 2","platform":"Steam","active_players":1086549,"weekly_points":729,"monthly_points":1215,"yearly_points":71182,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":14,"name":"Cyberpunk 2077","platform":"Steam","active_players":1161973,"weekly_points":874,"monthly_points":4853,"yearly_points":27988,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":8,"name":"EA SPORTS FC 24","platform":"Steam","active_players":398998,"weekly_points":872,"monthly_points":5333,"yearly_points":81468,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":2,"name":"Elden Ring","platform":"Steam","active_players":715531,"weekly_points":697,"monthly_points":7369,"yearly_points":44291,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":6,"name":"Fortnite","platform":"Epic Games","active_players":1091171,"weekly_points":611,"monthly_points":5678,"yearly_points":96832,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":7,"name":"Grand Theft Auto V","platform":"Steam","active_players":262363,"weekly_points":199,"monthly_points":2257,"yearly_points":62350,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":4,"name":"Helldivers 2","platform":"Steam","active_players":217823,"weekly_points":617,"monthly_points":5232,"yearly_points":24531,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":10,"name":"League of Legends","platform":"Riot Launcher","active_players":1166370,"weekly_points":786,"monthly_points":4506,"yearly_points":21445,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":13,"name":"Minecraft","platform":"Multiplataforma","active_players":242066,"weekly_points":184,"monthly_points":9278,"yearly_points":33053,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":9,"name":"Roblox","platform":"Multiplataforma","active_players":991415,"weekly_points":770,"monthly_points":2080,"yearly_points":22209,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":15,"name":"Stardew Valley","platform":"Steam","active_players":1117483,"weekly_points":702,"monthly_points":7545,"yearly_points":42912,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"},{"id":3,"name":"Valorant","platform":"Riot Launcher","active_players":821498,"weekly_points":241,"monthly_points":1030,"yearly_points":57266,"created_at":"2026-05-18T21:57:31.000000Z","updated_at":"2026-05-18T21:57:31.000000Z"}]'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '53'
access-control-allow-origin: '*'
description: null
responseFields: []
@@ -290,7 +406,7 @@ endpoints:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '54'
x-ratelimit-remaining: '52'
access-control-allow-origin: '*'
description: null
responseFields: []

View File

@@ -145,6 +145,7 @@ Principais variáveis usadas pelo projeto:
| `JWT_ISSUER` | Emissor esperado no token JWT |
| `JWT_AUDIENCE` | Audiência esperada no token JWT |
| `JWT_PUBLIC_KEY_PEM` | Chave pública usada para validar JWT RS256 |
| `JWT_ALLOW_ANY_TOKEN` | Quando `true`, aceita qualquer Bearer token. Use apenas para integração/demo |
| `SCRIBE_AUTH_KEY` | Token usado apenas para gerar exemplos 200 na documentação |
---
@@ -171,6 +172,14 @@ O token precisa:
Sem o header `Authorization`, a API retorna `401`.
Para ambientes de demonstração ou integração inicial com o frontend, é possível configurar:
```env
JWT_ALLOW_ANY_TOKEN=true
```
Nesse modo, a API aceita qualquer valor enviado como `Bearer token`. Para produção, mantenha `JWT_ALLOW_ANY_TOKEN=false` e use a validação JWT completa.
---
## Executando o Projeto
@@ -220,8 +229,10 @@ Links auxiliares:
| GET | `/api/v1/rankings/weekly` | Lista o top 10 jogos por pontuação semanal | JWT |
| GET | `/api/v1/rankings/monthly` | Lista o top 10 jogos por pontuação mensal | JWT |
| GET | `/api/v1/rankings/yearly` | Lista o top 10 jogos por pontuação anual | JWT |
| GET | `/api/v1/rankings/history?id={id}` | Retorna o histórico de pontuação usando query string | JWT |
| GET | `/api/v1/rankings/history/{id}` | Retorna o histórico de pontuação de um jogo | JWT |
| GET | `/api/v1/rankings/platforms/{platform}` | Lista jogos filtrados por plataforma | JWT |
| GET | `/api/v1/games` | Lista os jogos cadastrados com seus IDs | JWT |
| GET | `/api/v1/games/most-played` | Lista o top 10 jogos por jogadores ativos | JWT |
Existe também a rota técnica `GET /api/test-auth`, usada apenas para validar o token JWT. Ela não faz parte da documentação pública principal.
@@ -257,6 +268,24 @@ Accept: application/json
Authorization: Bearer SEU_TOKEN_JWT
```
Histórico de um jogo usando query string:
```http
GET /api/v1/rankings/history?id=1 HTTP/1.1
Host: localhost:8000
Accept: application/json
Authorization: Bearer SEU_TOKEN_JWT
```
Listar jogos para o frontend escolher o ID:
```http
GET /api/v1/games HTTP/1.1
Host: localhost:8000
Accept: application/json
Authorization: Bearer SEU_TOKEN_JWT
```
---
## Exemplo de Resposta
@@ -286,6 +315,7 @@ Authorization: Bearer SEU_TOKEN_JWT
| 200 | Requisição autenticada com sucesso | Lista de jogos ou histórico |
| 401 | Token ausente, inválido ou expirado | `{"message":"Missing Authorization header"}` |
| 404 | Jogo inexistente em `/rankings/history/{id}` | Resposta padrão do Laravel para model não encontrado |
| 422 | ID ausente ou inválido em `/rankings/history?id={id}` | Erro de validação |
| 500 | Erro inesperado no servidor | Falha interna |
Observação: quando uma plataforma não possui jogos, `/api/v1/rankings/platforms/{platform}` retorna `200` com lista vazia.

View File

@@ -10,6 +10,17 @@ use Illuminate\Http\Request;
*/
class GameController extends Controller
{
/**
* Listar jogos
*
* Retorna os jogos cadastrados com seus IDs para o frontend escolher qual histórico consultar.
*/
public function index()
{
$games = Game::orderBy('name')->get();
return response()->json($games);
}
/**
* Top semanal
*
@@ -73,6 +84,23 @@ class GameController extends Controller
]
]);
}
/**
* Histórico de ranking por query string
*
* Retorna a evolução de um jogo específico usando o parâmetro `id` na query string.
*
* @queryParam id int required O ID do jogo. Example: 1
*/
public function historyByQuery(Request $request)
{
$request->validate([
'id' => ['required', 'integer', 'exists:games,id'],
]);
return $this->history($request->integer('id'));
}
/**
* Ranking por Plataforma
*

View File

@@ -22,6 +22,15 @@ class JwtAuthMiddleware
$token = $matches[1];
if (config('jwt.allow_any_token')) {
$request->attributes->set('auth', [
'id' => $this->subjectFromUnverifiedToken($token),
'token' => $token
]);
return $next($request);
}
[$header, $payload, $signature] = $this->decodeToken($token);
if (($header['alg'] ?? null) !== 'RS256') {
@@ -113,4 +122,20 @@ class JwtAuthMiddleware
return time() >= (int) $payload['exp'];
}
private function subjectFromUnverifiedToken(string $token): string
{
$parts = explode('.', $token);
if (count($parts) !== 3) {
return 'external-consumer';
}
try {
$payload = $this->base64UrlDecodeJson($parts[1]);
return (string) ($payload['sub'] ?? 'external-consumer');
} catch (\Exception $e) {
return 'external-consumer';
}
}
}

View File

@@ -4,4 +4,5 @@ return [
'issuer' => env('JWT_ISSUER'),
'audience' => env('JWT_AUDIENCE'),
'public_key' => env('JWT_PUBLIC_KEY_PEM'),
'allow_any_token' => env('JWT_ALLOW_ANY_TOKEN', false),
];

View File

@@ -46,6 +46,7 @@ return [
// Exclude these routes even if they matched the rules above.
'exclude' => [
'GET api/test-auth',
'GET api/health',
],
],
],

View File

@@ -79,12 +79,18 @@
</li>
<li class="tocify-item level-2" data-unique="rankings-GETapi-v1-rankings-yearly">
<a href="#rankings-GETapi-v1-rankings-yearly">Top anual</a>
</li>
<li class="tocify-item level-2" data-unique="rankings-GETapi-v1-rankings-history">
<a href="#rankings-GETapi-v1-rankings-history">Histórico de ranking por query string</a>
</li>
<li class="tocify-item level-2" data-unique="rankings-GETapi-v1-rankings-history--id-">
<a href="#rankings-GETapi-v1-rankings-history--id-">Histórico de ranking</a>
</li>
<li class="tocify-item level-2" data-unique="rankings-GETapi-v1-rankings-platforms--platform-">
<a href="#rankings-GETapi-v1-rankings-platforms--platform-">Ranking por Plataforma</a>
</li>
<li class="tocify-item level-2" data-unique="rankings-GETapi-v1-games">
<a href="#rankings-GETapi-v1-games">Listar jogos</a>
</li>
<li class="tocify-item level-2" data-unique="rankings-GETapi-v1-games-most-played">
<a href="#rankings-GETapi-v1-games-most-played">Jogos mais jogados</a>
@@ -100,7 +106,7 @@
</ul>
<ul class="toc-footer" id="last-updated">
<li>Last updated: May 18, 2026</li>
<li>Last updated: May 19, 2026</li>
</ul>
</div>
@@ -884,6 +890,195 @@ You can check the Dev Tools console for debugging information.</code></pre>
</div>
</form>
<h2 id="rankings-GETapi-v1-rankings-history">Histórico de ranking por query string</h2>
<p>
<small class="badge badge-darkred">requires authentication</small>
</p>
<p>Retorna a evolução de um jogo específico usando o parâmetro <code>id</code> na query string.</p>
<span id="example-requests-GETapi-v1-rankings-history">
<blockquote>Example request:</blockquote>
<div class="bash-example">
<pre><code class="language-bash">curl --request GET \
--get "http://127.0.0.1:8000/api/v1/rankings/history?id=1" \
--header "Authorization: Bearer {YOUR_JWT_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"id\": 16
}"
</code></pre></div>
<div class="javascript-example">
<pre><code class="language-javascript">const url = new URL(
"http://127.0.0.1:8000/api/v1/rankings/history"
);
const params = {
"id": "1",
};
Object.keys(params)
.forEach(key =&gt; url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_JWT_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"id": 16
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response =&gt; response.json());</code></pre></div>
</span>
<span id="example-responses-GETapi-v1-rankings-history">
<blockquote>
<p>Example response (422):</p>
</blockquote>
<details class="annotation">
<summary style="cursor: pointer;">
<small onclick="textContent = parentElement.parentElement.open ? 'Show headers' : 'Hide headers'">Show headers</small>
</summary>
<pre><code class="language-http">cache-control: no-cache, private
content-type: application/json
x-ratelimit-limit: 60
x-ratelimit-remaining: 56
access-control-allow-origin: *
</code></pre></details> <pre>
<code class="language-json" style="max-height: 300px;">{
&quot;message&quot;: &quot;The selected id is invalid.&quot;,
&quot;errors&quot;: {
&quot;id&quot;: [
&quot;The selected id is invalid.&quot;
]
}
}</code>
</pre>
</span>
<span id="execution-results-GETapi-v1-rankings-history" hidden>
<blockquote>Received response<span
id="execution-response-status-GETapi-v1-rankings-history"></span>:
</blockquote>
<pre class="json"><code id="execution-response-content-GETapi-v1-rankings-history"
data-empty-response-text="<Empty response>" style="max-height: 400px;"></code></pre>
</span>
<span id="execution-error-GETapi-v1-rankings-history" hidden>
<blockquote>Request failed with error:</blockquote>
<pre><code id="execution-error-message-GETapi-v1-rankings-history">
Tip: Check that you&#039;re properly connected to the network.
If you&#039;re a maintainer of ths API, verify that your API is running and you&#039;ve enabled CORS.
You can check the Dev Tools console for debugging information.</code></pre>
</span>
<form id="form-GETapi-v1-rankings-history" data-method="GET"
data-path="api/v1/rankings/history"
data-authed="1"
data-hasfiles="0"
data-isarraybody="0"
autocomplete="off"
onsubmit="event.preventDefault(); executeTryOut('GETapi-v1-rankings-history', this);">
<h3>
Request&nbsp;&nbsp;&nbsp;
<button type="button"
style="background-color: #8fbcd4; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-tryout-GETapi-v1-rankings-history"
onclick="tryItOut('GETapi-v1-rankings-history');">Try it out
</button>
<button type="button"
style="background-color: #c97a7e; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-canceltryout-GETapi-v1-rankings-history"
onclick="cancelTryOut('GETapi-v1-rankings-history');" hidden>Cancel 🛑
</button>&nbsp;&nbsp;
<button type="submit"
style="background-color: #6ac174; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-executetryout-GETapi-v1-rankings-history"
data-initial-text="Send Request 💥"
data-loading-text="⏱ Sending..."
hidden>Send Request 💥
</button>
</h3>
<p>
<small class="badge badge-green">GET</small>
<b><code>api/v1/rankings/history</code></b>
</p>
<h4 class="fancy-heading-panel"><b>Headers</b></h4>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Authorization</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Authorization" class="auth-value" data-endpoint="GETapi-v1-rankings-history"
value="Bearer {YOUR_JWT_TOKEN}"
data-component="header">
<br>
<p>Example: <code>Bearer {YOUR_JWT_TOKEN}</code></p>
</div>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Content-Type</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Content-Type" data-endpoint="GETapi-v1-rankings-history"
value="application/json"
data-component="header">
<br>
<p>Example: <code>application/json</code></p>
</div>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Accept</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Accept" data-endpoint="GETapi-v1-rankings-history"
value="application/json"
data-component="header">
<br>
<p>Example: <code>application/json</code></p>
</div>
<h4 class="fancy-heading-panel"><b>Query Parameters</b></h4>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>id</code></b>&nbsp;&nbsp;
<small>integer</small>&nbsp;
&nbsp;
&nbsp;
<input type="number" style="display: none"
step="any" name="id" data-endpoint="GETapi-v1-rankings-history"
value="1"
data-component="query">
<br>
<p>O ID do jogo. Example: <code>1</code></p>
</div>
<h4 class="fancy-heading-panel"><b>Body Parameters</b></h4>
<div style=" padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>id</code></b>&nbsp;&nbsp;
<small>integer</small>&nbsp;
&nbsp;
&nbsp;
<input type="number" style="display: none"
step="any" name="id" data-endpoint="GETapi-v1-rankings-history"
value="16"
data-component="body">
<br>
<p>The <code>id</code> of an existing record in the games table. Example: <code>16</code></p>
</div>
</form>
<h2 id="rankings-GETapi-v1-rankings-history--id-">Histórico de ranking</h2>
<p>
@@ -934,7 +1129,7 @@ fetch(url, {
<pre><code class="language-http">cache-control: no-cache, private
content-type: application/json
x-ratelimit-limit: 60
x-ratelimit-remaining: 56
x-ratelimit-remaining: 55
access-control-allow-origin: *
</code></pre></details> <pre>
@@ -1105,7 +1300,7 @@ fetch(url, {
<pre><code class="language-http">cache-control: no-cache, private
content-type: application/json
x-ratelimit-limit: 60
x-ratelimit-remaining: 55
x-ratelimit-remaining: 54
access-control-allow-origin: *
</code></pre></details> <pre>
@@ -1310,6 +1505,314 @@ You can check the Dev Tools console for debugging information.</code></pre>
</div>
</form>
<h2 id="rankings-GETapi-v1-games">Listar jogos</h2>
<p>
<small class="badge badge-darkred">requires authentication</small>
</p>
<p>Retorna os jogos cadastrados com seus IDs para o frontend escolher qual histórico consultar.</p>
<span id="example-requests-GETapi-v1-games">
<blockquote>Example request:</blockquote>
<div class="bash-example">
<pre><code class="language-bash">curl --request GET \
--get "http://127.0.0.1:8000/api/v1/games" \
--header "Authorization: Bearer {YOUR_JWT_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"</code></pre></div>
<div class="javascript-example">
<pre><code class="language-javascript">const url = new URL(
"http://127.0.0.1:8000/api/v1/games"
);
const headers = {
"Authorization": "Bearer {YOUR_JWT_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response =&gt; response.json());</code></pre></div>
</span>
<span id="example-responses-GETapi-v1-games">
<blockquote>
<p>Example response (200):</p>
</blockquote>
<details class="annotation">
<summary style="cursor: pointer;">
<small onclick="textContent = parentElement.parentElement.open ? 'Show headers' : 'Hide headers'">Show headers</small>
</summary>
<pre><code class="language-http">cache-control: no-cache, private
content-type: application/json
x-ratelimit-limit: 60
x-ratelimit-remaining: 53
access-control-allow-origin: *
</code></pre></details> <pre>
<code class="language-json" style="max-height: 300px;">[
{
&quot;id&quot;: 11,
&quot;name&quot;: &quot;Apex Legends&quot;,
&quot;platform&quot;: &quot;Steam&quot;,
&quot;active_players&quot;: 218457,
&quot;weekly_points&quot;: 945,
&quot;monthly_points&quot;: 8776,
&quot;yearly_points&quot;: 56526,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 5,
&quot;name&quot;: &quot;Baldur&#039;s Gate 3&quot;,
&quot;platform&quot;: &quot;Steam&quot;,
&quot;active_players&quot;: 296988,
&quot;weekly_points&quot;: 352,
&quot;monthly_points&quot;: 3595,
&quot;yearly_points&quot;: 62260,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 12,
&quot;name&quot;: &quot;Call of Duty: Warzone&quot;,
&quot;platform&quot;: &quot;Battle.net&quot;,
&quot;active_players&quot;: 243114,
&quot;weekly_points&quot;: 877,
&quot;monthly_points&quot;: 2426,
&quot;yearly_points&quot;: 36655,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 1,
&quot;name&quot;: &quot;Counter-Strike 2&quot;,
&quot;platform&quot;: &quot;Steam&quot;,
&quot;active_players&quot;: 1086549,
&quot;weekly_points&quot;: 729,
&quot;monthly_points&quot;: 1215,
&quot;yearly_points&quot;: 71182,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 14,
&quot;name&quot;: &quot;Cyberpunk 2077&quot;,
&quot;platform&quot;: &quot;Steam&quot;,
&quot;active_players&quot;: 1161973,
&quot;weekly_points&quot;: 874,
&quot;monthly_points&quot;: 4853,
&quot;yearly_points&quot;: 27988,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 8,
&quot;name&quot;: &quot;EA SPORTS FC 24&quot;,
&quot;platform&quot;: &quot;Steam&quot;,
&quot;active_players&quot;: 398998,
&quot;weekly_points&quot;: 872,
&quot;monthly_points&quot;: 5333,
&quot;yearly_points&quot;: 81468,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 2,
&quot;name&quot;: &quot;Elden Ring&quot;,
&quot;platform&quot;: &quot;Steam&quot;,
&quot;active_players&quot;: 715531,
&quot;weekly_points&quot;: 697,
&quot;monthly_points&quot;: 7369,
&quot;yearly_points&quot;: 44291,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 6,
&quot;name&quot;: &quot;Fortnite&quot;,
&quot;platform&quot;: &quot;Epic Games&quot;,
&quot;active_players&quot;: 1091171,
&quot;weekly_points&quot;: 611,
&quot;monthly_points&quot;: 5678,
&quot;yearly_points&quot;: 96832,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 7,
&quot;name&quot;: &quot;Grand Theft Auto V&quot;,
&quot;platform&quot;: &quot;Steam&quot;,
&quot;active_players&quot;: 262363,
&quot;weekly_points&quot;: 199,
&quot;monthly_points&quot;: 2257,
&quot;yearly_points&quot;: 62350,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 4,
&quot;name&quot;: &quot;Helldivers 2&quot;,
&quot;platform&quot;: &quot;Steam&quot;,
&quot;active_players&quot;: 217823,
&quot;weekly_points&quot;: 617,
&quot;monthly_points&quot;: 5232,
&quot;yearly_points&quot;: 24531,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 10,
&quot;name&quot;: &quot;League of Legends&quot;,
&quot;platform&quot;: &quot;Riot Launcher&quot;,
&quot;active_players&quot;: 1166370,
&quot;weekly_points&quot;: 786,
&quot;monthly_points&quot;: 4506,
&quot;yearly_points&quot;: 21445,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 13,
&quot;name&quot;: &quot;Minecraft&quot;,
&quot;platform&quot;: &quot;Multiplataforma&quot;,
&quot;active_players&quot;: 242066,
&quot;weekly_points&quot;: 184,
&quot;monthly_points&quot;: 9278,
&quot;yearly_points&quot;: 33053,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 9,
&quot;name&quot;: &quot;Roblox&quot;,
&quot;platform&quot;: &quot;Multiplataforma&quot;,
&quot;active_players&quot;: 991415,
&quot;weekly_points&quot;: 770,
&quot;monthly_points&quot;: 2080,
&quot;yearly_points&quot;: 22209,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 15,
&quot;name&quot;: &quot;Stardew Valley&quot;,
&quot;platform&quot;: &quot;Steam&quot;,
&quot;active_players&quot;: 1117483,
&quot;weekly_points&quot;: 702,
&quot;monthly_points&quot;: 7545,
&quot;yearly_points&quot;: 42912,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
},
{
&quot;id&quot;: 3,
&quot;name&quot;: &quot;Valorant&quot;,
&quot;platform&quot;: &quot;Riot Launcher&quot;,
&quot;active_players&quot;: 821498,
&quot;weekly_points&quot;: 241,
&quot;monthly_points&quot;: 1030,
&quot;yearly_points&quot;: 57266,
&quot;created_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;,
&quot;updated_at&quot;: &quot;2026-05-18T21:57:31.000000Z&quot;
}
]</code>
</pre>
</span>
<span id="execution-results-GETapi-v1-games" hidden>
<blockquote>Received response<span
id="execution-response-status-GETapi-v1-games"></span>:
</blockquote>
<pre class="json"><code id="execution-response-content-GETapi-v1-games"
data-empty-response-text="<Empty response>" style="max-height: 400px;"></code></pre>
</span>
<span id="execution-error-GETapi-v1-games" hidden>
<blockquote>Request failed with error:</blockquote>
<pre><code id="execution-error-message-GETapi-v1-games">
Tip: Check that you&#039;re properly connected to the network.
If you&#039;re a maintainer of ths API, verify that your API is running and you&#039;ve enabled CORS.
You can check the Dev Tools console for debugging information.</code></pre>
</span>
<form id="form-GETapi-v1-games" data-method="GET"
data-path="api/v1/games"
data-authed="1"
data-hasfiles="0"
data-isarraybody="0"
autocomplete="off"
onsubmit="event.preventDefault(); executeTryOut('GETapi-v1-games', this);">
<h3>
Request&nbsp;&nbsp;&nbsp;
<button type="button"
style="background-color: #8fbcd4; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-tryout-GETapi-v1-games"
onclick="tryItOut('GETapi-v1-games');">Try it out
</button>
<button type="button"
style="background-color: #c97a7e; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-canceltryout-GETapi-v1-games"
onclick="cancelTryOut('GETapi-v1-games');" hidden>Cancel 🛑
</button>&nbsp;&nbsp;
<button type="submit"
style="background-color: #6ac174; padding: 5px 10px; border-radius: 5px; border-width: thin;"
id="btn-executetryout-GETapi-v1-games"
data-initial-text="Send Request 💥"
data-loading-text="⏱ Sending..."
hidden>Send Request 💥
</button>
</h3>
<p>
<small class="badge badge-green">GET</small>
<b><code>api/v1/games</code></b>
</p>
<h4 class="fancy-heading-panel"><b>Headers</b></h4>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Authorization</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Authorization" class="auth-value" data-endpoint="GETapi-v1-games"
value="Bearer {YOUR_JWT_TOKEN}"
data-component="header">
<br>
<p>Example: <code>Bearer {YOUR_JWT_TOKEN}</code></p>
</div>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Content-Type</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Content-Type" data-endpoint="GETapi-v1-games"
value="application/json"
data-component="header">
<br>
<p>Example: <code>application/json</code></p>
</div>
<div style="padding-left: 28px; clear: unset;">
<b style="line-height: 2;"><code>Accept</code></b>&nbsp;&nbsp;
&nbsp;
&nbsp;
&nbsp;
<input type="text" style="display: none"
name="Accept" data-endpoint="GETapi-v1-games"
value="application/json"
data-component="header">
<br>
<p>Example: <code>application/json</code></p>
</div>
</form>
<h2 id="rankings-GETapi-v1-games-most-played">Jogos mais jogados</h2>
<p>
@@ -1360,7 +1863,7 @@ fetch(url, {
<pre><code class="language-http">cache-control: no-cache, private
content-type: application/json
x-ratelimit-limit: 60
x-ratelimit-remaining: 54
x-ratelimit-remaining: 52
access-control-allow-origin: *
</code></pre></details> <pre>

View File

@@ -16,10 +16,12 @@ Route::prefix('v1')->middleware(['jwt.auth'])->group(function () {
Route::get('/rankings/weekly', [GameController::class, 'weeklyRanking']);
Route::get('/rankings/monthly', [GameController::class, 'monthlyRanking']);
Route::get('/rankings/yearly', [GameController::class, 'yearlyRanking']);
Route::get('/rankings/history', [GameController::class, 'historyByQuery']);
Route::get('/rankings/history/{id}', [GameController::class, 'history']);
Route::get('/rankings/platforms/{platform}', [GameController::class, 'platformRanking']);
// Jogos
Route::get('/games', [GameController::class, 'index']);
Route::get('/games/most-played', [GameController::class, 'mostPlayed']);
});

View File

@@ -20,11 +20,14 @@ class DocumentationRoutesTest extends TestCase
'/api/v1/rankings/weekly',
'/api/v1/rankings/monthly',
'/api/v1/rankings/yearly',
'/api/v1/rankings/history',
'/api/v1/rankings/history/{id}',
'/api/v1/rankings/platforms/{platform}',
'/api/v1/games',
'/api/v1/games/most-played',
], $paths);
$this->assertNotContains('/api/test-auth', $paths);
$this->assertNotContains('/api/health', $paths);
}
}

View File

@@ -54,6 +54,15 @@ class GameRankingApiTest extends TestCase
->assertJsonPath('9.name', 'Game 3');
}
public function test_games_route_returns_ids_for_frontend_selection(): void
{
$this->getJsonWithJwt('/api/v1/games')
->assertOk()
->assertJsonCount(12)
->assertJsonPath('0.id', 1)
->assertJsonPath('0.name', 'Game 1');
}
public function test_history_returns_score_evolution_for_a_game(): void
{
$this->getJsonWithJwt('/api/v1/rankings/history/5')
@@ -67,6 +76,14 @@ class GameRankingApiTest extends TestCase
->assertJsonPath('history.2.points', 50000);
}
public function test_history_can_be_requested_with_query_string_id(): void
{
$this->getJsonWithJwt('/api/v1/rankings/history?id=6')
->assertOk()
->assertJsonPath('game', 'Game 6')
->assertJsonPath('history.0.points', 600);
}
public function test_platform_ranking_returns_only_requested_platform_ordered_by_active_players(): void
{
$this->getJsonWithJwt('/api/v1/rankings/platforms/Steam')
@@ -84,6 +101,16 @@ class GameRankingApiTest extends TestCase
->assertJson(['userId' => 'consumer-project']);
}
public function test_can_accept_any_bearer_token_when_enabled_for_demo_integration(): void
{
config(['jwt.allow_any_token' => true]);
$this->withHeader('Authorization', 'Bearer token-do-front')
->getJson('/api/v1/rankings/weekly')
->assertOk()
->assertJsonCount(10);
}
private function getJsonWithJwt(string $uri)
{
return $this->withHeader('Authorization', 'Bearer '.$this->jwt)