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
110 changes: 110 additions & 0 deletions audit/AUDITORIA_FRONTEND_MCP_2026-05-24.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Auditoria de Front-end via MCP — promo-gifts-v4

**Data:** 2026-05-24
**Alvo:** https://www.promogifts.com.br/ (produção · Vercel)
**Backend:** Supabase `doufsxqlfjyuvxuezpln`
**Sessão:** login `adm01@promobrindes.com.br` — papel **Supervisor** (usuário "Joaquim Ataides")
**Método:** navegador remoto via MCP (sessão persistente), captura de rede, snapshots ARIA, screenshots + análise de código-fonte + advisors do Supabase.

---

## Resumo executivo

| # | Severidade | Achado | Status |
|---|------------|--------|--------|
| 1 | **Alta** | Texto corrompido (mojibake UTF-8 duplo) em ~22 arquivos / 500 ocorrências, visível em login, simulador de preços, Kit Maker, formulários admin | ✅ **Corrigido neste PR** |
| 2 | **Alta** | 17 funções `SECURITY DEFINER` executáveis por `anon`/`authenticated` (ex.: `get_quote_token_by_value`, `cleanup_expired_webhook_request_nonces` por **anon**) | ⚠️ Documentado — requer revisão |
| 3 | **Média/Alta** | 373 tabelas expostas ao papel `anon` via API (inclui `admin_audit_log`, `admin_settings`, `access_security_settings`, `auth_login_attempts`) | ⚠️ Verificar RLS |
| 4 | **Média** | Política RLS "sempre verdadeira" em `password_reset_requests` (INSERT, role público) | ⚠️ Validar rate-limit |
| 5 | **Média** | Kit Maker (`/montar-kit`) renderiza dados de **mock** (`src/lib/kit-builder/mock-data.ts`) em produção | ⚠️ Documentado |
| 6 | **Média** | **Todos** os produtos do catálogo exibem o aviso "Preço próximo do limite de validade" | ⚠️ Investigar threshold/dados |
| 7 | **Baixa** | Widget de calendário do Dashboard preso em skeleton (loading infinito aparente) | ⚠️ Documentado |
| 8 | Info | Área `/admin/*` exige cadastro de MFA — não auditada em profundidade (não habilitamos MFA na conta do usuário) | — |

> Itens 1 e a varredura de rede foram a parte executável; os demais são achados de auditoria com recomendação. Dívida técnica já rastreada (1.010 erros TS no baseline, 442 ESLint) **não** é recontada aqui.

---

## 1. Mojibake UTF-8 (CORRIGIDO) — Severidade Alta

### Evidência
Strings com dupla codificação UTF-8 → Latin-1 → UTF-8 apareciam renderizadas no site:

- **Login** (`/auth`): *"Entre com suas credenciais para Brilhar, **você** nasce para isso!"* e *"Verificação em tempo real das instâncias Supabase"*.
- **Kit Maker** (`/montar-kit`): dimensões *"15× 10 × 8 cm"* (× corrompido) e material *"Papelão Revestido"*.
- **Simulador de preços** / personalização: mensagens de validação *"Quantidade mínima é ... unidades"*, *"Número de cores excede máximo"*, *"Ãrea de gravação"*.
- Formulários admin (componentes de kit, fornecedores, importação em massa).

### Causa-raiz
Arquivos salvos com bytes duplo-codificados (ex.: `você` gravado como bytes `C3 83 C2 AA`). Confirmado em `src/pages/auth/Auth.tsx:604`. Atingia **22 arquivos** — incluindo lógica de domínio (`src/lib/personalization/*`, `src/lib/kit-builder/*`).

> Verificado que **não** havia mojibake dentro de comparações (`===`, `includes()`, `case`) — ou seja, era corrupção de **exibição/comentário**, sem quebra lógica. Severidade alta por impacto de UX/profissionalismo em telas centrais.

### Correção aplicada
Reversão seletiva por runs de bytes Latin-1 que formam UTF-8 válido (preserva caracteres legítimos). **500 ocorrências corrigidas em 22 arquivos**, 0 mojibake restante. Diff balanceado (329/329), apenas conteúdo de strings/comentários — sem mudança estrutural de código.

Arquivos: `auth/Auth.tsx`, `lib/personalization/{validators,selectors,calculators,transformers}.ts`, `lib/kit-builder/{mock-data,price-calculator,volume-calculator}.ts`, `components/pricing/**`, `components/admin/products/**`, `components/admin/suppliers-manager/SupplierTable.tsx`, `hooks/voice/processTranscript.ts`.

---

## 2–4. Backend Supabase — Advisors de segurança

`get_advisors(security)` retornou **783 avisos (WARN)**:

| Avisos | Tipo | Risco |
|--------|------|-------|
| 392 | `pg_graphql_authenticated_table_exposed` | tabelas expostas a usuários autenticados via API |
| 373 | `pg_graphql_anon_table_exposed` | **tabelas expostas ao papel `anon` (não autenticado)** |
| 12 | `authenticated_security_definer_function_executable` | funções `SECURITY DEFINER` chamáveis por autenticados |
| 5 | `anon_security_definer_function_executable` | **funções `SECURITY DEFINER` chamáveis por `anon`** |
| 1 | `rls_policy_always_true` | política RLS sem restrição |

### 2. Funções `SECURITY DEFINER` executáveis por anon (revisar)
`SECURITY DEFINER` roda com privilégios do owner. Expostas a **anon**: `check_login_rate_limit`, `cleanup_expired_webhook_request_nonces`, `get_public_schema_signatures`, `get_quote_token_by_value`, `submit_quote_response`.
- `get_quote_token_by_value` por anon → risco de **enumeração de tokens** de orçamento.
- `cleanup_expired_webhook_request_nonces` por anon → função de manutenção não deveria ser pública.
- `get_public_schema_signatures` por anon → possível vazamento de estrutura de schema.

### 3. 373 tabelas expostas ao papel `anon`
Inclui tabelas sensíveis: `admin_audit_log`, `admin_settings`, `access_security_settings`, `auth_login_attempts`, `audit_logs`, `ai_usage_logs`, `analytics_events`, `api_usage`. **Exposição via API ≠ leitura permitida** (depende de RLS), mas a quantidade e a natureza (auditoria/segurança/admin) exigem verificação de que há RLS restritiva ativa em cada uma.

### 4. RLS sempre-verdadeira — `password_reset_requests`
Política `"Anyone can request a password reset"` (INSERT, `WITH CHECK true`, role público). Provavelmente **intencional** (qualquer um precisa solicitar reset antes de logar), porém deve ser protegida por **rate-limit/captcha** para evitar abuso/flood.

> Remediação: https://supabase.com/docs/guides/database/database-linter

---

## 5. Kit Maker servindo dados de mock em produção — Média

As caixas exibidas em `/montar-kit` (Caixa Kraft P/M/G, Caixa Premium Preta M, com materiais "Papelão Revestido", "Cerâmica", "Aço Inox") correspondem exatamente a `src/lib/kit-builder/mock-data.ts`. Indica que o catálogo de embalagens do Kit Maker **não está ligado a dados reais** do banco — funcionalidade aparentemente incompleta/placeholder.

## 6. Aviso de validade de preço em todos os produtos — Média
Em `/` e `/produtos`, **todos** os cards exibem o status *"Preço próximo do limite de validade"*. Ou os preços estão genuinamente desatualizados em massa, ou o threshold de "price freshness" está mal calibrado (gera ruído e mascara produtos realmente desatualizados). Verificar `src/utils/price-freshness.ts` e a data-base dos preços.

## 7. Widget de calendário do Dashboard — Baixa
Em `/dashboard`, o widget abaixo de "Suas Métricas do Mês" permaneceu em skeleton (carregamento aparentemente infinito) durante a sessão. Confirmar se há fetch travado/sem fallback.

---

## Cobertura da auditoria

**Rotas verificadas (render + rede 2xx OK):** `/auth`, `/` (catálogo), `/dashboard`, `/orcamentos`, `/orcamentos/novo`, `/clientes`, `/montar-kit`, `/ferramentas/bi`, `/busca-preco`, `/produtos`. Login e carregamento inicial: todas as chamadas a `doufsxqlfjyuvxuezpln.supabase.co` retornaram 200/201/204 (sem 4xx/5xx).

**Não auditado em profundidade (honestidade de escopo):**
- **`/admin/*`** — bloqueado por modal obrigatório de cadastro de MFA ("contas com acesso administrativo precisam ter MFA ativado"). Não habilitamos MFA na conta do usuário.
- Interação botão-a-botão exaustiva em cada uma das ~70 rotas — inviável via navegador remoto numa sessão; foi feita varredura em nível de página + fluxos principais (login, navegação, builder de orçamento).
- Rotas dev-only (`DevRoute`) — papel atual é Supervisor, não dev.

**Observação técnica:** o campo de senha (`#login-password`, `type=password`) resistiu a `.fill()` de automação até alternar para `type=text` via "Mostrar senha" — comportamento compatível com proteção anti-bot ou animação de overlay (starfield). Não é bug funcional para usuários reais.

---

## Recomendações priorizadas
1. **Mergear o fix de mojibake** (este PR) e adicionar um guard no CI (`grep -rP "Ã[\\x80-\\xBF]"` em `src/`) para impedir regressão.
2. **Revisar grants das funções `SECURITY DEFINER` expostas a `anon`** — `REVOKE EXECUTE ... FROM anon` nas que não forem fluxo público (especialmente `cleanup_*`, `get_public_schema_signatures`, `get_quote_token_by_value`).
3. **Auditar RLS** das 373 tabelas anon-expostas; priorizar `admin_*`, `*audit*`, `access_security_settings`, `auth_login_attempts`.
4. Adicionar **rate-limit/captcha** ao fluxo de `password_reset_requests`.
5. Ligar o Kit Maker a dados reais (ou marcar claramente como demo).
6. Calibrar o threshold de validade de preço.
7. Executar `get_advisors(performance)` (não rodado nesta sessão) para fechar o lado de performance.
8 changes: 4 additions & 4 deletions src/components/admin/products/bulk-import/StepPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function StepPreview({
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
<Badge variant="default" className="gap-1">
<CheckCircle2 className="h-3 w-3" /> {validCount} válidos
<CheckCircle2 className="h-3 w-3" /> {validCount} válidos
</Badge>
{invalidCount > 0 && (
<Badge variant="destructive" className="gap-1">
Expand All @@ -60,7 +60,7 @@ export function StepPreview({
)}
{existsCount > 0 && (
<Badge variant="outline" className="gap-1 border-primary/30 text-primary">
<RefreshCw className="h-3 w-3" /> {existsCount} já existem no BD
<RefreshCw className="h-3 w-3" /> {existsCount} existem no BD
</Badge>
)}
<Badge variant="outline" className="gap-1 border-success/30 text-success">
Expand All @@ -69,7 +69,7 @@ export function StepPreview({
</div>

<div className="space-y-2 rounded-lg border p-3">
<p className="text-sm font-medium">Modo de Importação</p>
<p className="text-sm font-medium">Modo de Importação</p>
<RadioGroup
value={importMode}
onValueChange={(v) => setImportMode(v as ImportMode)}
Expand Down Expand Up @@ -104,7 +104,7 @@ export function StepPreview({
<TableHead className="w-16">Status</TableHead>
<TableHead>SKU</TableHead>
<TableHead>Nome</TableHead>
<TableHead>Preço</TableHead>
<TableHead>Preço</TableHead>
<TableHead>BD</TableHead>
<TableHead>Detalhes</TableHead>
</TableRow>
Expand Down
24 changes: 12 additions & 12 deletions src/components/admin/products/kit-components/ComponentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function ComponentForm({ initial, onSave, onCancel, isSaving }: Props) {

const handleSave = () => {
if (!form.component_name.trim()) {
toast.error('Nome do componente é obrigatório');
toast.error('Nome do componente é obrigatório');
return;
}
onSave(form);
Expand All @@ -37,7 +37,7 @@ export function ComponentForm({ initial, onSave, onCancel, isSaving }: Props) {
<Input
value={form.component_name}
onChange={(e) => set('component_name', e.target.value)}
placeholder="Ex: Tábua de corte"
placeholder="Ex: Tábua de corte"
className="h-8 text-sm"
/>
</div>
Expand All @@ -51,7 +51,7 @@ export function ComponentForm({ initial, onSave, onCancel, isSaving }: Props) {
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Código</Label>
<Label className="text-xs">Código</Label>
<Input
value={form.component_code}
onChange={(e) => set('component_code', e.target.value)}
Expand All @@ -76,7 +76,7 @@ export function ComponentForm({ initial, onSave, onCancel, isSaving }: Props) {
<Input
value={form.supplier_component_code}
onChange={(e) => set('supplier_component_code', e.target.value)}
placeholder="Código fornecedor"
placeholder="Código fornecedor"
className="h-8 font-mono text-sm"
/>
</div>
Expand Down Expand Up @@ -167,8 +167,8 @@ export function ComponentForm({ initial, onSave, onCancel, isSaving }: Props) {
[
['is_optional', 'Opcional'],
['is_packaging', 'Embalagem'],
['is_replaceable', 'Substituível'],
['allows_personalization', 'Personalizável'],
['is_replaceable', 'Substituível'],
['allows_personalization', 'Personalizável'],
] as const
).map(([key, label]) => (
<label key={key} className="flex cursor-pointer items-center gap-2 text-xs">
Expand All @@ -190,32 +190,32 @@ export function ComponentForm({ initial, onSave, onCancel, isSaving }: Props) {

<div className="grid grid-cols-1 gap-3">
<div className="space-y-1">
<Label className="text-xs">Descrição</Label>
<Label className="text-xs">Descrição</Label>
<Input
value={form.component_description}
onChange={(e) => set('component_description', e.target.value)}
placeholder="Descrição / dimensões descritivas"
placeholder="Descrição / dimensões descritivas"
className="h-8 text-sm"
/>
</div>
{form.allows_personalization && (
<div className="space-y-1">
<Label className="text-xs">Notas de Personalização</Label>
<Label className="text-xs">Notas de Personalização</Label>
<Textarea
value={form.personalization_notes}
onChange={(e) => set('personalization_notes', e.target.value)}
placeholder="Instruções de personalização..."
placeholder="Instruções de personalização..."
rows={2}
className="text-sm"
/>
</div>
)}
<div className="space-y-1">
<Label className="text-xs">Observações</Label>
<Label className="text-xs">Observações</Label>
<Input
value={form.notes}
onChange={(e) => set('notes', e.target.value)}
placeholder="Observações internas"
placeholder="Observações internas"
className="h-8 text-sm"
/>
</div>
Expand Down
22 changes: 11 additions & 11 deletions src/components/admin/products/kit-components/PrintAreaForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function PrintAreaForm({ initial, onSave, onCancel, isSaving }: Props) {

const handleSave = () => {
if (!form.location_name.trim()) {
toast.error('Nome do local é obrigatório');
toast.error('Nome do local é obrigatório');
return;
}
onSave(form);
Expand All @@ -33,12 +33,12 @@ export function PrintAreaForm({ initial, onSave, onCancel, isSaving }: Props) {
<div className="ml-6 space-y-2.5 rounded-md border border-primary/20 bg-primary/5 p-2.5">
<div className="flex items-center gap-1.5 text-xs font-medium text-primary">
<Target className="h-3 w-3" />
Área de Gravação
Área de Gravação
</div>

<div className="grid grid-cols-3 gap-2">
<div className="space-y-1">
<Label className="text-[10px]">Código Local</Label>
<Label className="text-[10px]">Código Local</Label>
<Input
value={form.location_code}
onChange={(e) => set('location_code', e.target.value)}
Expand All @@ -51,12 +51,12 @@ export function PrintAreaForm({ initial, onSave, onCancel, isSaving }: Props) {
<Input
value={form.location_name}
onChange={(e) => set('location_name', e.target.value)}
placeholder="Ex: Cabo, Frente, 360°"
placeholder="Ex: Cabo, Frente, 360°"
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
<Label className="text-[10px]">Técnica</Label>
<Label className="text-[10px]">Técnica</Label>
<Input
value={form.technique_name}
onChange={(e) => set('technique_name', e.target.value)}
Expand All @@ -74,7 +74,7 @@ export function PrintAreaForm({ initial, onSave, onCancel, isSaving }: Props) {

<div className="grid grid-cols-5 gap-2">
<div className="space-y-1">
<Label className="text-[10px]">Larg. Máx (mm)</Label>
<Label className="text-[10px]">Larg. Máx (mm)</Label>
<Input
type="number"
value={form.max_width_mm ?? ''}
Expand All @@ -85,7 +85,7 @@ export function PrintAreaForm({ initial, onSave, onCancel, isSaving }: Props) {
/>
</div>
<div className="space-y-1">
<Label className="text-[10px]">Alt. Máx (mm)</Label>
<Label className="text-[10px]">Alt. Máx (mm)</Label>
<Input
type="number"
value={form.max_height_mm ?? ''}
Expand All @@ -96,7 +96,7 @@ export function PrintAreaForm({ initial, onSave, onCancel, isSaving }: Props) {
/>
</div>
<div className="space-y-1">
<Label className="text-[10px]">ID Técnica</Label>
<Label className="text-[10px]">ID Técnica</Label>
<Input
value={form.technique_id}
onChange={(e) => set('technique_id', e.target.value)}
Expand All @@ -105,7 +105,7 @@ export function PrintAreaForm({ initial, onSave, onCancel, isSaving }: Props) {
/>
</div>
<div className="space-y-1">
<Label className="text-[10px]">ID Tabela Preço</Label>
<Label className="text-[10px]">ID Tabela Preço</Label>
<Input
value={form.tabela_preco_id}
onChange={(e) => set('tabela_preco_id', e.target.value)}
Expand All @@ -126,11 +126,11 @@ export function PrintAreaForm({ initial, onSave, onCancel, isSaving }: Props) {
</div>

<div className="space-y-1">
<Label className="text-[10px]">Observações</Label>
<Label className="text-[10px]">Observações</Label>
<Input
value={form.notes}
onChange={(e) => set('notes', e.target.value)}
placeholder="Observações sobre a área de gravação"
placeholder="Observações sobre a área de gravação"
className="h-7 text-xs"
/>
</div>
Expand Down
Loading
Loading