Compare commits

...

36 Commits

Author SHA1 Message Date
5f30042799 Use app URL for route index links 2026-05-21 22:26:01 -05:00
98e5635eb7 Fix route index JavaScript rendering 2026-05-21 22:18:21 -05:00
90c68a69b1 Add route tester controls 2026-05-21 22:14:15 -05:00
33eea15326 Add route index page 2026-05-21 19:22:32 -05:00
f9cc5f77c8 Add protected test game seeding 2026-05-21 19:07:35 -05:00
da51e2af24 Expose JWT public key fingerprint 2026-05-21 14:24:10 -05:00
b7f13d8511 Add JWT token diagnostics route 2026-05-21 13:52:52 -05:00
6c54a438dd Recover malformed JWT public key end marker 2026-05-21 13:39:42 -05:00
99f35c64ad Harden JWT PEM normalization diagnostics 2026-05-21 13:35:26 -05:00
7836b72d6d Normalize JWT public key PEM 2026-05-21 13:27:35 -05:00
94064b27c3 Restore platform route and db diagnostics 2026-05-21 13:22:17 -05:00
c477643781 Expose health diagnostics at root 2026-05-21 13:02:25 -05:00
961662a10e Add JWT key diagnostics 2026-05-21 12:46:28 -05:00
fcbafce44c funcional a parte de token 2026-05-19 16:49:40 -05:00
edc6e6486b funcional a parte de token 2026-05-19 16:24:51 -05:00
cd38287503 funcional a parte de token 2026-05-19 14:48:58 -05:00
abb1fae70d fix start command 2026-05-19 00:00:03 -05:00
bc90a16c14 fix start command 2026-05-18 23:55:43 -05:00
6fb93f04db fix start command 2026-05-18 23:42:20 -05:00
df742ac5ea fix deploy 2026-05-18 23:23:45 -05:00
4b8097e9f2 Run migrations during Railway deploy 2026-05-18 23:16:48 -05:00
336c19b971 Add static Railway healthcheck 2026-05-18 22:56:23 -05:00
41455664dc Set Railway Laravel start command 2026-05-18 22:49:16 -05:00
8f183fc0ed Simplify Railway healthcheck 2026-05-18 22:40:01 -05:00
99810f0695 Do not block Railway deploy on migrations 2026-05-18 22:36:34 -05:00
dd1d5afd00 Fix Railway build requirements 2026-05-18 22:33:56 -05:00
cef15730f3 Prepare Laravel API for Railway 2026-05-18 22:25:24 -05:00
8e9211bafd reade on 2026-05-18 17:45:52 -05:00
ff3eb18954 otimizacao 2026-05-18 17:39:13 -05:00
b97c049c97 otimizacao 2026-05-18 17:13:16 -05:00
516a6fb179 otimizacao 2026-05-18 17:04:50 -05:00
4b4a902fe8 fix output directory 2026-05-17 20:53:31 -05:00
5ab3e83698 trigger new deploy 2026-05-17 20:52:14 -05:00
4da99919bd fix vercel php runtime 2026-05-17 20:25:34 -05:00
6d30d160b0 config vercel deploy 2026-05-17 20:24:25 -05:00
5fc86102a3 config vercel deploy 2026-05-17 20:20:58 -05:00
29 changed files with 1950 additions and 1695 deletions

View File

@@ -9,6 +9,7 @@ LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=sqlite DB_CONNECTION=sqlite
DATABASE_URL=
BROADCAST_DRIVER=log BROADCAST_DRIVER=log
CACHE_DRIVER=file CACHE_DRIVER=file
@@ -53,7 +54,18 @@ VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
JWT_ISSUER= JWT_ISSUER=https://sistema-distribuido-trabalho-faculd.vercel.app
JWT_AUDIENCE= JWT_AUDIENCE=internal-apis
JWT_PUBLIC_KEY_PEM= JWT_PUBLIC_KEY_PEM="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAEAXf0r0pF9jsaUb+T5ue\nm/6lJby0lLqNstP4bpXg4izqzVl8ad9gM/mOS+1M8U204/CMBowC2XFVKQITI78y\n3o1KrlyUpmTfZrrDHidCII53v3E/N6Vou4hEV5xLQhuE61sXB4bwDpr+JgAq17IV\nTfUR+ePFY6xmPCimTuGTXNPOJprkYlV1jEYzMHvtk6FSV39eZDp2GM3wnGYk95ib\n5fFd+xRB8kUdrtub5Cif/ayyF2vsmgsjN41d2qOw6MFsNsXsOXVcrCE/0GvvW5C8\nRTPCifEPbHJ/Du7ye1yDjHDyjYXnnoZ3cOg6VIa12OnlBRfL6sJBT6VCvIbzQN1z\n7QIDAQAB\n-----END PUBLIC KEY-----"
JWT_TOKEN= JWT_TOKEN=
# Railway production example:
# APP_ENV=production
# APP_DEBUG=false
# APP_URL=https://your-service.up.railway.app
# LOG_CHANNEL=stderr
# DB_CONNECTION=pgsql
# DATABASE_URL=${{Postgres.DATABASE_URL}}
# CACHE_DRIVER=file
# SESSION_DRIVER=file
# QUEUE_CONNECTION=sync

View File

@@ -1,4 +1,4 @@
# GENERATED. YOU SHOULDN'T MODIFY OR DELETE THIS FILE. # GENERATED. YOU SHOULDN'T MODIFY OR DELETE THIS FILE.
# Scribe uses this file to know when you change something manually in your docs. # Scribe uses this file to know when you change something manually in your docs.
.scribe/intro.md=4bf90470e636417926ae5d9227747d45 .scribe/intro.md=7b0dd61cd08d5f1bff8f917a5c809588
.scribe/auth.md=9bee2b1ef8a238b2e58613fa636d5f39 .scribe/auth.md=8bb19ce54cd9ee69ae447231bc375761

View File

@@ -1,3 +1,7 @@
# Authenticating requests # Authenticating requests
This API is not authenticated. To authenticate requests, include an **`Authorization`** header with the value **`"Bearer {YOUR_JWT_TOKEN}"`**.
All authenticated endpoints are marked with a `requires authentication` badge in the documentation below.
Use um token JWT RS256 emitido pelo serviço de autenticação integrado ao GameVerse.

View File

@@ -14,13 +14,12 @@ endpoints:
groupDescription: '' groupDescription: ''
subgroup: '' subgroup: ''
subgroupDescription: '' subgroupDescription: ''
title: |- title: 'Top semanal'
Top semanal description: 'Retorna o ranking dos jogos com melhor desempenho na última semana.'
* Retorna o ranking dos jogos com melhor desempenho na última semana. authenticated: true
description: ''
authenticated: false
deprecated: false deprecated: false
headers: headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json Content-Type: application/json
Accept: application/json Accept: application/json
urlParameters: [] urlParameters: []
@@ -33,17 +32,20 @@ endpoints:
responses: responses:
- -
custom: [] custom: []
status: 200 status: 401
content: '[{"id":12,"name":"Call of Duty: Warzone","platform":"Battle.net","active_players":933732,"weekly_points":857,"monthly_points":4936,"yearly_points":44623,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":4,"name":"Helldivers 2","platform":"Steam","active_players":589021,"weekly_points":833,"monthly_points":9947,"yearly_points":78223,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":6,"name":"Fortnite","platform":"Epic Games","active_players":418738,"weekly_points":813,"monthly_points":6995,"yearly_points":22527,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":7,"name":"Grand Theft Auto V","platform":"Steam","active_players":1509381,"weekly_points":812,"monthly_points":7911,"yearly_points":17211,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":8,"name":"EA SPORTS FC 24","platform":"Steam","active_players":1075170,"weekly_points":776,"monthly_points":6337,"yearly_points":70015,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":13,"name":"Minecraft","platform":"Multiplataforma","active_players":1058688,"weekly_points":768,"monthly_points":6013,"yearly_points":97008,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":15,"name":"Stardew Valley","platform":"Steam","active_players":94038,"weekly_points":682,"monthly_points":5436,"yearly_points":54743,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":2,"name":"Elden Ring","platform":"Steam","active_players":799796,"weekly_points":647,"monthly_points":8422,"yearly_points":76612,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":9,"name":"Roblox","platform":"Multiplataforma","active_players":139569,"weekly_points":636,"monthly_points":8679,"yearly_points":12637,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":10,"name":"League of Legends","platform":"Riot Launcher","active_players":1682586,"weekly_points":587,"monthly_points":1858,"yearly_points":56745,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"}]' content: '{"message":"Invalid or expired token"}'
headers: headers:
cache-control: 'no-cache, private' cache-control: 'no-cache, private'
content-type: application/json content-type: application/json
x-ratelimit-limit: '60' x-ratelimit-limit: '60'
x-ratelimit-remaining: '49' x-ratelimit-remaining: '54'
access-control-allow-origin: '*' access-control-allow-origin: '*'
description: null description: null
responseFields: [] responseFields: []
auth: [] auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null controller: null
method: null method: null
route: null route: null
@@ -58,13 +60,12 @@ endpoints:
groupDescription: '' groupDescription: ''
subgroup: '' subgroup: ''
subgroupDescription: '' subgroupDescription: ''
title: |- title: 'Top mensal'
Top mensal description: 'Retorna o ranking dos jogos com melhor desempenho no último mês.'
* Retorna o ranking dos jogos com melhor desempenho no último mês. authenticated: true
description: ''
authenticated: false
deprecated: false deprecated: false
headers: headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json Content-Type: application/json
Accept: application/json Accept: application/json
urlParameters: [] urlParameters: []
@@ -77,17 +78,20 @@ endpoints:
responses: responses:
- -
custom: [] custom: []
status: 200 status: 401
content: '[{"id":4,"name":"Helldivers 2","platform":"Steam","active_players":589021,"weekly_points":833,"monthly_points":9947,"yearly_points":78223,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":9,"name":"Roblox","platform":"Multiplataforma","active_players":139569,"weekly_points":636,"monthly_points":8679,"yearly_points":12637,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":2,"name":"Elden Ring","platform":"Steam","active_players":799796,"weekly_points":647,"monthly_points":8422,"yearly_points":76612,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":11,"name":"Apex Legends","platform":"Steam","active_players":558948,"weekly_points":219,"monthly_points":8214,"yearly_points":80587,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":7,"name":"Grand Theft Auto V","platform":"Steam","active_players":1509381,"weekly_points":812,"monthly_points":7911,"yearly_points":17211,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":6,"name":"Fortnite","platform":"Epic Games","active_players":418738,"weekly_points":813,"monthly_points":6995,"yearly_points":22527,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":8,"name":"EA SPORTS FC 24","platform":"Steam","active_players":1075170,"weekly_points":776,"monthly_points":6337,"yearly_points":70015,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":13,"name":"Minecraft","platform":"Multiplataforma","active_players":1058688,"weekly_points":768,"monthly_points":6013,"yearly_points":97008,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":15,"name":"Stardew Valley","platform":"Steam","active_players":94038,"weekly_points":682,"monthly_points":5436,"yearly_points":54743,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":1,"name":"Counter-Strike 2","platform":"Steam","active_players":564671,"weekly_points":554,"monthly_points":5004,"yearly_points":60724,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"}]' content: '{"message":"Invalid or expired token"}'
headers: headers:
cache-control: 'no-cache, private' cache-control: 'no-cache, private'
content-type: application/json content-type: application/json
x-ratelimit-limit: '60' x-ratelimit-limit: '60'
x-ratelimit-remaining: '48' x-ratelimit-remaining: '53'
access-control-allow-origin: '*' access-control-allow-origin: '*'
description: null description: null
responseFields: [] responseFields: []
auth: [] auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null controller: null
method: null method: null
route: null route: null
@@ -102,13 +106,12 @@ endpoints:
groupDescription: '' groupDescription: ''
subgroup: '' subgroup: ''
subgroupDescription: '' subgroupDescription: ''
title: |- title: 'Top anual'
Top anual description: 'Retorna o ranking dos jogos com melhor desempenho no último ano.'
* Retorna o ranking dos jogos com melhor desempenho no último ano. authenticated: true
description: ''
authenticated: false
deprecated: false deprecated: false
headers: headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json Content-Type: application/json
Accept: application/json Accept: application/json
urlParameters: [] urlParameters: []
@@ -121,17 +124,20 @@ endpoints:
responses: responses:
- -
custom: [] custom: []
status: 200 status: 401
content: '[{"id":3,"name":"Valorant","platform":"Riot Launcher","active_players":1153799,"weekly_points":155,"monthly_points":2662,"yearly_points":99544,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":13,"name":"Minecraft","platform":"Multiplataforma","active_players":1058688,"weekly_points":768,"monthly_points":6013,"yearly_points":97008,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":11,"name":"Apex Legends","platform":"Steam","active_players":558948,"weekly_points":219,"monthly_points":8214,"yearly_points":80587,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":4,"name":"Helldivers 2","platform":"Steam","active_players":589021,"weekly_points":833,"monthly_points":9947,"yearly_points":78223,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":2,"name":"Elden Ring","platform":"Steam","active_players":799796,"weekly_points":647,"monthly_points":8422,"yearly_points":76612,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":8,"name":"EA SPORTS FC 24","platform":"Steam","active_players":1075170,"weekly_points":776,"monthly_points":6337,"yearly_points":70015,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":5,"name":"Baldur''s Gate 3","platform":"Steam","active_players":847989,"weekly_points":198,"monthly_points":1404,"yearly_points":66933,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":1,"name":"Counter-Strike 2","platform":"Steam","active_players":564671,"weekly_points":554,"monthly_points":5004,"yearly_points":60724,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":10,"name":"League of Legends","platform":"Riot Launcher","active_players":1682586,"weekly_points":587,"monthly_points":1858,"yearly_points":56745,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":14,"name":"Cyberpunk 2077","platform":"Steam","active_players":1700019,"weekly_points":221,"monthly_points":2723,"yearly_points":56740,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"}]' content: '{"message":"Invalid or expired token"}'
headers: headers:
cache-control: 'no-cache, private' cache-control: 'no-cache, private'
content-type: application/json content-type: application/json
x-ratelimit-limit: '60' x-ratelimit-limit: '60'
x-ratelimit-remaining: '47' x-ratelimit-remaining: '52'
access-control-allow-origin: '*' access-control-allow-origin: '*'
description: null description: null
responseFields: [] responseFields: []
auth: [] auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null controller: null
method: null method: null
route: null route: null
@@ -146,13 +152,12 @@ endpoints:
groupDescription: '' groupDescription: ''
subgroup: '' subgroup: ''
subgroupDescription: '' subgroupDescription: ''
title: |- title: 'Histórico de ranking'
Histórico de ranking description: 'Retorna a evolução de um jogo específico ao longo do tempo.'
* Retorna a evolução de um jogo específico ao longo do tempo. authenticated: true
description: ''
authenticated: false
deprecated: false deprecated: false
headers: headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json Content-Type: application/json
Accept: application/json Accept: application/json
urlParameters: urlParameters:
@@ -177,17 +182,20 @@ endpoints:
responses: responses:
- -
custom: [] custom: []
status: 200 status: 401
content: '{"game":"Counter-Strike 2","history":[{"period":"Semana 1","points":554},{"period":"M\u00eas Atual","points":5004},{"period":"Ano Atual","points":60724}]}' content: '{"message":"Invalid or expired token"}'
headers: headers:
cache-control: 'no-cache, private' cache-control: 'no-cache, private'
content-type: application/json content-type: application/json
x-ratelimit-limit: '60' x-ratelimit-limit: '60'
x-ratelimit-remaining: '46' x-ratelimit-remaining: '51'
access-control-allow-origin: '*' access-control-allow-origin: '*'
description: null description: null
responseFields: [] responseFields: []
auth: [] auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null controller: null
method: null method: null
route: null route: null
@@ -202,13 +210,12 @@ endpoints:
groupDescription: '' groupDescription: ''
subgroup: '' subgroup: ''
subgroupDescription: '' subgroupDescription: ''
title: |- title: 'Jogos mais jogados'
Jogos mais jogados description: 'Retorna o top 10 jogos com base no número de jogadores ativos.'
* Retorna o top 10 jogos com base no número de jogadores ativos. authenticated: true
description: ''
authenticated: false
deprecated: false deprecated: false
headers: headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json Content-Type: application/json
Accept: application/json Accept: application/json
urlParameters: [] urlParameters: []
@@ -221,73 +228,20 @@ endpoints:
responses: responses:
- -
custom: [] custom: []
status: 200 status: 401
content: '[{"id":14,"name":"Cyberpunk 2077","platform":"Steam","active_players":1700019,"weekly_points":221,"monthly_points":2723,"yearly_points":56740,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":10,"name":"League of Legends","platform":"Riot Launcher","active_players":1682586,"weekly_points":587,"monthly_points":1858,"yearly_points":56745,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":7,"name":"Grand Theft Auto V","platform":"Steam","active_players":1509381,"weekly_points":812,"monthly_points":7911,"yearly_points":17211,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":3,"name":"Valorant","platform":"Riot Launcher","active_players":1153799,"weekly_points":155,"monthly_points":2662,"yearly_points":99544,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":8,"name":"EA SPORTS FC 24","platform":"Steam","active_players":1075170,"weekly_points":776,"monthly_points":6337,"yearly_points":70015,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":13,"name":"Minecraft","platform":"Multiplataforma","active_players":1058688,"weekly_points":768,"monthly_points":6013,"yearly_points":97008,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":12,"name":"Call of Duty: Warzone","platform":"Battle.net","active_players":933732,"weekly_points":857,"monthly_points":4936,"yearly_points":44623,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":5,"name":"Baldur''s Gate 3","platform":"Steam","active_players":847989,"weekly_points":198,"monthly_points":1404,"yearly_points":66933,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":2,"name":"Elden Ring","platform":"Steam","active_players":799796,"weekly_points":647,"monthly_points":8422,"yearly_points":76612,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":4,"name":"Helldivers 2","platform":"Steam","active_players":589021,"weekly_points":833,"monthly_points":9947,"yearly_points":78223,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"}]' content: '{"message":"Invalid or expired token"}'
headers: headers:
cache-control: 'no-cache, private' cache-control: 'no-cache, private'
content-type: application/json content-type: application/json
x-ratelimit-limit: '60' x-ratelimit-limit: '60'
x-ratelimit-remaining: '45' x-ratelimit-remaining: '50'
access-control-allow-origin: '*' access-control-allow-origin: '*'
description: null description: null
responseFields: [] responseFields: []
auth: [] auth:
controller: null - headers
method: null - Authorization
route: null - 'Bearer 6g43cv8PD1aE5beadkZfhV6'
-
custom: []
httpMethods:
- GET
uri: 'api/v1/rankings/platforms/{platform}'
metadata:
custom: []
groupName: Rankings
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: |-
Ranking por Plataforma
* Retorna os jogos mais bem ranqueados de uma plataforma específica.
description: ''
authenticated: false
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
platform:
custom: []
name: platform
description: 'O nome da plataforma.'
required: true
example: Steam
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
platform: Steam
queryParameters: []
cleanQueryParameters: []
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: '[{"id":14,"name":"Cyberpunk 2077","platform":"Steam","active_players":1700019,"weekly_points":221,"monthly_points":2723,"yearly_points":56740,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":7,"name":"Grand Theft Auto V","platform":"Steam","active_players":1509381,"weekly_points":812,"monthly_points":7911,"yearly_points":17211,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":8,"name":"EA SPORTS FC 24","platform":"Steam","active_players":1075170,"weekly_points":776,"monthly_points":6337,"yearly_points":70015,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":5,"name":"Baldur''s Gate 3","platform":"Steam","active_players":847989,"weekly_points":198,"monthly_points":1404,"yearly_points":66933,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":2,"name":"Elden Ring","platform":"Steam","active_players":799796,"weekly_points":647,"monthly_points":8422,"yearly_points":76612,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":4,"name":"Helldivers 2","platform":"Steam","active_players":589021,"weekly_points":833,"monthly_points":9947,"yearly_points":78223,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":1,"name":"Counter-Strike 2","platform":"Steam","active_players":564671,"weekly_points":554,"monthly_points":5004,"yearly_points":60724,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":11,"name":"Apex Legends","platform":"Steam","active_players":558948,"weekly_points":219,"monthly_points":8214,"yearly_points":80587,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":15,"name":"Stardew Valley","platform":"Steam","active_players":94038,"weekly_points":682,"monthly_points":5436,"yearly_points":54743,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"}]'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '44'
access-control-allow-origin: '*'
description: null
responseFields: []
auth: []
controller: null controller: null
method: null method: null
route: null route: null

View File

@@ -12,13 +12,12 @@ endpoints:
groupDescription: '' groupDescription: ''
subgroup: '' subgroup: ''
subgroupDescription: '' subgroupDescription: ''
title: |- title: 'Top semanal'
Top semanal description: 'Retorna o ranking dos jogos com melhor desempenho na última semana.'
* Retorna o ranking dos jogos com melhor desempenho na última semana. authenticated: true
description: ''
authenticated: false
deprecated: false deprecated: false
headers: headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json Content-Type: application/json
Accept: application/json Accept: application/json
urlParameters: [] urlParameters: []
@@ -31,17 +30,20 @@ endpoints:
responses: responses:
- -
custom: [] custom: []
status: 200 status: 401
content: '[{"id":12,"name":"Call of Duty: Warzone","platform":"Battle.net","active_players":933732,"weekly_points":857,"monthly_points":4936,"yearly_points":44623,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":4,"name":"Helldivers 2","platform":"Steam","active_players":589021,"weekly_points":833,"monthly_points":9947,"yearly_points":78223,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":6,"name":"Fortnite","platform":"Epic Games","active_players":418738,"weekly_points":813,"monthly_points":6995,"yearly_points":22527,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":7,"name":"Grand Theft Auto V","platform":"Steam","active_players":1509381,"weekly_points":812,"monthly_points":7911,"yearly_points":17211,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":8,"name":"EA SPORTS FC 24","platform":"Steam","active_players":1075170,"weekly_points":776,"monthly_points":6337,"yearly_points":70015,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":13,"name":"Minecraft","platform":"Multiplataforma","active_players":1058688,"weekly_points":768,"monthly_points":6013,"yearly_points":97008,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":15,"name":"Stardew Valley","platform":"Steam","active_players":94038,"weekly_points":682,"monthly_points":5436,"yearly_points":54743,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":2,"name":"Elden Ring","platform":"Steam","active_players":799796,"weekly_points":647,"monthly_points":8422,"yearly_points":76612,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":9,"name":"Roblox","platform":"Multiplataforma","active_players":139569,"weekly_points":636,"monthly_points":8679,"yearly_points":12637,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":10,"name":"League of Legends","platform":"Riot Launcher","active_players":1682586,"weekly_points":587,"monthly_points":1858,"yearly_points":56745,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"}]' content: '{"message":"Invalid or expired token"}'
headers: headers:
cache-control: 'no-cache, private' cache-control: 'no-cache, private'
content-type: application/json content-type: application/json
x-ratelimit-limit: '60' x-ratelimit-limit: '60'
x-ratelimit-remaining: '49' x-ratelimit-remaining: '54'
access-control-allow-origin: '*' access-control-allow-origin: '*'
description: null description: null
responseFields: [] responseFields: []
auth: [] auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null controller: null
method: null method: null
route: null route: null
@@ -56,13 +58,12 @@ endpoints:
groupDescription: '' groupDescription: ''
subgroup: '' subgroup: ''
subgroupDescription: '' subgroupDescription: ''
title: |- title: 'Top mensal'
Top mensal description: 'Retorna o ranking dos jogos com melhor desempenho no último mês.'
* Retorna o ranking dos jogos com melhor desempenho no último mês. authenticated: true
description: ''
authenticated: false
deprecated: false deprecated: false
headers: headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json Content-Type: application/json
Accept: application/json Accept: application/json
urlParameters: [] urlParameters: []
@@ -75,17 +76,20 @@ endpoints:
responses: responses:
- -
custom: [] custom: []
status: 200 status: 401
content: '[{"id":4,"name":"Helldivers 2","platform":"Steam","active_players":589021,"weekly_points":833,"monthly_points":9947,"yearly_points":78223,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":9,"name":"Roblox","platform":"Multiplataforma","active_players":139569,"weekly_points":636,"monthly_points":8679,"yearly_points":12637,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":2,"name":"Elden Ring","platform":"Steam","active_players":799796,"weekly_points":647,"monthly_points":8422,"yearly_points":76612,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":11,"name":"Apex Legends","platform":"Steam","active_players":558948,"weekly_points":219,"monthly_points":8214,"yearly_points":80587,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":7,"name":"Grand Theft Auto V","platform":"Steam","active_players":1509381,"weekly_points":812,"monthly_points":7911,"yearly_points":17211,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":6,"name":"Fortnite","platform":"Epic Games","active_players":418738,"weekly_points":813,"monthly_points":6995,"yearly_points":22527,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":8,"name":"EA SPORTS FC 24","platform":"Steam","active_players":1075170,"weekly_points":776,"monthly_points":6337,"yearly_points":70015,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":13,"name":"Minecraft","platform":"Multiplataforma","active_players":1058688,"weekly_points":768,"monthly_points":6013,"yearly_points":97008,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":15,"name":"Stardew Valley","platform":"Steam","active_players":94038,"weekly_points":682,"monthly_points":5436,"yearly_points":54743,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":1,"name":"Counter-Strike 2","platform":"Steam","active_players":564671,"weekly_points":554,"monthly_points":5004,"yearly_points":60724,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"}]' content: '{"message":"Invalid or expired token"}'
headers: headers:
cache-control: 'no-cache, private' cache-control: 'no-cache, private'
content-type: application/json content-type: application/json
x-ratelimit-limit: '60' x-ratelimit-limit: '60'
x-ratelimit-remaining: '48' x-ratelimit-remaining: '53'
access-control-allow-origin: '*' access-control-allow-origin: '*'
description: null description: null
responseFields: [] responseFields: []
auth: [] auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null controller: null
method: null method: null
route: null route: null
@@ -100,13 +104,12 @@ endpoints:
groupDescription: '' groupDescription: ''
subgroup: '' subgroup: ''
subgroupDescription: '' subgroupDescription: ''
title: |- title: 'Top anual'
Top anual description: 'Retorna o ranking dos jogos com melhor desempenho no último ano.'
* Retorna o ranking dos jogos com melhor desempenho no último ano. authenticated: true
description: ''
authenticated: false
deprecated: false deprecated: false
headers: headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json Content-Type: application/json
Accept: application/json Accept: application/json
urlParameters: [] urlParameters: []
@@ -119,17 +122,20 @@ endpoints:
responses: responses:
- -
custom: [] custom: []
status: 200 status: 401
content: '[{"id":3,"name":"Valorant","platform":"Riot Launcher","active_players":1153799,"weekly_points":155,"monthly_points":2662,"yearly_points":99544,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":13,"name":"Minecraft","platform":"Multiplataforma","active_players":1058688,"weekly_points":768,"monthly_points":6013,"yearly_points":97008,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":11,"name":"Apex Legends","platform":"Steam","active_players":558948,"weekly_points":219,"monthly_points":8214,"yearly_points":80587,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":4,"name":"Helldivers 2","platform":"Steam","active_players":589021,"weekly_points":833,"monthly_points":9947,"yearly_points":78223,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":2,"name":"Elden Ring","platform":"Steam","active_players":799796,"weekly_points":647,"monthly_points":8422,"yearly_points":76612,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":8,"name":"EA SPORTS FC 24","platform":"Steam","active_players":1075170,"weekly_points":776,"monthly_points":6337,"yearly_points":70015,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":5,"name":"Baldur''s Gate 3","platform":"Steam","active_players":847989,"weekly_points":198,"monthly_points":1404,"yearly_points":66933,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":1,"name":"Counter-Strike 2","platform":"Steam","active_players":564671,"weekly_points":554,"monthly_points":5004,"yearly_points":60724,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":10,"name":"League of Legends","platform":"Riot Launcher","active_players":1682586,"weekly_points":587,"monthly_points":1858,"yearly_points":56745,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":14,"name":"Cyberpunk 2077","platform":"Steam","active_players":1700019,"weekly_points":221,"monthly_points":2723,"yearly_points":56740,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"}]' content: '{"message":"Invalid or expired token"}'
headers: headers:
cache-control: 'no-cache, private' cache-control: 'no-cache, private'
content-type: application/json content-type: application/json
x-ratelimit-limit: '60' x-ratelimit-limit: '60'
x-ratelimit-remaining: '47' x-ratelimit-remaining: '52'
access-control-allow-origin: '*' access-control-allow-origin: '*'
description: null description: null
responseFields: [] responseFields: []
auth: [] auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null controller: null
method: null method: null
route: null route: null
@@ -144,13 +150,12 @@ endpoints:
groupDescription: '' groupDescription: ''
subgroup: '' subgroup: ''
subgroupDescription: '' subgroupDescription: ''
title: |- title: 'Histórico de ranking'
Histórico de ranking description: 'Retorna a evolução de um jogo específico ao longo do tempo.'
* Retorna a evolução de um jogo específico ao longo do tempo. authenticated: true
description: ''
authenticated: false
deprecated: false deprecated: false
headers: headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json Content-Type: application/json
Accept: application/json Accept: application/json
urlParameters: urlParameters:
@@ -175,17 +180,20 @@ endpoints:
responses: responses:
- -
custom: [] custom: []
status: 200 status: 401
content: '{"game":"Counter-Strike 2","history":[{"period":"Semana 1","points":554},{"period":"M\u00eas Atual","points":5004},{"period":"Ano Atual","points":60724}]}' content: '{"message":"Invalid or expired token"}'
headers: headers:
cache-control: 'no-cache, private' cache-control: 'no-cache, private'
content-type: application/json content-type: application/json
x-ratelimit-limit: '60' x-ratelimit-limit: '60'
x-ratelimit-remaining: '46' x-ratelimit-remaining: '51'
access-control-allow-origin: '*' access-control-allow-origin: '*'
description: null description: null
responseFields: [] responseFields: []
auth: [] auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null controller: null
method: null method: null
route: null route: null
@@ -200,13 +208,12 @@ endpoints:
groupDescription: '' groupDescription: ''
subgroup: '' subgroup: ''
subgroupDescription: '' subgroupDescription: ''
title: |- title: 'Jogos mais jogados'
Jogos mais jogados description: 'Retorna o top 10 jogos com base no número de jogadores ativos.'
* Retorna o top 10 jogos com base no número de jogadores ativos. authenticated: true
description: ''
authenticated: false
deprecated: false deprecated: false
headers: headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json Content-Type: application/json
Accept: application/json Accept: application/json
urlParameters: [] urlParameters: []
@@ -219,73 +226,20 @@ endpoints:
responses: responses:
- -
custom: [] custom: []
status: 200 status: 401
content: '[{"id":14,"name":"Cyberpunk 2077","platform":"Steam","active_players":1700019,"weekly_points":221,"monthly_points":2723,"yearly_points":56740,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":10,"name":"League of Legends","platform":"Riot Launcher","active_players":1682586,"weekly_points":587,"monthly_points":1858,"yearly_points":56745,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":7,"name":"Grand Theft Auto V","platform":"Steam","active_players":1509381,"weekly_points":812,"monthly_points":7911,"yearly_points":17211,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":3,"name":"Valorant","platform":"Riot Launcher","active_players":1153799,"weekly_points":155,"monthly_points":2662,"yearly_points":99544,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":8,"name":"EA SPORTS FC 24","platform":"Steam","active_players":1075170,"weekly_points":776,"monthly_points":6337,"yearly_points":70015,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":13,"name":"Minecraft","platform":"Multiplataforma","active_players":1058688,"weekly_points":768,"monthly_points":6013,"yearly_points":97008,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":12,"name":"Call of Duty: Warzone","platform":"Battle.net","active_players":933732,"weekly_points":857,"monthly_points":4936,"yearly_points":44623,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":5,"name":"Baldur''s Gate 3","platform":"Steam","active_players":847989,"weekly_points":198,"monthly_points":1404,"yearly_points":66933,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":2,"name":"Elden Ring","platform":"Steam","active_players":799796,"weekly_points":647,"monthly_points":8422,"yearly_points":76612,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":4,"name":"Helldivers 2","platform":"Steam","active_players":589021,"weekly_points":833,"monthly_points":9947,"yearly_points":78223,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"}]' content: '{"message":"Invalid or expired token"}'
headers: headers:
cache-control: 'no-cache, private' cache-control: 'no-cache, private'
content-type: application/json content-type: application/json
x-ratelimit-limit: '60' x-ratelimit-limit: '60'
x-ratelimit-remaining: '45' x-ratelimit-remaining: '50'
access-control-allow-origin: '*' access-control-allow-origin: '*'
description: null description: null
responseFields: [] responseFields: []
auth: [] auth:
controller: null - headers
method: null - Authorization
route: null - 'Bearer 6g43cv8PD1aE5beadkZfhV6'
-
custom: []
httpMethods:
- GET
uri: 'api/v1/rankings/platforms/{platform}'
metadata:
custom: []
groupName: Rankings
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: |-
Ranking por Plataforma
* Retorna os jogos mais bem ranqueados de uma plataforma específica.
description: ''
authenticated: false
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
platform:
custom: []
name: platform
description: 'O nome da plataforma.'
required: true
example: Steam
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
platform: Steam
queryParameters: []
cleanQueryParameters: []
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: '[{"id":14,"name":"Cyberpunk 2077","platform":"Steam","active_players":1700019,"weekly_points":221,"monthly_points":2723,"yearly_points":56740,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":7,"name":"Grand Theft Auto V","platform":"Steam","active_players":1509381,"weekly_points":812,"monthly_points":7911,"yearly_points":17211,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":8,"name":"EA SPORTS FC 24","platform":"Steam","active_players":1075170,"weekly_points":776,"monthly_points":6337,"yearly_points":70015,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":5,"name":"Baldur''s Gate 3","platform":"Steam","active_players":847989,"weekly_points":198,"monthly_points":1404,"yearly_points":66933,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":2,"name":"Elden Ring","platform":"Steam","active_players":799796,"weekly_points":647,"monthly_points":8422,"yearly_points":76612,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":4,"name":"Helldivers 2","platform":"Steam","active_players":589021,"weekly_points":833,"monthly_points":9947,"yearly_points":78223,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":1,"name":"Counter-Strike 2","platform":"Steam","active_players":564671,"weekly_points":554,"monthly_points":5004,"yearly_points":60724,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":11,"name":"Apex Legends","platform":"Steam","active_players":558948,"weekly_points":219,"monthly_points":8214,"yearly_points":80587,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"},{"id":15,"name":"Stardew Valley","platform":"Steam","active_players":94038,"weekly_points":682,"monthly_points":5436,"yearly_points":54743,"created_at":"2026-04-18T01:47:16.000000Z","updated_at":"2026-04-18T01:47:16.000000Z"}]'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '44'
access-control-allow-origin: '*'
description: null
responseFields: []
auth: []
controller: null controller: null
method: null method: null
route: null route: null

View File

@@ -1,13 +1,12 @@
# Introduction # Introduction
Microsserviço de rankings e métricas de jogos para integração com o ecossistema GameVerse.
<aside> <aside>
<strong>Base URL</strong>: <code>http://localhost</code> <strong>Base URL</strong>: <code>http://127.0.0.1:8000</code>
</aside> </aside>
This documentation aims to provide all the information you need to work with our API. Esta API expõe rankings semanais, mensais e anuais, jogos mais jogados e histórico de pontuação.
<aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile). <aside>Use os exemplos da documentação para demonstrar como o frontend ou outros microsserviços podem consumir os dados de ranking.</aside>
You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside>

449
README.md
View File

@@ -1,127 +1,126 @@
# 🎮 Microsserviço de Rankings de Jogos (Game Ranking API) # Game Ranking API
![Laravel](https://img.shields.io/badge/Laravel-11-red) Microsserviço backend responsável por disponibilizar rankings e métricas de jogos para integração com o ecossistema GameVerse.
![PHP](https://img.shields.io/badge/PHP-8.2-blue)
![Laravel](https://img.shields.io/badge/Laravel-10-red)
![PHP](https://img.shields.io/badge/PHP-%3E%3D8.1-blue)
![SQLite](https://img.shields.io/badge/SQLite-Database-green) ![SQLite](https://img.shields.io/badge/SQLite-Database-green)
![Scribe](https://img.shields.io/badge/Docs-Scribe-purple)
--- ---
# 📌 Sobre o Projeto ## Sobre o Projeto
Este microsserviço faz parte do ecossistema **GameVerse**. Este serviço centraliza dados de engajamento de jogos e disponibiliza endpoints JSON para que um site, frontend ou outro microsserviço consiga exibir rankings dinâmicos.
Ele é responsável por processar, armazenar e disponibilizar estatísticas de engajamento, permitindo que a plataforma exiba rankings dinâmicos e tendências globais.
O projeto contém apenas o backend da API. A interface visual do usuário final fica em outro projeto consumidor.
--- ---
# 👥 Integrantes ## Integrantes
* Kaiky Andrade de Oliveira * Kaiky Andrade de Oliveira
* Gabriel Henrique Lina Batista Pereira Nunes * Gabriel Henrique Lina Batista Pereira Nunes
--- ---
# 📝 Descrição do Serviço ## Responsabilidades do Microsserviço
O serviço centraliza métricas de performance dos jogos, como: * Fornecer ranking semanal, mensal e anual de jogos
* Listar os jogos mais jogados
* pontuação * Consultar histórico de pontuação de um jogo
* tempo de jogo * Retornar dados estatísticos em formato JSON
* quantidade de jogadores ativos * Proteger as rotas da API usando JWT
* evolução de desempenho * Disponibilizar documentação interativa com Scribe
Ele resolve o problema de sobrecarga do sistema principal ao isolar o processamento de grandes volumes de dados estatísticos em um microsserviço dedicado, garantindo que as tabelas de classificação sejam atualizadas e entregues rapidamente aos usuários finais.
--- ---
# 🎯 Responsabilidades do Microsserviço ## Tecnologias Utilizadas
O serviço possui as seguintes responsabilidades: * PHP >= 8.1
* Laravel 10
* Fornecer rankings de desempenho semanal, mensal e anual
* Listar os jogos mais populares
* Exibir histórico de evolução de pontuação
* Segmentar rankings por plataforma
* Disponibilizar dados estatísticos para outros microsserviços
---
# 🛠️ Tecnologias Utilizadas
* PHP 8.2
* Laravel 11
* SQLite * SQLite
* Composer * Composer
* Laravel Scribe (Documentação OpenAPI) * Laravel Scribe
* PHPUnit
--- ---
# ✅ Requisitos Necessários ## Estrutura do Repositório
Antes de executar o projeto, é necessário possuir instalado: Este repositório não está organizado como monorepo. Ele contém apenas o microsserviço backend da API de rankings de jogos.
* PHP >= 8.2 | Serviço | Pasta | Descrição |
| ------- | ----- | --------- |
| API de Rankings de Jogos | `/` | Backend Laravel responsável pelas rotas, autenticação JWT, rankings e documentação Scribe |
Principais pastas:
| Pasta | Finalidade |
| ----- | ---------- |
| `app/Http/Controllers` | Controllers da API |
| `app/Http/Middleware` | Middleware de autenticação JWT |
| `app/Models` | Models Eloquent |
| `routes/api.php` | Rotas da API |
| `routes/web.php` | Rotas web básicas |
| `database/migrations` | Estrutura das tabelas |
| `database/seeders` | Dados iniciais para demonstração |
| `resources/views/scribe` | Documentação HTML gerada pelo Scribe |
| `public/vendor/scribe` | Assets públicos da documentação |
| `tests/Feature` | Testes dos endpoints e da documentação |
---
## Requisitos
Antes de executar o projeto, instale:
* PHP >= 8.1
* Composer * Composer
* Git * Git
* SQLite * SQLite
--- ---
# ⚙️ Variáveis de Ambiente ## Instalação
O projeto utiliza variáveis configuradas no arquivo `.env`. Clone o repositório:
Exemplo:
| Variável | Descrição |
| ------------- | ------------------------ |
| APP_NAME | Nome da aplicação |
| APP_ENV | Ambiente da aplicação |
| APP_KEY | Chave do Laravel |
| APP_DEBUG | Modo de depuração |
| DB_CONNECTION | Banco de dados utilizado |
---
# 📥 Instalação do Projeto
## 1. Clone o repositório
```bash ```bash
git clone https://github.com/gabriellina640/api-ranking-jogos.git git clone https://github.com/gabriellina640/api-ranking-jogos.git
```
## 2. Acesse a pasta do projeto
```bash
cd api-ranking-jogos cd api-ranking-jogos
``` ```
## 3. Instale as dependências Instale as dependências:
```bash ```bash
composer install composer install
``` ```
--- Crie o arquivo `.env`:
# ⚙️ Configuração do .env
Crie uma cópia do arquivo de ambiente:
```bash ```bash
cp .env.example .env cp .env.example .env
``` ```
Configure o banco SQLite no arquivo `.env`: Gere a chave da aplicação:
```bash
php artisan key:generate
```
Crie o banco SQLite local:
```bash
touch database/database.sqlite
```
No `.env`, configure:
```env ```env
DB_CONNECTION=sqlite DB_CONNECTION=sqlite
``` ```
---
# 🗄️ Preparação do Banco de Dados
Execute as migrations e seeders: Execute as migrations e seeders:
```bash ```bash
@@ -130,17 +129,50 @@ php artisan migrate:fresh --seed
--- ---
# 📚 Gerar Documentação da API ## Variáveis de Ambiente
Execute o comando: Principais variáveis usadas pelo projeto:
```bash | Variável | Descrição |
php artisan scribe:generate | -------- | --------- |
``` | `APP_NAME` | Nome da aplicação |
| `APP_ENV` | Ambiente da aplicação |
| `APP_KEY` | Chave interna do Laravel |
| `APP_DEBUG` | Ativa ou desativa debug |
| `APP_URL` | URL base da aplicação |
| `DB_CONNECTION` | Driver do banco, atualmente `sqlite` |
| `JWT_ISSUER` | Emissor esperado no token JWT: `https://sistema-distribuido-trabalho-faculd.vercel.app` |
| `JWT_AUDIENCE` | Audiência esperada no token JWT: `internal-apis` |
| `JWT_PUBLIC_KEY_PEM` | Chave pública usada para validar JWT RS256 |
| `SCRIBE_AUTH_KEY` | Token usado apenas para gerar exemplos 200 na documentação |
--- ---
# 🚀 Executando o Projeto ## Autenticação
As rotas da API usam autenticação via JWT no padrão Bearer Token.
Todas as requisições para os endpoints `/api/v1/*` devem enviar:
```http
Authorization: Bearer SEU_TOKEN_JWT
Accept: application/json
```
O token precisa:
* usar algoritmo `RS256`
* ter `iss` igual ao valor de `JWT_ISSUER`
* ter `aud` igual ao valor de `JWT_AUDIENCE`
* ter `sub` preenchido
* ter `exp` válido
* ter assinatura compatível com `JWT_PUBLIC_KEY_PEM`
Sem o header `Authorization`, a API retorna `401`.
---
## Executando o Projeto
Inicie o servidor Laravel: Inicie o servidor Laravel:
@@ -150,213 +182,178 @@ php artisan serve
A aplicação ficará disponível em: A aplicação ficará disponível em:
```bash ```text
http://localhost:8000 http://localhost:8000
``` ```
--- ---
# 📖 Documentação Interativa da API ## Documentação da API
Após iniciar o projeto, acesse: Gere a documentação:
```bash ```bash
http://localhost:8000/docs php artisan scribe:generate
``` ```
A documentação gerada pelo Scribe permite: Para deploy, as rotas públicas do Scribe ficam desabilitadas para que a API exponha somente os endpoints de consumo. Os arquivos gerados ficam disponíveis no projeto:
* visualizar endpoints | Recurso | Caminho |
* testar requisições | ------- | ------- |
* consultar parâmetros | Documentação HTML | `resources/views/scribe/index.blade.php` |
* visualizar respostas JSON | Collection Postman | `storage/app/scribe/collection.json` |
| OpenAPI | `storage/app/scribe/openapi.yaml` |
--- ---
# 🧪 Como Testar o Projeto ## Rotas da API
Você pode testar a API utilizando: | Método | Endpoint | Descrição | Autenticação |
| ------ | -------- | --------- | ------------ |
* Scribe | GET | `/api/v1/rankings/weekly` | Lista o top 10 jogos por pontuação semanal | JWT |
* Postman | GET | `/api/v1/rankings/monthly` | Lista o top 10 jogos por pontuação mensal | JWT |
* Insomnia | GET | `/api/v1/rankings/yearly` | Lista o top 10 jogos por pontuação anual | JWT |
* Thunder Client | GET | `/api/v1/rankings/history/{id}` | Retorna o histórico de pontuação de um jogo | JWT |
| GET | `/api/v1/games/most-played` | Lista o top 10 jogos por jogadores ativos | JWT |
Exemplo:
```http
GET http://localhost:8000/api/v1/rankings/weekly
```
--- ---
# 📑 Rotas da API ## Exemplos de Requisição
| Método | Endpoint | Descrição | Ranking semanal:
| ------ | ------------------------------------- | ---------------------------------------- |
| GET | /api/v1/rankings/weekly | Lista o Top 10 jogos da última semana |
| GET | /api/v1/rankings/monthly | Lista o Top 10 jogos do último mês |
| GET | /api/v1/rankings/yearly | Lista o Top 10 jogos do último ano |
| GET | /api/v1/rankings/history/{id} | Busca a evolução de pontuação de um jogo |
| GET | /api/v1/games/most-played | Lista os jogos mais jogados |
| GET | /api/v1/rankings/platforms/{platform} | Lista rankings por plataforma |
---
# 📥 Exemplo de Requisição
## Buscar ranking semanal
```http ```http
GET /api/v1/rankings/weekly HTTP/1.1 GET /api/v1/rankings/weekly HTTP/1.1
Host: localhost:8000 Host: localhost:8000
Accept: application/json Accept: application/json
Authorization: Bearer SEU_TOKEN_JWT
```
Histórico de um jogo:
```http
GET /api/v1/rankings/history/1 HTTP/1.1
Host: localhost:8000
Accept: application/json
Authorization: Bearer SEU_TOKEN_JWT
```
Jogos mais jogados:
```http
GET /api/v1/games/most-played HTTP/1.1
Host: localhost:8000
Accept: application/json
Authorization: Bearer SEU_TOKEN_JWT
``` ```
--- ---
# 📤 Exemplo de Resposta JSON ## Exemplo de Resposta
```json ```json
[ [
{ {
"id": 1, "id": 1,
"name": "Elden Ring", "name": "Counter-Strike 2",
"platform": "Steam", "platform": "Steam",
"active_players": 1500000, "active_players": 1086549,
"weekly_points": 850, "weekly_points": 729,
"monthly_points": 7000, "monthly_points": 1215,
"yearly_points": 85000, "yearly_points": 71182,
"created_at": "2026-05-04T22:00:00.000000Z", "created_at": "2026-05-18T21:57:31.000000Z",
"updated_at": "2026-05-04T22:00:00.000000Z" "updated_at": "2026-05-18T21:57:31.000000Z"
} }
] ]
``` ```
--- ---
# 🔗 Integrações com Outros Microsserviços ## Retornos Esperados
## Quais dados recebe | Código | Situação | Exemplo |
| ------ | -------- | ------- |
O microsserviço recebe: | 200 | Requisição autenticada com sucesso | Lista de jogos ou histórico |
| 401 | Token ausente, inválido ou expirado | `{"message":"Missing Authorization header"}` |
* IDs de jogos | 404 | Jogo inexistente em `/rankings/history/{id}` | Resposta padrão do Laravel para model não encontrado |
* parâmetros de filtro | 500 | Erro inesperado no servidor | Falha interna |
* plataformas
* períodos de ranking
--- ---
## Quais dados retorna ## Testes
O serviço retorna: Execute:
* rankings
* estatísticas
* histórico de pontuação
* quantidade de jogadores ativos
Todos os dados são retornados em formato JSON.
---
## Quais serviços consome
O microsserviço consome:
* Microsserviço de Telemetria
* Microsserviço de Catálogo de Jogos
---
## Quais serviços utilizam esta API
Os serviços que utilizam esta API são:
* Front-end GameVerse
* Microsserviço de Loja
* Sistema de Recomendações
---
# 🔄 Fluxo Principal do Serviço
1. O usuário acessa a plataforma GameVerse
2. O Front-end solicita os rankings
3. O microsserviço consulta o banco SQLite
4. Os dados são processados e ordenados
5. O JSON é retornado ao Front-end
6. Os rankings são exibidos ao usuário
---
# ⚠️ Possíveis Erros e Retornos Esperados
| Código | Erro | Descrição |
| ------ | ---------------------- | ------------------------- |
| 400 | Dados inválidos | Parâmetros incorretos |
| 404 | Jogo inexistente | Jogo não encontrado |
| 404 | Plataforma inexistente | Plataforma não encontrada |
| 500 | Erro interno | Falha no servidor |
| 503 | Serviço indisponível | Banco indisponível |
---
# 📤 Exemplo de Erro JSON
```json
{
"success": false,
"message": "Jogo não encontrado"
}
```
---
# 📁 Estrutura do Projeto
```bash ```bash
app/ php artisan test
bootstrap/
config/
database/
public/
resources/
routes/
storage/
tests/
``` ```
Os testes cobrem:
* exigência de autenticação JWT
* ranking semanal, mensal e anual
* jogos mais jogados
* histórico por jogo
* correspondência entre OpenAPI/Scribe e rotas públicas da API
--- ---
# 📦 Arquivos Obrigatórios da Entrega ## Integração com o Projeto Consumidor
O site ou microsserviço consumidor deve:
1. obter um JWT válido no serviço de autenticação do ecossistema;
2. enviar o token no header `Authorization`;
3. consumir os endpoints `/api/v1/*`;
4. tratar respostas `401` quando o token estiver ausente, inválido ou expirado.
Este microsserviço atualmente não emite tokens. Ele apenas valida tokens JWT RS256.
---
## Dados do Serviço
Atualmente os dados são armazenados na tabela `games` e podem ser populados pelos seeders do Laravel.
Campos principais:
| Campo | Descrição |
| ----- | --------- |
| `name` | Nome do jogo |
| `platform` | Plataforma do jogo |
| `active_players` | Quantidade de jogadores ativos |
| `weekly_points` | Pontuação semanal |
| `monthly_points` | Pontuação mensal |
| `yearly_points` | Pontuação anual |
---
## Fluxo Principal
1. O consumidor solicita um ranking.
2. A API valida o JWT.
3. O controller consulta a tabela `games`.
4. Os dados são ordenados conforme o endpoint.
5. A resposta JSON é retornada ao consumidor.
---
## Arquivos da Entrega
Este repositório contém: Este repositório contém:
* README.md * `README.md`
* .env.example * `.env.example`
* Código-fonte completo * código-fonte Laravel
* migrations
* seeders
* testes
* documentação Scribe gerada
⚠️ A pasta `vendor/` não deve ser enviada para o GitHub. A pasta `vendor/` não deve ser enviada para o GitHub.
--- ---
# 📌 Participação no Ecossistema GameVerse ## Contato
Este microsserviço é responsável por fornecer estatísticas e rankings em tempo real dentro do GameVerse. Projeto acadêmico desenvolvido para a disciplina de Microsserviços no contexto do ecossistema GameVerse.
Ele participa diretamente:
* das vitrines de jogos populares
* das recomendações de destaque
* dos rankings competitivos
* das estatísticas globais da plataforma
Seu objetivo é garantir alta performance na consulta de dados estatísticos.
---
# 📬 Contato
Projeto acadêmico desenvolvido para a disciplina de Microsserviços — GameVerse.

3
api/index.php Normal file
View File

@@ -0,0 +1,3 @@
<?php
require __DIR__ . '/../public/index.php';

View File

@@ -3,7 +3,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Game; use App\Models\Game;
use Illuminate\Http\Request;
/** /**
* @group Rankings * @group Rankings
@@ -12,7 +11,8 @@ class GameController extends Controller
{ {
/** /**
* Top semanal * Top semanal
* * Retorna o ranking dos jogos com melhor desempenho na última semana. *
* Retorna o ranking dos jogos com melhor desempenho na última semana.
*/ */
public function weeklyRanking() public function weeklyRanking()
{ {
@@ -22,7 +22,8 @@ class GameController extends Controller
/** /**
* Top mensal * Top mensal
* * Retorna o ranking dos jogos com melhor desempenho no último mês. *
* Retorna o ranking dos jogos com melhor desempenho no último mês.
*/ */
public function monthlyRanking() public function monthlyRanking()
{ {
@@ -32,7 +33,8 @@ class GameController extends Controller
/** /**
* Top anual * Top anual
* * Retorna o ranking dos jogos com melhor desempenho no último ano. *
* Retorna o ranking dos jogos com melhor desempenho no último ano.
*/ */
public function yearlyRanking() public function yearlyRanking()
{ {
@@ -42,7 +44,8 @@ class GameController extends Controller
/** /**
* Jogos mais jogados * Jogos mais jogados
* * Retorna o top 10 jogos com base no número de jogadores ativos. *
* Retorna o top 10 jogos com base no número de jogadores ativos.
*/ */
public function mostPlayed() public function mostPlayed()
{ {
@@ -50,9 +53,24 @@ class GameController extends Controller
return response()->json($games); return response()->json($games);
} }
/**
* Ranking por plataforma
*/
public function platformRanking($platform)
{
$games = Game::where('platform', $platform)
->orderBy('weekly_points', 'desc')
->take(10)
->get();
return response()->json($games);
}
/** /**
* Histórico de ranking * Histórico de ranking
* * Retorna a evolução de um jogo específico ao longo do tempo. *
* Retorna a evolução de um jogo específico ao longo do tempo.
*
* @urlParam id int required O ID do jogo. Example: 1 * @urlParam id int required O ID do jogo. Example: 1
*/ */
public function history($id) public function history($id)
@@ -67,17 +85,5 @@ class GameController extends Controller
] ]
]); ]);
} }
/**
* Ranking por Plataforma }
* * Retorna os jogos mais bem ranqueados de uma plataforma específica.
* @urlParam platform string required O nome da plataforma. Example: Steam
*/
public function platformRanking($platform)
{
$games = Game::where('platform', $platform)
->orderBy('active_players', 'desc')
->get();
return response()->json($games);
}
}

View File

@@ -28,14 +28,24 @@ class JwtAuthMiddleware
return response()->json(['message' => 'Invalid token algorithm'], 401); return response()->json(['message' => 'Invalid token algorithm'], 401);
} }
if ( if (!$this->signatureIsValid($token, $signature)) {
!$this->signatureIsValid($token, $signature) || return response()->json(['message' => 'Invalid token signature'], 401);
($payload['iss'] ?? null) !== config('jwt.issuer') || }
($payload['aud'] ?? null) !== config('jwt.audience') ||
empty($payload['sub']) || if (($payload['iss'] ?? null) !== config('jwt.issuer')) {
$this->tokenIsExpired($payload) return response()->json(['message' => 'Invalid token issuer'], 401);
) { }
return response()->json(['message' => 'Invalid token'], 401);
if (!$this->audienceIsValid($payload['aud'] ?? null)) {
return response()->json(['message' => 'Invalid token audience'], 401);
}
if (empty($payload['sub'])) {
return response()->json(['message' => 'Invalid token subject'], 401);
}
if ($this->tokenIsExpired($payload)) {
return response()->json(['message' => 'Invalid or expired token'], 401);
} }
$request->attributes->set('auth', [ $request->attributes->set('auth', [
@@ -45,8 +55,10 @@ class JwtAuthMiddleware
return $next($request); return $next($request);
} catch (\Exception $e) { } catch (\InvalidArgumentException $e) {
return response()->json(['message' => 'Invalid or expired token'], 401); return response()->json(['message' => 'Invalid or expired token'], 401);
} catch (\Throwable $e) {
return response()->json(['message' => $e->getMessage()], 500);
} }
} }
@@ -91,18 +103,110 @@ class JwtAuthMiddleware
private function signatureIsValid(string $token, string $signature): bool private function signatureIsValid(string $token, string $signature): bool
{ {
[$header, $payload] = explode('.', $token, 3); [$header, $payload] = explode('.', $token, 3);
$publicKey = str_replace('\\n', "\n", (string) config('jwt.public_key')); $publicKey = $this->normalizePublicKey((string) config('jwt.public_key'));
if ($publicKey === '') { if ($publicKey === '') {
return false; throw new \RuntimeException('JWT public key is empty');
} }
return openssl_verify( $this->flushOpenSslErrors();
$keyResource = openssl_pkey_get_public($publicKey);
if ($keyResource === false) {
throw new \RuntimeException($this->openSslErrorMessage('OpenSSL could not read JWT public key'));
}
$result = openssl_verify(
$header . '.' . $payload, $header . '.' . $payload,
$signature, $signature,
$publicKey, $keyResource,
OPENSSL_ALGO_SHA256 OPENSSL_ALGO_SHA256
) === 1; );
if ($result === false) {
throw new \RuntimeException($this->openSslErrorMessage('OpenSSL could not verify JWT signature'));
}
return $result === 1;
}
private function normalizePublicKey(string $publicKey): string
{
$publicKey = trim($publicKey);
if (
(str_starts_with($publicKey, '"') && str_ends_with($publicKey, '"')) ||
(str_starts_with($publicKey, "'") && str_ends_with($publicKey, "'"))
) {
$publicKey = substr($publicKey, 1, -1);
}
$publicKey = trim(str_replace(['\\r\\n', '\\n', '\\r', "\r\n", "\r"], "\n", $publicKey));
if ($publicKey === '') {
return '';
}
if (
preg_match(
'/-----BEGIN ([A-Z ]*PUBLIC KEY)-----(.*?)-----END \1-----/s',
$publicKey,
$matches
)
) {
$type = $matches[1];
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $matches[2]);
if ($body === '') {
return '';
}
return "-----BEGIN {$type}-----\n"
. chunk_split($body, 64, "\n")
. "-----END {$type}-----\n";
}
if (preg_match('/-----BEGIN ([A-Z ]*PUBLIC KEY)-----(.*)/s', $publicKey, $matches)) {
$type = $matches[1];
$bodySource = preg_split('/-----END|END\s+(?:RSA\s+)?PUBLIC\s+KEY/i', $matches[2], 2)[0];
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $bodySource);
if (strlen($body) > 100) {
return "-----BEGIN {$type}-----\n"
. chunk_split($body, 64, "\n")
. "-----END {$type}-----\n";
}
}
if (!str_contains($publicKey, '-----BEGIN')) {
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $publicKey);
if (strlen($body) > 100) {
return "-----BEGIN PUBLIC KEY-----\n"
. chunk_split($body, 64, "\n")
. "-----END PUBLIC KEY-----\n";
}
}
return trim($publicKey) . "\n";
}
private function flushOpenSslErrors(): void
{
while (openssl_error_string() !== false) {
// Clear stale OpenSSL errors before reading the next operation result.
}
}
private function openSslErrorMessage(string $fallback): string
{
$errors = [];
while (($error = openssl_error_string()) !== false) {
$errors[] = $error;
}
return $errors === [] ? $fallback : implode(' | ', $errors);
} }
private function tokenIsExpired(array $payload): bool private function tokenIsExpired(array $payload): bool
@@ -113,4 +217,20 @@ class JwtAuthMiddleware
return time() >= (int) $payload['exp']; return time() >= (int) $payload['exp'];
} }
private function audienceIsValid(mixed $audience): bool
{
$expectedAudience = config('jwt.audience');
if (is_string($audience)) {
return $audience === $expectedAudience;
}
if (is_array($audience)) {
return in_array($expectedAudience, $audience, true);
}
return false;
}
} }

View File

@@ -8,4 +8,6 @@ use Illuminate\Database\Eloquent\Model;
class Game extends Model class Game extends Model
{ {
use HasFactory; use HasFactory;
protected $guarded = [];
} }

View File

@@ -5,7 +5,9 @@
"keywords": ["laravel", "framework"], "keywords": ["laravel", "framework"],
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.1", "php": "^8.2",
"ext-mbstring": "*",
"ext-pdo_pgsql": "*",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10", "laravel/framework": "^10.10",
"laravel/sanctum": "^3.3", "laravel/sanctum": "^3.3",
@@ -18,8 +20,7 @@
"laravel/sail": "^1.18", "laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0", "nunomaduro/collision": "^7.0",
"phpunit/phpunit": "^10.1", "phpunit/phpunit": "^10.1"
"spatie/laravel-ignition": "^2.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

390
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "dfdbaa9a791903e1a015e16cbcf49f3b", "content-hash": "aebe5262c364c90855a307eb2f8f54d9",
"packages": [ "packages": [
{ {
"name": "brick/math", "name": "brick/math",
@@ -7942,390 +7942,6 @@
], ],
"time": "2024-02-20T11:51:46+00:00" "time": "2024-02-20T11:51:46+00:00"
}, },
{
"name": "spatie/backtrace",
"version": "1.8.2",
"source": {
"type": "git",
"url": "https://github.com/spatie/backtrace.git",
"reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/backtrace/zipball/8ffe78be5ed355b5009e3dd989d183433e9a5adc",
"reference": "8ffe78be5ed355b5009e3dd989d183433e9a5adc",
"shasum": ""
},
"require": {
"php": "^7.3 || ^8.0"
},
"require-dev": {
"ext-json": "*",
"laravel/serializable-closure": "^1.3 || ^2.0",
"phpunit/phpunit": "^9.3 || ^11.4.3",
"spatie/phpunit-snapshot-assertions": "^4.2 || ^5.1.6",
"symfony/var-dumper": "^5.1|^6.0|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\Backtrace\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van de Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "A better backtrace",
"homepage": "https://github.com/spatie/backtrace",
"keywords": [
"Backtrace",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/backtrace/issues",
"source": "https://github.com/spatie/backtrace/tree/1.8.2"
},
"funding": [
{
"url": "https://github.com/sponsors/spatie",
"type": "github"
},
{
"url": "https://spatie.be/open-source/support-us",
"type": "other"
}
],
"time": "2026-03-11T13:48:28+00:00"
},
{
"name": "spatie/error-solutions",
"version": "1.1.3",
"source": {
"type": "git",
"url": "https://github.com/spatie/error-solutions.git",
"reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/error-solutions/zipball/e495d7178ca524f2dd0fe6a1d99a1e608e1c9936",
"reference": "e495d7178ca524f2dd0fe6a1d99a1e608e1c9936",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"illuminate/broadcasting": "^10.0|^11.0|^12.0",
"illuminate/cache": "^10.0|^11.0|^12.0",
"illuminate/support": "^10.0|^11.0|^12.0",
"livewire/livewire": "^2.11|^3.5.20",
"openai-php/client": "^0.10.1",
"orchestra/testbench": "8.22.3|^9.0|^10.0",
"pestphp/pest": "^2.20|^3.0",
"phpstan/phpstan": "^2.1",
"psr/simple-cache": "^3.0",
"psr/simple-cache-implementation": "^3.0",
"spatie/ray": "^1.28",
"symfony/cache": "^5.4|^6.0|^7.0",
"symfony/process": "^5.4|^6.0|^7.0",
"vlucas/phpdotenv": "^5.5"
},
"suggest": {
"openai-php/client": "Require get solutions from OpenAI",
"simple-cache-implementation": "To cache solutions from OpenAI"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\Ignition\\": "legacy/ignition",
"Spatie\\ErrorSolutions\\": "src",
"Spatie\\LaravelIgnition\\": "legacy/laravel-ignition"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ruben Van Assche",
"email": "ruben@spatie.be",
"role": "Developer"
}
],
"description": "This is my package error-solutions",
"homepage": "https://github.com/spatie/error-solutions",
"keywords": [
"error-solutions",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/error-solutions/issues",
"source": "https://github.com/spatie/error-solutions/tree/1.1.3"
},
"funding": [
{
"url": "https://github.com/Spatie",
"type": "github"
}
],
"time": "2025-02-14T12:29:50+00:00"
},
{
"name": "spatie/flare-client-php",
"version": "1.11.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/flare-client-php.git",
"reference": "fb3ffb946675dba811fbde9122224db2f84daca9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/flare-client-php/zipball/fb3ffb946675dba811fbde9122224db2f84daca9",
"reference": "fb3ffb946675dba811fbde9122224db2f84daca9",
"shasum": ""
},
"require": {
"illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
"php": "^8.0",
"spatie/backtrace": "^1.6.1",
"symfony/http-foundation": "^5.2|^6.0|^7.0|^8.0",
"symfony/mime": "^5.2|^6.0|^7.0|^8.0",
"symfony/process": "^5.2|^6.0|^7.0|^8.0",
"symfony/var-dumper": "^5.2|^6.0|^7.0|^8.0"
},
"require-dev": {
"dms/phpunit-arraysubset-asserts": "^0.5.0",
"pestphp/pest": "^1.20|^2.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"spatie/pest-plugin-snapshots": "^1.0|^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.3.x-dev"
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Spatie\\FlareClient\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "Send PHP errors to Flare",
"homepage": "https://github.com/spatie/flare-client-php",
"keywords": [
"exception",
"flare",
"reporting",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/flare-client-php/issues",
"source": "https://github.com/spatie/flare-client-php/tree/1.11.0"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2026-03-17T08:06:16+00:00"
},
{
"name": "spatie/ignition",
"version": "1.16.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/ignition.git",
"reference": "b59385bb7aa24dae81bcc15850ebecfda7b40838"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/ignition/zipball/b59385bb7aa24dae81bcc15850ebecfda7b40838",
"reference": "b59385bb7aa24dae81bcc15850ebecfda7b40838",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"php": "^8.0",
"spatie/backtrace": "^1.7.1",
"spatie/error-solutions": "^1.1.2",
"spatie/flare-client-php": "^1.9",
"symfony/console": "^5.4.42|^6.0|^7.0|^8.0",
"symfony/http-foundation": "^5.4.42|^6.0|^7.0|^8.0",
"symfony/mime": "^5.4.42|^6.0|^7.0|^8.0",
"symfony/var-dumper": "^5.4.42|^6.0|^7.0|^8.0"
},
"require-dev": {
"illuminate/cache": "^9.52|^10.0|^11.0|^12.0|^13.0",
"mockery/mockery": "^1.4",
"pestphp/pest": "^1.20|^2.0|^3.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"psr/simple-cache-implementation": "*",
"symfony/cache": "^5.4.38|^6.0|^7.0|^8.0",
"symfony/process": "^5.4.35|^6.0|^7.0|^8.0",
"vlucas/phpdotenv": "^5.5"
},
"suggest": {
"openai-php/client": "Require get solutions from OpenAI",
"simple-cache-implementation": "To cache solutions from OpenAI"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.5.x-dev"
}
},
"autoload": {
"psr-4": {
"Spatie\\Ignition\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Spatie",
"email": "info@spatie.be",
"role": "Developer"
}
],
"description": "A beautiful error page for PHP applications.",
"homepage": "https://flareapp.io/ignition",
"keywords": [
"error",
"flare",
"laravel",
"page"
],
"support": {
"docs": "https://flareapp.io/docs/ignition-for-laravel/introduction",
"forum": "https://twitter.com/flareappio",
"issues": "https://github.com/spatie/ignition/issues",
"source": "https://github.com/spatie/ignition"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2026-03-17T10:51:08+00:00"
},
{
"name": "spatie/laravel-ignition",
"version": "2.9.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-ignition.git",
"reference": "1baee07216d6748ebd3a65ba97381b051838707a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/1baee07216d6748ebd3a65ba97381b051838707a",
"reference": "1baee07216d6748ebd3a65ba97381b051838707a",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*",
"illuminate/support": "^10.0|^11.0|^12.0",
"php": "^8.1",
"spatie/ignition": "^1.15",
"symfony/console": "^6.2.3|^7.0",
"symfony/var-dumper": "^6.2.3|^7.0"
},
"require-dev": {
"livewire/livewire": "^2.11|^3.3.5",
"mockery/mockery": "^1.5.1",
"openai-php/client": "^0.8.1|^0.10",
"orchestra/testbench": "8.22.3|^9.0|^10.0",
"pestphp/pest": "^2.34|^3.7",
"phpstan/extension-installer": "^1.3.1",
"phpstan/phpstan-deprecation-rules": "^1.1.1|^2.0",
"phpstan/phpstan-phpunit": "^1.3.16|^2.0",
"vlucas/phpdotenv": "^5.5"
},
"suggest": {
"openai-php/client": "Require get solutions from OpenAI",
"psr/simple-cache-implementation": "Needed to cache solutions from OpenAI"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Flare": "Spatie\\LaravelIgnition\\Facades\\Flare"
},
"providers": [
"Spatie\\LaravelIgnition\\IgnitionServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Spatie\\LaravelIgnition\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Spatie",
"email": "info@spatie.be",
"role": "Developer"
}
],
"description": "A beautiful error page for Laravel applications.",
"homepage": "https://flareapp.io/ignition",
"keywords": [
"error",
"flare",
"laravel",
"page"
],
"support": {
"docs": "https://flareapp.io/docs/ignition-for-laravel/introduction",
"forum": "https://twitter.com/flareappio",
"issues": "https://github.com/spatie/laravel-ignition/issues",
"source": "https://github.com/spatie/laravel-ignition"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-02-20T13:13:55+00:00"
},
{ {
"name": "symfony/var-exporter", "name": "symfony/var-exporter",
"version": "v6.4.36", "version": "v6.4.36",
@@ -8540,7 +8156,9 @@
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^8.1" "php": "^8.2",
"ext-mbstring": "*",
"ext-pdo_pgsql": "*"
}, },
"platform-dev": {}, "platform-dev": {},
"plugin-api-version": "2.9.0" "plugin-api-version": "2.9.0"

View File

@@ -15,7 +15,7 @@ return [
| |
*/ */
'paths' => ['api/*', 'sanctum/csrf-cookie'], 'paths' => ['api/*'],
'allowed_methods' => ['*'], 'allowed_methods' => ['*'],

View File

@@ -15,7 +15,11 @@ return [
| |
*/ */
'default' => env('DB_CONNECTION', 'mysql'), 'default' => env('DB_CONNECTION') ?: (
str_starts_with((string) env('DATABASE_URL'), 'postgres')
? 'pgsql'
: 'mysql'
),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -59,7 +63,7 @@ return [
'strict' => true, 'strict' => true,
'engine' => null, 'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([ 'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), (defined('Pdo\Mysql::ATTR_SSL_CA') ? Pdo\Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [], ]) : [],
], ],

View File

@@ -3,6 +3,7 @@
use Laravel\Sanctum\Sanctum; use Laravel\Sanctum\Sanctum;
return [ return [
'routes' => false,
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

256
config/scribe.php Normal file
View File

@@ -0,0 +1,256 @@
<?php
use Knuckles\Scribe\Config\AuthIn;
use Knuckles\Scribe\Config\Defaults;
use Knuckles\Scribe\Extracting\Strategies;
use function Knuckles\Scribe\Config\configureStrategy;
use function Knuckles\Scribe\Config\removeStrategies;
// Only the most common configs are shown. See the https://scribe.knuckles.wtf/laravel/reference/config for all.
return [
// The HTML <title> for the generated documentation.
'title' => 'Game Ranking API Documentation',
// A short description of your API. Will be included in the docs webpage, Postman collection and OpenAPI spec.
'description' => 'Microsserviço de rankings e métricas de jogos para integração com o ecossistema GameVerse.',
// Text to place in the "Introduction" section, right after the `description`. Markdown and HTML are supported.
'intro_text' => <<<'INTRO'
Esta API expõe rankings semanais, mensais e anuais, jogos mais jogados e histórico de pontuação.
<aside>Use os exemplos da documentação para demonstrar como o frontend ou outros microsserviços podem consumir os dados de ranking.</aside>
INTRO,
// The base URL displayed in the docs.
// If you're using `laravel` type, you can set this to a dynamic string, like '{{ config("app.tenant_url") }}' to get a dynamic base URL.
'base_url' => config('app.url'),
// Routes to include in the docs
'routes' => [
[
'match' => [
// Match only routes whose paths match this pattern (use * as a wildcard to match any characters). Example: 'users/*'.
'prefixes' => ['api/*'],
// Match only routes whose domains match this pattern (use * as a wildcard to match any characters). Example: 'api.*'.
'domains' => ['*'],
],
// Include these routes even if they did not match the rules above.
'include' => [
// 'users.index', 'POST /new', '/auth/*'
],
// Exclude these routes even if they matched the rules above.
'exclude' => [],
],
],
// The type of documentation output to generate.
// - "static" will generate a static HTMl page in the /public/docs folder,
// - "laravel" will generate the documentation as a Blade view, so you can add routing and authentication.
// - "external_static" and "external_laravel" do the same as above, but pass the OpenAPI spec as a URL to an external UI template
'type' => 'laravel',
// See https://scribe.knuckles.wtf/laravel/reference/config#theme for supported options
'theme' => 'default',
'static' => [
// HTML documentation, assets and Postman collection will be generated to this folder.
// Source Markdown will still be in resources/docs.
'output_path' => 'public/docs',
],
'laravel' => [
// Whether to automatically create a docs route for you to view your generated docs. You can still set up routing manually.
'add_routes' => false,
// URL path to use for the docs endpoint (if `add_routes` is true).
// By default, `/docs` opens the HTML page, `/docs.postman` opens the Postman collection, and `/docs.openapi` the OpenAPI spec.
'docs_url' => '/docs',
// Directory within `public` in which to store CSS and JS assets.
// By default, assets are stored in `public/vendor/scribe`.
// If set, assets will be stored in `public/{{assets_directory}}`
'assets_directory' => null,
// Middleware to attach to the docs endpoint (if `add_routes` is true).
'middleware' => [],
],
'external' => [
'html_attributes' => [],
],
'try_it_out' => [
// Add a Try It Out button to your endpoints so consumers can test endpoints right from their browser.
// Don't forget to enable CORS headers for your endpoints.
'enabled' => true,
// The base URL to use in the API tester. Leave as null to be the same as the displayed URL (`scribe.base_url`).
'base_url' => null,
// [Laravel Sanctum] Fetch a CSRF token before each request, and add it as an X-XSRF-TOKEN header.
'use_csrf' => false,
// The URL to fetch the CSRF token from (if `use_csrf` is true).
'csrf_url' => '/sanctum/csrf-cookie',
],
// How is your API authenticated? This information will be used in the displayed docs, generated examples and response calls.
'auth' => [
// Set this to true if ANY endpoints in your API use authentication.
'enabled' => true,
// Set this to true if your API should be authenticated by default. If so, you must also set `enabled` (above) to true.
// You can then use @unauthenticated or @authenticated on individual endpoints to change their status from the default.
'default' => true,
// Where is the auth value meant to be sent in a request?
'in' => AuthIn::BEARER->value,
// The name of the auth parameter (e.g. token, key, apiKey) or header (e.g. Authorization, Api-Key).
'name' => 'Authorization',
// The value of the parameter to be used by Scribe to authenticate response calls.
// This will NOT be included in the generated documentation. If empty, Scribe will use a random value.
'use_value' => env('SCRIBE_AUTH_KEY'),
// Placeholder your users will see for the auth parameter in the example requests.
// Set this to null if you want Scribe to use a random value as placeholder instead.
'placeholder' => '{YOUR_JWT_TOKEN}',
// Any extra authentication-related info for your users. Markdown and HTML are supported.
'extra_info' => 'Use um token JWT RS256 emitido pelo serviço de autenticação integrado ao GameVerse.',
],
// Example requests for each endpoint will be shown in each of these languages.
// Supported options are: bash, javascript, php, python
// To add a language of your own, see https://scribe.knuckles.wtf/laravel/advanced/example-requests
// Note: does not work for `external` docs types
'example_languages' => [
'bash',
'javascript',
],
// Generate a Postman collection (v2.1.0) in addition to HTML docs.
// For 'static' docs, the collection will be generated to public/docs/collection.json.
// For 'laravel' docs, it will be generated to storage/app/scribe/collection.json.
// Setting `laravel.add_routes` to true (above) will also add a route for the collection.
'postman' => [
'enabled' => true,
'overrides' => [
// 'info.version' => '2.0.0',
],
],
// Generate an OpenAPI spec in addition to docs webpage.
// For 'static' docs, the collection will be generated to public/docs/openapi.yaml.
// For 'laravel' docs, it will be generated to storage/app/scribe/openapi.yaml.
// Setting `laravel.add_routes` to true (above) will also add a route for the spec.
'openapi' => [
'enabled' => true,
// The OpenAPI spec version to generate. Supported versions: '3.0.3', '3.1.0'.
// OpenAPI 3.1 is more compatible with JSON Schema and is becoming the dominant version.
// See https://spec.openapis.org/oas/v3.1.0 for details on 3.1 changes.
'version' => '3.0.3',
'overrides' => [
// 'info.version' => '2.0.0',
],
// Additional generators to use when generating the OpenAPI spec.
// Should extend `Knuckles\Scribe\Writing\OpenApiSpecGenerators\OpenApiGenerator`.
'generators' => [],
],
'groups' => [
// Endpoints which don't have a @group will be placed in this default group.
'default' => 'Endpoints',
// By default, Scribe will sort groups alphabetically, and endpoints in the order their routes are defined.
// You can override this by listing the groups, subgroups and endpoints here in the order you want them.
// See https://scribe.knuckles.wtf/blog/laravel-v4#easier-sorting and https://scribe.knuckles.wtf/laravel/reference/config#order for details
// Note: does not work for `external` docs types
'order' => [],
],
// Custom logo path. This will be used as the value of the src attribute for the <img> tag,
// so make sure it points to an accessible URL or path. Set to false to not use a logo.
// For example, if your logo is in public/img:
// - 'logo' => '../img/logo.png' // for `static` type (output folder is public/docs)
// - 'logo' => 'img/logo.png' // for `laravel` type
'logo' => false,
// Customize the "Last updated" value displayed in the docs by specifying tokens and formats.
// Examples:
// - {date:F j Y} => March 28, 2022
// - {git:short} => Short hash of the last Git commit
// Available tokens are `{date:<format>}` and `{git:<format>}`.
// The format you pass to `date` will be passed to PHP's `date()` function.
// The format you pass to `git` can be either "short" or "long".
// Note: does not work for `external` docs types
'last_updated' => 'Last updated: {date:F j, Y}',
'examples' => [
// Set this to any number to generate the same example values for parameters on each run,
'faker_seed' => 1234,
// With API resources and transformers, Scribe tries to generate example models to use in your API responses.
// By default, Scribe will try the model's factory, and if that fails, try fetching the first from the database.
// You can reorder or remove strategies here.
'models_source' => ['factoryCreate', 'factoryMake', 'databaseFirst'],
],
// The strategies Scribe will use to extract information about your routes at each stage.
// Use configureStrategy() to specify settings for a strategy in the list.
// Use removeStrategies() to remove an included strategy.
'strategies' => [
'metadata' => [
...Defaults::METADATA_STRATEGIES,
],
'headers' => [
...Defaults::HEADERS_STRATEGIES,
Strategies\StaticData::withSettings(data: [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
]),
],
'urlParameters' => [
...Defaults::URL_PARAMETERS_STRATEGIES,
],
'queryParameters' => [
...Defaults::QUERY_PARAMETERS_STRATEGIES,
],
'bodyParameters' => [
...Defaults::BODY_PARAMETERS_STRATEGIES,
],
'responses' => configureStrategy(
Defaults::RESPONSES_STRATEGIES,
Strategies\Responses\ResponseCalls::withSettings(
only: ['GET *'],
// Recommended: disable debug mode in response calls to avoid error stack traces in responses
config: [
'app.debug' => false,
]
)
),
'responseFields' => [
...Defaults::RESPONSE_FIELDS_STRATEGIES,
],
],
// For response calls, API resource responses and transformer responses,
// Scribe will try to start database transactions, so no changes are persisted to your database.
// Tell Scribe which connections should be transacted here. If you only use one db connection, you can leave this as is.
'database_connections_to_transact' => [config('database.default')],
'fractal' => [
// If you are using a custom serializer with league/fractal, you can specify it here.
'serializer' => null,
],
];

View File

@@ -13,32 +13,25 @@ class DatabaseSeeder extends Seeder
public function run(): void public function run(): void
{ {
$jogosAtuais = [ $jogosAtuais = [
['name' => 'Counter-Strike 2', 'platform' => 'Steam'], ['id' => 1, 'name' => 'Counter-Strike 2', 'platform' => 'Steam', 'active_players' => 1450000, 'weekly_points' => 980, 'monthly_points' => 9400, 'yearly_points' => 91000],
['name' => 'Elden Ring', 'platform' => 'Steam'], ['id' => 2, 'name' => 'Elden Ring', 'platform' => 'Steam', 'active_players' => 320000, 'weekly_points' => 870, 'monthly_points' => 8500, 'yearly_points' => 88000],
['name' => 'Valorant', 'platform' => 'Riot Launcher'], ['id' => 3, 'name' => 'Valorant', 'platform' => 'Riot Launcher', 'active_players' => 1180000, 'weekly_points' => 920, 'monthly_points' => 9100, 'yearly_points' => 96000],
['name' => 'Helldivers 2', 'platform' => 'Steam'], ['id' => 4, 'name' => 'Helldivers 2', 'platform' => 'Steam', 'active_players' => 410000, 'weekly_points' => 820, 'monthly_points' => 7900, 'yearly_points' => 74000],
['name' => 'Baldur\'s Gate 3', 'platform' => 'Steam'], ['id' => 5, 'name' => 'Baldur\'s Gate 3', 'platform' => 'Steam', 'active_players' => 260000, 'weekly_points' => 760, 'monthly_points' => 8300, 'yearly_points' => 90000],
['name' => 'Fortnite', 'platform' => 'Epic Games'], ['id' => 6, 'name' => 'Fortnite', 'platform' => 'Epic Games', 'active_players' => 1800000, 'weekly_points' => 950, 'monthly_points' => 9300, 'yearly_points' => 99000],
['name' => 'Grand Theft Auto V', 'platform' => 'Steam'], ['id' => 7, 'name' => 'Grand Theft Auto V', 'platform' => 'Steam', 'active_players' => 720000, 'weekly_points' => 700, 'monthly_points' => 7200, 'yearly_points' => 81000],
['name' => 'EA SPORTS FC 24', 'platform' => 'Steam'], ['id' => 8, 'name' => 'EA SPORTS FC 24', 'platform' => 'Steam', 'active_players' => 540000, 'weekly_points' => 650, 'monthly_points' => 6900, 'yearly_points' => 76000],
['name' => 'Roblox', 'platform' => 'Multiplataforma'], ['id' => 9, 'name' => 'Roblox', 'platform' => 'Multiplataforma', 'active_players' => 1600000, 'weekly_points' => 890, 'monthly_points' => 9000, 'yearly_points' => 97000],
['name' => 'League of Legends', 'platform' => 'Riot Launcher'], ['id' => 10, 'name' => 'League of Legends', 'platform' => 'Riot Launcher', 'active_players' => 1500000, 'weekly_points' => 930, 'monthly_points' => 9200, 'yearly_points' => 98000],
['name' => 'Apex Legends', 'platform' => 'Steam'], ['id' => 11, 'name' => 'Apex Legends', 'platform' => 'Steam', 'active_players' => 680000, 'weekly_points' => 610, 'monthly_points' => 6600, 'yearly_points' => 70000],
['name' => 'Call of Duty: Warzone', 'platform' => 'Battle.net'], ['id' => 12, 'name' => 'Call of Duty: Warzone', 'platform' => 'Battle.net', 'active_players' => 830000, 'weekly_points' => 810, 'monthly_points' => 7800, 'yearly_points' => 86000],
['name' => 'Minecraft', 'platform' => 'Multiplataforma'], ['id' => 13, 'name' => 'Minecraft', 'platform' => 'Multiplataforma', 'active_players' => 1750000, 'weekly_points' => 880, 'monthly_points' => 8800, 'yearly_points' => 95000],
['name' => 'Cyberpunk 2077', 'platform' => 'Steam'], ['id' => 14, 'name' => 'Cyberpunk 2077', 'platform' => 'Steam', 'active_players' => 210000, 'weekly_points' => 560, 'monthly_points' => 6100, 'yearly_points' => 72000],
['name' => 'Stardew Valley', 'platform' => 'Steam'], ['id' => 15, 'name' => 'Stardew Valley', 'platform' => 'Steam', 'active_players' => 180000, 'weekly_points' => 520, 'monthly_points' => 5400, 'yearly_points' => 68000],
]; ];
foreach ($jogosAtuais as $jogo) { foreach ($jogosAtuais as $jogo) {
Game::create([ Game::updateOrCreate(['id' => $jogo['id']], $jogo);
'name' => $jogo['name'],
'platform' => $jogo['platform'],
'active_players' => fake()->numberBetween(50000, 1800000), // Jogadores de 50k a 1.8M
'weekly_points' => fake()->numberBetween(100, 1000),
'monthly_points' => fake()->numberBetween(1000, 10000),
'yearly_points' => fake()->numberBetween(10000, 100000),
]);
} }
} }
} }

View File

@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build" "build": "node -e \"console.log('No frontend build required for this API')\""
}, },
"devDependencies": { "devDependencies": {
"axios": "^1.6.4", "axios": "^1.6.4",

View File

@@ -21,8 +21,8 @@
<env name="APP_ENV" value="testing"/> <env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/> <env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/> <env name="CACHE_DRIVER" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> --> <env name="DB_CONNECTION" value="sqlite"/>
<!-- <env name="DB_DATABASE" value=":memory:"/> --> <env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/> <env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/> <env name="PULSE_ENABLED" value="false"/>
<env name="QUEUE_CONNECTION" value="sync"/> <env name="QUEUE_CONNECTION" value="sync"/>

1
public/health Normal file
View File

@@ -0,0 +1 @@
ok

14
railway.json Normal file
View File

@@ -0,0 +1,14 @@
{
"$schema": "https://railway.com/railway.schema.json",
"build": {
"builder": "RAILPACK"
},
"deploy": {
"preDeployCommand": "php artisan migrate --force",
"startCommand": "php artisan serve --host=0.0.0.0 --port=${PORT:-8000}",
"healthcheckPath": "/health",
"healthcheckTimeout": 600,
"restartPolicyType": "ON_FAILURE",
"restartPolicyMaxRetries": 10
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,8 @@
<?php <?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use App\Http\Controllers\GameController; use App\Http\Controllers\GameController;
/* /*
@@ -21,12 +22,228 @@ Route::prefix('v1')->middleware(['jwt.auth'])->group(function () {
// Jogos // Jogos
Route::get('/games/most-played', [GameController::class, 'mostPlayed']); Route::get('/games/most-played', [GameController::class, 'mostPlayed']);
}); });
// 🔓 Rota de teste (opcional) Route::middleware(['jwt.auth'])->post('/dev/seed-games', function () {
Route::middleware(['jwt.auth'])->get('/test-auth', function (Request $request) { app(\Database\Seeders\DatabaseSeeder::class)->run();
return response()->json([ return response()->json([
'userId' => $request->attributes->get('auth')['id'] 'status' => 'seeded',
'games_count' => DB::table('games')->count(),
]); ]);
}); });
Route::get('/health', function () {
return response()->json(['status' => 'ok']);
});
Route::get('/health-check-key', function () {
$rawPublicKey = (string) config('jwt.public_key');
$formattedPublicKey = trim($rawPublicKey);
if (
(str_starts_with($formattedPublicKey, '"') && str_ends_with($formattedPublicKey, '"')) ||
(str_starts_with($formattedPublicKey, "'") && str_ends_with($formattedPublicKey, "'"))
) {
$formattedPublicKey = substr($formattedPublicKey, 1, -1);
}
$formattedPublicKey = trim(str_replace(['\\r\\n', '\\n', '\\r', "\r\n", "\r"], "\n", $formattedPublicKey));
$pemType = null;
$bodyLength = null;
if (preg_match('/-----BEGIN ([A-Z ]*PUBLIC KEY)-----(.*?)-----END \1-----/s', $formattedPublicKey, $matches)) {
$pemType = $matches[1];
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $matches[2]);
$bodyLength = strlen($body);
$formattedPublicKey = "-----BEGIN {$pemType}-----\n"
. chunk_split($body, 64, "\n")
. "-----END {$pemType}-----\n";
} elseif (preg_match('/-----BEGIN ([A-Z ]*PUBLIC KEY)-----(.*)/s', $formattedPublicKey, $matches)) {
$pemType = $matches[1];
$bodySource = preg_split('/-----END|END\s+(?:RSA\s+)?PUBLIC\s+KEY/i', $matches[2], 2)[0];
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $bodySource);
$bodyLength = strlen($body);
if ($bodyLength > 100) {
$formattedPublicKey = "-----BEGIN {$pemType}-----\n"
. chunk_split($body, 64, "\n")
. "-----END {$pemType}-----\n";
}
} elseif (!str_contains($formattedPublicKey, '-----BEGIN')) {
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $formattedPublicKey);
$bodyLength = strlen($body);
if ($bodyLength > 100) {
$pemType = 'PUBLIC KEY';
$formattedPublicKey = "-----BEGIN PUBLIC KEY-----\n"
. chunk_split($body, 64, "\n")
. "-----END PUBLIC KEY-----\n";
}
}
while (openssl_error_string() !== false) {
// Clear stale OpenSSL errors before testing the current key.
}
$publicKeyResource = openssl_pkey_get_public($formattedPublicKey);
$openSslErrors = [];
$publicKeyDetails = $publicKeyResource === false ? null : openssl_pkey_get_details($publicKeyResource);
$publicKeyPem = is_array($publicKeyDetails) ? ($publicKeyDetails['key'] ?? null) : null;
while (($error = openssl_error_string()) !== false) {
$openSslErrors[] = $error;
}
return response()->json([
'raw_key_empty' => $rawPublicKey === '',
'raw_key_length' => strlen($rawPublicKey),
'formatted_key_length' => strlen($formattedPublicKey),
'pem_type' => $pemType,
'pem_body_length' => $bodyLength,
'has_begin_marker' => str_contains($rawPublicKey, '-----BEGIN PUBLIC KEY-----'),
'has_rsa_begin_marker' => str_contains($rawPublicKey, '-----BEGIN RSA PUBLIC KEY-----'),
'has_end_marker' => str_contains($rawPublicKey, '-----END PUBLIC KEY-----'),
'has_rsa_end_marker' => str_contains($rawPublicKey, '-----END RSA PUBLIC KEY-----'),
'openssl_accepted' => $publicKeyResource !== false,
'public_key_fingerprint_sha256' => is_string($publicKeyPem) ? hash('sha256', $publicKeyPem) : null,
'openssl_errors' => $openSslErrors,
]);
});
Route::get('/health-check-db', function () {
try {
$hasGamesTable = Schema::hasTable('games');
return response()->json([
'connection' => config('database.default'),
'driver' => DB::connection()->getDriverName(),
'database' => DB::connection()->getDatabaseName(),
'games_table_exists' => $hasGamesTable,
'games_count' => $hasGamesTable ? DB::table('games')->count() : null,
]);
} catch (Throwable $e) {
return response()->json([
'connection' => config('database.default'),
'error' => $e->getMessage(),
], 500);
}
});
Route::get('/health-check-token', function (\Illuminate\Http\Request $request) {
$authHeader = $request->header('Authorization');
if (!$authHeader || !preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
return response()->json([
'authorization_header_present' => (bool) $authHeader,
'error' => 'Missing or invalid Bearer token',
], 401);
}
$token = $matches[1];
$parts = explode('.', $token);
if (count($parts) !== 3) {
return response()->json(['error' => 'Invalid token structure'], 401);
}
$decodeBase64Url = function (string $value): string|false {
$value .= str_repeat('=', (4 - strlen($value) % 4) % 4);
return base64_decode(strtr($value, '-_', '+/'), true);
};
$headerJson = $decodeBase64Url($parts[0]);
$payloadJson = $decodeBase64Url($parts[1]);
$signature = $decodeBase64Url($parts[2]);
if ($headerJson === false || $payloadJson === false || $signature === false) {
return response()->json(['error' => 'Invalid base64url token segment'], 401);
}
$header = json_decode($headerJson, true);
$payload = json_decode($payloadJson, true);
if (!is_array($header) || !is_array($payload)) {
return response()->json(['error' => 'Invalid token JSON'], 401);
}
$rawPublicKey = (string) config('jwt.public_key');
$formattedPublicKey = trim($rawPublicKey);
if (
(str_starts_with($formattedPublicKey, '"') && str_ends_with($formattedPublicKey, '"')) ||
(str_starts_with($formattedPublicKey, "'") && str_ends_with($formattedPublicKey, "'"))
) {
$formattedPublicKey = substr($formattedPublicKey, 1, -1);
}
$formattedPublicKey = trim(str_replace(['\\r\\n', '\\n', '\\r', "\r\n", "\r"], "\n", $formattedPublicKey));
if (preg_match('/-----BEGIN ([A-Z ]*PUBLIC KEY)-----(.*?)-----END \1-----/s', $formattedPublicKey, $keyMatches)) {
$pemType = $keyMatches[1];
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $keyMatches[2]);
$formattedPublicKey = "-----BEGIN {$pemType}-----\n"
. chunk_split($body, 64, "\n")
. "-----END {$pemType}-----\n";
} elseif (preg_match('/-----BEGIN ([A-Z ]*PUBLIC KEY)-----(.*)/s', $formattedPublicKey, $keyMatches)) {
$pemType = $keyMatches[1];
$bodySource = preg_split('/-----END|END\s+(?:RSA\s+)?PUBLIC\s+KEY/i', $keyMatches[2], 2)[0];
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $bodySource);
if (strlen($body) > 100) {
$formattedPublicKey = "-----BEGIN {$pemType}-----\n"
. chunk_split($body, 64, "\n")
. "-----END {$pemType}-----\n";
}
}
while (openssl_error_string() !== false) {
// Clear stale OpenSSL errors before verifying the current token.
}
$publicKeyResource = openssl_pkey_get_public($formattedPublicKey);
$publicKeyDetails = $publicKeyResource === false ? null : openssl_pkey_get_details($publicKeyResource);
$publicKeyPem = is_array($publicKeyDetails) ? ($publicKeyDetails['key'] ?? null) : null;
$signatureResult = $publicKeyResource === false
? false
: openssl_verify($parts[0] . '.' . $parts[1], $signature, $publicKeyResource, OPENSSL_ALGO_SHA256);
$openSslErrors = [];
while (($error = openssl_error_string()) !== false) {
$openSslErrors[] = $error;
}
return response()->json([
'token_header' => [
'alg' => $header['alg'] ?? null,
'typ' => $header['typ'] ?? null,
],
'token_claims' => [
'iss' => $payload['iss'] ?? null,
'aud' => $payload['aud'] ?? null,
'sub_present' => !empty($payload['sub']),
'exp' => $payload['exp'] ?? null,
'expired' => isset($payload['exp']) && is_numeric($payload['exp'])
? time() >= (int) $payload['exp']
: null,
],
'expected' => [
'alg' => 'RS256',
'iss' => config('jwt.issuer'),
'aud' => config('jwt.audience'),
],
'checks' => [
'public_key_loaded' => $publicKeyResource !== false,
'public_key_fingerprint_sha256' => is_string($publicKeyPem) ? hash('sha256', $publicKeyPem) : null,
'signature_bytes' => strlen($signature),
'signature_valid' => $signatureResult === 1,
'signature_result' => $signatureResult,
'issuer_valid' => ($payload['iss'] ?? null) === config('jwt.issuer'),
'audience_valid' => is_array($payload['aud'] ?? null)
? in_array(config('jwt.audience'), $payload['aud'], true)
: ($payload['aud'] ?? null) === config('jwt.audience'),
],
'openssl_errors' => $openSslErrors,
]);
});

View File

@@ -1,18 +1,551 @@
<?php <?php
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\DB;
/* use Illuminate\Support\Facades\Schema;
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "web" middleware group. Make something great!
|
*/
Route::get('/', function () { Route::get('/', function () {
return view('welcome'); $baseUrl = rtrim((string) config('app.url'), '/') ?: request()->root();
$routes = [
['name' => 'base', 'method' => 'GET', 'path' => '/', 'auth' => 'Sem JWT', 'type' => 'open'],
['name' => 'health', 'method' => 'GET', 'path' => '/health', 'auth' => 'Sem JWT', 'type' => 'open'],
['name' => 'api_health', 'method' => 'GET', 'path' => '/api/health', 'auth' => 'Sem JWT', 'type' => 'open'],
['name' => 'health_check_key', 'method' => 'GET', 'path' => '/api/health-check-key', 'auth' => 'Sem JWT', 'type' => 'open'],
['name' => 'health_check_db', 'method' => 'GET', 'path' => '/api/health-check-db', 'auth' => 'Sem JWT', 'type' => 'open'],
['name' => 'health_check_token', 'method' => 'GET', 'path' => '/api/health-check-token', 'auth' => 'Bearer para diagnostico', 'type' => 'diagnostic'],
['name' => 'ranking_semanal', 'method' => 'GET', 'path' => '/api/v1/rankings/weekly', 'auth' => 'JWT obrigatorio', 'type' => 'jwt'],
['name' => 'ranking_mensal', 'method' => 'GET', 'path' => '/api/v1/rankings/monthly', 'auth' => 'JWT obrigatorio', 'type' => 'jwt'],
['name' => 'ranking_anual', 'method' => 'GET', 'path' => '/api/v1/rankings/yearly', 'auth' => 'JWT obrigatorio', 'type' => 'jwt'],
['name' => 'ranking_plataforma', 'method' => 'GET', 'path' => '/api/v1/rankings/platforms/Steam', 'auth' => 'JWT obrigatorio', 'type' => 'jwt'],
['name' => 'historico_jogador', 'method' => 'GET', 'path' => '/api/v1/rankings/history/1', 'auth' => 'JWT obrigatorio', 'type' => 'jwt'],
['name' => 'mais_jogados', 'method' => 'GET', 'path' => '/api/v1/games/most-played', 'auth' => 'JWT obrigatorio', 'type' => 'jwt'],
];
$cards = collect($routes)->map(function (array $route) use ($baseUrl) {
$url = $baseUrl . ($route['path'] === '/' ? '' : $route['path']);
$authClass = match ($route['type']) {
'jwt' => 'auth-jwt',
'diagnostic' => 'auth-diagnostic',
default => 'auth-open',
};
return sprintf(
'<article class="route-card">
<div class="route-heading">
<span class="method">%s</span>
<h2>%s</h2>
</div>
<div class="auth %s">%s</div>
<a href="%s" target="_blank" rel="noreferrer">%s</a>
<div class="actions">
<button type="button" data-action="open" data-url="%s">Abrir</button>
<button type="button" data-action="copy" data-url="%s">Copiar</button>
<button type="button" data-action="request" data-auth="%s" data-url="%s">Testar</button>
</div>
</article>',
e($route['method']),
e($route['name']),
$authClass,
e($route['auth']),
e($url),
e($url),
e($url),
e($url),
e($route['type']),
e($url),
);
})->implode('');
return response(<<<HTML
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>API Ranking Jogos</title>
<style>
:root {
color-scheme: dark;
--bg: #111225;
--panel: #17213d;
--panel-border: #27396a;
--text: #f5f7ff;
--muted: #aab4d4;
--gold: #ffd84a;
--green: #2bd071;
--red: #ff6b6b;
--blue: #5aa7ff;
--code: #080b18;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: radial-gradient(circle at top, #1d2040 0, var(--bg) 48rem);
color: var(--text);
}
main {
width: min(1120px, calc(100% - 32px));
margin: 0 auto;
padding: 40px 0 48px;
}
header {
text-align: center;
margin-bottom: 32px;
}
h1 {
margin: 0;
color: var(--gold);
font-size: clamp(2rem, 6vw, 3.4rem);
letter-spacing: 0;
font-weight: 900;
}
header p {
margin: 10px 0 0;
color: var(--muted);
font-size: 1rem;
}
.status {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
width: min(560px, 100%);
margin: 28px auto 34px;
padding: 14px 18px;
border: 1px solid var(--panel-border);
border-radius: 8px;
background: rgba(23, 33, 61, 0.86);
}
.token-panel {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
width: min(860px, 100%);
margin: 0 auto 28px;
padding: 16px;
border: 1px solid var(--panel-border);
border-radius: 8px;
background: rgba(23, 33, 61, 0.82);
}
.token-panel label {
grid-column: 1 / -1;
color: var(--muted);
font-size: 0.92rem;
font-weight: 700;
}
.token-panel input {
min-width: 0;
width: 100%;
height: 42px;
padding: 0 12px;
border: 1px solid var(--panel-border);
border-radius: 6px;
background: var(--code);
color: var(--text);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
button {
min-height: 38px;
padding: 0 14px;
border: 1px solid rgba(255, 216, 74, 0.42);
border-radius: 6px;
background: rgba(255, 216, 74, 0.12);
color: var(--gold);
font: inherit;
font-weight: 800;
cursor: pointer;
}
button:hover {
background: rgba(255, 216, 74, 0.2);
}
.status strong {
color: var(--gold);
}
.dot {
width: 12px;
height: 12px;
margin-top: 5px;
border-radius: 999px;
background: var(--green);
box-shadow: 0 0 14px var(--green);
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 18px;
}
.route-card {
min-width: 0;
padding: 20px;
border: 1px solid var(--panel-border);
border-radius: 8px;
background: rgba(23, 33, 61, 0.92);
}
.route-heading {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.method {
flex: 0 0 auto;
padding: 5px 12px;
border-radius: 4px;
background: #127b3a;
color: #d9ffe7;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.78rem;
font-weight: 800;
}
h2 {
min-width: 0;
margin: 0;
color: var(--gold);
font-size: 1.08rem;
overflow-wrap: anywhere;
}
.auth {
display: inline-flex;
margin-bottom: 12px;
padding: 5px 10px;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 700;
}
.auth-open {
color: #d9ffe7;
background: rgba(43, 208, 113, 0.16);
}
.auth-jwt {
color: #ffe1e1;
background: rgba(255, 107, 107, 0.16);
}
.auth-diagnostic {
color: #dcecff;
background: rgba(90, 167, 255, 0.16);
}
a {
display: block;
width: 100%;
padding: 12px;
border-left: 3px solid var(--gold);
border-radius: 5px;
background: var(--code);
color: #dfe7ff;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.84rem;
line-height: 1.35;
text-decoration: none;
overflow-wrap: anywhere;
}
a:hover {
color: #ffffff;
outline: 1px solid var(--gold);
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 12px;
}
.actions button {
flex: 1 1 108px;
}
.result {
display: none;
margin-top: 28px;
padding: 18px;
border: 1px solid var(--panel-border);
border-radius: 8px;
background: rgba(8, 11, 24, 0.9);
}
.result.visible {
display: block;
}
.result-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
color: var(--muted);
}
pre {
max-height: 420px;
margin: 0;
overflow: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
color: #e8edff;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.88rem;
line-height: 1.55;
}
footer {
margin-top: 34px;
color: #697399;
text-align: center;
font-size: 0.82rem;
}
@media (max-width: 760px) {
main {
width: min(100% - 20px, 1120px);
padding-top: 28px;
}
.grid {
grid-template-columns: 1fr;
}
.token-panel {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<main>
<header>
<h1>API RANKING JOGOS</h1>
<p>Menu de navegacao - rotas GET disponiveis</p>
</header>
<section class="status" aria-label="Status da API">
<span class="dot" aria-hidden="true"></span>
<span>Status: <strong>ok</strong></span>
<span>Service: <strong>api-ranking-jogos</strong></span>
</section>
<section class="token-panel" aria-label="Token JWT">
<label for="jwt-token">JWT para testar rotas privadas</label>
<input id="jwt-token" type="password" autocomplete="off" placeholder="Cole somente o token, sem Bearer">
<button type="button" id="save-token">Salvar token</button>
</section>
<section class="grid">
{$cards}
</section>
<section id="result" class="result" aria-live="polite">
<div class="result-header">
<strong id="result-title">Resposta</strong>
<span id="result-status"></span>
</div>
<pre id="result-body"></pre>
</section>
<footer>api-ranking-jogos - Railway</footer>
</main>
<script>
const tokenInput = document.getElementById('jwt-token');
const saveToken = document.getElementById('save-token');
const result = document.getElementById('result');
const resultTitle = document.getElementById('result-title');
const resultStatus = document.getElementById('result-status');
const resultBody = document.getElementById('result-body');
tokenInput.value = localStorage.getItem('rankingJwtToken') || '';
saveToken.addEventListener('click', () => {
localStorage.setItem('rankingJwtToken', tokenInput.value.trim());
saveToken.textContent = 'Salvo';
setTimeout(() => saveToken.textContent = 'Salvar token', 1400);
});
function showResult(title, status, body) {
result.classList.add('visible');
resultTitle.textContent = title;
resultStatus.textContent = status;
resultBody.textContent = body;
result.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
async function copyText(text) {
await navigator.clipboard.writeText(text);
}
document.addEventListener('click', async (event) => {
const button = event.target.closest('button[data-action]');
if (!button) {
return;
}
const url = button.dataset.url;
const action = button.dataset.action;
if (action === 'open') {
window.open(url, '_blank', 'noreferrer');
return;
}
if (action === 'copy') {
await copyText(url);
const previous = button.textContent;
button.textContent = 'Copiado';
setTimeout(() => button.textContent = previous, 1200);
return;
}
const headers = { Accept: 'application/json' };
const authType = button.dataset.auth;
const token = tokenInput.value.trim();
if ((authType === 'jwt' || authType === 'diagnostic') && token) {
headers.Authorization = 'Bearer ' + token;
}
if ((authType === 'jwt' || authType === 'diagnostic') && !token) {
showResult('Token ausente', '401 local', 'Cole um JWT no campo acima para testar esta rota.');
return;
}
try {
showResult('Carregando', '...', '');
const response = await fetch(url, { headers });
const contentType = response.headers.get('content-type') || '';
const body = contentType.includes('application/json')
? JSON.stringify(await response.json(), null, 2)
: await response.text();
showResult(url, response.status + ' ' + response.statusText, body);
} catch (error) {
showResult(url, 'Erro', error.message);
}
});
</script>
</body>
</html>
HTML);
});
Route::get('/health', function () {
return response()->json(['status' => 'ok']);
});
Route::get('/health-check-key', function () {
$rawPublicKey = (string) config('jwt.public_key');
$formattedPublicKey = trim($rawPublicKey);
if (
(str_starts_with($formattedPublicKey, '"') && str_ends_with($formattedPublicKey, '"')) ||
(str_starts_with($formattedPublicKey, "'") && str_ends_with($formattedPublicKey, "'"))
) {
$formattedPublicKey = substr($formattedPublicKey, 1, -1);
}
$formattedPublicKey = trim(str_replace(['\\r\\n', '\\n', '\\r', "\r\n", "\r"], "\n", $formattedPublicKey));
$pemType = null;
$bodyLength = null;
if (preg_match('/-----BEGIN ([A-Z ]*PUBLIC KEY)-----(.*?)-----END \1-----/s', $formattedPublicKey, $matches)) {
$pemType = $matches[1];
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $matches[2]);
$bodyLength = strlen($body);
$formattedPublicKey = "-----BEGIN {$pemType}-----\n"
. chunk_split($body, 64, "\n")
. "-----END {$pemType}-----\n";
} elseif (preg_match('/-----BEGIN ([A-Z ]*PUBLIC KEY)-----(.*)/s', $formattedPublicKey, $matches)) {
$pemType = $matches[1];
$bodySource = preg_split('/-----END|END\s+(?:RSA\s+)?PUBLIC\s+KEY/i', $matches[2], 2)[0];
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $bodySource);
$bodyLength = strlen($body);
if ($bodyLength > 100) {
$formattedPublicKey = "-----BEGIN {$pemType}-----\n"
. chunk_split($body, 64, "\n")
. "-----END {$pemType}-----\n";
}
} elseif (!str_contains($formattedPublicKey, '-----BEGIN')) {
$body = preg_replace('/[^A-Za-z0-9+\/=]/', '', $formattedPublicKey);
$bodyLength = strlen($body);
if ($bodyLength > 100) {
$pemType = 'PUBLIC KEY';
$formattedPublicKey = "-----BEGIN PUBLIC KEY-----\n"
. chunk_split($body, 64, "\n")
. "-----END PUBLIC KEY-----\n";
}
}
while (openssl_error_string() !== false) {
// Clear stale OpenSSL errors before testing the current key.
}
$publicKeyResource = openssl_pkey_get_public($formattedPublicKey);
$openSslErrors = [];
while (($error = openssl_error_string()) !== false) {
$openSslErrors[] = $error;
}
return response()->json([
'raw_key_empty' => $rawPublicKey === '',
'raw_key_length' => strlen($rawPublicKey),
'formatted_key_length' => strlen($formattedPublicKey),
'pem_type' => $pemType,
'pem_body_length' => $bodyLength,
'has_begin_marker' => str_contains($rawPublicKey, '-----BEGIN PUBLIC KEY-----'),
'has_rsa_begin_marker' => str_contains($rawPublicKey, '-----BEGIN RSA PUBLIC KEY-----'),
'has_end_marker' => str_contains($rawPublicKey, '-----END PUBLIC KEY-----'),
'has_rsa_end_marker' => str_contains($rawPublicKey, '-----END RSA PUBLIC KEY-----'),
'openssl_accepted' => $publicKeyResource !== false,
'openssl_errors' => $openSslErrors,
]);
});
Route::get('/health-check-db', function () {
try {
$hasGamesTable = Schema::hasTable('games');
return response()->json([
'connection' => config('database.default'),
'driver' => DB::connection()->getDriverName(),
'database' => DB::connection()->getDatabaseName(),
'games_table_exists' => $hasGamesTable,
'games_count' => $hasGamesTable ? DB::table('games')->count() : null,
]);
} catch (Throwable $e) {
return response()->json([
'connection' => config('database.default'),
'error' => $e->getMessage(),
], 500);
}
}); });

View File

@@ -0,0 +1,47 @@
<?php
namespace Tests\Feature;
use Illuminate\Support\Facades\Route;
use Symfony\Component\Yaml\Yaml;
use Tests\TestCase;
class DocumentationRoutesTest extends TestCase
{
public function test_openapi_documentation_matches_public_api_routes(): void
{
$openApi = Yaml::parse(file_get_contents(storage_path('app/scribe/openapi.yaml')));
$paths = array_keys($openApi['paths']);
$this->assertSame([
'/api/v1/rankings/weekly',
'/api/v1/rankings/monthly',
'/api/v1/rankings/yearly',
'/api/v1/rankings/history/{id}',
'/api/v1/games/most-played',
], $paths);
$this->assertNotContains('/api/test-auth', $paths);
$this->assertNotContains('/api/health', $paths);
$this->assertNotContains('/api/v1/games', $paths);
$this->assertNotContains('/api/v1/rankings/platforms/{platform}', $paths);
$this->assertNotContains('/api/v1/rankings/history', $paths);
}
public function test_registered_api_v1_routes_are_only_the_requested_endpoints(): void
{
$routes = collect(Route::getRoutes())
->filter(fn ($route) => str_starts_with($route->uri(), 'api/v1/'))
->map(fn ($route) => $route->uri())
->values()
->all();
$this->assertSame([
'api/v1/rankings/weekly',
'api/v1/rankings/monthly',
'api/v1/rankings/yearly',
'api/v1/rankings/history/{id}',
'api/v1/games/most-played',
], $routes);
}
}

View File

@@ -1,19 +0,0 @@
<?php
namespace Tests\Feature;
// use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class GameRankingApiTest extends TestCase
{
use RefreshDatabase;
private string $jwt;
private string $privateKey;
protected function setUp(): void
{
parent::setUp();
$this->configureJwt();
$this->seedGames();
}
public function test_rankings_require_jwt_authentication(): void
{
$this->getJson('/api/v1/rankings/weekly')
->assertUnauthorized()
->assertJson(['message' => 'Missing Authorization header']);
}
public function test_weekly_monthly_yearly_and_most_played_rankings_return_top_ten_in_expected_order(): void
{
$this->getJsonWithJwt('/api/v1/rankings/weekly')
->assertOk()
->assertJsonCount(10)
->assertJsonPath('0.name', 'Game 12')
->assertJsonPath('9.name', 'Game 3');
$this->getJsonWithJwt('/api/v1/rankings/monthly')
->assertOk()
->assertJsonCount(10)
->assertJsonPath('0.name', 'Game 12')
->assertJsonPath('9.name', 'Game 3');
$this->getJsonWithJwt('/api/v1/rankings/yearly')
->assertOk()
->assertJsonCount(10)
->assertJsonPath('0.name', 'Game 12')
->assertJsonPath('9.name', 'Game 3');
$this->getJsonWithJwt('/api/v1/games/most-played')
->assertOk()
->assertJsonCount(10)
->assertJsonPath('0.name', 'Game 12')
->assertJsonPath('9.name', 'Game 3');
}
public function test_history_returns_score_evolution_for_a_game(): void
{
$this->getJsonWithJwt('/api/v1/rankings/history/5')
->assertOk()
->assertJsonPath('game', 'Game 5')
->assertJsonPath('history.0.period', 'Semana 1')
->assertJsonPath('history.0.points', 500)
->assertJsonPath('history.1.period', 'Mês Atual')
->assertJsonPath('history.1.points', 5000)
->assertJsonPath('history.2.period', 'Ano Atual')
->assertJsonPath('history.2.points', 50000);
}
public function test_accepts_token_with_audience_array_containing_expected_audience(): void
{
$this->jwt = $this->makeJwt($this->privateKey, ['other-api', 'ranking-api']);
$this->getJsonWithJwt('/api/v1/games/most-played')
->assertOk()
->assertJsonCount(10);
}
public function test_rejects_generic_bearer_token(): void
{
$this->withHeader('Authorization', 'Bearer token-do-front')
->getJson('/api/v1/rankings/weekly')
->assertUnauthorized()
->assertJson(['message' => 'Invalid or expired token']);
}
private function getJsonWithJwt(string $uri)
{
return $this->withHeader('Authorization', 'Bearer '.$this->jwt)
->getJson($uri);
}
private function configureJwt(): void
{
$key = openssl_pkey_new([
'private_key_type' => OPENSSL_KEYTYPE_RSA,
'private_key_bits' => 2048,
]);
openssl_pkey_export($key, $privateKey);
$this->privateKey = $privateKey;
$publicKey = openssl_pkey_get_details($key)['key'];
config([
'jwt.issuer' => 'gameverse-auth',
'jwt.audience' => 'ranking-api',
'jwt.public_key' => $publicKey,
]);
$this->jwt = $this->makeJwt($privateKey);
}
private function makeJwt(string $privateKey, string|array $audience = 'ranking-api'): string
{
$encode = fn (string $value): string => rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
$header = $encode(json_encode(['alg' => 'RS256', 'typ' => 'JWT']));
$payload = $encode(json_encode([
'iss' => 'gameverse-auth',
'aud' => $audience,
'sub' => 'consumer-project',
'iat' => time(),
'exp' => time() + 3600,
]));
$data = $header.'.'.$payload;
openssl_sign($data, $signature, $privateKey, OPENSSL_ALGO_SHA256);
return $data.'.'.$encode($signature);
}
private function seedGames(): void
{
$now = now();
for ($id = 1; $id <= 12; $id++) {
DB::table('games')->insert([
'id' => $id,
'name' => 'Game '.$id,
'platform' => $id % 2 === 0 ? 'Steam' : 'Riot Launcher',
'active_players' => $id * 1000,
'weekly_points' => $id * 100,
'monthly_points' => $id * 1000,
'yearly_points' => $id * 10000,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
}

15
vercel.json Normal file
View File

@@ -0,0 +1,15 @@
{
"version": 2,
"outputDirectory": "api",
"functions": {
"api/index.php": {
"runtime": "vercel-php@0.7.3"
}
},
"routes": [
{
"src": "/(.*)",
"dest": "/api/index.php"
}
]
}