funifier-mcp 0.1.0 → 0.2.0
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/README.md +182 -351
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +152 -0
- package/datasource-funifier-docs/knowledge/guides/database-access.md +132 -0
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +373 -0
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +330 -0
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +509 -0
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +271 -0
- package/datasource-funifier-docs/knowledge/index.md +121 -0
- package/datasource-funifier-docs/knowledge/modules/achievement.md +46 -0
- package/datasource-funifier-docs/knowledge/modules/action-log.md +88 -0
- package/datasource-funifier-docs/knowledge/modules/action.md +80 -0
- package/datasource-funifier-docs/knowledge/modules/auth.md +104 -0
- package/datasource-funifier-docs/knowledge/modules/avatar.md +28 -0
- package/datasource-funifier-docs/knowledge/modules/backup.md +40 -0
- package/datasource-funifier-docs/knowledge/modules/challenge.md +91 -0
- package/datasource-funifier-docs/knowledge/modules/compact.md +40 -0
- package/datasource-funifier-docs/knowledge/modules/competition.md +149 -0
- package/datasource-funifier-docs/knowledge/modules/crossword.md +41 -0
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +30 -0
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +53 -0
- package/datasource-funifier-docs/knowledge/modules/database.md +241 -0
- package/datasource-funifier-docs/knowledge/modules/folder.md +111 -0
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +23 -0
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +45 -0
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +98 -0
- package/datasource-funifier-docs/knowledge/modules/level.md +83 -0
- package/datasource-funifier-docs/knowledge/modules/lottery.md +112 -0
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +27 -0
- package/datasource-funifier-docs/knowledge/modules/mystery.md +82 -0
- package/datasource-funifier-docs/knowledge/modules/notification.md +40 -0
- package/datasource-funifier-docs/knowledge/modules/patterns.md +1096 -0
- package/datasource-funifier-docs/knowledge/modules/player.md +101 -0
- package/datasource-funifier-docs/knowledge/modules/point.md +67 -0
- package/datasource-funifier-docs/knowledge/modules/public.md +253 -0
- package/datasource-funifier-docs/knowledge/modules/question.md +136 -0
- package/datasource-funifier-docs/knowledge/modules/quiz.md +163 -0
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +58 -0
- package/datasource-funifier-docs/knowledge/modules/security.md +169 -0
- package/datasource-funifier-docs/knowledge/modules/staging.md +28 -0
- package/datasource-funifier-docs/knowledge/modules/static-repo.md +41 -0
- package/datasource-funifier-docs/knowledge/modules/story.md +42 -0
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +180 -0
- package/datasource-funifier-docs/knowledge/modules/swap.md +132 -0
- package/datasource-funifier-docs/knowledge/modules/team.md +75 -0
- package/datasource-funifier-docs/knowledge/modules/trigger.md +189 -0
- package/datasource-funifier-docs/knowledge/modules/upload.md +155 -0
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +99 -0
- package/datasource-funifier-docs/knowledge/modules/webhook.md +41 -0
- package/datasource-funifier-docs/knowledge/modules/websocket.md +41 -0
- package/datasource-funifier-docs/knowledge/modules/widget.md +42 -0
- package/datasource-funifier-docs/process-gtm-saas.md +143 -0
- package/datasource-funifier-docs/process-instagram.md +88 -0
- package/datasource-funifier-docs/process.md +1826 -0
- package/datasource-funifier-docs/readme.md +132 -0
- package/dist/cli/config-writers.d.ts +15 -0
- package/dist/cli/config-writers.d.ts.map +1 -0
- package/dist/cli/config-writers.js +55 -0
- package/dist/cli/config-writers.js.map +1 -0
- package/dist/cli/config-writers.test.d.ts +2 -0
- package/dist/cli/config-writers.test.d.ts.map +1 -0
- package/dist/cli/config-writers.test.js +55 -0
- package/dist/cli/config-writers.test.js.map +1 -0
- package/dist/cli/copy.d.ts +6 -0
- package/dist/cli/copy.d.ts.map +1 -0
- package/dist/cli/copy.js +63 -0
- package/dist/cli/copy.js.map +1 -0
- package/dist/cli/copy.test.d.ts +2 -0
- package/dist/cli/copy.test.d.ts.map +1 -0
- package/dist/cli/copy.test.js +94 -0
- package/dist/cli/copy.test.js.map +1 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +167 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/paths.d.ts +7 -0
- package/dist/cli/paths.d.ts.map +1 -0
- package/dist/cli/paths.js +62 -0
- package/dist/cli/paths.js.map +1 -0
- package/dist/cli/paths.test.d.ts +2 -0
- package/dist/cli/paths.test.d.ts.map +1 -0
- package/dist/cli/paths.test.js +50 -0
- package/dist/cli/paths.test.js.map +1 -0
- package/dist/cli/platforms.d.ts +22 -0
- package/dist/cli/platforms.d.ts.map +1 -0
- package/dist/cli/platforms.js +50 -0
- package/dist/cli/platforms.js.map +1 -0
- package/dist/cli/prompts.d.ts +7 -0
- package/dist/cli/prompts.d.ts.map +1 -0
- package/dist/cli/prompts.js +49 -0
- package/dist/cli/prompts.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/bundle.js +94 -49
- package/dist/mcp/index.js +18 -1
- package/dist/mcp/index.js.map +1 -1
- package/package.json +4 -2
- package/skills/funifier-create-action/SKILL.md +86 -86
- package/skills/funifier-create-aggregate/SKILL.md +39 -0
- package/skills/funifier-create-challenge/SKILL.md +87 -87
- package/skills/funifier-create-custom-page/SKILL.md +39 -0
- package/skills/funifier-create-leaderboard/SKILL.md +87 -87
- package/skills/funifier-create-level/SKILL.md +86 -86
- package/skills/funifier-create-point/SKILL.md +86 -86
- package/skills/funifier-create-quiz/SKILL.md +86 -86
- package/skills/funifier-create-scheduler/SKILL.md +39 -0
- package/skills/funifier-create-trigger/SKILL.md +39 -0
- package/skills/funifier-create-virtual-good/SKILL.md +86 -86
- package/skills/funifier-debug/SKILL.md +90 -90
- package/skills/funifier-help/SKILL.md +85 -85
- package/skills/funifier-implement-frontend/SKILL.md +89 -89
- package/skills/funifier-index/SKILL.md +50 -50
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
# Patterns (Design Patterns)
|
|
2
|
+
|
|
3
|
+
Padrões de implementação da Funifier, criados com base na experiência em projetos reais.
|
|
4
|
+
|
|
5
|
+
## Quando usar
|
|
6
|
+
|
|
7
|
+
- Ao implementar funcionalidades comuns em frontends Funifier
|
|
8
|
+
- Como referência de boas práticas de segurança e arquitetura
|
|
9
|
+
- Para garantir consistência entre projetos
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Signup Pattern
|
|
14
|
+
|
|
15
|
+
**Problema:** Implementar cadastro de jogador em área pública (frontend) que acessa diretamente a API da Funifier, sem expor o endpoint `/v3/player` para escrita direta.
|
|
16
|
+
|
|
17
|
+
**Solução:** Usar um objeto customizado `signup__c` como intermediário, com uma trigger `before_update` que valida os dados, cria o jogador com senha criptografada, e retorna o resultado para o frontend.
|
|
18
|
+
|
|
19
|
+
### Arquitetura
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Frontend (público) → PUT /v3/database/signup__c → Trigger before_update → Cria Player
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> ⚠️ **DEVE usar PUT**, não POST! A trigger `before_update` só é disparada pelo método PUT. POST dispara `before_create`.
|
|
26
|
+
|
|
27
|
+
### Vantagens
|
|
28
|
+
|
|
29
|
+
- O token público só tem permissão de escrita em `signup__c` — não acessa `/v3/player`
|
|
30
|
+
- A senha é criptografada no servidor via BCrypt (nunca armazenada em texto plano)
|
|
31
|
+
- Validação server-side (campos obrigatórios, duplicidade de username)
|
|
32
|
+
- Dados sensíveis (password, email) são removidos do registro `signup__c` após processamento
|
|
33
|
+
- Permite adicionar lógica extra (e-mail de boas-vindas, atribuição de equipe, etc.)
|
|
34
|
+
|
|
35
|
+
### Configuração
|
|
36
|
+
|
|
37
|
+
#### 1. Criar Role "public"
|
|
38
|
+
|
|
39
|
+
Em **Studio > Security > Roles**, criar:
|
|
40
|
+
|
|
41
|
+
| Campo | Valor |
|
|
42
|
+
|-------|-------|
|
|
43
|
+
| Role | `public` |
|
|
44
|
+
| Scope | `write_database_signup__c` |
|
|
45
|
+
| Timeout | `1d` |
|
|
46
|
+
|
|
47
|
+
#### 2. Gerar Token Basic
|
|
48
|
+
|
|
49
|
+
O token Basic é gerado a partir da API Key da gamificação, sem usuário/senha:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
1. Concatenar: ApiKey + ":"
|
|
53
|
+
2. Codificar em Base64
|
|
54
|
+
3. Prefixar com "Basic "
|
|
55
|
+
4. Usar no header: Authorization: Basic <token>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Exemplo (JavaScript):**
|
|
59
|
+
```javascript
|
|
60
|
+
var BASIC_TOKEN = 'Basic ' + btoa(API_KEY + ':');
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Este token terá apenas as permissões da role `public` — escrita em `signup__c` e nada mais. Seguro para usar em código público.
|
|
64
|
+
|
|
65
|
+
#### 3. Habilitar Password Required
|
|
66
|
+
|
|
67
|
+
Em **Studio > Security**, habilitar a opção **Password Required**. Isso garante que o login exija senha, já que agora as senhas são gerenciadas de forma segura via trigger.
|
|
68
|
+
|
|
69
|
+
#### 4. Criar Trigger
|
|
70
|
+
|
|
71
|
+
Em **Studio > Trigger**, criar:
|
|
72
|
+
|
|
73
|
+
| Campo | Valor |
|
|
74
|
+
|-------|-------|
|
|
75
|
+
| Name | Signup Handler |
|
|
76
|
+
| Entity | `signup__c` |
|
|
77
|
+
| Event | `before_update` |
|
|
78
|
+
|
|
79
|
+
**Script:**
|
|
80
|
+
```java
|
|
81
|
+
void trigger(event, entity, player, database){
|
|
82
|
+
entity.status = "Unauthorized";
|
|
83
|
+
if(entity.password == null || entity.password.trim().length() == 0) {
|
|
84
|
+
entity.message = "Senha deve ser informada para se cadastrar!";
|
|
85
|
+
}
|
|
86
|
+
else if(entity.email == null || entity.email.trim().length() == 0) {
|
|
87
|
+
entity.message = "Email deve ser informado para se cadastrar!";
|
|
88
|
+
}
|
|
89
|
+
else if(entity.name == null || entity.name.trim().length() == 0) {
|
|
90
|
+
entity.message = "Nome deve ser informado para se cadastrar!";
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
Player current = manager.getPlayerManager().findById(entity._id);
|
|
94
|
+
if(current != null) {
|
|
95
|
+
entity.message = "Este usuário já está cadastrado!";
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// Criptografa senha e salva jogador
|
|
99
|
+
Player p = JsonUtil.fromJson(JsonUtil.toJson(entity), Player.class);
|
|
100
|
+
p.password = com.funifier.engine.util.BCrypt.hashpw(
|
|
101
|
+
entity.password, com.funifier.engine.util.BCrypt.gensalt()
|
|
102
|
+
);
|
|
103
|
+
p.setCreated(new Date());
|
|
104
|
+
manager.getPlayerManager().insert(p);
|
|
105
|
+
entity.message = "Usuário registrado com sucesso!";
|
|
106
|
+
entity.status = "OK";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Remove campos sensíveis antes de salvar no signup__c
|
|
110
|
+
entity.remove("password");
|
|
111
|
+
entity.remove("email");
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Uso no Frontend
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
// Token público — seguro para código client-side
|
|
119
|
+
var BASIC_TOKEN = 'Basic ' + btoa(API_KEY + ':');
|
|
120
|
+
|
|
121
|
+
// Cadastro via signup__c — DEVE usar PUT (não POST!)
|
|
122
|
+
// PUT dispara a trigger before_update; POST dispara before_create
|
|
123
|
+
$http.put(API + '/v3/database/signup__c', {
|
|
124
|
+
_id: username,
|
|
125
|
+
name: fullName,
|
|
126
|
+
email: email,
|
|
127
|
+
password: password
|
|
128
|
+
}, {
|
|
129
|
+
headers: { 'Authorization': BASIC_TOKEN }
|
|
130
|
+
}).then(function(response) {
|
|
131
|
+
// Verificar response.data.status === "OK"
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Extensões Comuns
|
|
136
|
+
|
|
137
|
+
- **E-mail de boas-vindas:** Adicionar envio de email na trigger após `insert`
|
|
138
|
+
- **Termos de uso:** Adicionar campo `terms` (boolean) e validar na trigger
|
|
139
|
+
- **Atribuição de equipe:** Adicionar lógica de team assignment na trigger
|
|
140
|
+
- **Campos extras:** Qualquer campo adicional pode ser passado e tratado na trigger
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Email Template Pattern
|
|
145
|
+
|
|
146
|
+
**Problema:** Enviar emails automáticos a partir de triggers, com conteúdo editável pelo administrador sem alterar código.
|
|
147
|
+
|
|
148
|
+
**Solução:** Criar coleção `email_template__c` com templates HTML usando variáveis Mustache, e usar `MustacheUtils.parse()` + `MailContext` nas triggers para montar e enviar.
|
|
149
|
+
|
|
150
|
+
### Arquitetura
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
email_template__c (templates editáveis) → Trigger (busca template + parse Mustache) → SMTP (envia)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 1. Estrutura do Template (`email_template__c`)
|
|
157
|
+
|
|
158
|
+
Cada template é um documento na coleção `email_template__c`:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"_id": "signup_email_confirm",
|
|
163
|
+
"fromName": "Funifier Team",
|
|
164
|
+
"fromEmail": "no-reply@funifier.com",
|
|
165
|
+
"subject": "Funifier : Email Confirmation",
|
|
166
|
+
"content": "Hi {{user.name}}<br/><br/>Thank you for signing up. Confirm your email:<br/><br/><a href=\"{{link}}\">Confirm your Email</a><br/><br/>The Funifier Team"
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Variáveis Mustache:** Usar `{{campo}}` no subject e content. Podem ser campos da entity, do player, ou valores calculados.
|
|
171
|
+
|
|
172
|
+
### 2. Uso na Trigger
|
|
173
|
+
|
|
174
|
+
```java
|
|
175
|
+
// 1. Buscar template
|
|
176
|
+
Object templateObj = database.findOne("email_template__c", "{_id: 'signup_welcome'}");
|
|
177
|
+
org.bson.Document template = (org.bson.Document) templateObj;
|
|
178
|
+
|
|
179
|
+
// 2. Montar valores para substituição Mustache
|
|
180
|
+
// Opção A: usar a própria entity (se já tem os campos necessários)
|
|
181
|
+
// Opção B: montar um mapa de valores calculados
|
|
182
|
+
Map values = new HashMap();
|
|
183
|
+
values.put("name", entity.name);
|
|
184
|
+
values.put("email", entity.email);
|
|
185
|
+
values.put("link", "https://app.exemplo.com/confirm/" + entity._id);
|
|
186
|
+
|
|
187
|
+
// 3. Parse Mustache — substitui {{variavel}} pelos valores
|
|
188
|
+
String subject = com.funifier.engine.util.MustacheUtils.parse(template.getString("subject"), values);
|
|
189
|
+
String content = com.funifier.engine.util.MustacheUtils.parse(template.getString("content"), values);
|
|
190
|
+
|
|
191
|
+
// 4. Enviar email
|
|
192
|
+
Email email = EmailBuilder.startingBlank()
|
|
193
|
+
.from(template.getString("fromName"), template.getString("fromEmail"))
|
|
194
|
+
.to(entity.name, entity.email)
|
|
195
|
+
.withSubject(subject)
|
|
196
|
+
.withHTMLText(content)
|
|
197
|
+
.buildEmail();
|
|
198
|
+
|
|
199
|
+
com.funifier.engine.mail.MailContext ctx = com.funifier.controller.Configuration.getCurrentConfiguration().getMailContext();
|
|
200
|
+
MailerBuilder.withSMTPServer(ctx.hostName, ctx.smtpPort, ctx.authUser, ctx.authPassword)
|
|
201
|
+
.buildMailer()
|
|
202
|
+
.sendMail(email);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 3. Página Customizada no Studio (opcional)
|
|
206
|
+
|
|
207
|
+
Criar uma página customizada em **Studio > Pages** para que o administrador possa editar os templates de email visualmente, sem acessar a API ou o banco diretamente.
|
|
208
|
+
|
|
209
|
+
### Vantagens
|
|
210
|
+
|
|
211
|
+
- **Separação de responsabilidades:** conteúdo do email separado da lógica da trigger
|
|
212
|
+
- **Administrável:** admin edita templates sem alterar código
|
|
213
|
+
- **Reutilizável:** mesmo template pode ser usado em múltiplas triggers
|
|
214
|
+
- **Flexível:** variáveis Mustache permitem personalização dinâmica
|
|
215
|
+
- **Auditável:** templates ficam versionados na coleção `email_template__c`
|
|
216
|
+
|
|
217
|
+
### Classes Disponíveis no Contexto
|
|
218
|
+
|
|
219
|
+
| Classe | Uso |
|
|
220
|
+
|--------|-----|
|
|
221
|
+
| `EmailBuilder` | Construir email (Simple Java Mail) |
|
|
222
|
+
| `MailerBuilder` | Construir mailer SMTP (Simple Java Mail) |
|
|
223
|
+
| `com.funifier.engine.mail.MailContext` | Config SMTP da gamificação |
|
|
224
|
+
| `com.funifier.controller.Configuration` | Acesso à configuração atual |
|
|
225
|
+
| `com.funifier.engine.util.MustacheUtils` | Parse de templates Mustache |
|
|
226
|
+
|
|
227
|
+
### Propriedades do MailContext
|
|
228
|
+
|
|
229
|
+
- `ctx.hostName` — servidor SMTP
|
|
230
|
+
- `ctx.smtpPort` — porta SMTP
|
|
231
|
+
- `ctx.authUser` — usuário de autenticação
|
|
232
|
+
- `ctx.authPassword` — senha de autenticação
|
|
233
|
+
|
|
234
|
+
> ⚠️ O envio de email deve acontecer **antes** do `entity.remove("email")` no Signup Pattern, para ter acesso ao endereço do destinatário.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### ⚠️ Notas Importantes
|
|
239
|
+
|
|
240
|
+
- O token Basic com apenas a API Key recebe as permissões da role `public`
|
|
241
|
+
- Nunca expor o endpoint `/v3/player` para escrita em área pública
|
|
242
|
+
- A trigger roda no servidor — validações client-side são complementares, não substitutas
|
|
243
|
+
- O registro em `signup__c` serve como log de tentativas de cadastro (sem dados sensíveis)
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Payment Gateway Pattern (Asaas + Public Endpoints)
|
|
248
|
+
|
|
249
|
+
**Problema:** Implementar pagamento recorrente (assinatura) em um app Funifier sem backend proprio, usando apenas o frontend e a infraestrutura Funifier.
|
|
250
|
+
|
|
251
|
+
**Solução:** Usar Public Endpoints do Funifier como proxy server-side para a API do gateway de pagamento (Asaas), evitando CORS e protegendo a API key.
|
|
252
|
+
|
|
253
|
+
### Arquitetura
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
Frontend (SPA)
|
|
257
|
+
|
|
|
258
|
+
v
|
|
259
|
+
Funifier Public Endpoints (server-side proxy)
|
|
260
|
+
|
|
|
261
|
+
v
|
|
262
|
+
Asaas API (gateway de pagamento BR)
|
|
263
|
+
|
|
|
264
|
+
v (webhook async)
|
|
265
|
+
Funifier Public Endpoint (webhook receiver)
|
|
266
|
+
|
|
|
267
|
+
v
|
|
268
|
+
Player.extra.plan (atualiza plano do jogador via Jongo)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Endpoints Necessarios
|
|
272
|
+
|
|
273
|
+
| Endpoint | Metodo | Funcao |
|
|
274
|
+
|----------|--------|--------|
|
|
275
|
+
| `validate_coupon` | POST | Valida cupom de desconto em `coupon__c` |
|
|
276
|
+
| `create_subscription` | POST | Cria customer + subscription no Asaas, retorna URL de pagamento |
|
|
277
|
+
| `asaas_webhook` | POST | Recebe eventos de pagamento do Asaas, atualiza plano do jogador |
|
|
278
|
+
| `manage_subscription` | POST | Cancel, downgrade, reactivate, status — gerencia assinatura existente |
|
|
279
|
+
|
|
280
|
+
### Fluxo de Assinatura (Novo Usuario)
|
|
281
|
+
|
|
282
|
+
1. Usuario completa onboarding e escolhe plano (Standard/Premium)
|
|
283
|
+
2. Frontend chama `create_subscription` com: `playerId`, `planType`, `couponCode` (opcional)
|
|
284
|
+
3. Endpoint cria customer no Asaas (ou reutiliza existente via `externalReference`)
|
|
285
|
+
4. Endpoint cria subscription no Asaas com:
|
|
286
|
+
- `billingType: "UNDEFINED"` (cliente escolhe Pix/cartao/boleto no checkout)
|
|
287
|
+
- `nextDueDate` com trial (ex: `DateUtil.fromKeyword("+7d")`)
|
|
288
|
+
- `externalReference: playerId` (vincula ao jogador Funifier)
|
|
289
|
+
5. Endpoint retorna `invoiceUrl` (checkout hospedado do Asaas)
|
|
290
|
+
6. Frontend redireciona usuario para `invoiceUrl`
|
|
291
|
+
7. Usuario paga no checkout Asaas
|
|
292
|
+
8. Asaas envia webhook `PAYMENT_CONFIRMED` para `asaas_webhook`
|
|
293
|
+
9. Webhook atualiza `player.extra.plan` via Jongo
|
|
294
|
+
|
|
295
|
+
### Sistema de Cupons
|
|
296
|
+
|
|
297
|
+
Colecao `coupon__c` com estrutura:
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"_id": "FITEVOLVE20",
|
|
302
|
+
"type": "PERCENTAGE",
|
|
303
|
+
"value": 20,
|
|
304
|
+
"maxUses": 100,
|
|
305
|
+
"usedCount": 0,
|
|
306
|
+
"active": true
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Tipos: `PERCENTAGE` (desconto %) e `FIXED` (desconto em R$).
|
|
311
|
+
|
|
312
|
+
O endpoint `validate_coupon` valida e retorna o desconto. O endpoint `create_subscription` aplica o desconto no `value` da subscription.
|
|
313
|
+
|
|
314
|
+
### Campos do Player (`extra.plan`)
|
|
315
|
+
|
|
316
|
+
```json
|
|
317
|
+
{
|
|
318
|
+
"type": "premium",
|
|
319
|
+
"plan_status": "active",
|
|
320
|
+
"asaas_customer_id": "cus_xxx",
|
|
321
|
+
"asaas_subscription_id": "sub_xxx",
|
|
322
|
+
"plan_end_date": null,
|
|
323
|
+
"pending_plan": null,
|
|
324
|
+
"plan_downgrade_date": null,
|
|
325
|
+
"changesUsed": 0
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
| Campo | Descricao |
|
|
330
|
+
|-------|-----------|
|
|
331
|
+
| `type` | `standard` ou `premium` |
|
|
332
|
+
| `plan_status` | `active`, `canceled`, `pending_downgrade` |
|
|
333
|
+
| `asaas_customer_id` | ID do customer no Asaas |
|
|
334
|
+
| `asaas_subscription_id` | ID da subscription ativa no Asaas |
|
|
335
|
+
| `plan_end_date` | Data fim do acesso apos cancelamento |
|
|
336
|
+
| `pending_plan` | Plano futuro em caso de downgrade |
|
|
337
|
+
| `plan_downgrade_date` | Data em que o downgrade sera efetivado |
|
|
338
|
+
|
|
339
|
+
### Gestao de Assinatura
|
|
340
|
+
|
|
341
|
+
| Acao | Logica |
|
|
342
|
+
|------|--------|
|
|
343
|
+
| **Cancel** | Cancela subscription no Asaas. Seta `plan_status: "canceled"` e `plan_end_date` = fim do ciclo atual. Usuario mantem acesso ate `plan_end_date`. |
|
|
344
|
+
| **Downgrade** | Cancela Premium no Asaas, cria nova Standard com `nextDueDate` = fim do ciclo Premium. Seta `plan_status: "pending_downgrade"`, `pending_plan: "standard"`. |
|
|
345
|
+
| **Reactivate** | Cria nova subscription no Asaas (com trial). Reseta `plan_status: "active"`. Suporta cupom. |
|
|
346
|
+
| **Status** | Consulta subscription no Asaas e retorna status, proximo vencimento, valor. |
|
|
347
|
+
|
|
348
|
+
### Webhook: Eventos Importantes
|
|
349
|
+
|
|
350
|
+
| Evento Asaas | Acao |
|
|
351
|
+
|-------------|------|
|
|
352
|
+
| `PAYMENT_CONFIRMED` / `PAYMENT_RECEIVED` | Ativa plano. Se `pending_downgrade`, efetiva downgrade. |
|
|
353
|
+
| `PAYMENT_OVERDUE` | Marca como inadimplente (opcional). |
|
|
354
|
+
| `PAYMENT_DELETED` / `PAYMENT_REFUNDED` | Cancela plano. |
|
|
355
|
+
|
|
356
|
+
### Seguranca do Webhook
|
|
357
|
+
|
|
358
|
+
- Validar `authToken` no header/body (configurado ao criar webhook no Asaas)
|
|
359
|
+
- Registrar dominio do app nas configuracoes do Asaas (para callback de checkout)
|
|
360
|
+
- Webhook criado via API: `POST /v3/webhooks` no Asaas
|
|
361
|
+
|
|
362
|
+
### Padroes Groovy para Scripts de Pagamento
|
|
363
|
+
|
|
364
|
+
```groovy
|
|
365
|
+
// 1. Escapar $ (MongoDB operators e API keys)
|
|
366
|
+
def d = String.valueOf((char)0x24) // = "$"
|
|
367
|
+
def setCmd = '{"' + d + 'set": {"extra.plan.type": "premium"}}'
|
|
368
|
+
|
|
369
|
+
// 2. Atualizar player via Jongo (PlayerManager NAO tem update)
|
|
370
|
+
manager.getJongoConnection().getCollection("player")
|
|
371
|
+
.update("{_id: #}", playerId)
|
|
372
|
+
.with(setCmd)
|
|
373
|
+
|
|
374
|
+
// 3. Chamar API externa (Asaas) via Unirest
|
|
375
|
+
def response = Unirest.post("https://api.asaas.com/v3/subscriptions")
|
|
376
|
+
.header("Content-Type", "application/json")
|
|
377
|
+
.header("access_token", asaasApiKey)
|
|
378
|
+
.body(JsonUtil.toJson(bodyMap))
|
|
379
|
+
.asString()
|
|
380
|
+
def result = new groovy.json.JsonSlurper().parseText(response.getBody())
|
|
381
|
+
|
|
382
|
+
// 4. Datas com DateUtil
|
|
383
|
+
def trialEnd = DateUtil.fromKeyword("+7d") // 7 dias no futuro
|
|
384
|
+
|
|
385
|
+
// 5. Ler payload do request
|
|
386
|
+
def slurper = new groovy.json.JsonSlurper()
|
|
387
|
+
def body = slurper.parseText(JsonUtil.toJson(payload))
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Licoes Aprendidas
|
|
391
|
+
|
|
392
|
+
1. **Asaas `billingType: UNDEFINED`** permite o cliente escolher metodo de pagamento no checkout
|
|
393
|
+
2. **Asaas `billingType: CREDIT_CARD`** e obrigatorio para cobranças recorrentes automaticas; PIX requer `chargeType: DETACHED`
|
|
394
|
+
3. **`externalReference`** no Asaas e essencial para vincular subscription ao playerId
|
|
395
|
+
4. **Dominio deve ser registrado no Asaas** para callbacks de checkout funcionarem
|
|
396
|
+
5. **Scripts Groovy no Funifier tem timeout de 5s** — chamadas HTTP devem ser rapidas
|
|
397
|
+
6. **Jongo com dotted notation** (`extra.plan.type`) funciona corretamente em `$set`
|
|
398
|
+
7. **Nunca usar `import`** nos scripts — ver `public.md` → Script Runtime Environment
|
|
399
|
+
8. **NÃO usar `groovy.json.JsonSlurper`** para parsear responses — usar `JsonUtil.fromJsonToMap()` (evita `LazyMap` → `ClassCastException`)
|
|
400
|
+
9. **Player.extra** é campo público — acessar direto, salvar com `insert()` (upsert)
|
|
401
|
+
10. **`instanceof` bloqueado** pelo SecureAST — usar alternativas
|
|
402
|
+
11. **Public Endpoints podem ser atualizados via API** — `POST /v3/public` com `Basic` auth (apiKey)
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## Database Strict Mode Pattern (BSON Types)
|
|
407
|
+
|
|
408
|
+
**Problema:** O MongoDB armazena dados em formato BSON com tipos especiais (`$date`, `$oid`, `$numberLong`, etc.). O endpoint `/v3/database` por padrão retorna os dados em JSON puro, onde campos tipados perdem a informação de tipo. Se você lê dados sem tipagem e salva de volta, o MongoDB muda o tipo do campo (ex: `Date` vira `String` ou `Number`), quebrando consultas por data e ordenação.
|
|
409
|
+
|
|
410
|
+
**Solução:** Sempre usar `strict=true` como query parameter ao consultar via `/v3/database`. Isso retorna os dados no formato BSON estendido, preservando os tipos.
|
|
411
|
+
|
|
412
|
+
### Exemplo sem strict (JSON puro — PERIGOSO para escrita)
|
|
413
|
+
|
|
414
|
+
```
|
|
415
|
+
GET /v3/database/player/ricardo
|
|
416
|
+
```
|
|
417
|
+
```json
|
|
418
|
+
{
|
|
419
|
+
"_id": "ricardo",
|
|
420
|
+
"name": "Ricardo",
|
|
421
|
+
"created": 1772145212606
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
O campo `created` aparece como número (timestamp). Se salvar isso de volta, o MongoDB armazena como `Number`, não como `Date`.
|
|
425
|
+
|
|
426
|
+
### Exemplo com strict (BSON estendido — CORRETO)
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
GET /v3/database/player/ricardo?strict=true
|
|
430
|
+
```
|
|
431
|
+
```json
|
|
432
|
+
{
|
|
433
|
+
"_id": "ricardo",
|
|
434
|
+
"name": "Ricardo",
|
|
435
|
+
"created": { "$date": "2026-02-26T22:33:32.606Z" }
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
O campo `created` vem com o tipo `$date`. Ao salvar de volta, o MongoDB preserva como `Date`.
|
|
439
|
+
|
|
440
|
+
### Regras de Uso
|
|
441
|
+
|
|
442
|
+
1. **Sempre usar `strict=true` em GETs do `/v3/database`** — leitura de coleções customizadas (`__c`) e coleções nativas
|
|
443
|
+
2. **Ao acessar campos tipados, usar o formato BSON:** `record.created.$date` em vez de `record.created`
|
|
444
|
+
3. **Ao salvar (PUT/POST), enviar os dados no formato BSON** para preservar tipos:
|
|
445
|
+
```json
|
|
446
|
+
{ "created": { "$date": "2026-02-27T10:00:00.000Z" } }
|
|
447
|
+
```
|
|
448
|
+
4. **Ao criar novos registros com datas**, formatar assim:
|
|
449
|
+
```javascript
|
|
450
|
+
{ created: { $date: new Date().toISOString() } }
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Tipos BSON Comuns
|
|
454
|
+
|
|
455
|
+
| Tipo | Formato JSON strict | Exemplo |
|
|
456
|
+
|------|-------------------|---------|
|
|
457
|
+
| Date | `{ "$date": "ISO-8601" }` | `{ "$date": "2026-02-27T10:00:00.000Z" }` |
|
|
458
|
+
| ObjectId | `{ "$oid": "hex" }` | `{ "$oid": "69a16149434ba01017676d07" }` |
|
|
459
|
+
| Long | `{ "$numberLong": "num" }` | `{ "$numberLong": "1234567890" }` |
|
|
460
|
+
|
|
461
|
+
### Helper JavaScript para Frontend
|
|
462
|
+
|
|
463
|
+
```javascript
|
|
464
|
+
// Criar campo date no formato BSON
|
|
465
|
+
function bsonDate(date) {
|
|
466
|
+
return { $date: (date || new Date()).toISOString() };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Ler campo date do formato BSON
|
|
470
|
+
function readDate(field) {
|
|
471
|
+
if (!field) return null;
|
|
472
|
+
if (field.$date) return new Date(field.$date);
|
|
473
|
+
if (typeof field === 'string') return new Date(field);
|
|
474
|
+
if (typeof field === 'number') return new Date(field);
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### ⚠️ Impacto de NÃO usar strict
|
|
480
|
+
|
|
481
|
+
- Campos `Date` viram `String` ou `Number` no MongoDB
|
|
482
|
+
- Consultas com `$gt`, `$lt` por data param de funcionar
|
|
483
|
+
- Ordenação por data (`_sort=-created`) retorna resultados incorretos
|
|
484
|
+
- Dados perdem integridade progressivamente a cada leitura+escrita
|
|
485
|
+
|
|
486
|
+
### Onde se aplica
|
|
487
|
+
|
|
488
|
+
- **Apenas** no endpoint `/v3/database` (coleções customizadas `__c` e nativas via database)
|
|
489
|
+
- **NÃO** se aplica a endpoints específicos como `/v3/player`, `/v3/action`, `/v3/challenge` etc. (estes já têm tipagem própria)
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## App Version Pattern
|
|
494
|
+
|
|
495
|
+
**Problema:** Equipe de desenvolvimento e testes precisa saber se está vendo a versão correta do app, evitando trabalhar sobre versão em cache do browser.
|
|
496
|
+
|
|
497
|
+
**Solução:** Exibir a versão no rodapé de todas as páginas e gerenciá-la de forma consistente.
|
|
498
|
+
|
|
499
|
+
### Convenção de Versionamento
|
|
500
|
+
|
|
501
|
+
```
|
|
502
|
+
MAJOR.MINOR
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
- **MINOR** — Incrementa +1 a cada alteração pequena (bug fix, ajuste visual, texto). Ex: `1.0` → `1.1` → `1.2` → ... → `1.120`
|
|
506
|
+
- **MAJOR** — Incrementa +1 e zera o MINOR em mudanças grandes (refatoração, nova feature significativa, redesign). Ex: `1.120` → `2.0`
|
|
507
|
+
|
|
508
|
+
### Implementação
|
|
509
|
+
|
|
510
|
+
#### 1. Definir versão no `config.js`
|
|
511
|
+
|
|
512
|
+
```javascript
|
|
513
|
+
var CONFIG = {
|
|
514
|
+
// ... outras configs
|
|
515
|
+
VERSION: '1.0'
|
|
516
|
+
};
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
#### 2. Expor no `$rootScope` (AngularJS)
|
|
520
|
+
|
|
521
|
+
```javascript
|
|
522
|
+
app.run(function($rootScope) {
|
|
523
|
+
$rootScope.appVersion = CONFIG.VERSION;
|
|
524
|
+
});
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
#### 3. Rodapé no `index.html`
|
|
528
|
+
|
|
529
|
+
```html
|
|
530
|
+
<div class="version-footer">version {{appVersion}}</div>
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
#### 4. CSS
|
|
534
|
+
|
|
535
|
+
```css
|
|
536
|
+
.version-footer {
|
|
537
|
+
text-align: center;
|
|
538
|
+
font-size: 11px;
|
|
539
|
+
color: rgba(255,255,255,0.2);
|
|
540
|
+
padding: 8px 0 calc(env(safe-area-inset-bottom, 0px) + 80px);
|
|
541
|
+
letter-spacing: 0.5px;
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### Regras
|
|
546
|
+
|
|
547
|
+
1. **Sempre incrementar a versão** ao fazer deploy — nunca fazer deploy sem mudar a versão
|
|
548
|
+
2. **Checar a versão no rodapé** após deploy para confirmar que o cache foi atualizado
|
|
549
|
+
3. Se a versão exibida não bate com a esperada → cache desatualizado → forçar refresh (Ctrl+Shift+R)
|
|
550
|
+
4. Em caso de dúvida sobre qual versão está rodando, basta olhar o rodapé
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
## Campo `timeout` em Scripts (Public Endpoint, Trigger, Scheduler)
|
|
554
|
+
|
|
555
|
+
O timeout de execucao dos scripts e controlado por um executor Java (`Future.get(timeout, TimeUnit.SECONDS)`). O padrao e **10 segundos**. Para scripts que precisam de mais tempo (chamadas HTTP externas, BCrypt, etc.), o timeout pode ser customizado via API:
|
|
556
|
+
|
|
557
|
+
```bash
|
|
558
|
+
# Public Endpoint
|
|
559
|
+
POST /v3/public → {"_id": "slug", "timeout": 30}
|
|
560
|
+
|
|
561
|
+
# Trigger
|
|
562
|
+
POST /v3/trigger → {"_id": "trigger_id", "timeout": 30}
|
|
563
|
+
|
|
564
|
+
# Scheduler
|
|
565
|
+
POST /v3/scheduler → {"_id": "scheduler_id", "timeout": 30}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
- Valor em **segundos** (30 = 30s)
|
|
569
|
+
- Campo `timeout` **nao aparece no formulario do Studio** — so via API
|
|
570
|
+
- `null` = usa padrao de 10 segundos
|
|
571
|
+
- Fonte: classe Java `PublicEndpoint` (campo `public Long timeout`) + metodo `executor()` que usa `TimeUnit.SECONDS`
|
|
572
|
+
- Nota: `@TimedInterrupt(5s)` no wrapper Groovy e uma segunda camada de protecao independente
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
## CRITICO: POST parcial em /v3/public apaga o script!
|
|
576
|
+
|
|
577
|
+
O endpoint `POST /v3/public` faz **upsert completo** — se voce enviar apenas `{"_id": "slug", "timeout": 30}`, o campo `script` sera apagado (setado como null/vazio) porque nao foi incluido no payload.
|
|
578
|
+
|
|
579
|
+
**Regra:** Sempre inclua TODOS os campos importantes ao atualizar um endpoint via API:
|
|
580
|
+
```json
|
|
581
|
+
{
|
|
582
|
+
"_id": "slug",
|
|
583
|
+
"active": true,
|
|
584
|
+
"method": "POST",
|
|
585
|
+
"timeout": 30,
|
|
586
|
+
"script": "public Object handle(Object payload) { ... }"
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**Nunca faca update parcial** como `{"_id": "slug", "timeout": 30}` — isso apaga o script!
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## Public Endpoint Sandbox — Classes Bloqueadas vs Permitidas
|
|
595
|
+
|
|
596
|
+
Resultado de testes extensivos no Orvya (março 2026). O sandbox Groovy do Funifier tem restrições severas via `SecureASTCustomizer`.
|
|
597
|
+
|
|
598
|
+
### BLOQUEADO (não usar)
|
|
599
|
+
|
|
600
|
+
| Item | Erro |
|
|
601
|
+
|------|------|
|
|
602
|
+
| `com.*` fully qualified names | `MissingPropertyException: No such property: com` |
|
|
603
|
+
| `org.*` fully qualified names | `MissingPropertyException: No such property: org` |
|
|
604
|
+
| `BCrypt` (unqualified) | Não disponível no sandbox de Public Endpoints |
|
|
605
|
+
| `Class.forName()` | `ClassNotFoundException` |
|
|
606
|
+
| `groovy.json.JsonSlurper` (para response) | `ClassCastException: [B incompatible with [C` |
|
|
607
|
+
| `groovy.json.JsonOutput` | Mesma `ClassCastException` |
|
|
608
|
+
| Regex `=~` operator | Bloqueado pelo `SecureASTCustomizer` |
|
|
609
|
+
| `for` loop + `return` na sequência | Parser error (`expecting '}', found 'return'`) |
|
|
610
|
+
| `System.currentTimeMillis()` | Bloqueado — usar `new Date().getTime()` |
|
|
611
|
+
| `instanceof` | Bloqueado — usar `getClass().getName()` ou try/catch |
|
|
612
|
+
|
|
613
|
+
### PERMITIDO (seguro para usar)
|
|
614
|
+
|
|
615
|
+
| Item | Notas |
|
|
616
|
+
|------|-------|
|
|
617
|
+
| `java.net.URL` / `openConnection()` | Para chamadas HTTP externas |
|
|
618
|
+
| `java.security.MessageDigest` (SHA-256) | Para hashing |
|
|
619
|
+
| `java.util.Base64` | Encode/decode |
|
|
620
|
+
| `java.net.URLEncoder` | URL encoding |
|
|
621
|
+
| `java.text.SimpleDateFormat` | Formatação de datas |
|
|
622
|
+
| `while` loops | Usar no lugar de `for` quando `return` segue |
|
|
623
|
+
| `String.split()`, `String.replace()`, `StringBuilder` | Manipulação de strings |
|
|
624
|
+
| `manager.getAuthenticationManager()` | Auth operations |
|
|
625
|
+
| `manager.getPlayerManager()` | Player CRUD |
|
|
626
|
+
| `new Player()` com `.id`, `.name`, `.email`, `.password`, `.extra` | Criação de player |
|
|
627
|
+
|
|
628
|
+
### Implicações para Desenvolvimento
|
|
629
|
+
|
|
630
|
+
- **Para HTTP em Public Endpoints:** usar `java.net.URL` (não Unirest — `com.mashape.*` é bloqueado)
|
|
631
|
+
- **Para JSON em Public Endpoints:** parsear manualmente com `while` loops e `String.split()`
|
|
632
|
+
- **Para hashing de senha:** delegar para `signup__c` PUT trigger (que tem acesso ao BCrypt)
|
|
633
|
+
- **Em Triggers e Schedulers:** `com.*` e `org.*` FUNCIONAM normalmente (Unirest, BCrypt, etc.)
|
|
634
|
+
|
|
635
|
+
> ⚠️ Essas restrições se aplicam APENAS a Public Endpoints. Triggers e Schedulers têm acesso completo às classes importadas no wrapper.
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## Google OAuth Login Pattern
|
|
640
|
+
|
|
641
|
+
**Problema:** Implementar login com Google em app Funifier sem backend próprio.
|
|
642
|
+
|
|
643
|
+
**Solução:** Google Sign-In (GSI) no frontend + Public Endpoint no Funifier para verificação do token e criação/login do player.
|
|
644
|
+
|
|
645
|
+
### Arquitetura
|
|
646
|
+
|
|
647
|
+
```
|
|
648
|
+
Frontend: Google GSI renderButton → User clica → Google retorna id_token
|
|
649
|
+
|
|
|
650
|
+
v
|
|
651
|
+
Funifier Public Endpoint "google_login"
|
|
652
|
+
|→ Verifica token via Google tokeninfo API (java.net.URL)
|
|
653
|
+
|→ Se player não existe: cria via signup__c PUT (para BCrypt hash)
|
|
654
|
+
|→ Gera auth token via POST /v3/auth/token (java.net.URL)
|
|
655
|
+
|→ Retorna token + player info ao frontend
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### Lições Importantes
|
|
659
|
+
|
|
660
|
+
1. **Usar `google.accounts.id.renderButton()`** direto no DOM — o `prompt()` (One Tap) falha em mobile
|
|
661
|
+
2. **Nunca usar `<a href="#">` com `ng-click` em AngularJS** — o `#` reseta o hash antes do ng-click, causando redirect. Usar `href=""`
|
|
662
|
+
3. **Google users recebem plano Standard** por padrão
|
|
663
|
+
4. **`$scope.$applyAsync`** em vez de `$scope.$apply` em callbacks async (evita "already in digest")
|
|
664
|
+
5. **Player properties em Groovy:** usar acesso direto (`newPlayer._id = email`) não setter methods
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## Cross-User Data Isolation Pattern
|
|
669
|
+
|
|
670
|
+
**Problema:** Em apps que usam localStorage para cache, trocar de usuário sem limpar cache causa vazamento de dados entre usuários.
|
|
671
|
+
|
|
672
|
+
**Solução:** Defesa em 3 camadas:
|
|
673
|
+
|
|
674
|
+
### Camada 1: Limpar dados no logout
|
|
675
|
+
```javascript
|
|
676
|
+
function clearUserData() {
|
|
677
|
+
// Limpar todas as chaves do app no localStorage
|
|
678
|
+
Object.keys(localStorage).forEach(function(key) {
|
|
679
|
+
if (key.startsWith('fitness_') || key.startsWith('water_')) {
|
|
680
|
+
localStorage.removeItem(key);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
// Limpar $rootScope
|
|
684
|
+
$rootScope.player = null;
|
|
685
|
+
$rootScope.profileData = null;
|
|
686
|
+
// ... etc
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Camada 2: Limpar dados no login (se usuário diferente)
|
|
691
|
+
```javascript
|
|
692
|
+
var lastUser = localStorage.getItem('fitness_user');
|
|
693
|
+
if (lastUser && lastUser !== currentUser) {
|
|
694
|
+
clearUserData();
|
|
695
|
+
}
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### Camada 3: DB é source of truth
|
|
699
|
+
- `loadFromDB()` sempre roda após login
|
|
700
|
+
- Se DB não tem dados para um campo, localStorage é LIMPO (não mantém valor antigo)
|
|
701
|
+
- Nunca usar localStorage como fallback quando DB retorna vazio
|
|
702
|
+
|
|
703
|
+
### Lição: Parâmetro correto de filtro é `q`, não `_filter`
|
|
704
|
+
|
|
705
|
+
O endpoint `/v3/database` **NÃO tem** parâmetro `_filter`. O parâmetro correto é `q` com sintaxe MongoDB:
|
|
706
|
+
|
|
707
|
+
```
|
|
708
|
+
GET /v3/database/body_checkin__c?q=userId:"ricardo@funifier.com"&strict=true
|
|
709
|
+
POST /v3/database/body_checkin__c/aggregate?q=userId:"ricardo@funifier.com"&strict=true
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
`_filter`, `_sort`, `_limit` são **silenciosamente ignorados** pelo backend, fazendo com que a query retorne todos os registros sem filtro.
|
|
713
|
+
|
|
714
|
+
Para queries com ordenação, usar o endpoint `aggregate` com pipeline `$sort`:
|
|
715
|
+
```javascript
|
|
716
|
+
$http({
|
|
717
|
+
method: 'POST',
|
|
718
|
+
url: API + '/v3/database/collection__c/aggregate?q=userId:"' + userId + '"&strict=true',
|
|
719
|
+
headers: { 'Authorization': 'Bearer ' + token, 'Range': 'items=0-19' },
|
|
720
|
+
data: [{ $sort: { created: -1 } }]
|
|
721
|
+
});
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
**Defense-in-depth:** Ainda é boa prática verificar userId client-side:
|
|
725
|
+
```javascript
|
|
726
|
+
results = results.filter(function(r) { return r.userId === currentUserId; });
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## CRM Integration Pattern (Cross-Gamification)
|
|
732
|
+
|
|
733
|
+
**Problema:** Integrar app de produto (gamificação A) com CRM Funifier (gamificação B) para criar leads automaticamente no signup.
|
|
734
|
+
|
|
735
|
+
**Solução:** Trigger na gamificação do produto faz HTTP POST para a gamificação do CRM.
|
|
736
|
+
|
|
737
|
+
### Arquitetura
|
|
738
|
+
|
|
739
|
+
```
|
|
740
|
+
Gamificação "Produto" (Orvya Fitness)
|
|
741
|
+
|
|
|
742
|
+
→ Trigger after_create (entity: player)
|
|
743
|
+
|
|
|
744
|
+
→ HTTP POST para Gamificação "CRM" (/v3/database/person + /v3/database/deal)
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### Lições Importantes
|
|
748
|
+
|
|
749
|
+
1. **CRM endpoints `/v3/crm/*` retornam 404 com Basic auth** — usar `/v3/database/person` e `/v3/database/deal`
|
|
750
|
+
2. **Deals precisam de campos específicos para Kanban:** `owner`, `add_time`, `visible_to`
|
|
751
|
+
3. **CRM App scope DEVE incluir a palavra "database"** — sem ela, writes silenciosamente falham (201 sem persistência)
|
|
752
|
+
4. **Usar App token (não-expirável)** da Security → Apps — gerar Basic token pelo ícone do olho
|
|
753
|
+
5. **Um CRM para todos os produtos** — diferenciar por Pipeline
|
|
754
|
+
|
|
755
|
+
---
|
|
756
|
+
|
|
757
|
+
## Player Management Pattern
|
|
758
|
+
|
|
759
|
+
### Leitura e Escrita de Player
|
|
760
|
+
|
|
761
|
+
```groovy
|
|
762
|
+
// Ler player
|
|
763
|
+
Player p = manager.getPlayerManager().findById(playerId)
|
|
764
|
+
|
|
765
|
+
// Modificar
|
|
766
|
+
p.extra.plan = [type: "premium"]
|
|
767
|
+
|
|
768
|
+
// Salvar (upsert)
|
|
769
|
+
manager.getPlayerManager().insert(p)
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### Campos obrigatórios no POST /v3/player
|
|
773
|
+
|
|
774
|
+
Ao atualizar player via `POST /v3/player` com apenas `_id` + `extra`, os campos `name` e `email` são APAGADOS. **Sempre incluir:**
|
|
775
|
+
|
|
776
|
+
```json
|
|
777
|
+
{
|
|
778
|
+
"_id": "user@email.com",
|
|
779
|
+
"name": "Nome do Usuário",
|
|
780
|
+
"email": "user@email.com",
|
|
781
|
+
"extra": { ... }
|
|
782
|
+
}
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### Player.image Structure
|
|
786
|
+
|
|
787
|
+
Para salvar foto do player, usar a estrutura completa:
|
|
788
|
+
|
|
789
|
+
```json
|
|
790
|
+
{
|
|
791
|
+
"image": {
|
|
792
|
+
"small": { "url": "https://...", "size": 0, "width": 0, "height": 0, "depth": 0 },
|
|
793
|
+
"medium": { "url": "https://...", "size": 0, "width": 0, "height": 0, "depth": 0 },
|
|
794
|
+
"original": { "url": "https://...", "size": 0, "width": 0, "height": 0, "depth": 0 }
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
Os campos `size`, `width`, `height`, `depth` são obrigatórios (mesmo que 0) — Funifier espera essa estrutura.
|
|
800
|
+
|
|
801
|
+
### PlayerManager — Métodos Corretos
|
|
802
|
+
|
|
803
|
+
| Correto | Errado |
|
|
804
|
+
|---------|--------|
|
|
805
|
+
| `pm.findById(id)` | — |
|
|
806
|
+
| `pm.find()` (sem args) | `pm.find("{}")` (signature não existe) |
|
|
807
|
+
| `pm.insert(player)` | `pm.save(player)` |
|
|
808
|
+
| `player.id` (em Groovy) | `player._id` (não funciona em Groovy) |
|
|
809
|
+
| `player.extra` (acesso direto) | `player.getExtra()` (não existe) |
|
|
810
|
+
|
|
811
|
+
---
|
|
812
|
+
|
|
813
|
+
## AngularJS Patterns (Frontend)
|
|
814
|
+
|
|
815
|
+
Lições aprendidas em projetos AngularJS 1.x com Funifier:
|
|
816
|
+
|
|
817
|
+
### ng-if cria child scope
|
|
818
|
+
`ng-model="varName"` dentro de `ng-if` escreve no child scope, não no controller. **Fix:** usar `$parent.varName`:
|
|
819
|
+
|
|
820
|
+
```html
|
|
821
|
+
<div ng-if="editing">
|
|
822
|
+
<input ng-model="$parent.editName"> <!-- correto -->
|
|
823
|
+
<input ng-model="editName"> <!-- BUG: escreve no child scope -->
|
|
824
|
+
</div>
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
### $q.reject() em vez de Promise.reject()
|
|
828
|
+
Dentro de `$http.then()` chains, nunca usar `Promise.reject()` — o digest cycle do AngularJS não detecta. Usar `$q.reject()`.
|
|
829
|
+
|
|
830
|
+
### input[type="time"] requer Date object
|
|
831
|
+
AngularJS `<input type="time">` não aceita strings `"07:00"`. Converter para `Date` ao carregar:
|
|
832
|
+
|
|
833
|
+
```javascript
|
|
834
|
+
var parts = timeStr.split(':');
|
|
835
|
+
var d = new Date(1970, 0, 1, parseInt(parts[0]), parseInt(parts[1]), 0);
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
### href="#" causa redirect
|
|
839
|
+
`<a href="#" ng-click="fn()">` reseta hash antes de ng-click → causa redirect. **Fix:** usar `href=""`
|
|
840
|
+
|
|
841
|
+
### iOS Safari limitations
|
|
842
|
+
- `navigator.vibrate` não funciona — usar `new Audio('audio/beep.mp3')`
|
|
843
|
+
- `Notification` API não disponível (non-PWA) — `'Notification' in window` é false
|
|
844
|
+
- `beforeinstallprompt` não existe — Apple usa Share → Add to Home Screen
|
|
845
|
+
- `google.accounts.id.prompt()` falha em mobile — renderizar botão oficial
|
|
846
|
+
|
|
847
|
+
---
|
|
848
|
+
|
|
849
|
+
## OpenAI Realtime API — Voice/Video Call Pattern
|
|
850
|
+
|
|
851
|
+
### Arquitetura
|
|
852
|
+
Stack: OpenAI Realtime API + WebRTC. Fluxo:
|
|
853
|
+
1. Frontend chama Funifier Public Endpoint para obter dados do usuário + API key
|
|
854
|
+
2. Frontend gera chave efêmera via `/v1/realtime/client_secrets`
|
|
855
|
+
3. Frontend estabelece WebRTC via `/v1/realtime/calls` (SDP exchange)
|
|
856
|
+
4. Data channel `oai-events` para controle bidirecional
|
|
857
|
+
|
|
858
|
+
### Endpoints OpenAI (atualizados 2026-03)
|
|
859
|
+
- **Chave efêmera:** `POST /v1/realtime/client_secrets` (NÃO usar o antigo `/v1/realtime/sessions`)
|
|
860
|
+
- **SDP exchange:** `POST /v1/realtime/calls` (NÃO usar o antigo `/v1/realtime?model=...`)
|
|
861
|
+
- **Modelo:** `gpt-realtime-mini` (NÃO usar nome completo `gpt-4o-realtime-mini-2025-01-21`)
|
|
862
|
+
- `input_audio_transcription` NÃO é suportado dentro do objeto `session` no endpoint `client_secrets`
|
|
863
|
+
- `voice` deve estar em `session.audio.output.voice`, NÃO em `session.voice`
|
|
864
|
+
|
|
865
|
+
### ⚠️ CRITICAL: Tools DEVEM ser registradas na criação da chave efêmera
|
|
866
|
+
|
|
867
|
+
**Problema:** `session.update` via data channel NÃO aplica tools de forma confiável. A IA pode não "ver" as ferramentas e nunca chamá-las.
|
|
868
|
+
|
|
869
|
+
**Solução:** Incluir `tools` no body do `POST /v1/realtime/client_secrets`:
|
|
870
|
+
|
|
871
|
+
```javascript
|
|
872
|
+
fetch('https://api.openai.com/v1/realtime/client_secrets', {
|
|
873
|
+
method: 'POST',
|
|
874
|
+
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
|
|
875
|
+
body: JSON.stringify({
|
|
876
|
+
session: {
|
|
877
|
+
type: 'realtime',
|
|
878
|
+
model: 'gpt-realtime-mini',
|
|
879
|
+
instructions: instructions,
|
|
880
|
+
tools: voiceTools, // ← CRITICAL: incluir aqui!
|
|
881
|
+
audio: { output: { voice: 'coral' } }
|
|
882
|
+
}
|
|
883
|
+
})
|
|
884
|
+
});
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
O `session.update` via data channel pode ser enviado como **backup**, mas NÃO como fonte primária de configuração.
|
|
888
|
+
|
|
889
|
+
### Tool end_call — Finalização automática de ligação
|
|
890
|
+
|
|
891
|
+
Sempre incluir uma tool `end_call` para que a IA possa desligar a ligação quando a conversa terminar naturalmente:
|
|
892
|
+
|
|
893
|
+
```javascript
|
|
894
|
+
var voiceTools = [
|
|
895
|
+
// ... outras tools ...
|
|
896
|
+
{
|
|
897
|
+
type: 'function',
|
|
898
|
+
name: 'end_call',
|
|
899
|
+
description: 'Encerra a ligacao quando o usuario disser tchau, que ja entendeu, ou quando a conversa terminar. Sempre se despeca antes de chamar.',
|
|
900
|
+
parameters: { type: 'object', properties: {}, required: [] }
|
|
901
|
+
}
|
|
902
|
+
];
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
No handler de eventos:
|
|
906
|
+
```javascript
|
|
907
|
+
case 'response.function_call_arguments.done':
|
|
908
|
+
if (evt.name === 'end_call') {
|
|
909
|
+
sendToolResult(evt.call_id, { success: true });
|
|
910
|
+
setTimeout(function() {
|
|
911
|
+
$scope.endCall(); // mesma função do botão "Desligar"
|
|
912
|
+
$scope.$applyAsync();
|
|
913
|
+
}, 2000); // delay para IA terminar de falar
|
|
914
|
+
}
|
|
915
|
+
break;
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
Nas instruções, mencionar a tool explicitamente:
|
|
919
|
+
```
|
|
920
|
+
Voce tem a ferramenta end_call para encerrar a ligacao.
|
|
921
|
+
Quando o usuario disser tchau ou pedir para desligar, despeca-se e chame end_call.
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
### Checklist para implementar conversa por voz
|
|
925
|
+
1. [ ] Criar Public Endpoint no Funifier para retornar dados do usuário + API key
|
|
926
|
+
2. [ ] Incluir **todas as tools** (incluindo `end_call`) na criação da chave efêmera
|
|
927
|
+
3. [ ] Usar modelo `gpt-realtime-mini` (hardcode no frontend como safety net)
|
|
928
|
+
4. [ ] Usar `voice` em `session.audio.output.voice`
|
|
929
|
+
5. [ ] Implementar handler para `response.function_call_arguments.done`
|
|
930
|
+
6. [ ] Tool `end_call` deve chamar a mesma função do botão manual "Desligar"
|
|
931
|
+
7. [ ] Enviar `session.update` via data channel como backup (não como fonte primária)
|
|
932
|
+
8. [ ] Incluir instruções em português com contexto completo do usuário
|
|
933
|
+
|
|
934
|
+
---
|
|
935
|
+
|
|
936
|
+
## Funifier API — Endpoints Corretos (CRÍTICO)
|
|
937
|
+
|
|
938
|
+
**Problema:** Usar endpoints errados causa perda silenciosa de dados — campos desaparecem, queries retornam vazio, e é difícil debugar.
|
|
939
|
+
|
|
940
|
+
### Player (entidade nativa)
|
|
941
|
+
|
|
942
|
+
O `player` é uma entidade nativa do Funifier, **NÃO** uma collection do `/v3/database`.
|
|
943
|
+
|
|
944
|
+
| Operação | ✅ Correto | ❌ Errado (não funciona) |
|
|
945
|
+
|----------|-----------|--------------------------|
|
|
946
|
+
| **Ler por ID** | `GET /v3/player/{id}` | `GET /v3/database/player/{id}` (404 ou vazio) |
|
|
947
|
+
| **Salvar/Atualizar** | `POST /v3/player` (com objeto completo) | `PUT /v3/database/player` (faz REPLACE, perde campos) |
|
|
948
|
+
| **Deletar** | `DELETE /v3/player/{id}` | — |
|
|
949
|
+
|
|
950
|
+
**Padrão correto para atualizar player (read-merge-write):**
|
|
951
|
+
```javascript
|
|
952
|
+
// 1. Ler player completo
|
|
953
|
+
ApiService.getPlayer(playerId).then(function(res) {
|
|
954
|
+
var player = res.data;
|
|
955
|
+
|
|
956
|
+
// 2. Merge apenas os campos que mudaram
|
|
957
|
+
player.image = imageObj; // ex: atualizar foto
|
|
958
|
+
// player.extra permanece intacto!
|
|
959
|
+
|
|
960
|
+
// 3. Salvar objeto completo via POST /v3/player
|
|
961
|
+
$http.post(API + '/v3/player', player, authHeader);
|
|
962
|
+
});
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
**Exemplo do fitness (funciona):**
|
|
966
|
+
```javascript
|
|
967
|
+
$http.post(API + '/v3/player', {
|
|
968
|
+
_id: userId,
|
|
969
|
+
name: $rootScope.player.name,
|
|
970
|
+
email: $rootScope.player.email,
|
|
971
|
+
image: imgObj,
|
|
972
|
+
extra: $rootScope.player.extra // SEMPRE incluir extra!
|
|
973
|
+
}, AuthService.authHeader());
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
### Database (collections customizadas `__c`)
|
|
977
|
+
|
|
978
|
+
O endpoint `/v3/database` serve para collections customizadas (ex: `profile__c`, `signup__c`).
|
|
979
|
+
|
|
980
|
+
| Operação | ✅ Correto | ❌ Errado (não funciona) |
|
|
981
|
+
|----------|-----------|--------------------------|
|
|
982
|
+
| **Ler por ID** | `GET /v3/database/{collection}?strict=true&q=_id:'{id}'` | `GET /v3/database/{collection}/{id}` (padrão não existe) |
|
|
983
|
+
| **Salvar** | `PUT /v3/database/{collection}` (com objeto completo) | — |
|
|
984
|
+
| **Listar/Filtrar** | `GET /v3/database/{collection}?q={mongo-query}&strict=true` | — |
|
|
985
|
+
| **Aggregate** | `POST /v3/database/{collection}/aggregate?strict=true` | `POST /v3/database/{collection}` (CRIA documento!) |
|
|
986
|
+
|
|
987
|
+
**⚠️ CUIDADO:** `POST /v3/database/{collection}` com um body JSON **CRIA** um novo documento em vez de fazer query!
|
|
988
|
+
|
|
989
|
+
**⚠️ `PUT /v3/database/{collection}` faz REPLACE:** Substitui o documento inteiro. Se não enviar `extra`, `password`, etc., esses campos são apagados.
|
|
990
|
+
|
|
991
|
+
**Query por ID retorna ARRAY — normalizar:**
|
|
992
|
+
```javascript
|
|
993
|
+
getProfile: function(userId) {
|
|
994
|
+
return $http.get(
|
|
995
|
+
API + "/v3/database/profile__c?strict=true&q=_id:'" + userId + "'",
|
|
996
|
+
authHeader
|
|
997
|
+
).then(function(res) {
|
|
998
|
+
// q= retorna array, pegar primeiro elemento
|
|
999
|
+
res.data = Array.isArray(res.data) && res.data.length > 0 ? res.data[0] : null;
|
|
1000
|
+
return res;
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
### strict=true (BSON types)
|
|
1006
|
+
|
|
1007
|
+
- Sempre usar `?strict=true` no `/v3/database` para preservar tipos BSON
|
|
1008
|
+
- Datas: ler como `field.$date`, escrever como `{ $date: "ISO-8601" }`
|
|
1009
|
+
- Sem strict, datas viram strings/números e quebram queries
|
|
1010
|
+
- **Não se aplica** a `/v3/player`, `/v3/action`, etc. (entidades nativas)
|
|
1011
|
+
|
|
1012
|
+
---
|
|
1013
|
+
|
|
1014
|
+
## Cache-Busting em SPAs (Netlify)
|
|
1015
|
+
|
|
1016
|
+
**Problema:** Browser serve arquivos JS/CSS do cache, ignorando deploys novos. Usuário vê versão nova no rodapé (config.js atualizado) mas executa código antigo (api.js, controllers do cache).
|
|
1017
|
+
|
|
1018
|
+
**Solução:** Query string com versão em todos os `<script>` e `<link>`:
|
|
1019
|
+
|
|
1020
|
+
```html
|
|
1021
|
+
<script src="app.js?v=0.20.2"></script>
|
|
1022
|
+
<script src="services/api.js?v=0.20.2"></script>
|
|
1023
|
+
<link rel="stylesheet" href="css/style.css?v=0.20.2">
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
**Complemento:** Arquivo `_headers` na raiz do projeto (Netlify):
|
|
1027
|
+
```
|
|
1028
|
+
/*.js
|
|
1029
|
+
Cache-Control: no-cache, must-revalidate
|
|
1030
|
+
/*.css
|
|
1031
|
+
Cache-Control: no-cache, must-revalidate
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
**Regra:** A cada deploy, atualizar o `?v=` em todos os tags do `index.html` para a versão nova. Isso garante que o browser baixa os arquivos atualizados.
|
|
1035
|
+
|
|
1036
|
+
**Checklist de deploy:**
|
|
1037
|
+
1. [ ] Bumpar VERSION nos dois `config.js` (`app/config.js` e `config.js` raiz)
|
|
1038
|
+
2. [ ] Atualizar `?v=` em todos os `<script>` e `<link>` do `index.html`
|
|
1039
|
+
3. [ ] `git add -A && git commit && git push`
|
|
1040
|
+
4. [ ] Deploy via API Netlify (zip)
|
|
1041
|
+
5. [ ] Informar versão ao Ricardo
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
## Alteração de Senha
|
|
1045
|
+
|
|
1046
|
+
A senha do jogador é armazenada criptografada (BCrypt). Não pode ser alterada simplesmente editando o campo password diretamente.
|
|
1047
|
+
|
|
1048
|
+
### Jogador lembra a senha atual (área logada)
|
|
1049
|
+
|
|
1050
|
+
```
|
|
1051
|
+
PUT /v3/player/password?player={playerId}&old_password={senhaAtual}&new_password={novaSenha}
|
|
1052
|
+
Authorization: Basic {token}
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
### Jogador esqueceu a senha (área pública — "Forgot Password")
|
|
1056
|
+
|
|
1057
|
+
**Passo 1:** Solicitar código de recuperação (enviado por email):
|
|
1058
|
+
```
|
|
1059
|
+
GET /v3/player/password/change?player={playerId}
|
|
1060
|
+
Authorization: Basic {token}
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
**Passo 2:** Usar o código recebido por email para definir nova senha:
|
|
1064
|
+
```
|
|
1065
|
+
PUT /v3/player/password?player={playerId}&code={codigoRecebido}&new_password={novaSenha}
|
|
1066
|
+
Authorization: Basic {token}
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
### Alteração via Trigger (server-side)
|
|
1070
|
+
|
|
1071
|
+
Para alterar a senha via trigger (ex: admin reset), usar BCrypt:
|
|
1072
|
+
|
|
1073
|
+
```groovy
|
|
1074
|
+
void trigger(event, entity, player, database) {
|
|
1075
|
+
Player p = manager.getPlayerManager().findById(entity._id);
|
|
1076
|
+
if (entity.password != null) {
|
|
1077
|
+
p.password = com.funifier.engine.util.BCrypt.hashpw(
|
|
1078
|
+
entity.password, com.funifier.engine.util.BCrypt.gensalt()
|
|
1079
|
+
);
|
|
1080
|
+
entity.remove("password");
|
|
1081
|
+
}
|
|
1082
|
+
manager.getPlayerManager().insert(p);
|
|
1083
|
+
}
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
Chamada:
|
|
1087
|
+
```
|
|
1088
|
+
PUT /v3/database/change_password__c
|
|
1089
|
+
{"_id": "playerId", "password": "novaSenha"}
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
### Observações
|
|
1093
|
+
- O endpoint `GET /v3/player/password/change` envia email automaticamente usando o template de email configurado na gamificação
|
|
1094
|
+
- O `player` param aceita tanto o `_id` quanto o `email` do jogador
|
|
1095
|
+
- O `code` tem validade limitada (expira após uso ou timeout)
|
|
1096
|
+
- Basic auth é suficiente (não precisa de Bearer/session token)
|