# Usuários — Regras e Portabilidade

> Regras de negócio não óbvias, acoplamentos e armadilhas do módulo de
> usuários. Todas as afirmações são rastreáveis aos arquivos citados.

---

## 1. Regras de negócio não óbvias

### 1.1 "Ativo" é derivado de `activations`, não de uma coluna

Não existe `status` em `users`. O estado vem da existência de uma linha em
`activations` para o usuário:

- **Ativo**: existe `activations` com `completed = 1`.
- **Inativo**: não existe linha em `activations`.

Consequência prática: `Activation::remove($user)` **apaga** a ativação e
`Activation::create()` + `Activation::complete()` a recriam
(`v6/app/Http/Controllers/Admin/UserController.php:320-322,438-439`). A
remoção é definitiva (a tabela `activations` não tem `deleted_at`).

### 1.2 Inativar um usuário já inativo devolve erro

`UserController@inactivate` (linha 320) testa
`if ($activation) { ... inativar ... }` e, se a ativação **não** existe
(usuário já inativo), cai no `return response()->json(['erro' => 'Ocorreu um
erro! ...'], 500)`. A resposta HTTP 500 para uma condição de negócio
esperada — ver `08-GAPS-E-DIVIDAS.md`.

### 1.3 O perfil `super-admin` é intocável por não-super-admin

Bloqueios no código:

| Situação | Comportamento | Linha |
|---|---|---|
| Listar perfis na tela de criação de usuário | `super-admin` sai da lista | 58-59 |
| Atribuir `super-admin` a um usuário | flash de aviso + redirect | 99-101, 230-232 |
| Editar usuário que tenha `super-admin` | flash de aviso + redirect | 188-190, 236-238 |
| Editar o perfil `super-admin` | flash de aviso + redirect | 104-107 |

### 1.4 Quem não tem `perfil.create` não escolhe perfis

Sem `perfil.create`, o formulário de usuário exibe os perfis do próprio ator
como labels desabilitadas (`v6/resources/views/admin/user/form.blade.php`)
e o controller sobrescreve o campo:

```php
$request['selectProfiles'] = Sentinel::getUser()->roles->pluck('id');
```

O usuário fica preso aos perfis de quem o criou (ou dos próprios).

### 1.5 Union de permissões, não interseção

`App\Admin\User::createPermissions()`
(`v6/app/Http/Model/Admin/User.php:449-465`) monta um
`Cartalyst\Sentinel\Permissions\Permissions` com as permissões do próprio
usuário **e** a lista de permissões de cada perfil. O `PermissionsTrait` do
Sentinel resolve por **união**: basta um perfil ter a chave para o acesso
ser concedido. Não existe "negação" nem escopo por perfil.

### 1.6 A chave de permissão é `<recurso>.<ação>`

- `recurso` = `resources.slug` (ex.: `usuario`, `perfil`, `usuario.index`).
- `ação` = `permissions.slug` (ex.: `index`, `create`, `edit`, `delete`,
  `inactive`, `inactivate`, `restore`, `show`, `publish`).

O mesmo par é gravado em `roles.permissions` como JSON e é o que
`has_access()` consulta.

### 1.7 Módulo desativado esconde a linha da matriz

`v6/resources/views/admin/role/form.blade.php:43` filtra por
`$resource->modulo == null || $resource->modulo->status`. Desativar um módulo
em `/cms/modulos` **remove** o recurso correspondente da matriz de
permissões, efetivamente revogando as permissões daquele módulo (a chave
permanece no JSON, mas deixa de ser administrável).

---

## 2. Acoplamentos

### 2.1 Dependências de saída (este módulo usa)

| Dependência | Tipo | Onde |
|---|---|---|
| `cartalyst/sentinel` | externo | login, papéis, ativações |
| `spatie/laravel-activitylog` | externo | trilha de auditoria |
| `yajra/laravel-datatables` | externo | listagens AJAX |
| `modulos` (novo na v6) | interno | filtro da matriz de permissões |
| `menu_orders` | interno | contador de cliques e ordenação do menu |
| `resources` / `permissions` | interno | catálogo da matriz |

### 2.2 Dependências de entrada (quem usa este módulo)

| Consumidor | Dependência | Risco se o formato mudar |
|---|---|---|
| **Todos** os controllers do CMS | `has_access('<recurso>.<ação>')` | silencioso: a tela some |
| **Todos** os Blades do CMS | `has_access([...])` | silencioso: botão some |
| `MenuServiceProvider` | `has_access('usuario.*')`, `perfil.index`, `recurso.index`, `permissao.index` | item de menu some |
| `CheckRoutePermission` | nome da rota como nome da permissão | rota liberada ou bloqueada |
| `adminlte/page.blade.php` | `has_access(['modulos'])` para o item "Módulos" | item de menu some |
| `App\Admin\Person` | `persons.users_id` → `users.id` | dados pessoais órfãos |

> **O formato da chave é a interface mais crítica do sistema.** A v7 deve
> mantê-lo ou migrar **todos** os consumidores de uma vez.

---

## 3. Armadilhas

### 3.1 `check.route.permission` libera `store`/`update`/`destroy`

`v6/app/Http/Middleware/CheckRoutePermission.php:48-52` define:

```php
$resourceRoutes = [
    'edit'   => 'update',
    'create' => 'store',
    'delete' => 'destroy',
];
```

e a checagem (linha 82) é:

```php
$open = array_where($accessArr, function ($value, $key) use ($openRoutes, $resourceRoutes) {
    if (in_array($value, $openRoutes) || in_array($value, $resourceRoutes)) {
        return true;
    }
});
```

`in_array($value, $resourceRoutes)` compara com os **valores** do array
(`update`, `store`, `destroy`), e `$accessArr` são os segmentos do **nome da
rota**. Como toda rota de recurso tem nome `xxx.store`, `xxx.update` ou
`xxx.destroy`, o array `$open` nunca fica vazio e o middleware **retorna
`true` sem checar permissão alguma**.

Efeito prático: `POST /cms/perfil` (`perfil.store`) é acessível a qualquer
usuário autenticado — e `RoleController@store` **não** faz checagem
própria de permissão. Isso é uma **escalada de privilégio**: um operador
com acesso mínimo ao CMS pode criar um perfil com permissões arbitrárias.

O mesmo vale para `usuario.store` (embora o controller do usuário cheque
`usuario.create` no corpo) e para `perfil.update` / `perfil.destroy`.

### 3.2 `RoleController@store` não valida permissão alguma

Não há `has_access()` nem `Sentinel::inRole()` no corpo de
`RoleController@store` (`v6/app/Http/Controllers/Admin/RoleController.php:53-93`).
A única defesa é o middleware — que está quebrado (3.1).

### 3.3 `RoleController@update` usa `$permissions` não inicializado

```php
$permissions[$permission] = (bool)$value;   // linha ~140
```

`$permissions` só é definido dentro do `if (!is_null($request->permissions))`.
Se o campo não vier na requisição, o `foreach` nem executa — mas se vier
vazio (`permissions[]`), o `foreach` externo também não executa e
`$roleDate['permissions']` fica `[]`, **apagando todas as permissões** do
perfil. Salvar o formulário de perfil com a matriz desmarcada zera o perfil.

### 3.4 `UserController@update` tem `return` + `exit` dentro da transação

```php
if ($user->roles->first()->slug == 'super-admin' && !Sentinel::inRole('super-admin')) {
    flash(...)->warning();
    return redirect()->route('dashboard.index');
    exit;
}
```

O `return` dentro do closure de `DB::transaction` não encerra a transação
com sucesso: o código continua e o `$user` é atualizado mesmo assim. O
`exit` (linha 238) é código morto. O `flash` também é perdido, porque o
`redirect` não é retornado da action.

### 3.5 `UserController@inactivate` usa `Roles()` com R maiúsculo

`Sentinel::findById($id)->Roles()->get()` (linha 310). Funciona porque nomes
de método PHP são case-insensitive, mas quebra qualquer busca por `roles(`.

### 3.6 `UserController@inactive` pagina só para `super-admin`

```php
if (Sentinel::inRole('super-admin')) {
    $users = Sentinel::getUserRepository()->doesntHave('activations')->paginate(16);
} else {
    $users = ...->doesntHave('activations')->get();
}
```

Operadores comuns recebem **todos** os usuários inativos de uma vez (sem
paginação). Além disso, `$users` não é usado pela view: a tela renderiza
`admin.user.inactive` que inclui `admin.user.list` (DataTable), ignorando a
coleção. A paginação de 16 é, portanto, inefetiva.

### 3.7 `UserController@all` duplica `active`

`@all` (linha 413) e `@active` (linha 400) usam exatamente a mesma query
(`has('activations')`). O filtro "Todos" da listagem **não** inclui usuários
inativos. A diferença real está na view SQL `listar_todos_usuarios`, que
não filtra por `activations`.

### 3.8 `usuario.addRoles` declarado duas vezes

`v6/routes/basepro.php:50` e `:52` usam o mesmo nome `usuario.addRoles`. A
segunda (sem parâmetro `{user}`) sobrescreve a primeira. Como
`UserController@addRules($id)` exige um argumento, chamar
`route('usuario.addRoles')` gera `/cms/usuario/add/roles` e o controller
recebe `$id = null`. A tela renderizada seria `admin.user.roles` — **que não
existe**.

### 3.9 Views referenciadas que não existem

| View referenciada | Onde | Existe? |
|---|---|---|
| `admin.user.show` | `UserController@show` (171), `@profile` (466) | **Não** — existe `admin/user_new/show.blade.php` |
| `admin.user.roles` | `UserController@addRules` (490) | **Não** — existe `admin/user_new/roles.blade.php` |
| `admin.user.profile.edit` | `UserController@editProfile` (475) | **Não** — existe `admin/user_new/profile/edit.blade.php` |

Consequência: `GET /cms/usuario/{id}`, `GET /cms/usuario/{user}/add/roles` e
os métodos `profile`/`editProfile` produzem **erro de view não encontrada**.
`GET /cms/usuario/{id}/edit` continua funcionando porque `edit` usa
`admin.user.edit`.

> Atenção: `GET /cms/usuario/{id}` também é alcançável na prática, já que
> `Route::resource` gera `usuario.show` e a rota `usuario.inactive`
> (`/cms/usuario/inativos`) é declarada antes — mas `/cms/usuario/5` não tem
> rota anterior e cai em `usuario.show`.

### 3.10 Caminhos de asset com barras invertidas

`v6/resources/views/admin/user/index.blade.php` e `active.blade.php` usam:

```blade
<script src="{{ asset('assets\js\custom\contents\usuarios\listar_todos\datatable.js') }}"></script>
```

Em Linux, `\` não é separador de diretório: o `asset()` resolve a URL com
barras invertidas e o navegador não encontra o arquivo. O mesmo padrão
ocorre em `active.blade.php`. `admin/role/index.blade.php` e
`admin/permission/index.blade.php` usam o caminho correto (barras normais).

### 3.11 `has_access()` sem argumento retorna `true`

```php
if (!isset($access)) { return true; }
```

`v6/app/Library/Helper/appHelpers.php:28-30`. Qualquer chamada
`has_access()` sem parâmetro libera o acesso. O mesmo vale para
`has_access(null)`.

### 3.12 `Sentinel::inRole('super-admin')` em `SentinelFilter`

`v6/app/Library/Menu/Filters/SentinelFilter.php:33` verifica
`Sentinel::inRole('administrators')` (com "s" no final) — um perfil que
**não existe** na base (os perfis reais são `administrador`, sem "s").
Consequência: o atalho para o filtro do AdminLTE nunca é acionado.

### 3.13 `menuClicks` faz update em toda visita

`v6/app/Helpers/functions.php:865-871` faz `SELECT` + `UPDATE` em
`menu_orders` a cada `index` de módulo. Em um CMS com muitos operadores,
é uma escrita por carregamento de página.

### 3.14 `route('usuario.saveRules')` chama `RoleController@store`

`v6/routes/basepro.php:54` — `GET /cms/usuario/save/rules` executa a
**criação de perfil** por GET. Além do método HTTP inadequado, é a mesma
action que sofre do problema 3.1.

---

## 4. Código duplicado entre v5 e v6

| Arquivo | v5 vs v6 |
|---|---|
| `app/Http/Controllers/Admin/UserController.php` | **idêntico** |
| `app/Http/Controllers/Admin/PermissionController.php` | **idêntico** |
| `app/Http/Controllers/Admin/RoleController.php` | difere só em `edit()`: a v6 usa `Resource::with('modulo')->whereNotIn(...)->orderBy('modulo_id','asc')`; a v5 usava `Resource::whereNotIn(...)->get()` |
| `app/Http/Model/Admin/User.php` | idêntico (fork do Sentinel) |
| `resources/views/admin/user/**` | idênticos |
| `resources/views/admin/role/**` | idênticos |
| `resources/views/admin/permission/**` | idênticos |
| `resources/views/admin/activities-logs/index.blade.php` | idêntico |

---

## 5. Onde a v6 divergiu da v5 (e por quê)

| Mudança | Efeito |
|---|---|
| Nova tabela `modulos` (migration `2024_11_01_150206`) | habilita ligar/desligar módulos e **filtrar a matriz de permissões por módulo ativo** |
| `resources.modulo_id` (migration `2024_11_05_104026`) | filtro do item 1.7 |
| `menu_orders.modulo_id` (migration `2024_11_04_094157`) + `2026_09_01_100000_fix_menu_orders_missing_modulo_id` | o menu do CMS só mostra itens de módulo ativo |
| `RoleController@edit` passou a eager-loadar `modulo` e ordenar por `modulo_id` | matriz de permissões agrupada por módulo |
| `ModulosServiceProvider` injeta `$modulos` nas views | o portal e o dashboard passam a respeitar o liga/desliga |
| Nova tela `/cms/modulos` (`Admin\ModulosController`) | gestão dos módulos |

Motivo inferido: a unificação das linhas de produto (Prefeitura, Câmara, IPM)
descrita no changelog "Public V6" exigiu uma chave única de ativação por
módulo, e não apenas por recurso.

---

## 6. Riscos de migração de schema

1. **`roles.permissions` (JSON) → tabela.** Requer um script que leia o JSON
   de cada perfil e crie as linhas; **a ordem das chaves e o valor `false`**
   importam (o JSON armazena `false` explicitamente para permissões
   revogadas).
2. **`users.permissions`** pode ser descartado (não é exercitado) — mas
   confirme em produção antes de dropar a coluna.
3. **`role_users`** precisa de FK para `users` e `roles` (hoje só tem PK
   composta) e de unicidade efetiva (o `softDeletes` na pivô permite
   duplicatas "apagadas").
4. **Recriar a sequência de `permissions`** (hoje ids começam em 2) para
   evitar colisão ao recriar o catálogo.
5. **A matriz `resources` x `permissions`** deve virar um conjunto de
   "capacidades" (`usuario.listar`, `usuario.criar`, ...), e não a
   multiplicação atual de 79 x 9 = 711 combinações.
