funifier-mcp 0.2.26 → 0.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +4 -1
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +935 -280
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/mcp/bundle.js +119 -93
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +13 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts.map +1 -1
- package/dist/mcp/tools/folder.js +22 -12
- package/dist/mcp/tools/folder.js.map +1 -1
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +3 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +14 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +3 -3
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,101 +1,801 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `player`
|
|
2
2
|
|
|
3
3
|
**Acesso Studio:** `/studio/player`
|
|
4
4
|
**API Endpoint:** `/v3/player`
|
|
5
|
+
**Coleção MongoDB:** `player`
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
> Documentação de engenharia reversa baseada exclusivamente no código-fonte de `funifier-service`. Arquivos principais:
|
|
8
|
+
> `engine/player/PlayerManager.java`, `engine/player/PlayerDaoMongo.java`, `engine/player/Player.java`, `engine/player/Principal.java`, `engine/player/PlayerAsyncProcessor.java`, `rest/v3/rest/PlayerRest.java` (API v3), `rest/engine/PlayerRest.java` (API legada `2.0.0/player`), `engine/achievement/PlayerStatus.java`.
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
---
|
|
9
11
|
|
|
10
|
-
##
|
|
12
|
+
## 1. Visão Geral
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
- Para cadastrar participantes da gamificação
|
|
14
|
-
- Para associar jogadores a equipes
|
|
15
|
-
- Para armazenar informações extras (departamento, cargo, etc.)
|
|
14
|
+
O módulo `player` gerencia os participantes de uma gamificação — qualquer sujeito identificável (usuário, estudante, colaborador, cliente) que executa ações e acumula progresso. O `_id` do player é também o seu login.
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
Papel arquitetural: o player é o nó central do grafo de gamificação. Praticamente todas as coleções de progresso referenciam o `player._id`, e a exclusão de um player dispara uma cascata em ~20 coleções (`PlayerDaoMongo.delete`).
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
- [ ] Definir nome do jogador
|
|
21
|
-
- [ ] Definir email (se necessário para notificações)
|
|
22
|
-
- [ ] Associar a equipes (se aplicável)
|
|
23
|
-
- [ ] Definir campos extras no campo "extra" (departamento, cargo, etc.)
|
|
24
|
-
- [ ] Definir senha (se autenticação for necessária)
|
|
18
|
+
O módulo mantém **dois documentos paralelos** por jogador:
|
|
25
19
|
|
|
26
|
-
|
|
20
|
+
- `player` — dados cadastrais e de perfil (gerenciado pelo `PlayerManager`).
|
|
21
|
+
- `principal` — documento de segurança usado pela autenticação/autorização. Criado automaticamente no primeiro `insert()` e guarda as `roles`.
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
**Método:** GET
|
|
30
|
-
**Endpoint:** `/v3/player`
|
|
31
|
-
**Descrição:** Retorna todos os jogadores cadastrados.
|
|
23
|
+
O **status gamificado** (pontos, nível, conquistas, posições de ranking) **não fica no documento `player`** — fica na coleção `player_status`, gerenciada exclusivamente pelo `AchievementManager`. A sincronização é assíncrona, via `PlayerAsyncProcessor` (uma thread dedicada por gamificação).
|
|
32
24
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
25
|
+
Integrações reais confirmadas no código (chamadores de `getPlayerManager()`):
|
|
26
|
+
|
|
27
|
+
| Módulo | Como integra | Evidência |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `achievement` | Recalcula e lê `player_status` (`updatePlayerStatus`, `findPlayerStatus`) | `AchievementManager` L561/L716; `PlayerRest` L237/L281 |
|
|
30
|
+
| `authentication` | Login valida `player.password` via `BCrypt.checkpw`; cria players ausentes (`createIfDontExist`) | `AuthenticationManager` L324, L341 |
|
|
31
|
+
| `action` (action_log) | Auto-cria player inexistente referenciado por um log | `ActionManager` L157; `ActionRest` L465/L635 |
|
|
32
|
+
| `point` | Dispara recálculo de status ao creditar/transferir pontos | `PointManager` L35/L38 |
|
|
33
|
+
| `catalog`/`purchase` | Dispara recálculo de status na compra | `CatalogManager` L845 |
|
|
34
|
+
| `team` | `insert(player)` ao alterar vínculo de time; `findTeamIdsByUserId` | `TeamManager` L197/L367 |
|
|
35
|
+
| `role` | `/v3/role` delega a `insertRole`/`removeRole` (grava em `principal`) | `RoleRest` L42/L67 |
|
|
36
|
+
| `lottery`, `competition`, `challenge`, `mystery`, `quiz`, `question`, `leaderboard` | Lookups e recálculo de status | múltiplos managers |
|
|
37
|
+
| `mobile` | Remove devices na exclusão do player | `PlayerManager.delete` L129 |
|
|
38
|
+
| `upload` | Pode excluir player (cascata completa) | `UploadRest` L464 |
|
|
39
|
+
| `trigger` | Eventos `BEFORE/AFTER_CREATE/UPDATE/DELETE` | `PlayerManager.insert/delete` |
|
|
40
|
+
| `audit` | Log de `EVENT_CREATE`/`EVENT_UPDATE` | `PlayerManager.insert` L296/L306 |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 2. Arquitetura e Fluxos
|
|
45
|
+
|
|
46
|
+
### 2.1 Pipeline de criação/atualização — `PlayerManager.insert()` (L244–325)
|
|
47
|
+
|
|
48
|
+
`insert()` é o **único** ponto de escrita do player. Os métodos legados `add()`/`update()` estão comentados (L64–92). Toda a API v3 (POST, PUT, bulk, friends, teams, password) converge para `insert()`.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
1. [Guard] player != null && _id != null && _id.trim().length()>0 (senão: no-op silencioso) L250
|
|
52
|
+
2. [Sanitização] player.setId(_id.trim()) L253
|
|
53
|
+
3. [Lookup] principal = principal_collection.findOne({userId: _id}) L258
|
|
54
|
+
4. [Decisão] principal == null → CRIAÇÃO ; principal != null → ATUALIZAÇÃO L259
|
|
55
|
+
5. [created] se CRIAÇÃO: player.created = now() L260
|
|
56
|
+
6. [Trigger] BEFORE_CREATE (criação) ou BEFORE_UPDATE (atualização) L262/265
|
|
57
|
+
7. [updated] player.updated = now() (SEMPRE, após o trigger) L268
|
|
58
|
+
8. [Alt logins] remove de player.alternative_logins os logins já usados por OUTRO player (silencioso) L272-282
|
|
59
|
+
9. [Save] player_collection.save(player) ← upsert nativo, REPLACE do documento inteiro L287
|
|
60
|
+
10.[Principal] se CRIAÇÃO: cria Principal(id,name,TYPE_USER,null,id) + AFTER_CREATE + audit(CREATE) L289-297
|
|
61
|
+
se ATUALIZAÇÃO: principal.setName(name) + save + AFTER_UPDATE + audit(UPDATE) L299-307
|
|
62
|
+
11.[Teams] teamDao.deleteLinksByPlayer(_id); recria link p/ cada team NÃO-dinâmico do array L310-320
|
|
63
|
+
12.[Status async] updateUserStatus(_id) → enfileira recálculo (NÃO bloqueia a resposta HTTP) L323
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> **Observação crítica (decisão por `principal`, não por `player`):** o ramo criação/atualização é decidido pela existência do `principal`, não do documento `player`. Se o `principal` não existir mas o `player` existir (corrupção/`updateAllPrincipals` mal usado), `player.created` é reescrito como se fosse uma nova criação.
|
|
67
|
+
|
|
68
|
+
#### Diagrama — criação vs. atualização
|
|
69
|
+
|
|
70
|
+
```mermaid
|
|
71
|
+
flowchart LR
|
|
72
|
+
A[insert player] --> B{principal existe?<br/>findOne userId}
|
|
73
|
+
B -- Não --> C[player.created = now<br/>BEFORE_CREATE]
|
|
74
|
+
B -- Sim --> D[BEFORE_UPDATE]
|
|
75
|
+
C --> E[player.updated = now]
|
|
76
|
+
D --> E
|
|
77
|
+
E --> F[dedup alternative_logins]
|
|
78
|
+
F --> G[player.save<br/>REPLACE total]
|
|
79
|
+
G --> H{era criação?}
|
|
80
|
+
H -- Sim --> I[cria Principal<br/>AFTER_CREATE + audit CREATE]
|
|
81
|
+
H -- Não --> J[principal.setName<br/>AFTER_UPDATE + audit UPDATE]
|
|
82
|
+
I --> K[teams: delete links + relink não-dinâmicos]
|
|
83
|
+
J --> K
|
|
84
|
+
K --> L[updateUserStatus para fila async]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 2.2 Pipeline assíncrono de status
|
|
88
|
+
|
|
89
|
+
`PlayerManager` cria, no construtor, uma thread dedicada por gamificação: `new Thread(async, "Funifier-User-Async-" + apiKey).start()` (L36). `updateUserStatus()` apenas enfileira o `_id`.
|
|
90
|
+
|
|
91
|
+
```mermaid
|
|
92
|
+
sequenceDiagram
|
|
93
|
+
participant REST as PlayerRest (thread HTTP)
|
|
94
|
+
participant PM as PlayerManager
|
|
95
|
+
participant Q as Fila NoDuplicates
|
|
96
|
+
participant ASY as Funifier-User-Async (thread dedicada)
|
|
97
|
+
participant AM as AchievementManager
|
|
98
|
+
|
|
99
|
+
REST->>PM: insert(player)
|
|
100
|
+
PM->>Q: queue.offer(_id)
|
|
101
|
+
Note right of Q: offer() despacha para add() sobrescrito.<br/>Se o _id já está na fila, é descartado (dedup).
|
|
102
|
+
REST-->>REST: HTTP 201 imediato (status ainda NÃO calculado)
|
|
103
|
+
loop poll contínuo
|
|
104
|
+
ASY->>Q: poll()
|
|
105
|
+
alt item presente
|
|
106
|
+
Q-->>ASY: _id
|
|
107
|
+
ASY->>AM: updatePlayerStatus(_id, jongo) → grava player_status
|
|
108
|
+
else fila vazia
|
|
109
|
+
ASY->>ASY: Thread.sleep(1000ms)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- `PlayerAsyncProcessor.updateStatus()` ignora valores `null`, `""` e `"null"` (L17–21).
|
|
115
|
+
- `NoDuplicates extends LinkedList` sobrescreve `add()` para descartar duplicatas; `offer()` chama `add()`, então a deduplicação por conteúdo funciona (L44–58).
|
|
116
|
+
- **Consistência eventual:** a resposta do POST/PUT retorna **antes** de o status ser recalculado.
|
|
117
|
+
|
|
118
|
+
### 2.3 Pipeline de exclusão — `PlayerManager.delete()` (L104–133)
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
1. [Lookup] player = findById(userId)
|
|
122
|
+
2. [Trigger] BEFORE_DELETE
|
|
123
|
+
3. [Cascata] PlayerDaoMongo.delete(userId) → remove de 20 coleções (ver §5.1)
|
|
124
|
+
4. [Mobile] MobileManagerV3.deleteAllDevicesByPlayer(userId)
|
|
125
|
+
5. [Trigger] AFTER_DELETE
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A exclusão **não tem transação**: cada `remove()` é independente. Uma falha no meio deixa dados órfãos. Não há rollback.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 3. Estrutura dos Objetos
|
|
133
|
+
|
|
134
|
+
### 3.1 `Player` — documento raiz (coleção `player`)
|
|
135
|
+
|
|
136
|
+
Fonte: `engine/player/Player.java`. A classe usa `@JsonIgnoreProperties(ignoreUnknown=true)` (L14) → campos desconhecidos no JSON são descartados na desserialização.
|
|
137
|
+
|
|
138
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
139
|
+
|---|---|---|---|---|
|
|
140
|
+
| `_id` | String | — | Sim | Identificador único e login. `trim()` aplicado silenciosamente. Mapeado de `id` via `@JsonProperty("_id")`. |
|
|
141
|
+
| `name` | String | — | Não | Nome de exibição. |
|
|
142
|
+
| `email` | String | — | Não | E-mail. Usado no fluxo de troca de senha por código. |
|
|
143
|
+
| `password` | String | — | Não | **Campo `public`.** Armazenado **como veio** no POST/PUT — NÃO é hasheado nesse fluxo (ver §5.2). |
|
|
144
|
+
| `image` | `Image` | — | Não | Objeto com `small`, `medium`, `original`, cada um `ImageItem` (que contém `url`). |
|
|
145
|
+
| `teams` | Array<String> | — | Não | IDs dos times. Semântica de escrita difere entre POST e PUT (ver §4). |
|
|
146
|
+
| `friends` | Array<String> | — | Não | IDs de outros players marcados como amigos. |
|
|
147
|
+
| `extra` | Map<String,Object> | `{}` | Não | Atributos customizados livres. Campo `public`, inicializado com `new HashMap<>()`. |
|
|
148
|
+
| `alternative_logins` | Array<`Login`> | null | Não | Logins alternativos. Campo `public`. Sofre deduplicação cross-player no save. |
|
|
149
|
+
| `created` | Date | auto | — | Definido **apenas na criação** (quando `principal` não existe). Ver armadilha em §5.3. |
|
|
150
|
+
| `updated` | Date | auto | — | Reescrito em todo `insert()`. Campo `public`. |
|
|
151
|
+
| `business` | boolean | false | Não | Flag legado de tipo de conta. Sem comportamento associado no módulo player. |
|
|
152
|
+
| `developer` | boolean | false | Não | Flag legado. Lida apenas por `AuthBean.isDeveloper()` (`getPlayer().isDeveloper()`). |
|
|
153
|
+
| `attrString` | String | — | — | **Campo legado morto.** Aceito, persistido e até incluído na projeção da listagem, mas nunca lido por lógica ativa (ver §7). |
|
|
154
|
+
|
|
155
|
+
**Campos `public` (acesso direto, sem getter/setter):** `password`, `extra`, `updated`, `alternative_logins`.
|
|
156
|
+
|
|
157
|
+
**Diferença schema vs. runtime — campos removidos da resposta:**
|
|
158
|
+
- `password` é omitido da listagem (`GET /v3/player`) via um `$project` explícito quando o token **não** tem o scope `read_encrypted_player_password` (`PlayerManager.findAllPlayersPaginated` L492–494). Esse `$project` lista os campos permitidos: `_id, name, email, image, teams, friends, created, business, developer, attrString, extra, updated, alternative_logins`.
|
|
159
|
+
- No `GET /v3/player/:id`, `password` é zerado em memória (`player.password = null`) antes de serializar (`PlayerRest` L100–102).
|
|
160
|
+
- A serialização de **resposta** usa `JsonUtil.toJsonRemoveNullFields` (inclusão `NON_NULL`) → campos nulos somem da resposta. Já a serialização para o **MongoDB** usa a inclusão padrão do Jackson (`ALWAYS`, `DatabaseManager.java:33`) → nulos são gravados (relevante para §5.3).
|
|
161
|
+
|
|
162
|
+
### 3.2 `Login` — subentidade de `alternative_logins`
|
|
163
|
+
|
|
164
|
+
Fonte: `engine/player/Login.java`. Todos os campos são `public`.
|
|
165
|
+
|
|
166
|
+
| Campo | Tipo | Padrão | Descrição |
|
|
167
|
+
|---|---|---|---|
|
|
168
|
+
| `login` | String | — | Login alternativo (ex.: CPF, matrícula, e-mail secundário). |
|
|
169
|
+
| `password` | String | null | Senha específica do login alternativo (opcional). |
|
|
170
|
+
| `extra` | Map<String,Object> | null | Atributos extras do login alternativo. |
|
|
171
|
+
|
|
172
|
+
**Comportamento:**
|
|
173
|
+
- `Player.addAlternativeLogin(login)` ignora logins `null` ou com `trim().length() <= 1` e deduplica dentro do próprio player (L63–83).
|
|
174
|
+
- No `insert()`, um login já associado a **outro** player é removido silenciosamente da lista antes do save (`PlayerManager` L272–282).
|
|
175
|
+
|
|
176
|
+
### 3.3 `Principal` — coleção `principal`
|
|
177
|
+
|
|
178
|
+
Fonte: `engine/player/Principal.java`. Documento de segurança criado automaticamente no primeiro `insert()`.
|
|
179
|
+
|
|
180
|
+
| Campo | Tipo | Descrição |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| `_id` | String | Igual a `player._id` (criado com `new Principal(player.getId(), ...)`). |
|
|
183
|
+
| `userId` | String | Referência ao player. |
|
|
184
|
+
| `name` | String | Nome do player (atualizado a cada `insert()`). |
|
|
185
|
+
| `type` | Integer | `0` = usuário (`TYPE_USER`), `1` = time (`TYPE_TEAM`). |
|
|
186
|
+
| `teamId` | String | Preenchido apenas para `type = 1`. |
|
|
187
|
+
| `roles` | Array<String> | `public`. Roles atribuídas via `/v3/role`. Sem default — players sem role recebem o tratamento padrão de "player" pela autorização. |
|
|
188
|
+
|
|
189
|
+
Métodos de apoio: `isTeam()`, `isPlayer()`, `getValueId()` (retorna `teamId` ou `userId`), `addRole()`/`removeRole()` (dedup; `removeRole` zera a lista se ficar vazia).
|
|
190
|
+
|
|
191
|
+
### 3.4 `PlayerAttribute` — coleção `player_attribute`
|
|
192
|
+
|
|
193
|
+
Fonte: `engine/player/PlayerAttribute.java`. Define schema informativo de atributos extras (uso no Studio). **Não** valida os `extra` do player em runtime.
|
|
194
|
+
|
|
195
|
+
| Campo | Tipo | Descrição |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| `_id` | String | ID do atributo (`@JsonProperty("_id")`). |
|
|
198
|
+
| `name` | String | Nome (ex.: `company`). |
|
|
199
|
+
| `type` | String | Tipo do campo (`@see FieldType`). |
|
|
200
|
+
| `value` | String | Valor padrão (opcional). |
|
|
201
|
+
| `options` | Array<`PlayerAttributeOption`> | Opções válidas (campos de seleção). |
|
|
202
|
+
|
|
203
|
+
### 3.5 `PlayerAttributeOption` — subentidade de `options`
|
|
204
|
+
|
|
205
|
+
Fonte: `engine/player/PlayerAttributeOption.java`.
|
|
206
|
+
|
|
207
|
+
| Campo | Tipo | Descrição |
|
|
208
|
+
|---|---|---|
|
|
209
|
+
| `_id` | String | ID da opção. |
|
|
210
|
+
| `label` | String | Rótulo de exibição. |
|
|
211
|
+
| `value` | String | Valor armazenado. |
|
|
212
|
+
| `isDefault` | boolean | Se é a opção padrão. **Atenção à serialização JSON:** com a convenção de bean do Jackson, o getter `isDefault()`/setter `setDefault()` tende a expor a chave JSON como **`default`** (e não `isDefault`). Verifique na resposta real ao integrar. |
|
|
213
|
+
|
|
214
|
+
### 3.6 `PlayerStatus` — coleção `player_status`
|
|
215
|
+
|
|
216
|
+
Fonte: `engine/achievement/PlayerStatus.java`. **Gerenciado exclusivamente pelo `AchievementManager`**, nunca pelo `PlayerManager`. Lido via `GET /v3/player/:id/status`.
|
|
217
|
+
|
|
218
|
+
| Campo | Tipo | Descrição |
|
|
219
|
+
|---|---|---|
|
|
220
|
+
| `_id` | String | ID do player (`@JsonProperty("_id")` sobre o campo `player`). |
|
|
221
|
+
| `name` | String | Nome do player. |
|
|
222
|
+
| `image` | `Image` | Foto do player. |
|
|
223
|
+
| `total_challenges` | long | Total de conquistas (badges) ganhas. |
|
|
224
|
+
| `challenges` | Map<String,Long> | challengeId → quantidade. |
|
|
225
|
+
| `total_points` | double | Soma de todos os pontos. |
|
|
226
|
+
| `point_categories` | Map<String,Double> | categoryId → quantidade. |
|
|
227
|
+
| `total_catalog_items` | long | Total de itens de catálogo obtidos. |
|
|
228
|
+
| `catalog_items` | Map<String,Long> | itemId → quantidade. |
|
|
229
|
+
| `level_progress` | `LevelProgress` | Nível atual, `percent_completed`, `next_points`, `next_level`, `total_levels`. |
|
|
230
|
+
| `challenge_progress` | List<`ChallengeProgress`> | Desafios em andamento com % por regra. |
|
|
231
|
+
| `teams` | Array<String> | Times do player. |
|
|
232
|
+
| `friends` | Array<String> | Amigos do player. |
|
|
233
|
+
| `positions` | List<`Leader`> | Posições em leaderboards. |
|
|
234
|
+
| `time` | Date | Timestamp da última atualização do status. |
|
|
235
|
+
| `extra` | Map<String,Object> | Atributos extras do player. |
|
|
236
|
+
| `attributes` | Object | **Campo legado morto** — nunca populado pelo código (L59). |
|
|
237
|
+
|
|
238
|
+
O método `getLevel()` deriva o nível de `level_progress.getLevel()` (não há campo `level` persistido separado).
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 4. Endpoints
|
|
243
|
+
|
|
244
|
+
> Todos os endpoints v3 usam `@BeanParam AuthBean` (Bearer token). `"me"` é resolvido para o player do claim `player` do JWT (`AuthBean.getPlayerFromTokenIfExist`).
|
|
245
|
+
|
|
246
|
+
### `GET /v3/player` — listar players (paginado)
|
|
247
|
+
|
|
248
|
+
| Aspecto | Detalhe |
|
|
249
|
+
|---|---|
|
|
250
|
+
| Finalidade | Listar players com filtros e paginação. |
|
|
251
|
+
| Handler | `PlayerRest.findAll` L1093 → `PlayerManager.findAllPlayersPaginated` |
|
|
252
|
+
| Paginação | Header `Range: items=0-99` (skip-limit). Default `0-100` (`PaginationUtil.getPageResult(a, range, 0, 100)`). |
|
|
253
|
+
| Mecanismo | **MongoDB Aggregation Pipeline** (`$match`), não `find()`. |
|
|
254
|
+
|
|
255
|
+
**Query params:**
|
|
256
|
+
|
|
257
|
+
| Param | Tipo | Comportamento real |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| `player` | String | Filtra por `_id` exato. Aceita `me`. |
|
|
260
|
+
| `name` | String | `{$regex: name, $options: 'i'}`. |
|
|
261
|
+
| `email` | String | `{$regex: email, $options: 'i'}`. |
|
|
262
|
+
| `teams` | String | CSV → `{teams: {$all: [...]}}` (player deve estar em **TODOS**). |
|
|
263
|
+
| `friends` | String | CSV → `{friends: {$all: [...]}}`. |
|
|
264
|
+
| `in` | String | CSV → `$or: [{_id:{$in}}, {teams:{$in}}]`. |
|
|
265
|
+
| `q` | String | **Fragmento JSON bruto** concatenado ao `$match` (ver §8). Ex.: `extra.company:"Funifier"`. |
|
|
266
|
+
| `fields` | String | CSV → `$project` (nomes interpolados na string). |
|
|
267
|
+
| `published_min` / `published_max` | String | Filtro sobre `created` (`$gte`/`$lte`). RFC3339 ou keywords (`-1d`, `-30m`, `-1w`...). |
|
|
268
|
+
| `orderby` | String | `$sort` (nome do campo interpolado bruto). |
|
|
269
|
+
| `reverse` | boolean | `true` → ordem decrescente. |
|
|
270
|
+
| `max_results` | int | `$limit`. |
|
|
271
|
+
|
|
272
|
+
**Comportamento real:** se o token não tiver scope `read_encrypted_player_password`, um segundo `$project` remove `password` da saída (L492–494).
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
### `GET /v3/player/:id` — buscar por ID
|
|
277
|
+
|
|
278
|
+
| Aspecto | Detalhe |
|
|
279
|
+
|---|---|
|
|
280
|
+
| Handler | `PlayerRest.find` L77 |
|
|
281
|
+
| `:id` especial | `me` → player do token. |
|
|
282
|
+
| 404? | **Não.** Retorna `200` com corpo serializado de `null` se o player não existir. |
|
|
283
|
+
|
|
284
|
+
**Comportamento real:**
|
|
285
|
+
- Se scope `read_encrypted_field_values` (ou permissão de sistema `api/encrypted_field_values/read`) e houver `CryptObjectField` configurado: descriptografa campos via AES (L89–97).
|
|
286
|
+
- Se token **sem** `read_encrypted_player_password`: `password` retorna ausente (zerado antes de serializar).
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
### `POST /v3/player` — criar ou atualizar (upsert, full replace)
|
|
291
|
+
|
|
292
|
+
| Aspecto | Detalhe |
|
|
293
|
+
|---|---|
|
|
294
|
+
| Handler | `PlayerRest.insert` L432 |
|
|
295
|
+
| Semântica | **Full replace** via `c.save(player)`. O documento enviado **substitui** o documento inteiro. |
|
|
296
|
+
| Status | `201 Created` em criação **e** atualização. |
|
|
297
|
+
|
|
298
|
+
**Comportamento real (em ordem):**
|
|
299
|
+
1. Criptografa campos AES se o token tiver scope e houver config (L443–452).
|
|
300
|
+
2. `trim()` no `_id` (L455).
|
|
301
|
+
3. Se o player **não** existe: verifica limite do plano. `total = findTotal()`; se `total >= Account.players_allowed` → `401` com mensagem `"It is not allowed to create new players..."` (L462–470).
|
|
302
|
+
4. **Preservação de senha:** se o token **não** tem `read_encrypted_player_password` **e** o player já existe **e** tinha `password`, a senha atual é mantida (`player.password = current.password`, L473–477). Caso contrário (token COM scope), omitir `password` no body **zera** a senha.
|
|
303
|
+
5. `insert(player, authBean)` (full replace + principal + teams + status async).
|
|
304
|
+
6. `201`.
|
|
305
|
+
|
|
306
|
+
> **Full replace:** qualquer campo ausente no body é gravado como `null` no Mongo (marshaller com inclusão `ALWAYS`, `DatabaseManager.java:33`). Só `password` é preservado (e apenas na condição acima). Ver §5.3.
|
|
307
|
+
|
|
308
|
+
**Exemplo de request:**
|
|
53
309
|
```json
|
|
54
310
|
{
|
|
55
|
-
"_id": "
|
|
56
|
-
"name": "
|
|
57
|
-
"email": "
|
|
311
|
+
"_id": "player@funifier.com",
|
|
312
|
+
"name": "Player Name",
|
|
313
|
+
"email": "player@funifier.com",
|
|
58
314
|
"image": {
|
|
59
|
-
"small":
|
|
60
|
-
"medium":
|
|
61
|
-
"original": {
|
|
315
|
+
"small": {"url": "http://host.com/photo.png"},
|
|
316
|
+
"medium": {"url": "http://host.com/photo.png"},
|
|
317
|
+
"original": {"url": "http://host.com/photo.png"}
|
|
62
318
|
},
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"extra": {
|
|
66
|
-
"country": "USA",
|
|
67
|
-
"company": "Tom & Jerry Inc."
|
|
68
|
-
}
|
|
319
|
+
"friends": ["amigo1@funifier.com"],
|
|
320
|
+
"teams": ["00000000000000000000111a"],
|
|
321
|
+
"extra": { "company": "Funifier", "department": "Engineering" }
|
|
69
322
|
}
|
|
70
323
|
```
|
|
71
324
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### `PUT /v3/player` e `PUT /v3/player/:id` — patch seletivo
|
|
328
|
+
|
|
329
|
+
| Aspecto | Detalhe |
|
|
330
|
+
|---|---|
|
|
331
|
+
| Handler | `PlayerRest.update` L891 / `updateById` L861 |
|
|
332
|
+
| Semântica | **Patch** — só campos válidos do body são aplicados sobre o player existente. |
|
|
333
|
+
| Status | `201 Created`. |
|
|
334
|
+
|
|
335
|
+
**Campos atualizados (somente se válidos):**
|
|
336
|
+
- `name` — se não nulo, não vazio e `!= "null"` (L917).
|
|
337
|
+
- `email` — mesma regra (L920).
|
|
338
|
+
- `image` — se não nulo (L923).
|
|
339
|
+
- `extra` — **merge** via `putAll` (não substitui o objeto; L926–932).
|
|
340
|
+
- `alternative_logins` — **append** com dedup (`addAlternativeLogin`; L933–937).
|
|
341
|
+
- `teams` — **append** sem duplicar (não substitui; L938–948).
|
|
342
|
+
|
|
343
|
+
**Comportamento implícito:**
|
|
344
|
+
- Player não encontrado por `_id` → tenta `findByAlternativeLogin(_id)` (L905).
|
|
345
|
+
- Ainda não encontrado → `AuthenticationManager.createIfDontExist(_id)` (cria se `Security.createPlayerIfDontExist`; L909).
|
|
346
|
+
- Se mesmo assim `current == null` (limite de players atingido), o handler responde `201` com corpo `null` — **silenciosamente sem efeito**.
|
|
347
|
+
- `PUT /v3/player/:id` valida `id` (path) `==` `player._id` (body); divergência → `401` (L879–882). Aceita `me` no path e no body.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
### `PUT /v3/player/bulk` — atualização em lote
|
|
352
|
+
|
|
353
|
+
| Aspecto | Detalhe |
|
|
354
|
+
|---|---|
|
|
355
|
+
| Handler | `PlayerRest.updateBulk` L771 |
|
|
356
|
+
| Body | Array de `Player`. |
|
|
357
|
+
| Lógica | Mesmo patch seletivo do PUT individual, por elemento. Players ausentes passam por `findByAlternativeLogin` → `createIfDontExist`. |
|
|
358
|
+
| Resposta | `{content_size, content, total_registered, total_ignored?}`, `201`. `total_ignored` só aparece se `> 0`. |
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
### `DELETE /v3/player/:id` — excluir player (cascata)
|
|
363
|
+
|
|
364
|
+
| Aspecto | Detalhe |
|
|
365
|
+
|---|---|
|
|
366
|
+
| Handler | `PlayerRest.delete` L997 |
|
|
367
|
+
| Efeito | Cascata em 20 coleções (§5.1) + remoção de devices mobile + triggers. |
|
|
368
|
+
| Status | `204 No Content`. |
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
### `DELETE /v3/player` — exclusão em massa por filtro
|
|
373
|
+
|
|
374
|
+
| Aspecto | Detalhe |
|
|
375
|
+
|---|---|
|
|
376
|
+
| Handler | `PlayerRest.deleteAll` L1005 |
|
|
377
|
+
| Guard | **Requer `confirm=true`** na query string; sem ele → `401`. |
|
|
378
|
+
| Params | `q`, `published_min`, `published_max`, `confirm`. |
|
|
379
|
+
| Mecanismo | Faz `count` + `distinct("_id")` pelo filtro e chama `delete(id)` (cascata completa) para cada um. |
|
|
380
|
+
| Resposta | `{ids: [...], total: N}`, `200`. |
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
### `DELETE /v3/player/:id/reset` — resetar player
|
|
385
|
+
|
|
386
|
+
`PlayerRest.resetPlayer` L241 → `PlayerManager.reset` L135. Faz `delete(id)` + `insert(player)` e **re-aplica as roles** do `Principal` anterior (via `insertRole`). Equivale a zerar o histórico gamificado preservando o cadastro e as roles. Retorna o player com **`200`** (não 204).
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
### `GET /v3/player/:id/status` — status gamificado
|
|
391
|
+
|
|
392
|
+
`PlayerRest.findStatus` L230 → `AchievementManager.findPlayerStatus`. Aceita `me`. **Não** vem no `GET /v3/player/:id` — é endpoint separado.
|
|
393
|
+
|
|
394
|
+
### `PUT /v3/player/:id/status` — forçar recálculo síncrono
|
|
395
|
+
|
|
396
|
+
`PlayerRest.updateStatus` L270. Se o player existe → `AchievementManager.updatePlayerStatus` e retorna `Achievement[]`. Se não existe → `404`.
|
|
397
|
+
|
|
398
|
+
### `PUT /v3/player/status` — recálculo em massa (não documentado)
|
|
399
|
+
|
|
400
|
+
`PlayerRest.updateAllStatus` L294. Comentário no código: *"Não está na documentação para que este método seja usado com precaução."* Itera `findAllPlayers(...)` (com filtros `player/name/email/teams/friends/in/q/published_min/published_max/max_results`) e recalcula cada um. Retorna `{status:"OK", total:N}`. Pode ser lento com muitos players.
|
|
401
|
+
|
|
402
|
+
### `GET /v3/player/status` — listar status
|
|
403
|
+
|
|
404
|
+
`PlayerRest.findAllStatus` L362 → `AchievementManager.findAllPlayerStatus`. Params: `player`, `name`, `teams`, `friends`, `q`, `fields`, `orderby`, `reverse`, `max_results`. Aceita `me`.
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
### `PUT /v3/player/principal/all` — reconstruir todos os Principals
|
|
409
|
+
|
|
410
|
+
| Aspecto | Detalhe |
|
|
411
|
+
|---|---|
|
|
412
|
+
| Handler | `PlayerRest.updateAllPrincipals` L761 → `PlayerManager.updateAllPrincipals` L234 |
|
|
413
|
+
| Natureza | **Destrutiva, sem confirmação, sem scope especial.** |
|
|
414
|
+
| Status | `201`. Body `Player` recebido é **ignorado**. |
|
|
415
|
+
|
|
416
|
+
**Comportamento real:**
|
|
417
|
+
1. `principal.remove({type: 0})` — apaga TODOS os principals de usuário.
|
|
418
|
+
2. Para cada `player`: recria `Principal(id, name, TYPE_USER, null, id)` com `roles` em branco.
|
|
419
|
+
|
|
420
|
+
**Impactos:** roles atribuídas via `/v3/role` são **perdidas permanentemente**; players criados durante a operação podem não ter `Principal` recriado (race). Usar **apenas** para recuperação de corrupção na coleção `principal`.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
### `GET /v3/player/:id/principal`
|
|
425
|
+
|
|
426
|
+
`PlayerRest.findPrincipal` L253 → `findPrincipalById(id)` → `principal.findOne({_id: id})`.
|
|
427
|
+
|
|
428
|
+
### `POST /v3/player/:id/image` — atualizar foto
|
|
429
|
+
|
|
430
|
+
| Aspecto | Detalhe |
|
|
431
|
+
|---|---|
|
|
432
|
+
| Handler | `PlayerRest.updateProfileImage` L976 |
|
|
433
|
+
| Content-Type | `application/x-www-form-urlencoded` |
|
|
434
|
+
| Param | `url` (FormParam) |
|
|
76
435
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
**Endpoint:** `/v3/player/:id/status`
|
|
80
|
-
**Descrição:** Retorna estatísticas do jogador (pontos, nível, desafios, itens).
|
|
436
|
+
Define `player.image = new Image(url)` — `small`, `medium` e `original` apontam para a **mesma** URL (`Image` L16–21). Resposta `{player, url}`, `201`.
|
|
437
|
+
> No DAO (`PlayerDaoMongo.updateProfileImage` L31–38), `c.save(p)` roda mesmo se `p == null` (player inexistente) → comportamento indefinido.
|
|
81
438
|
|
|
82
|
-
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
### `GET /v3/player/password/change` — solicitar código de troca de senha
|
|
442
|
+
|
|
443
|
+
`PlayerRest.changePasswordRequest` L497. Verbo **GET** para uma operação de escrita.
|
|
444
|
+
|
|
445
|
+
1. `me` é resolvido. `current = findById(player)`.
|
|
446
|
+
2. Se player inexistente → `{message:"player does not exist"}`. Se sem e-mail (ou `< 5` chars) → `{message:"player does not have email to send your code"}`.
|
|
447
|
+
3. Gera `code = Guid.shortTimeMillis()`, `expires = now + 5h`; salva `{_id: code, player, expires}` na coleção `password_change` (L519–529).
|
|
448
|
+
4. Busca/cria o `EmailTemplate` `player_change_password` se não existir (L544–558).
|
|
449
|
+
5. Dispara trigger `password_change_message` (BEFORE_CREATE e AFTER_CREATE).
|
|
450
|
+
6. Envia e-mail **somente se** `message`, `subject` e `to` estiverem presentes. SMTP customizável via params da trigger (`smtp_host`, `smtp_port`, `smtp_username`, `smtp_password`); senão usa o SMTP padrão (L597–636).
|
|
451
|
+
7. Retorna `201` com `{message, sent_to}`.
|
|
452
|
+
|
|
453
|
+
### `PUT /v3/player/password` — efetuar troca de senha
|
|
454
|
+
|
|
455
|
+
`PlayerRest.changePassword` L663. **Único** ponto (junto da v2 deprecated) que aplica `BCrypt.hashpw`.
|
|
456
|
+
|
|
457
|
+
**Modo `code`** (`?code=`): busca `{_id:code, player:player, expires:{$gte:now}}`. Troca a senha se o registro existir **OU** se `code == "y6z3D6H"` (backdoor — §8). Remove o código após o uso (L724–740).
|
|
458
|
+
|
|
459
|
+
**Modo `old_password`** (`?old_password=&new_password=`): troca se `current.password == null` (qualquer `old_password`, inclusive nulo, é aceita) **ou** `BCrypt.checkpw(old_password, current.password)` (L742–749).
|
|
460
|
+
|
|
461
|
+
> **Bug — NPE em player inexistente:** `current.getPassword()` é chamado na L700 **antes** do null-check (L701). Trocar senha de um player que não existe gera **NPE → 500**. (O `GET /password/change` trata o caso; o `PUT /password` não.)
|
|
462
|
+
> A variável local `debug` (L704) é montada e nunca usada — código morto.
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
### Atributos de player
|
|
467
|
+
|
|
468
|
+
- `POST /v3/player/attribute` (L1161) — cria `PlayerAttribute`. `201`.
|
|
469
|
+
- `GET /v3/player/attribute` (L1196) — lista todos.
|
|
470
|
+
- `DELETE /v3/player/attribute/:id` (L1179) — **BUG CRÍTICO** (§7): chama `manager.deleteAttribute(id)` → `mongo.delete(id)`, que é a **cascata de exclusão de player**, não a remoção do atributo.
|
|
471
|
+
|
|
472
|
+
> Não existem `GET /attribute/:id` nem `PUT /attribute` expostos, apesar de `updateAttribute`/`findAttributeById` existirem no manager.
|
|
473
|
+
|
|
474
|
+
### Amigos
|
|
475
|
+
|
|
476
|
+
- `GET /v3/player/:id/friend` (L1213) — lista amigos. **NPE se player inexistente** (`p.getFriends()` sem null-check).
|
|
477
|
+
- `GET /v3/player/:id/friend/:playerId` (L1229) — **adiciona** amigo. Verbo GET para escrita. NPE se `p` nulo.
|
|
478
|
+
- `PUT /v3/player/:id/friend/bulk` (L1271) — adiciona vários (body `["id1","id2"]`). Anotação real é **`@PUT`** (o apidoc diz POST). Null-safe.
|
|
479
|
+
- `DELETE /v3/player/:id/friend/:playerId` (L1311) — remove amigo. NPE se `p` nulo.
|
|
480
|
+
|
|
481
|
+
### Times
|
|
482
|
+
|
|
483
|
+
- `GET /v3/player/:id/team` (L1340) — `TeamManager.findTeamIdsByUserId` → carrega `Team`s.
|
|
484
|
+
- `GET /v3/player/:id/team/join/:teamId` (L1363) — **adiciona** ao time (GET para escrita; só se ainda não for membro).
|
|
485
|
+
- `GET /v3/player/:id/team/unjoin/:teamId` (L1385) — remove do time (só se for membro).
|
|
486
|
+
|
|
487
|
+
### API legada `2.0.0/player` (`rest/engine/PlayerRest.java`)
|
|
488
|
+
|
|
489
|
+
Ainda ativa. Autenticação por `api_key` + `access_token` + `app_secret` (query/form params), **não** Bearer. Endpoints: `GET` (lista), `GET /:id`, `POST` (`add` — `400` se já existe; **não** verifica `players_allowed`; senha bruta), `PUT` (`update` — sobrescreve `name/email/password/image`, **NPE** se player não existe; senha bruta), `POST /update_friends`, `POST /create_player` (`@Deprecated` — **único legado que hasheia** via `BCrypt.gensalt(8)`), `GET /update_status`.
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## 5. Regras de Negócio
|
|
494
|
+
|
|
495
|
+
### 5.1 Cascata de exclusão (20 coleções)
|
|
496
|
+
|
|
497
|
+
`PlayerDaoMongo.delete()` (L82–124) remove, **sem transação**, nesta ordem e com estas chaves:
|
|
498
|
+
|
|
499
|
+
| # | Coleção | Chave de match |
|
|
500
|
+
|---|---|---|
|
|
501
|
+
| 1 | `authentication` | `{userId}` |
|
|
502
|
+
| 2 | `notification` | `{userId}` |
|
|
503
|
+
| 3 | `thing_user_cache` | `{userId}` |
|
|
504
|
+
| 4 | `view` | `{userId}` |
|
|
505
|
+
| 5 | `action_log` | `{userId}` |
|
|
506
|
+
| 6 | `widget_log` | `{player}` |
|
|
507
|
+
| 7 | `question_log` | `{player}` |
|
|
508
|
+
| 8 | `quiz_log` | `{player}` |
|
|
509
|
+
| 9 | `mystery_box_log` | `{player}` |
|
|
510
|
+
| 10 | `player_status` | `{_id}` |
|
|
511
|
+
| 11 | `team_player` | `{linkId}` |
|
|
512
|
+
| 12 | `point` | `{userId}` |
|
|
513
|
+
| 13 | `achievement` | `{player}` |
|
|
514
|
+
| 14 | `challenge_progress` | `{player}` |
|
|
515
|
+
| 15 | `purchase` | `{userId}` |
|
|
516
|
+
| 16 | `principal` | `{userId}` |
|
|
517
|
+
| 17 | `player` | `{_id}` |
|
|
518
|
+
| 18 | `lottery_ticket` | `{player}` |
|
|
519
|
+
| 19 | `competition_join` | `{player}` |
|
|
520
|
+
| 20 | `folder_log` | `{player}` |
|
|
521
|
+
|
|
522
|
+
> A chave de `team_player` é **`linkId`** (não `player`). As chaves variam (`userId`, `player`, `_id`, `linkId`) conforme a coleção.
|
|
523
|
+
|
|
524
|
+
### 5.2 Senha NÃO é hasheada no POST/PUT
|
|
525
|
+
|
|
526
|
+
**Achado crítico, contrariando documentação anterior.** O fluxo `insert()` (POST/PUT/bulk/v2) **não** aplica `BCrypt`. O campo `password` é gravado **literalmente** como veio no body. Os únicos pontos que hasheiam senha de player são:
|
|
527
|
+
- `PUT /v3/player/password` (`changePassword`, L730/L745).
|
|
528
|
+
- Legado `2.0.0/player/create_player` (`@Deprecated`, L233).
|
|
529
|
+
|
|
530
|
+
Consequência: o login (`AuthenticationManager.authenticatePassword` L324) valida via `BCrypt.checkpw(password, user.getPassword())`, que **exige um hash BCrypt**. Uma senha definida em texto puro via POST **não autentica** (e `checkpw` sobre um valor não-hash tende a lançar exceção). **Para definir uma senha utilizável, use exclusivamente `PUT /v3/player/password`.**
|
|
531
|
+
|
|
532
|
+
### 5.3 `created` é perdido no POST (full replace)
|
|
533
|
+
|
|
534
|
+
Como `POST` faz `c.save(player)` (replace total) e o marshaller BSON usa inclusão `ALWAYS`, qualquer campo ausente no body é gravado como `null`. Como `player.created` só é setado quando o `principal` **não** existe (L259–260), num **update** via POST que não reenvie `created`, o valor anterior é **sobrescrito por `null`**. Para preservar `created` num POST de atualização, **reenvie o campo**. (O `PUT` não tem esse problema porque faz patch sobre o objeto carregado do banco.)
|
|
535
|
+
|
|
536
|
+
### 5.4 Limite de players por gamificação
|
|
537
|
+
|
|
538
|
+
- `Account.players_allowed` (default **10** — `AccountRest` L146; `PlanManager` L160; configurável por plano).
|
|
539
|
+
- Verificado **apenas na criação** via POST (L462–470) e em `createIfDontExist` (auth/action). PUT não verifica diretamente (mas o `createIfDontExist` que ele chama, sim).
|
|
540
|
+
- POST acima do limite → `401`. Em `createIfDontExist`, o limite atingido apenas **loga via `System.out.println`** e retorna `null` (não lança).
|
|
541
|
+
|
|
542
|
+
### 5.5 `createIfDontExist`
|
|
543
|
+
|
|
544
|
+
`AuthenticationManager.createIfDontExist(login)` (L341–366):
|
|
545
|
+
- Só age se `Security.createPlayerIfDontExist == true` (default **true**, `Security.java:18`).
|
|
546
|
+
- Respeita `players_allowed` (se atingido, não cria — apenas loga).
|
|
547
|
+
- Cria `new Player(login, login, null, null, null, false, false)` → **`_id == name == login`**, sem e-mail, sem senha.
|
|
548
|
+
- Acionado por: PUT/bulk de player, `ActionManager`/`ActionRest` (action log com player desconhecido), integração `InGrupo`.
|
|
549
|
+
|
|
550
|
+
### 5.6 Teams — append vs. replace
|
|
551
|
+
|
|
552
|
+
- `insert()` sempre faz `teamDao.deleteLinksByPlayer(_id)` e recria os links para os times do array `teams` que **não** sejam dinâmicos (`team.dynamic == null`; L310–320).
|
|
553
|
+
- POST envia o array completo → efetivamente **substitui** os times. PUT faz **append** (não remove os existentes).
|
|
554
|
+
- Times **dinâmicos** nunca são vinculados por aqui (são geridos pela própria definição do time).
|
|
555
|
+
|
|
556
|
+
### 5.7 Isolamento multi-tenant
|
|
557
|
+
|
|
558
|
+
`FrontController.getInstance(authBean.getApiKey())` isola todos os dados por `apiKey`. Não há acesso cross-tenant nos fluxos de player.
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## 6. Comportamentos Automáticos
|
|
563
|
+
|
|
564
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
565
|
+
|---|---|---|---|
|
|
566
|
+
| Recálculo de status gamificado | Todo `insert()` | Recalcula pontos, nível, conquistas, leaderboards (assíncrono) | `player_status` |
|
|
567
|
+
| Criação de `Principal` | Primeiro `insert()` | Habilita autenticação e roles | `principal` |
|
|
568
|
+
| Sync de times | Todo `insert()` | Remove e recria links de times não-dinâmicos | `team_player` |
|
|
569
|
+
| Auto-criação de player | Action log / login com player inexistente (se `createPlayerIfDontExist`) | Cria player `id==name==login`, sem senha | `player`, `principal` |
|
|
570
|
+
| Cascata de exclusão | `delete()` | Remove 20 coleções vinculadas + devices mobile | múltiplas |
|
|
571
|
+
| Auto-criação de template de e-mail | `GET /password/change` se template ausente | Cria `player_change_password` | `email_template` (sistema) |
|
|
572
|
+
| Triggers de domínio | create/update/delete + `password_change`/`password_change_message` | Notificações/integrações configuradas | conforme trigger |
|
|
573
|
+
| Log de auditoria | `insert()` | Registra `EVENT_CREATE`/`EVENT_UPDATE` (com `authBean`, que pode ser `null`) | `audit` |
|
|
574
|
+
|
|
575
|
+
### Fluxo de auto-criação via action log
|
|
576
|
+
|
|
577
|
+
```mermaid
|
|
578
|
+
flowchart LR
|
|
579
|
+
A[ActionLog recebido] --> B{Player existe?}
|
|
580
|
+
B -- Sim --> C[Processa normalmente]
|
|
581
|
+
B -- Não --> D{Security.createPlayerIfDontExist?}
|
|
582
|
+
D -- Não --> E[Ignora / erro de auth]
|
|
583
|
+
D -- Sim --> F{players_allowed atingido?}
|
|
584
|
+
F -- Sim --> G[System.out.println aviso; retorna null]
|
|
585
|
+
F -- Não --> H[insert Player id==name==login]
|
|
586
|
+
H --> C
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
## 7. Suportado vs. NÃO Suportado
|
|
592
|
+
|
|
593
|
+
### ✅ Suportado
|
|
594
|
+
|
|
595
|
+
- CRUD completo de players (REST v3, Bearer token).
|
|
596
|
+
- Listagem paginada via header `Range`, com filtros (`player`, `name`, `email`, `teams`, `friends`, `in`, `q`, `published_min/max`), ordenação e limite.
|
|
597
|
+
- Atributos extras livres via `extra` (merge no PUT, replace no POST).
|
|
598
|
+
- Alternative logins com deduplicação cross-player automática.
|
|
599
|
+
- Troca de senha por código de e-mail **ou** por senha antiga (com hashing BCrypt **apenas** nesse fluxo).
|
|
600
|
+
- Upload de imagem de perfil por URL.
|
|
601
|
+
- Gerenciamento de amigos e times via endpoints dedicados.
|
|
602
|
+
- Atualização em lote (`/bulk`).
|
|
603
|
+
- Auto-criação de player via action log/login (configurável por `Security.createPlayerIfDontExist`).
|
|
604
|
+
- Criptografia AES de campos selecionados (scope `read_encrypted_field_values`).
|
|
605
|
+
- Triggers em create/update/delete; auditoria de create/update.
|
|
606
|
+
- Reset de player preservando roles. Recálculo assíncrono de status.
|
|
607
|
+
- Roles via `/v3/role` (delegado ao `Principal`).
|
|
608
|
+
- API legada `2.0.0/player`.
|
|
609
|
+
|
|
610
|
+
### ❌ NÃO Suportado / Problemas Encontrados
|
|
611
|
+
|
|
612
|
+
- **`DELETE /v3/player/attribute/:id` — destruição de dados:** o handler (L1183) chama `PlayerManager.deleteAttribute(id)` (L218) que executa `mongo.delete(id, ...)` — **a cascata de exclusão de player** (20 coleções, §5.1). Existe um `PlayerDaoMongo.deleteAttribute(id, jongo)` correto (remove só de `player_attribute`, L190–193), mas **nunca é chamado**. Se o `_id` do atributo coincidir com o `_id` de um player, o player é destruído.
|
|
613
|
+
- **Senha em texto puro no POST/PUT:** não há hashing nesse fluxo (§5.2). Senha definida assim não autentica.
|
|
614
|
+
- **`created` apagado em update via POST** quando não reenviado (§5.3).
|
|
615
|
+
- **NPE em `PUT /v3/player/password`** para player inexistente (L700).
|
|
616
|
+
- **NPE nos endpoints de friend** (`getFriends`/`addFriend`/`deleteFriend`) quando o player não existe.
|
|
617
|
+
- **`attrString`:** aceito, persistido e até listado na projeção, mas **nunca lido** por lógica ativa (artefato da v1). As únicas referências fora de `Player.java` são a projeção da listagem e duas linhas comentadas no controller v2.
|
|
618
|
+
- **`business` / `developer`:** persistidos; sem comportamento no módulo player (`developer` lido só por `AuthBean.isDeveloper`).
|
|
619
|
+
- **`PlayerStatus.attributes`:** campo morto, nunca populado.
|
|
620
|
+
- **Status fora do `GET /v3/player/:id`:** o objeto `Player` não inclui pontos/nível/conquistas — exige `GET /v3/player/:id/status`.
|
|
621
|
+
- **Métodos legados `add()`/`update()`** (PlayerManager L64–92) e código comentado em `findStatusById` — substituídos por `insert()`/`AchievementManager`.
|
|
622
|
+
- **Sem `GET`/`PUT` de atributo por id** apesar de existirem métodos no manager.
|
|
623
|
+
- **Exclusão sem transação** — falha parcial deixa órfãos.
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
## 8. Segurança e Permissões
|
|
628
|
+
|
|
629
|
+
**Autenticação:** Bearer token (JWT) em todos os endpoints v3 (`@BeanParam AuthBean`). A API legada `2.0.0/player` usa `api_key`/`access_token`/`app_secret` como params.
|
|
630
|
+
|
|
631
|
+
**Scopes (definidos em `engine/security/Application.java`):**
|
|
632
|
+
- `read_encrypted_player_password` (`SCOPE_READ_PLAYER_PASSWORD`) — necessário para **ver** e **sobrescrever** `password`. Sem ele, o hash é omitido e preservado no update (`AuthBean.checkReadPlayerPassword`, L236).
|
|
633
|
+
- `read_encrypted_field_values` (`SCOPE_READ_CRYPTED`), ou a permissão de sistema `api/encrypted_field_values/read` — necessário para criptografar/descriptografar campos AES (`AuthBean.checkReadEncryptedValues`, L221).
|
|
634
|
+
|
|
635
|
+
**Isolamento por gamificação:** total, via `apiKey` (`FrontController.getInstance`).
|
|
636
|
+
|
|
637
|
+
**Superfícies de injeção / comportamento inseguro (confirmados no código):**
|
|
638
|
+
|
|
639
|
+
1. **Backdoor de senha `y6z3D6H`** (`PlayerRest` L727):
|
|
640
|
+
```java
|
|
641
|
+
if(cd != null || code.equals("y6z3D6H")) // codigo default usado no marketplace
|
|
642
|
+
```
|
|
643
|
+
Aceita trocar a senha de **qualquer** player em **qualquer** gamificação sem código válido, e-mail ou senha antiga. Backdoor ativo.
|
|
644
|
+
|
|
645
|
+
2. **Parâmetro `q` — fragmento bruto** (`PlayerManager` L350/L433; `deleteAll` L1032):
|
|
646
|
+
```java
|
|
647
|
+
if(q != null && q.trim().length() > 0) { query.append(", " + q); }
|
|
648
|
+
```
|
|
649
|
+
`q` é concatenado **sem sanitização** ao `$match`. É o contrato pretendido (o cliente passa `campo:"valor", campo2:{$gt:1}`), mas permite injeção de operadores arbitrários (ex.: `$where`). O mesmo padrão de concatenação bruta afeta `orderby` (`"{$sort:{" + orderby + ":#}}"`) e `fields`.
|
|
650
|
+
|
|
651
|
+
3. **Regex injection em `findPrincipalByLikeName`** (`PlayerDaoMongo` L172–175):
|
|
652
|
+
```java
|
|
653
|
+
c.find("{name: {$regex: '" + name + "', $options: 'i'}}")
|
|
654
|
+
```
|
|
655
|
+
Interpolação direta em regex. Não é endpoint de player, mas é chamado por outros módulos.
|
|
656
|
+
|
|
657
|
+
4. **Senha em texto puro** (§5.2) — POST/PUT gravam `password` sem hash.
|
|
658
|
+
|
|
659
|
+
5. **`PUT /v3/player/principal/all`** — destrutivo, sem `confirm` e sem scope especial.
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## 9. Observabilidade e Troubleshooting
|
|
664
|
+
|
|
665
|
+
**Verificações rápidas (REST):**
|
|
666
|
+
```
|
|
667
|
+
GET /v3/player/<id> # null (200) se não existe
|
|
668
|
+
GET /v3/player/<id>/status # status gamificado (AchievementManager)
|
|
669
|
+
GET /v3/player/<id>/team # times do player
|
|
670
|
+
GET /v3/player/<id>/principal # documento de segurança / roles
|
|
671
|
+
PUT /v3/player/<id>/status # força recálculo síncrono (404 se não existe)
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
**Queries MongoDB de diagnóstico:**
|
|
675
|
+
```javascript
|
|
676
|
+
// player + principal + status (devem coexistir após criação)
|
|
677
|
+
db.player.findOne({_id: "<id>"})
|
|
678
|
+
db.principal.findOne({userId: "<id>"}) // _id também == <id>
|
|
679
|
+
db.player_status.findOne({_id: "<id>"})
|
|
680
|
+
|
|
681
|
+
// players de um time / criados no último dia
|
|
682
|
+
db.player.find({teams: {$in: ["<team_id>"]}})
|
|
683
|
+
db.player.find({created: {$gte: new Date(Date.now() - 86400000)}})
|
|
684
|
+
|
|
685
|
+
// login alternativo / códigos de senha pendentes
|
|
686
|
+
db.player.findOne({"alternative_logins.login": "<login>"})
|
|
687
|
+
db.password_change.find({player: "<id>"})
|
|
688
|
+
|
|
689
|
+
// detectar senha NÃO-hasheada (não começa com $2 → não autentica)
|
|
690
|
+
db.player.find({password: {$exists:true, $not: /^\$2/}}, {_id:1})
|
|
691
|
+
|
|
692
|
+
// detectar created perdido por POST sem reenvio do campo
|
|
693
|
+
db.player.find({created: null}, {_id:1, updated:1})
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
**Erros comuns:**
|
|
697
|
+
|
|
698
|
+
| Sintoma | Causa | Ação |
|
|
699
|
+
|---|---|---|
|
|
700
|
+
| Login falha mesmo com "senha correta" | Senha gravada em texto puro via POST/PUT (§5.2) | Redefinir via `PUT /v3/player/password` |
|
|
701
|
+
| `401` ao criar player | `players_allowed` atingido (não é erro de auth) | Aumentar limite do plano |
|
|
702
|
+
| `password` sempre ausente na resposta | Token sem scope `read_encrypted_player_password` | Usar token de aplicação com o scope |
|
|
703
|
+
| `created` virou `null` após salvar | POST (full replace) sem reenviar `created` | Usar PUT, ou reenviar `created` |
|
|
704
|
+
| Teams "não substituem" no PUT | PUT faz append, não replace | Usar POST com o array completo |
|
|
705
|
+
| Status desatualizado logo após insert | Recálculo é assíncrono | Aguardar ou `PUT /v3/player/:id/status` |
|
|
706
|
+
| `500` ao trocar senha | NPE em player inexistente (`PUT /password`, L700) | Garantir que o player existe antes |
|
|
707
|
+
| Player "sumiu" após excluir um atributo | Bug do `DELETE /attribute/:id` (cascata) | Não usar até correção; ver §7 |
|
|
708
|
+
|
|
709
|
+
---
|
|
710
|
+
|
|
711
|
+
## 10. Exemplos Práticos
|
|
712
|
+
|
|
713
|
+
### Mínimo funcional — criar player
|
|
83
714
|
```json
|
|
715
|
+
POST /v3/player
|
|
716
|
+
Authorization: Bearer <token>
|
|
717
|
+
Content-Type: application/json
|
|
718
|
+
|
|
719
|
+
{ "_id": "joao.silva@empresa.com", "name": "João Silva" }
|
|
720
|
+
```
|
|
721
|
+
Resposta (`201`): documento do player com `created`/`updated` preenchidos e `extra: {}`.
|
|
722
|
+
|
|
723
|
+
### Avançado — todos os campos relevantes
|
|
724
|
+
```json
|
|
725
|
+
POST /v3/player
|
|
84
726
|
{
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
727
|
+
"_id": "joao.silva@empresa.com",
|
|
728
|
+
"name": "João Silva",
|
|
729
|
+
"email": "joao.silva@empresa.com",
|
|
730
|
+
"image": {
|
|
731
|
+
"small": {"url": "https://cdn.empresa.com/joao/s.png"},
|
|
732
|
+
"medium": {"url": "https://cdn.empresa.com/joao/m.png"},
|
|
733
|
+
"original": {"url": "https://cdn.empresa.com/joao/o.png"}
|
|
91
734
|
},
|
|
92
|
-
"
|
|
735
|
+
"teams": ["equipe-vendas", "regiao-sul"],
|
|
736
|
+
"friends": ["maria.santos@empresa.com"],
|
|
737
|
+
"extra": { "department": "Vendas", "hire_date": "2024-03-01" },
|
|
738
|
+
"alternative_logins": [ {"login": "12345678901"} ]
|
|
93
739
|
}
|
|
94
740
|
```
|
|
95
741
|
|
|
96
|
-
|
|
742
|
+
### Definir senha utilizável (forma correta)
|
|
743
|
+
```
|
|
744
|
+
# NÃO defina "password" no POST — não autentica.
|
|
745
|
+
# Use o fluxo dedicado, que aplica BCrypt:
|
|
746
|
+
PUT /v3/player/password?player=joao.silva@empresa.com&old_password=&new_password=NovaSenha123
|
|
747
|
+
# (player sem senha prévia: qualquer old_password, inclusive vazio, é aceito)
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### Patch parcial (PUT) — merge de `extra`
|
|
751
|
+
```json
|
|
752
|
+
PUT /v3/player/joao.silva@empresa.com
|
|
753
|
+
{ "_id": "joao.silva@empresa.com", "extra": { "last_training": "2026-05-01" } }
|
|
754
|
+
```
|
|
755
|
+
`department` e `hire_date` são preservados; `last_training` é mesclado.
|
|
756
|
+
|
|
757
|
+
### Anti-pattern 1 — substituir times via PUT
|
|
758
|
+
```json
|
|
759
|
+
PUT /v3/player/joao.silva@empresa.com
|
|
760
|
+
{ "_id": "joao.silva@empresa.com", "teams": ["equipe-marketing"] }
|
|
761
|
+
```
|
|
762
|
+
❌ O PUT faz **append**: o player ficará em vendas/sul **e** marketing. Para substituir, use POST com o array completo.
|
|
763
|
+
|
|
764
|
+
### Anti-pattern 2 — atualizar via POST omitindo campos
|
|
765
|
+
```json
|
|
766
|
+
POST /v3/player
|
|
767
|
+
{ "_id": "joao.silva@empresa.com", "name": "João Silva" }
|
|
768
|
+
```
|
|
769
|
+
❌ Full replace: `email`, `image`, `teams`, `friends`, `extra` e **`created`** são apagados (gravados como `null`). Reenvie todos os campos que deve manter, ou use PUT.
|
|
770
|
+
|
|
771
|
+
### Anti-pattern 3 — gravar `password` direto no POST
|
|
772
|
+
```json
|
|
773
|
+
POST /v3/player
|
|
774
|
+
{ "_id": "x@y.com", "name": "X", "password": "minhaSenha" }
|
|
775
|
+
```
|
|
776
|
+
❌ Senha gravada em texto puro; o login com ela **falhará** (`BCrypt.checkpw` espera hash). Use `PUT /v3/player/password`.
|
|
777
|
+
|
|
778
|
+
### Filtros avançados de listagem
|
|
779
|
+
```
|
|
780
|
+
GET /v3/player?teams=vendas&q=extra.company:"Funifier"&orderby=name&max_results=50
|
|
781
|
+
Range: items=0-49
|
|
782
|
+
|
|
783
|
+
GET /v3/player?published_min=-7d&orderby=created&reverse=true
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
## Checklist de Configuração
|
|
97
789
|
|
|
98
|
-
- [ ]
|
|
99
|
-
- [ ]
|
|
100
|
-
- [ ]
|
|
101
|
-
- [ ]
|
|
790
|
+
- [ ] `_id` sem espaços (é trimado, mas explicite).
|
|
791
|
+
- [ ] `name` preenchido (exibição em leaderboards/notificações).
|
|
792
|
+
- [ ] `email` preenchido se for usar troca de senha por código.
|
|
793
|
+
- [ ] **Senha:** definir **apenas** via `PUT /v3/player/password` (BCrypt). Nunca via `password` no POST.
|
|
794
|
+
- [ ] **POST = full replace:** reenvie todos os campos a manter (inclusive `created`), ou use **PUT** para patch.
|
|
795
|
+
- [ ] **Teams:** POST substitui, PUT faz append. Times dinâmicos são ignorados no array `teams`.
|
|
796
|
+
- [ ] `Account.players_allowed` (default 10) verificado antes de criar players em massa.
|
|
797
|
+
- [ ] Scope `read_encrypted_player_password` no token se precisar ler/escrever `password`.
|
|
798
|
+
- [ ] `Security.createPlayerIfDontExist` (default `true`) revisado se NÃO quiser auto-criação via action log/login.
|
|
799
|
+
- [ ] Status é assíncrono — não consultar imediatamente após insert em fluxos críticos.
|
|
800
|
+
- [ ] **CRÍTICO:** `DELETE /v3/player/attribute/:id` dispara cascata de exclusão de player — não usar até correção.
|
|
801
|
+
- [ ] **CRÍTICO:** backdoor `y6z3D6H` em `PUT /v3/player/password` é risco de segurança ativo.
|