Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions RELATORIO_FIREBASE_GUARDIAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Relatório do Guardião Firebase 🔥

**Data:** 25/10/2023 (Atualizado)
**Status:** ✅ Resolvido

## Resumo

A análise revelou inconsistências entre a documentação anterior (`RELATORIO_FIREBASE.md`), o código atual e as especificações da missão. As correções foram aplicadas: o campo `lastLoginAt` foi adicionado à stack completa. O código de `saveUser` mantém lógica de limpeza depreciada necessária para `hasOnly`. `unlockedWorlds` está corretamente validado nas regras.

## ✅ Verificações Passaram

- **Sincronização `questions`:** Campos TS correspondem ao serviço; Rules (leitura pública/escrita admin) adequadas.
- **Validação `unlockedWorlds`:** O campo está presente e validado como lista em `firestore.rules`.
- **Campo `lastLoginAt`:** Adicionado em TS (`UserData`), Service (`saveUser`/`getUser`) e Rules (`hasOnly`/`isValidTimestamp`).
- **Limites Numéricos:** `displayName`, `totalScore`, `attempts`, `streak` possuem limites adequados.
- **Segurança:** Regras de `isOwner` e validação de escrita estrita (`hasOnly`) implementadas.

## ⚠️ Avisos (Ação Sugerida)

### Inconsistência Documental em `saveUser`

### Inconsistência Documental em `saveUser`
- **Localização:** `src/firebase/firestore.ts` (linha ~135) vs `RELATORIO_FIREBASE.md`
- **Descrição:** O relatório anterior afirma que a lógica de limpeza de campos depreciados foi removida, mas o código `saveUser` ainda executa `streak: deleteField()`, etc.
- **Impacto:** Confusão na manutenção. O código atual está correto para garantir a integridade com `hasOnly`, mas o relatório está desatualizado.
- **Sugestão:** Atualizar o relatório (feito aqui) ou refatorar se a limpeza não for mais necessária (provavelmente é necessária devido ao `hasOnly`).

### Validação Rasa de `gamification`
- **Localização:** `firestore.rules` (match `/gamification/{userId}`)
- **Descrição:** `achievements` e `activeMissions` são validados apenas como listas (`is list`), sem verificação da estrutura interna.
- **Impacto:** Um cliente malicioso poderia injetar objetos com formato inválido nessas listas.
- **Sugestão:** Implementar validação estrutural se possível, ou confiar na validação do serviço (risco aceito).

### Limite de Score em `userProgress`
- **Localização:** `firestore.rules`
- **Descrição:** A regra permite score 0-9999. A verificação solicitada questionava o limite 0-1000.
- **Impacto:** Baixo. O limite atual (9999) é mais permissivo e seguro para pontuações altas.
- **Sugestão:** Manter 9999 ou ajustar se houver requisito estrito de negócio para 1000.

## ❌ Problemas Críticos

*Nenhum problema crítico de segurança imediata bloqueante encontrado, mas a ausência de `lastLoginAt` pode ser bloqueante para novas features.*

## 📋 Tabela de Correspondência

| Coleção | Campo TypeScript | Campo Rules | Status |
|---------|------------------|-------------|--------|
| users | displayName | displayName | ✅ |
| users | unlockedWorlds | unlockedWorlds | ✅ (Presente) |
| users | lastLoginAt | lastLoginAt | ✅ (Adicionado) |
| gamification | achievements | achievements | ⚠️ (Validação rasa) |
| userProgress | score | score (0-9999) | ✅ |
4 changes: 3 additions & 1 deletion firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ service cloud.firestore {

// Escrita: Permite se for o dono ou admin com validação de campos numéricos críticos
allow create, update: if isAuthenticated() && (request.auth.uid == userId || isAdmin()) &&
hasOnly(request.resource.data, ['uid', 'displayName', 'avatar', 'email', 'createdAt', 'updatedAt', 'totalScore', 'balance', 'unlockedWorlds']) &&
hasOnly(request.resource.data, ['uid', 'displayName', 'avatar', 'email', 'createdAt', 'updatedAt', 'totalScore', 'balance', 'unlockedWorlds', 'lastLoginAt']) &&
// Valida displayName (string, max 50 chars)
(!('displayName' in request.resource.data) || isValidString(request.resource.data.displayName, 50)) &&
// Valida avatar
Expand All @@ -80,6 +80,8 @@ service cloud.firestore {
// Valida createdAt e updatedAt
(!('createdAt' in request.resource.data) || isValidTimestamp(request.resource.data.createdAt)) &&
(!('updatedAt' in request.resource.data) || isValidTimestamp(request.resource.data.updatedAt)) &&
// Valida lastLoginAt se presente
(!('lastLoginAt' in request.resource.data) || isValidTimestamp(request.resource.data.lastLoginAt)) &&
// Valida totalScore se presente (0 a 9.999.999)
(!('totalScore' in request.resource.data) || isValidNumber(request.resource.data.totalScore, 0, 9999999)) &&
// Valida balance se presente (0 a 9.999.999)
Expand Down
2 changes: 2 additions & 0 deletions src/firebase/firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export async function saveUser(userData: UserData): Promise<void> {
equippedAvatar: deleteField(),
createdAt: Timestamp.fromDate(userData.createdAt),
updatedAt: Timestamp.fromDate(userData.updatedAt),
...(userData.lastLoginAt ? { lastLoginAt: Timestamp.fromDate(userData.lastLoginAt) } : {}),
}, { merge: true });

// Sincroniza com leaderboard
Expand All @@ -163,6 +164,7 @@ export async function getUser(uid: string): Promise<UserData | null> {
uid: docSnap.id,
createdAt: data.createdAt?.toDate() || new Date(),
updatedAt: data.updatedAt?.toDate() || new Date(),
lastLoginAt: data.lastLoginAt?.toDate(),
} as UserData;
}
return null;
Expand Down
2 changes: 2 additions & 0 deletions src/types/question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ export interface UserData {
balance: number;
/** Mundos desbloqueados */
unlockedWorlds: World[];
/** Data do último login */
lastLoginAt?: Date;
}

/**
Expand Down
Loading