# P2 — Gestão de perfis e usuários

| Campo | Valor |
|---|---|
| Programa | F7+ — operação, estrutura e conteúdo |
| Status | `revisado` — S1–S6 + revisão 1 (`task_00wl811i`) + verificação independente; `make check` verde com `a11y` (sniff 0.5.1 funcional); ficha `_to_revision/P2-usuarios-perfis.md` |
| Depende de | P0, P1 |
| Habilita | P3, P4, P6, P8 |
| Tamanho | **G** (4–6 sessões) |
| ADRs | ADR-033, ADR-037, ADR-035, ADR-020, ADR-021, ADR-025 |

## 1. Objetivo

Entregar a **área de gestão de perfis e usuários** que o modelo da ADR-033 já suporta mas não tem
tela: criar/editar/ativar/desativar operadores, definir perfis com a matriz `{módulo}.{ação}`
validada contra o manifesto, e permitir que cada pessoa edite o próprio perfil. Tudo com
deny-by-default, auditoria e proteção contra escalada de privilégio e auto-lockout.

**Resultado observável:** um administrador cria um operador, atribui um perfil, desativa a conta e
vê a mudança na auditoria; o operador sem `acesso.ver` não vê a tela; o último administrador não
consegue se desativar; o perfil `suporte` continua protegido.

## 2. Estado atual (reusar)

- `cms_users` (`src/core/Area/Cms/Database/Migrations/2026_06_11_000000_create_cms_users_table.php`):
  UUIDv7, `email` único, soft delete com trigger que proíbe DELETE físico, `lock_version`.
- `core_roles` (`src/core/Access/Database/Migrations/2026_10_16_000000_create_core_roles_table.php`):
  `slug`, `nome`, `protegido`, `permissoes` jsonb (lista de tokens), `CHECK` de array.
- `Role` (`src/core/Access/Models/Role.php`): normaliza tokens, protege `suporte` em create/update/
  delete, `assignable()`.
- `User` (`src/core/Area/Cms/Models/User.php`): `role_id`, `hasPermission`, `isSupport`.
- `Permission`, `AccessGate`, `SupportAccess`: vocabulário e `Gate`; grupo `Suporte` 404.
- `core:user:create`: criação via CLI (Argon2id, um perfil).
- **Não existe:** tela, `is_active/deactivated_at`, middleware de sessão inativa, validação de
  token declarado, política do último administrador, perfil próprio.

## 3. Escopo fechado

### S1 — Migração de estado e blindagem da autenticação

- Adicionar `deactivated_at timestamptz null` em `cms_users` (não usar `is_active`; data é
  auditável e ordenável) + índice parcial `WHERE deactivated_at IS NULL`.
- Garantir unicidade **case-insensitive** de e-mail: `CREATE UNIQUE INDEX ... ON cms_users (lower(email)) WHERE deleted_at IS NULL`;
  migrar duplicatas existentes com relatório (não silenciar).
- `AuthenticateController`: credenciais incluem `deactivated_at => null` (usuário inativo não
  autentica) e mensagem genérica continua.
- Middleware `EnsureActiveUser` no grupo `auth` do CMS: sessão aberta de usuário desativado é
  encerrada no próximo request (`logout` + `session()->invalidate()`), com auditoria.
- `core:user:create` aceita `--deactivate`/`--activate`? Não: o comando é bootstrap; a tela cobre o
  ciclo. O comando só garante `deactivated_at = null` ao criar/reativar com `--force`.
- Testes: login inativo, sessão desativada, e-mail com caixa diferente, soft delete não permite
  recriação duplicada.

### S2 — Catálogo de permissões e políticas de segurança

- `Core\Access\PermissionCatalog`: lê o manifesto de módulos e o catálogo de services, devolve
  `{group, module, title, actions[]}` somente com ações **declaradas**. Usado pela UI e pela
  validação de escrita (não pelo model).
- `Role` continua validando **sintaxe**; a Action de salvar valida **existência** do token no
  catálogo. Token órfão (módulo removido) é mostrado como aviso e pode ser removido; não é
  concedido a novos perfis.
- Regras de proteção:
  - perfil `suporte`: não editável, não excluível, não atribuível (já no model; a UI trata a
    exceção e mostra mensagem, nunca 500);
  - **último administrador**: não é possível desativar, excluir, trocar o perfil ou remover
    `acesso.editar` do último usuário ativo que possui essa permissão;
  - usuário não pode desativar a própria conta nem remover o próprio perfil se isso o deixar sem
    `acesso.ver`;
  - operador sem `acesso.editar` não altera perfis nem usuários.
- Auditoria: mutações de `User`/`Role` já passam pelo `GlobalAuditSubscriber`; as Actions emitem
  `log_event('acesso.{action}')` e span `acesso.{action}` (ADR-024/035). Nunca senha/token no payload.

### S3 — Módulo `Infraestrutura/Acesso` e telas de usuários

- Módulo token `acesso`, grupo `Infraestrutura`, permissions `[VIEW, EDIT]`.
- Rotas CMS (sem closure):
  - `GET /cms/usuarios` — lista com busca por nome/e-mail, filtro `todos|ativos|inativos`, paginação
    nativa, ordenação determinística (`name`, `email`, `created_at`); sem DataTables/AJAX.
  - `GET /cms/usuarios/criar`, `POST /cms/usuarios`
  - `GET /cms/usuarios/{user}/editar`, `PUT /cms/usuarios/{user}`
  - `POST /cms/usuarios/{user}/ativar`, `POST /cms/usuarios/{user}/desativar`
  - `POST /cms/usuarios/{user}/senha` (redefinição pelo administrador)
- Formulário: nome, e-mail, perfil (select de `Role::assignable()`), senha (criação) / nova senha
  (opcional na edição), confirmação. Validação: e-mail `email` + `max:255`, nome `required|max:120`,
  senha `min:12` + `confirmed` (Argon2id via cast `hashed`), perfil existente e atribuível.
- Ações de linha com `POST` + CSRF; confirmação nativa (`<x-cms::confirm>`); nada de `GET` que muta.
- Acessibilidade: tabela semântica, `caption`, escopo de coluna, foco no primeiro erro, alvos
  ≥2.75rem, status badge com texto (não só cor).

### S4 — Tela de perfis e matriz de permissões

- `GET /cms/papeis`, `GET /cms/papeis/criar`, `POST /cms/papeis`, `GET /cms/papeis/{role}/editar`,
  `PUT /cms/papeis/{role}`, `DELETE /cms/papeis/{role}` (bloqueado se houver usuários vinculados).
- Matriz agrupada por grupo do manifesto: linhas = módulos (e services com admin surface),
  colunas = as quatro ações; ação não declarada é **desabilitada, não omitida**; resumo (“N módulos
  com edição”); busca por nome; contagem de usuários vinculados.
- Sem herança, sem `PermissionService`, sem níveis `none/read/write`; o submit envia
  `permissoes[]` com tokens canônicos.
- `RoleRequest` valida tokens contra o catálogo; duplicatas são normalizadas; perfil protegido é
  recusado na rota e no model.
- Excluir perfil com usuários: bloqueio no servidor + mensagem; não confiar no botão desabilitado.
- Auditoria before/after de `permissoes` (o subscriber já cobre; teste dedicado).

### S5 — Perfil próprio

- `GET /cms/perfil`, `PUT /cms/perfil` (nome, e-mail), `PUT /cms/perfil/senha` (senha atual + nova
  + confirmação).
- Troca de senha: valida senha atual, `Hash::check`, novo hash Argon2id, `session()->regenerate()`,
  auditoria sem senha; invalida outras sessões? Decisão: manter a sessão atual e exigir novo login
  nas demais (se houver store compartilhado); no MVP, apenas regenerar a atual.
- E-mail: unicidade case-insensitive; auditoria; sem alterar `email_verified_at` (não há fluxo de
  verificação no MVP).
- A tela não expõe `role_id` (perfil é do administrador).

### S6 — Fechamento

- E2E: criar usuário → login → permissão funciona; desativar → login falha; matriz → menu muda;
  perfil próprio → senha trocada.
- `make check` 2× + `E2E=1`; axe desktop/mobile; teclado na matriz.
- Atualizar `INVENTARIO.md` §2 (gestão de perfis) e `wiki/specs/11-acesso-e-ativacao.md`.

## 4. Fora de escopo

- 2FA, gov.br SSO, “lembrar-me”, recuperação de senha por e-mail (ADR-037 backlog; P13 para SSO).
- Herança de perfis, múltiplos perfis por usuário, permissões por registro/row-level.
- API pública de usuários (não há consumidor; ADR-015).
- Exclusão física de usuário (trigger e LGPD de retenção).
- Gestão de sessões/dispositivos (backlog).

## 5. Fatias de sessão

| Sessão | Entrega | Prova | Depende |
|---|---|---|---|
| **P2.S1** | `deactivated_at` + índice lower(email) + auth/middleware | inativo não loga; sessão cai; caixa diferente não duplica | P1 |
| **P2.S2** | `PermissionCatalog` + políticas (último admin, self, suporte) | escalada/auto-lockout recusados; 403/422, nunca 500 | S1 |
| **P2.S3** | módulo `Acesso` + CRUD de usuários + ativar/desativar | E2E criar/login/desativar; lista/filtros; axe | S2 |
| **P2.S4** | telas de perfis + matriz + exclusão protegida | matriz reflete o manifesto; perfil suporte imutável; token órfão | S3 |
| **P2.S5** | perfil próprio (dados + senha) | senha atual errada falha; nova senha loga; auditoria | S3 |
| **P2.S6** | E2E, docs, 2× check | evidência no `kd` | S4–S5 |

## 6. Contratos e dados

- **Tabela alterada:** `cms_users` (`deactivated_at`, índice `lower(email)` parcial).
- **Módulo:** `Infraestrutura/Acesso` (token `acesso`).
- **Rotas:** `acesso.cms.usuarios.*`, `acesso.cms.papeis.*`, `acesso.cms.perfil.*`.
- **Permissões:** `acesso.ver`, `acesso.editar`.
- **Catálogo:** `Core\Access\PermissionCatalog` (manifesto + services).
- **Eventos:** `acesso.user_created`, `acesso.user_updated`, `acesso.user_deactivated`,
  `acesso.user_activated`, `acesso.role_updated`, `acesso.profile_updated` (log + span + auditoria).

## 7. DoD

- [ ] Usuários e perfis são gerenciáveis pelo painel, com validação de token declarado.
- [ ] Usuário inativo não autentica e sessão existente é encerrada.
- [ ] Último administrador e perfil `suporte` são protegidos no servidor.
- [ ] Nenhuma rota muta estado por `GET`; toda ação tem CSRF e permissão.
- [ ] Matriz acessível por teclado, sem JS obrigatório, com ações não suportadas desabilitadas.
- [ ] Auditoria before/after de usuário/perfil; nenhum segredo em log/auditoria.
- [ ] 4 locales; `make check` 2× + `E2E=1`; cobertura ≥90%; PHPStan max 0.

## Fechamento — ficha de revisão

> **Obrigatório antes de fechar:** criar `_to_revision/P2-usuarios-perfis.md` e seguir
> `_to_revision/README.md` + `_to_revision/PROTOCOLO.md` (v2.0). A ficha não é gate de build; é a
> entrada da fila de revisão profunda.

- Cabeçalho: fase, `kd` épico/tasks/slices, âncoras (este arquivo, ADRs e código) e status
  `⬜ pendente` → `🟡 em revisão` → `✅ revisado` / `🔧 com correções`.
- Conteúdo: o que foi implementado, arquivos/superfícies a revisar, checklist D1–D14 aplicável,
  riscos e observações.
- Fechamento: preencher “Notas da revisão” com data/revisor/resultado e achados (`kd write
  --anchor`), atualizar a fila em `_to_revision/README.md` e `_to_revision/_inventario.md`.
- Achado com correção exige teste vermelho→verde; a fase só é `closed` no `kd` com a ficha
  registrada.

## 8. Riscos

| Risco | Mitigação |
|---|---|
| Escalada de privilégio | token só do manifesto, deny-by-default, perfil protegido, teste de escalada |
| Auto-lockout do painel | política do último administrador + teste; mensagem clara |
| E-mail duplicado por caixa | índice `lower(email)` + normalização na escrita |
| Sessão de usuário desativado continuar | middleware `EnsureActiveUser` + teste de sessão |
| Matriz virar “níveis” | quatro ações fixas; ação não declarada desabilitada, sem herança |
| `RuntimeException` do model virar 500 | tratar exceção na Action/Controller e devolver 422/403 com mensagem |

## 9. Retomada

```bash
kd prime
kd ask "usuários perfis matriz ADR-033"
kd task list --ready
```

Ler: P0/P1 → `src/core/Access/**` → `src/core/Area/Cms/**` → `src/core/Internal/Navigation/PanelMenu.php`
→ `wiki/backlog/dossie_modules/usuarios/**` → `wiki/backlog/dossie_modules/lotep-v2/infraestrutura-gestaopermissoes/09-APROVEITAMENTO-V7.md`
(como referência de UX, não de modelo).

Referências irmãs (UX/desenho, não código): `../lotep/V2/Modules/Infraestrutura/GestaoPermissoes/**`
e `../sucesu-cms/V2/Modules/Infraestrutura/GestaoPermissoes/**` (matriz, contagem de usuários,
perfil reservado). Rejeitar `PermissionService`, níveis `none/read/write`, fail-open e bypass por
e-mail.
