# Roles no admin Payload (`users.roles`) Status: entregue 2026-07-27 Atualizado em: 2026-07-27 Item do roadmap: [docs/roadmap.md](../roadmap.md) (Admin Payload / Bloqueadores — campo `roles` em `users`) Impeccable: A — N/A sem superfície UI própria (campo renderiza no admin Payload) Appetite: ~0,75–1 dia eng; migration + predicados + testes; 15–30 min docs Responsável: — _Revisão 2026-07-27: auditoria do implement-roadmap-item. Unificação `users`+`campaignUser` rejeitada (25 FKs, isolamento estrutural → dado, janela de congelamento). Escopo mínimo: `admin` | `editor`; `isPayloadAdmin` exige papel `admin`; editor escreve `post`/`tag`/`media`. Entrega: migration `20260727_032523_add_users_roles` + backfill; predicados `hasPayloadPanelAccess`/`canManagePublishedContent`/`isPayloadEditor`; `canReadMunicipality` nega ator `users` sem admin; testes unit+int (ator editor + pin `payload.auth` com `roles`); unificação documentada como rejeitada com gatilho Fase 2 white-label._ ## Dados → decisão → apresentação Dados: N/A — RBAC de accounts do `/admin`; sem KPI/mapa/série/ranking nesta entrega. ## Contexto A collection `users` ([src/collections/Users.ts](../../src/collections/Users.ts)) não tem `roles`: todo account admin é omnipotente. O hardening Fase 0 (2026-07-23) já trancou collections CMS/PII com `payloadAdminOnly` / `isPayloadAdmin` (type guard em `user?.collection === 'users'`), mas isso não diferencia papéis **dentro** do realm admin. O gap está em AGENTS Known Gap #1, roadmap "Bloqueadores atuais" e TECH-DEBT P1. `/campanha` permanece em `campaignUser` (cookie `campaign-token`, roles `coordinator|advisor|candidate|leader`). As duas collections ficam separadas de propósito nesta entrega. ## Objetivos - Campo `roles` (`select` hasMany, `admin` | `editor`) em `users`, `required`, `defaultValue: ['admin']`, `saveToJWT: true`, field access admin-only. - Migration com backfill `admin` em todas as rows existentes + `down()` simétrico. - `isPayloadAdmin` exige `roles` contendo `admin` (falha fechado se ausente/vazio). - Editor entra em `/admin` e cria/edita/apaga `post`/`tag`/`media`; negado em PII, petições, campanha e gestão de `users` (exceto a própria conta). - Guardrails: `push: false`; Local API com `user` → `overrideAccess: false` nos testes; sem Consent novo; sem unificar collections. ## Decisões travadas - **Vocabulário `admin` + `editor`.** Cobre a razão real de abrir `/admin` (comunicação publicando notícias) com um predicado novo. **Rejeitado:** só `admin` (campo inútil além de revogar); matriz fina `content`/`legal`/`campaign-admin` (ninguém pede hoje). - **`defaultValue: ['admin']`.** Bootstrap seguro: o "create first user" do Payload não expõe `roles` (field access admin-only); default `editor` deixaria o primeiro usuário sem poder promover ninguém. Mantém fixtures de teste intactas. **Rejeitado:** default `editor`; required sem default. - **`petition` fora do escopo do editor na v1.** Abaixo-assinados carregam assinaturas (PII) e o export CSV é admin-only. **Rejeitado:** editor em `petition` nesta fatia. - **`saveToJWT: true`.** Consistência com `campaignUser.role`; revogação imediata porque `payload.auth` devolve o documento fresco — pin de teste anti-lockout. - **Não unificar `users` + `campaignUser`.** Decidido 2026-07-27. Números: 25 FKs → `campaign_user` em 22 tabelas (incl. `_vote_pledge_v`, `municipality_rels`, `activity_rels`, internals Payload com ambas as colunas); isolamento hoje é estrutural (`collection === 'users'`); unificado vira checagem de dado sobre ~400 `leader` accounts do outro lado de PII; `loginWithUsername` + template de reset de campanha colidiriam com `/admin`. Janela: congelamento ~20/09. **Gatilho para reabrir:** Fase 2 white-label/multi-tenant, ou "uma pessoa precisa de dois logins" deixar de ser hipotético. - **i18n e naming:** identificadores em inglês (`roles`, `admin`, `editor`, `isPayloadEditor`, `canManagePublishedContent`, `hasPayloadPanelAccess`); labels admin em pt-BR ("Papéis", "Administrador", "Editor"). ## Questões em aberto Nenhuma bloqueante. Defaults acima carregados como decisão. ## Abordagem proposta ```mermaid flowchart LR Users["users.roles
admin | editor"] Pred["isPayloadAdmin
exige admin"] Panel["hasPayloadPanelAccess
admin ou editor"] CMS["canManagePublishedContent
post/tag/media"] AdminUI["/admin"] Users --> Pred Users --> Panel Panel --> AdminUI Pred --> CMS Panel --> CMS ``` 1. Schema + migration `add_users_roles` (enum + tabela `users_roles` + backfill). 2. Apertar predicados em `shared.ts`; Users.access; Post/Tag/Media; corrigir `cliReader` do script de aggregates. 3. Testes: lockdown com ator editor; pin `payload.auth` devolve `roles`; fixtures + unit dos predicados. 4. Gate AGENTS.md + Aikido + docs. ## Não escopo - Unificar `users` + `campaignUser`. - UI de matriz de permissões por collection. - RBAC a nível de campo dentro do `/admin`. - Log de auditoria de mudança de papel. - Papéis por tenant / white-label. - Editor em `petition` / `signature` / `consent` / collections de campanha. ## Dependências Nenhuma dura. Soft: Onda 0 jurídica é independente (Consent keys). Soft: abrir `/admin` a comunicação só depois deste ship + smoke. ## Verificação - `pnpm migrate:status` + backfill local `roles: ['admin']`. - Conta `editor`: entra `/admin`, publica post; negada em signature/users/municipality. - Conta `admin`: comportamento pré-existente intacto. - `pnpm exec tsc --noEmit`, `pnpm lint`, `pnpm format:check`, `pnpm exec knip`, `pnpm check:cycles`, `pnpm test`, `pnpm test:e2e`, `pnpm build` contra banco local. - Scan Aikido nos arquivos first-party editados. ## Rabbit holes Unificação de identidade; matriz fina de papéis; editor em petition; auditoria de role change.