diff --git a/.env.example b/.env.example
index dd66977..8943d78 100644
--- a/.env.example
+++ b/.env.example
@@ -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
diff --git a/.scribe/endpoints.cache/00.yaml b/.scribe/endpoints.cache/00.yaml
index 2aa991a..a2cd4a3 100644
--- a/.scribe/endpoints.cache/00.yaml
+++ b/.scribe/endpoints.cache/00.yaml
@@ -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 id 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: []
diff --git a/.scribe/endpoints/00.yaml b/.scribe/endpoints/00.yaml
index 615cb59..1c9c91b 100644
--- a/.scribe/endpoints/00.yaml
+++ b/.scribe/endpoints/00.yaml
@@ -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 id 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: []
diff --git a/README.md b/README.md
index f0c3034..8b3fa80 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/app/Http/Controllers/GameController.php b/app/Http/Controllers/GameController.php
index 13fdb94..ecd266c 100644
--- a/app/Http/Controllers/GameController.php
+++ b/app/Http/Controllers/GameController.php
@@ -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
*
diff --git a/app/Http/Middleware/JwtAuthMiddleware.php b/app/Http/Middleware/JwtAuthMiddleware.php
index cdd72d8..8d25c2e 100644
--- a/app/Http/Middleware/JwtAuthMiddleware.php
+++ b/app/Http/Middleware/JwtAuthMiddleware.php
@@ -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';
+ }
+ }
}
diff --git a/config/jwt.php b/config/jwt.php
index b4f8547..98648be 100644
--- a/config/jwt.php
+++ b/config/jwt.php
@@ -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),
];
diff --git a/config/scribe.php b/config/scribe.php
index b9221f0..73262b3 100644
--- a/config/scribe.php
+++ b/config/scribe.php
@@ -46,6 +46,7 @@ return [
// Exclude these routes even if they matched the rules above.
'exclude' => [
'GET api/test-auth',
+ 'GET api/health',
],
],
],
diff --git a/resources/views/scribe/index.blade.php b/resources/views/scribe/index.blade.php
index b44a405..4a8795b 100644
--- a/resources/views/scribe/index.blade.php
+++ b/resources/views/scribe/index.blade.php
@@ -79,12 +79,18 @@
+requires authentication +
+ +Retorna a evolução de um jogo específico usando o parâmetro id na query string.
Example request:+ + +
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
+}"
+const url = new URL(
+ "http://127.0.0.1:8000/api/v1/rankings/history"
+);
+
+const params = {
+ "id": "1",
+};
+Object.keys(params)
+ .forEach(key => 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 => response.json());++Example response (422):
+
cache-control: no-cache, private
+content-type: application/json
+x-ratelimit-limit: 60
+x-ratelimit-remaining: 56
+access-control-allow-origin: *
+
+
+{
+ "message": "The selected id is invalid.",
+ "errors": {
+ "id": [
+ "The selected id is invalid."
+ ]
+ }
+}
+
+
+
+ Received response: ++
+
+
+ Request failed with error:+
+
+Tip: Check that you're properly connected to the network.
+If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
+You can check the Dev Tools console for debugging information.
+
+
+
@@ -934,7 +1129,7 @@ fetch(url, {
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: *
@@ -1105,7 +1300,7 @@ fetch(url, {
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: *
@@ -1310,6 +1505,314 @@ You can check the Dev Tools console for debugging information.
+ Listar jogos
+
+
+requires authentication
+
+
+Retorna os jogos cadastrados com seus IDs para o frontend escolher qual histórico consultar.
+
+
+Example request:
+
+
+
+ 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"
+
+
+
+ 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 => response.json());
+
+
+
+
+
+ Example response (200):
+
+
+
+ Show headers
+
+ cache-control: no-cache, private
+content-type: application/json
+x-ratelimit-limit: 60
+x-ratelimit-remaining: 53
+access-control-allow-origin: *
+
+
+[
+ {
+ "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"
+ }
+]
+
+
+
+ Received response:
+
+
+
+
+ Request failed with error:
+
+
+Tip: Check that you're properly connected to the network.
+If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
+You can check the Dev Tools console for debugging information.
+
+
+
@@ -1360,7 +1863,7 @@ fetch(url, {
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: *
diff --git a/routes/api.php b/routes/api.php
index 9947d32..5b312ef 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -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']);
});
diff --git a/tests/Feature/DocumentationRoutesTest.php b/tests/Feature/DocumentationRoutesTest.php
index c6a9179..df8a4fd 100644
--- a/tests/Feature/DocumentationRoutesTest.php
+++ b/tests/Feature/DocumentationRoutesTest.php
@@ -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);
}
}
diff --git a/tests/Feature/GameRankingApiTest.php b/tests/Feature/GameRankingApiTest.php
index afac60d..6f446ba 100644
--- a/tests/Feature/GameRankingApiTest.php
+++ b/tests/Feature/GameRankingApiTest.php
@@ -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)