From cd38287503d5f321f70af4e8eba71381900c1fb2 Mon Sep 17 00:00:00 2001 From: gabriellina640 Date: Tue, 19 May 2026 14:48:58 -0500 Subject: [PATCH] funcional a parte de token --- .env.example | 1 + .scribe/endpoints.cache/00.yaml | 122 +++++- .scribe/endpoints/00.yaml | 122 +++++- README.md | 30 ++ app/Http/Controllers/GameController.php | 28 ++ app/Http/Middleware/JwtAuthMiddleware.php | 25 ++ config/jwt.php | 1 + config/scribe.php | 1 + resources/views/scribe/index.blade.php | 511 +++++++++++++++++++++- routes/api.php | 2 + tests/Feature/DocumentationRoutesTest.php | 3 + tests/Feature/GameRankingApiTest.php | 27 ++ 12 files changed, 863 insertions(+), 10 deletions(-) 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 @@
  • Top anual +
  • +
  • + Histórico de ranking por query string
  • Histórico de ranking
  • Ranking por Plataforma +
  • +
  • + Listar jogos
  • Jogos mais jogados @@ -100,7 +106,7 @@ @@ -884,6 +890,195 @@ You can check the Dev Tools console for debugging information. +

    Histórico de ranking por query string

    + +

    +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):

    +
    +
    + + Show headers + +
    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."
    +        ]
    +    }
    +}
    + 
    +
    + + +
    +

    + Request    + +    + +

    +

    + GET + api/v1/rankings/history +

    +

    Headers

    +
    + Authorization   +  +   +   + +
    +

    Example: Bearer {YOUR_JWT_TOKEN}

    +
    +
    + Content-Type   +  +   +   + +
    +

    Example: application/json

    +
    +
    + Accept   +  +   +   + +
    +

    Example: application/json

    +
    +

    Query Parameters

    +
    + id   +integer  +   +   + +
    +

    O ID do jogo. Example: 1

    +
    +

    Body Parameters

    +
    + id   +integer  +   +   + +
    +

    The id of an existing record in the games table. Example: 16

    +
    +
    +

    Histórico de ranking

    @@ -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"
    +    }
    +]
    + 
    +
    + + +
    +

    + Request    + +    + +

    +

    + GET + api/v1/games +

    +

    Headers

    +
    + Authorization   +  +   +   + +
    +

    Example: Bearer {YOUR_JWT_TOKEN}

    +
    +
    + Content-Type   +  +   +   + +
    +

    Example: application/json

    +
    +
    + Accept   +  +   +   + +
    +

    Example: application/json

    +
    +
    +

    Jogos mais jogados

    @@ -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)