Compare commits
36 Commits
24169162aa
...
5f30042799
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f30042799 | |||
| 98e5635eb7 | |||
| 90c68a69b1 | |||
| 33eea15326 | |||
| f9cc5f77c8 | |||
| da51e2af24 | |||
| b7f13d8511 | |||
| 6c54a438dd | |||
| 99f35c64ad | |||
| 7836b72d6d | |||
| 94064b27c3 | |||
| c477643781 | |||
| 961662a10e | |||
| fcbafce44c | |||
| edc6e6486b | |||
| cd38287503 | |||
| abb1fae70d | |||
| bc90a16c14 | |||
| 6fb93f04db | |||
| df742ac5ea | |||
| 4b8097e9f2 | |||
| 336c19b971 | |||
| 41455664dc | |||
| 8f183fc0ed | |||
| 99810f0695 | |||
| dd1d5afd00 | |||
| cef15730f3 | |||
| 8e9211bafd | |||
| ff3eb18954 | |||
| b97c049c97 | |||
| 516a6fb179 | |||
| 4b4a902fe8 | |||
| 5ab3e83698 | |||
| 4da99919bd | |||
| 6d30d160b0 | |||
| 5fc86102a3 |
18
.env.example
18
.env.example
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
449
README.md
449
README.md
@@ -1,127 +1,126 @@
|
||||
# 🎮 Microsserviço de Rankings de Jogos (Game Ranking API)
|
||||
# Game Ranking API
|
||||
|
||||

|
||||

|
||||
Microsserviço backend responsável por disponibilizar rankings e métricas de jogos para integração com o ecossistema GameVerse.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
# 📌 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
|
||||
* PHP >= 8.1
|
||||
* Laravel 10
|
||||
* SQLite
|
||||
* 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
|
||||
Clone o repositório:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/gabriellina640/api-ranking-jogos.git
|
||||
```
|
||||
|
||||
## 2. Acesse a pasta do projeto
|
||||
|
||||
```bash
|
||||
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
3
api/index.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/../public/index.php';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,4 +8,6 @@ use Illuminate\Database\Eloquent\Model;
|
||||
class Game extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
}
|
||||
|
||||
@@ -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
390
composer.lock
generated
@@ -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"
|
||||
|
||||
@@ -15,7 +15,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||
'paths' => ['api/*'],
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
|
||||
@@ -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'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
return [
|
||||
'routes' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
256
config/scribe.php
Normal file
256
config/scribe.php
Normal 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,
|
||||
],
|
||||
];
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
1
public/health
Normal file
@@ -0,0 +1 @@
|
||||
ok
|
||||
14
railway.json
Normal file
14
railway.json
Normal 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
229
routes/api.php
229
routes/api.php
@@ -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,
|
||||
]);
|
||||
});
|
||||
|
||||
557
routes/web.php
557
routes/web.php
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
47
tests/Feature/DocumentationRoutesTest.php
Normal file
47
tests/Feature/DocumentationRoutesTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
151
tests/Feature/GameRankingApiTest.php
Normal file
151
tests/Feature/GameRankingApiTest.php
Normal 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
15
vercel.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user