Compare commits

...

38 Commits

Author SHA1 Message Date
bfad145639 Atualizar README.md 2026-05-22 04:44:00 +00:00
7645604736 Atualizar README.md 2026-05-22 04:43:08 +00:00
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 1952 additions and 1697 deletions

View File

@@ -9,6 +9,7 @@ LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
DATABASE_URL=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
@@ -53,7 +54,18 @@ VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
JWT_ISSUER=
JWT_AUDIENCE=
JWT_PUBLIC_KEY_PEM=
JWT_ISSUER=https://sistema-distribuido-trabalho-faculd.vercel.app
JWT_AUDIENCE=internal-apis
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=
# 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.
# Scribe uses this file to know when you change something manually in your docs.
.scribe/intro.md=4bf90470e636417926ae5d9227747d45
.scribe/auth.md=9bee2b1ef8a238b2e58613fa636d5f39
.scribe/intro.md=7b0dd61cd08d5f1bff8f917a5c809588
.scribe/auth.md=8bb19ce54cd9ee69ae447231bc375761

View File

@@ -1,3 +1,7 @@
# 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: ''
subgroup: ''
subgroupDescription: ''
title: |-
Top semanal
* Retorna o ranking dos jogos com melhor desempenho na última semana.
description: ''
authenticated: false
title: 'Top semanal'
description: 'Retorna o ranking dos jogos com melhor desempenho na última semana.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters: []
@@ -33,17 +32,20 @@ endpoints:
responses:
-
custom: []
status: 200
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"}]'
status: 401
content: '{"message":"Invalid or expired token"}'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '49'
x-ratelimit-remaining: '54'
access-control-allow-origin: '*'
description: null
responseFields: []
auth: []
auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null
method: null
route: null
@@ -58,13 +60,12 @@ endpoints:
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: |-
Top mensal
* Retorna o ranking dos jogos com melhor desempenho no último mês.
description: ''
authenticated: false
title: 'Top mensal'
description: 'Retorna o ranking dos jogos com melhor desempenho no último mês.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters: []
@@ -77,17 +78,20 @@ endpoints:
responses:
-
custom: []
status: 200
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"}]'
status: 401
content: '{"message":"Invalid or expired token"}'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '48'
x-ratelimit-remaining: '53'
access-control-allow-origin: '*'
description: null
responseFields: []
auth: []
auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null
method: null
route: null
@@ -102,13 +106,12 @@ endpoints:
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: |-
Top anual
* Retorna o ranking dos jogos com melhor desempenho no último ano.
description: ''
authenticated: false
title: 'Top anual'
description: 'Retorna o ranking dos jogos com melhor desempenho no último ano.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters: []
@@ -121,17 +124,20 @@ endpoints:
responses:
-
custom: []
status: 200
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"}]'
status: 401
content: '{"message":"Invalid or expired token"}'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '47'
x-ratelimit-remaining: '52'
access-control-allow-origin: '*'
description: null
responseFields: []
auth: []
auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null
method: null
route: null
@@ -146,13 +152,12 @@ endpoints:
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: |-
Histórico de ranking
* Retorna a evolução de um jogo específico ao longo do tempo.
description: ''
authenticated: false
title: 'Histórico de ranking'
description: 'Retorna a evolução de um jogo específico ao longo do tempo.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters:
@@ -177,17 +182,20 @@ endpoints:
responses:
-
custom: []
status: 200
content: '{"game":"Counter-Strike 2","history":[{"period":"Semana 1","points":554},{"period":"M\u00eas Atual","points":5004},{"period":"Ano Atual","points":60724}]}'
status: 401
content: '{"message":"Invalid or expired token"}'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '46'
x-ratelimit-remaining: '51'
access-control-allow-origin: '*'
description: null
responseFields: []
auth: []
auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null
method: null
route: null
@@ -202,13 +210,12 @@ endpoints:
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: |-
Jogos mais jogados
* Retorna o top 10 jogos com base no número de jogadores ativos.
description: ''
authenticated: false
title: 'Jogos mais jogados'
description: 'Retorna o top 10 jogos com base no número de jogadores ativos.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters: []
@@ -221,73 +228,20 @@ endpoints:
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":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"}]'
status: 401
content: '{"message":"Invalid or expired token"}'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '45'
x-ratelimit-remaining: '50'
access-control-allow-origin: '*'
description: null
responseFields: []
auth: []
controller: null
method: null
route: null
-
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: []
auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null
method: null
route: null

View File

@@ -12,13 +12,12 @@ endpoints:
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: |-
Top semanal
* Retorna o ranking dos jogos com melhor desempenho na última semana.
description: ''
authenticated: false
title: 'Top semanal'
description: 'Retorna o ranking dos jogos com melhor desempenho na última semana.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters: []
@@ -31,17 +30,20 @@ endpoints:
responses:
-
custom: []
status: 200
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"}]'
status: 401
content: '{"message":"Invalid or expired token"}'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '49'
x-ratelimit-remaining: '54'
access-control-allow-origin: '*'
description: null
responseFields: []
auth: []
auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null
method: null
route: null
@@ -56,13 +58,12 @@ endpoints:
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: |-
Top mensal
* Retorna o ranking dos jogos com melhor desempenho no último mês.
description: ''
authenticated: false
title: 'Top mensal'
description: 'Retorna o ranking dos jogos com melhor desempenho no último mês.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters: []
@@ -75,17 +76,20 @@ endpoints:
responses:
-
custom: []
status: 200
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"}]'
status: 401
content: '{"message":"Invalid or expired token"}'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '48'
x-ratelimit-remaining: '53'
access-control-allow-origin: '*'
description: null
responseFields: []
auth: []
auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null
method: null
route: null
@@ -100,13 +104,12 @@ endpoints:
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: |-
Top anual
* Retorna o ranking dos jogos com melhor desempenho no último ano.
description: ''
authenticated: false
title: 'Top anual'
description: 'Retorna o ranking dos jogos com melhor desempenho no último ano.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters: []
@@ -119,17 +122,20 @@ endpoints:
responses:
-
custom: []
status: 200
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"}]'
status: 401
content: '{"message":"Invalid or expired token"}'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '47'
x-ratelimit-remaining: '52'
access-control-allow-origin: '*'
description: null
responseFields: []
auth: []
auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null
method: null
route: null
@@ -144,13 +150,12 @@ endpoints:
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: |-
Histórico de ranking
* Retorna a evolução de um jogo específico ao longo do tempo.
description: ''
authenticated: false
title: 'Histórico de ranking'
description: 'Retorna a evolução de um jogo específico ao longo do tempo.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters:
@@ -175,17 +180,20 @@ endpoints:
responses:
-
custom: []
status: 200
content: '{"game":"Counter-Strike 2","history":[{"period":"Semana 1","points":554},{"period":"M\u00eas Atual","points":5004},{"period":"Ano Atual","points":60724}]}'
status: 401
content: '{"message":"Invalid or expired token"}'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '46'
x-ratelimit-remaining: '51'
access-control-allow-origin: '*'
description: null
responseFields: []
auth: []
auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null
method: null
route: null
@@ -200,13 +208,12 @@ endpoints:
groupDescription: ''
subgroup: ''
subgroupDescription: ''
title: |-
Jogos mais jogados
* Retorna o top 10 jogos com base no número de jogadores ativos.
description: ''
authenticated: false
title: 'Jogos mais jogados'
description: 'Retorna o top 10 jogos com base no número de jogadores ativos.'
authenticated: true
deprecated: false
headers:
Authorization: 'Bearer {YOUR_JWT_TOKEN}'
Content-Type: application/json
Accept: application/json
urlParameters: []
@@ -219,73 +226,20 @@ endpoints:
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":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"}]'
status: 401
content: '{"message":"Invalid or expired token"}'
headers:
cache-control: 'no-cache, private'
content-type: application/json
x-ratelimit-limit: '60'
x-ratelimit-remaining: '45'
x-ratelimit-remaining: '50'
access-control-allow-origin: '*'
description: null
responseFields: []
auth: []
controller: null
method: null
route: null
-
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: []
auth:
- headers
- Authorization
- 'Bearer 6g43cv8PD1aE5beadkZfhV6'
controller: null
method: null
route: null

View File

@@ -1,13 +1,12 @@
# Introduction
Microsserviço de rankings e métricas de jogos para integração com o ecossistema GameVerse.
<aside>
<strong>Base URL</strong>: <code>http://localhost</code>
<strong>Base URL</strong>: <code>http://127.0.0.1:8000</code>
</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).
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>
<aside>Use os exemplos da documentação para demonstrar como o frontend ou outros microsserviços podem consumir os dados de ranking.</aside>

453
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)
![PHP](https://img.shields.io/badge/PHP-8.2-blue)
Microsserviço backend responsável por disponibilizar rankings e métricas de jogos para integração com o ecossistema GameVerse.
![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)
![Scribe](https://img.shields.io/badge/Docs-Scribe-purple)
---
# 📌 Sobre o Projeto
## Sobre o Projeto
Este microsserviço faz parte do ecossistema **GameVerse**.
Ele é responsável por processar, armazenar e disponibilizar estatísticas de engajamento, permitindo que a plataforma exiba rankings dinâmicos e tendências globais.
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.
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
* 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:
* pontuação
* tempo de jogo
* quantidade de jogadores ativos
* evolução de desempenho
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.
* Fornecer ranking semanal, mensal e anual de jogos
* Listar os jogos mais jogados
* Consultar histórico de pontuação de um jogo
* Retornar dados estatísticos em formato JSON
* Proteger as rotas da API usando JWT
* Disponibilizar documentação interativa com Scribe
---
# 🎯 Responsabilidades do Microsserviço
## Tecnologias Utilizadas
O serviço possui as seguintes responsabilidades:
* 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
* PHP >= 8.1
* Laravel 10
* Postgres
* 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
* Git
* SQLite
---
# ⚙️ Variáveis de Ambiente
## Instalação
O projeto utiliza variáveis configuradas no arquivo `.env`.
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
git clone https://github.com/gabriellina640/api-ranking-jogos.git
```
## 2. Acesse a pasta do projeto
Clone o repositório:
```bash
git clone https://github.com/ykiakao/api-ranking-jogos.git
cd api-ranking-jogos
```
## 3. Instale as dependências
Instale as dependências:
```bash
composer install
```
---
# ⚙️ Configuração do .env
Crie uma cópia do arquivo de ambiente:
Crie o arquivo `.env`:
```bash
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
DB_CONNECTION=sqlite
```
---
# 🗄️ Preparação do Banco de Dados
Execute as migrations e seeders:
```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
php artisan scribe:generate
```
| Variável | Descrição |
| -------- | --------- |
| `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:
@@ -150,213 +182,178 @@ php artisan serve
A aplicação ficará disponível em:
```bash
```text
http://localhost:8000
```
---
# 📖 Documentação Interativa da API
## Documentação da API
Após iniciar o projeto, acesse:
Gere a documentação:
```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
* testar requisições
* consultar parâmetros
* visualizar respostas JSON
| Recurso | Caminho |
| ------- | ------- |
| Documentação HTML | `resources/views/scribe/index.blade.php` |
| 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:
* Scribe
* Postman
* Insomnia
* Thunder Client
Exemplo:
```http
GET http://localhost:8000/api/v1/rankings/weekly
```
| Método | Endpoint | Descrição | Autenticação |
| ------ | -------- | --------- | ------------ |
| GET | `/api/v1/rankings/weekly` | Lista o top 10 jogos por pontuação semanal | JWT |
| GET | `/api/v1/rankings/monthly` | Lista o top 10 jogos por pontuação mensal | JWT |
| GET | `/api/v1/rankings/yearly` | Lista o top 10 jogos por pontuação anual | JWT |
| GET | `/api/v1/rankings/history/{id}` | 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 |
---
# 📑 Rotas da API
## Exemplos de Requisição
| Método | Endpoint | Descrição |
| ------ | ------------------------------------- | ---------------------------------------- |
| 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
Ranking semanal:
```http
GET /api/v1/rankings/weekly HTTP/1.1
Host: localhost:8000
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
[
{
"id": 1,
"name": "Elden Ring",
"name": "Counter-Strike 2",
"platform": "Steam",
"active_players": 1500000,
"weekly_points": 850,
"monthly_points": 7000,
"yearly_points": 85000,
"created_at": "2026-05-04T22:00:00.000000Z",
"updated_at": "2026-05-04T22:00:00.000000Z"
"active_players": 1086549,
"weekly_points": 729,
"monthly_points": 1215,
"yearly_points": 71182,
"created_at": "2026-05-18T21:57:31.000000Z",
"updated_at": "2026-05-18T21:57:31.000000Z"
}
]
```
---
# 🔗 Integrações com Outros Microsserviços
## Retornos Esperados
## Quais dados recebe
O microsserviço recebe:
* IDs de jogos
* parâmetros de filtro
* plataformas
* períodos de ranking
| Código | Situação | Exemplo |
| ------ | -------- | ------- |
| 200 | Requisição autenticada com sucesso | Lista de jogos ou histórico |
| 401 | Token ausente, inválido ou expirado | `{"message":"Missing Authorization header"}` |
| 404 | Jogo inexistente em `/rankings/history/{id}` | Resposta padrão do Laravel para model não encontrado |
| 500 | Erro inesperado no servidor | Falha interna |
---
## Quais dados retorna
## Testes
O serviço retorna:
* 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
Execute:
```bash
app/
bootstrap/
config/
database/
public/
resources/
routes/
storage/
tests/
php artisan test
```
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:
* README.md
* .env.example
* Código-fonte completo
* `README.md`
* `.env.example`
* 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.
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.
Projeto acadêmico desenvolvido para a disciplina de Microsserviços no contexto do ecossistema 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;
use App\Models\Game;
use Illuminate\Http\Request;
/**
* @group Rankings
@@ -12,7 +11,8 @@ class GameController extends Controller
{
/**
* 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()
{
@@ -22,7 +22,8 @@ class GameController extends Controller
/**
* 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()
{
@@ -32,7 +33,8 @@ class GameController extends Controller
/**
* 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()
{
@@ -42,7 +44,8 @@ class GameController extends Controller
/**
* 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()
{
@@ -50,9 +53,24 @@ class GameController extends Controller
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
* * 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
*/
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);
}
if (
!$this->signatureIsValid($token, $signature) ||
($payload['iss'] ?? null) !== config('jwt.issuer') ||
($payload['aud'] ?? null) !== config('jwt.audience') ||
empty($payload['sub']) ||
$this->tokenIsExpired($payload)
) {
return response()->json(['message' => 'Invalid token'], 401);
if (!$this->signatureIsValid($token, $signature)) {
return response()->json(['message' => 'Invalid token signature'], 401);
}
if (($payload['iss'] ?? null) !== config('jwt.issuer')) {
return response()->json(['message' => 'Invalid token issuer'], 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', [
@@ -45,8 +55,10 @@ class JwtAuthMiddleware
return $next($request);
} catch (\Exception $e) {
} catch (\InvalidArgumentException $e) {
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
{
[$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 === '') {
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,
$signature,
$publicKey,
$keyResource,
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
@@ -113,4 +217,20 @@ class JwtAuthMiddleware
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
{
use HasFactory;
protected $guarded = [];
}

View File

@@ -5,7 +5,9 @@
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.1",
"php": "^8.2",
"ext-mbstring": "*",
"ext-pdo_pgsql": "*",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.3",
@@ -18,8 +20,7 @@
"laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0",
"phpunit/phpunit": "^10.1",
"spatie/laravel-ignition": "^2.0"
"phpunit/phpunit": "^10.1"
},
"autoload": {
"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",
"This file is @generated automatically"
],
"content-hash": "dfdbaa9a791903e1a015e16cbcf49f3b",
"content-hash": "aebe5262c364c90855a307eb2f8f54d9",
"packages": [
{
"name": "brick/math",
@@ -7942,390 +7942,6 @@
],
"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",
"version": "v6.4.36",
@@ -8540,7 +8156,9 @@
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.1"
"php": "^8.2",
"ext-mbstring": "*",
"ext-pdo_pgsql": "*"
},
"platform-dev": {},
"plugin-api-version": "2.9.0"

View File

@@ -15,7 +15,7 @@ return [
|
*/
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'paths' => ['api/*'],
'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,
'engine' => null,
'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;
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
{
$jogosAtuais = [
['name' => 'Counter-Strike 2', 'platform' => 'Steam'],
['name' => 'Elden Ring', 'platform' => 'Steam'],
['name' => 'Valorant', 'platform' => 'Riot Launcher'],
['name' => 'Helldivers 2', 'platform' => 'Steam'],
['name' => 'Baldur\'s Gate 3', 'platform' => 'Steam'],
['name' => 'Fortnite', 'platform' => 'Epic Games'],
['name' => 'Grand Theft Auto V', 'platform' => 'Steam'],
['name' => 'EA SPORTS FC 24', 'platform' => 'Steam'],
['name' => 'Roblox', 'platform' => 'Multiplataforma'],
['name' => 'League of Legends', 'platform' => 'Riot Launcher'],
['name' => 'Apex Legends', 'platform' => 'Steam'],
['name' => 'Call of Duty: Warzone', 'platform' => 'Battle.net'],
['name' => 'Minecraft', 'platform' => 'Multiplataforma'],
['name' => 'Cyberpunk 2077', 'platform' => 'Steam'],
['name' => 'Stardew Valley', 'platform' => 'Steam'],
['id' => 1, 'name' => 'Counter-Strike 2', 'platform' => 'Steam', 'active_players' => 1450000, 'weekly_points' => 980, 'monthly_points' => 9400, 'yearly_points' => 91000],
['id' => 2, 'name' => 'Elden Ring', 'platform' => 'Steam', 'active_players' => 320000, 'weekly_points' => 870, 'monthly_points' => 8500, 'yearly_points' => 88000],
['id' => 3, 'name' => 'Valorant', 'platform' => 'Riot Launcher', 'active_players' => 1180000, 'weekly_points' => 920, 'monthly_points' => 9100, 'yearly_points' => 96000],
['id' => 4, 'name' => 'Helldivers 2', 'platform' => 'Steam', 'active_players' => 410000, 'weekly_points' => 820, 'monthly_points' => 7900, 'yearly_points' => 74000],
['id' => 5, 'name' => 'Baldur\'s Gate 3', 'platform' => 'Steam', 'active_players' => 260000, 'weekly_points' => 760, 'monthly_points' => 8300, 'yearly_points' => 90000],
['id' => 6, 'name' => 'Fortnite', 'platform' => 'Epic Games', 'active_players' => 1800000, 'weekly_points' => 950, 'monthly_points' => 9300, 'yearly_points' => 99000],
['id' => 7, 'name' => 'Grand Theft Auto V', 'platform' => 'Steam', 'active_players' => 720000, 'weekly_points' => 700, 'monthly_points' => 7200, 'yearly_points' => 81000],
['id' => 8, 'name' => 'EA SPORTS FC 24', 'platform' => 'Steam', 'active_players' => 540000, 'weekly_points' => 650, 'monthly_points' => 6900, 'yearly_points' => 76000],
['id' => 9, 'name' => 'Roblox', 'platform' => 'Multiplataforma', 'active_players' => 1600000, 'weekly_points' => 890, 'monthly_points' => 9000, 'yearly_points' => 97000],
['id' => 10, 'name' => 'League of Legends', 'platform' => 'Riot Launcher', 'active_players' => 1500000, 'weekly_points' => 930, 'monthly_points' => 9200, 'yearly_points' => 98000],
['id' => 11, 'name' => 'Apex Legends', 'platform' => 'Steam', 'active_players' => 680000, 'weekly_points' => 610, 'monthly_points' => 6600, 'yearly_points' => 70000],
['id' => 12, 'name' => 'Call of Duty: Warzone', 'platform' => 'Battle.net', 'active_players' => 830000, 'weekly_points' => 810, 'monthly_points' => 7800, 'yearly_points' => 86000],
['id' => 13, 'name' => 'Minecraft', 'platform' => 'Multiplataforma', 'active_players' => 1750000, 'weekly_points' => 880, 'monthly_points' => 8800, 'yearly_points' => 95000],
['id' => 14, 'name' => 'Cyberpunk 2077', 'platform' => 'Steam', 'active_players' => 210000, 'weekly_points' => 560, 'monthly_points' => 6100, 'yearly_points' => 72000],
['id' => 15, 'name' => 'Stardew Valley', 'platform' => 'Steam', 'active_players' => 180000, 'weekly_points' => 520, 'monthly_points' => 5400, 'yearly_points' => 68000],
];
foreach ($jogosAtuais as $jogo) {
Game::create([
'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),
]);
Game::updateOrCreate(['id' => $jogo['id']], $jogo);
}
}
}
}

View File

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

View File

@@ -21,8 +21,8 @@
<env name="APP_ENV" value="testing"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="CACHE_DRIVER" value="array"/>
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<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
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use App\Http\Controllers\GameController;
/*
@@ -21,12 +22,228 @@ Route::prefix('v1')->middleware(['jwt.auth'])->group(function () {
// Jogos
Route::get('/games/most-played', [GameController::class, 'mostPlayed']);
});
// 🔓 Rota de teste (opcional)
Route::middleware(['jwt.auth'])->get('/test-auth', function (Request $request) {
Route::middleware(['jwt.auth'])->post('/dev/seed-games', function () {
app(\Database\Seeders\DatabaseSeeder::class)->run();
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
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| 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!
|
*/
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
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"
}
]
}