# Usuários — Migração para Laravel Moderno

> Plano de reconstrução do módulo de usuários, perfis e permissões em
> **PHP 8.x + Laravel 11/12**. O objetivo é **equivalência funcional**: as
> mesmas telas, as mesmas decisões de autorização e as mesmas listas.

---

## 1. Decisões de equivalência (o que NÃO pode mudar)

| Item | Regra |
|---|---|
| Chave de permissão | Manter o formato `<recurso-slug>.<acao-slug>` (ex.: `usuario.index`) ou migrar **todos** os consumidores de uma vez. Recomenda-se manter. |
| `super-admin` | Deve continuar sendo um perfil que **ignora** toda checagem, e continuar protegido contra edição por não-super-admin. |
| Estado ativo/inativo | Deve continuar sendo **derivado** (não há coluna `status`); sugiro trocar para coluna explícita `is_active` na v7, mas a semântica observável (inativo ⇒ não autentica) deve ser mantida. |
| Listagens | As três listas (Todos / Ativos / Inativos) devem continuar existindo e continuar retornando exatamente os mesmos conjuntos. |
| E-mail como login | Manter `users.email` único. |
| Auditoria | Toda criação/edição/inativação de usuário e perfil deve continuar gravando em log de auditoria. |

---

## 2. Schema de migração Laravel

```php
// 0001_01_01_000000_create_users_table.php
Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name')->nullable();
    $table->string('email')->unique();
    $table->timestamp('email_verified_at')->nullable();
    $table->string('password');
    $table->rememberToken();
    $table->boolean('is_active')->default(true);   // substitui a tabela 'activations'
    $table->timestamp('last_login')->nullable();
    $table->timestamps();
    $table->softDeletes();
});

Schema::create('roles', function (Blueprint $table) {
    $table->id();
    $table->string('slug')->unique();
    $table->string('name');
    $table->timestamps();
    $table->softDeletes();
});

Schema::create('permissions', function (Blueprint $table) {
    $table->id();
    $table->string('slug')->unique();     // index, create, edit, delete, inactive, inactivate, restore, show, publish
    $table->string('name');
    $table->string('description');
    $table->timestamps();
    $table->softDeletes();
});

// chave legada "recurso.acao" preservada
Schema::create('capabilities', function (Blueprint $table) {
    $table->id();
    $table->foreignId('resource_id')->constrained('resources');
    $table->foreignId('permission_id')->constrained('permissions');
    $table->string('key')->unique();      // "usuario.index"
    $table->timestamps();
});

Schema::create('role_user', function (Blueprint $table) {
    $table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
    $table->foreignId('role_id')->constrained('roles')->cascadeOnDelete();
    $table->timestamps();
    $table->primary(['user_id', 'role_id']);
});

Schema::create('role_capability', function (Blueprint $table) {
    $table->foreignId('role_id')->constrained('roles')->cascadeOnDelete();
    $table->foreignId('capability_id')->constrained('capabilities')->cascadeOnDelete();
    $table->primary(['role_id', 'capability_id']);
});
```

**Script de migração de dados** (a parte mais delicada):

```php
// 1. expanding: ler roles.permissions (JSON) e gravar role_capability
foreach (DB::table('roles')->get() as $role) {
    $perms = json_decode($role->permissions ?: '{}', true) ?: [];
    foreach ($perms as $key => $granted) {
        if (!$granted) continue;                       // false = revogada
        [$resourceSlug, $actionSlug] = explode('.', $key, 2);
        $resourceId = DB::table('resources')->where('slug', $resourceSlug)->value('id');
        $permissionId = DB::table('permissions')->where('slug', $actionSlug)->value('id');
        if (!$resourceId || !$permissionId) {
            Log::warning("Permissão órfã na migração: {$key} (role {$role->slug})");
            continue;
        }
        $capabilityId = DB::table('capabilities')->updateOrInsert(
            ['resource_id' => $resourceId, 'permission_id' => $permissionId],
            ['key' => $key, 'updated_at' => now(), 'created_at' => now()]
        );
        DB::table('role_capability')->insertOrIgnore([
            'role_id' => $role->id, 'capability_id' => $capabilityId,
        ]);
    }
}

// 2. users.is_active a partir de activations
DB::statement('UPDATE users SET is_active = EXISTS (
    SELECT 1 FROM activations a WHERE a.user_id = users.id AND a.completed = 1
)');
```

> O `Log::warning` é obrigatório: a base atual tem perfis com chaves de
> permissão cujo recurso não existe (ex.: `site.index`, `midias.index`),
> herdados de instalações antigas.

---

## 3. Modelos

```php
// app/Models/User.php
final class User extends Authenticatable
{
    use HasFactory, Notifiable, SoftDeletes;

    protected $fillable = ['name', 'email', 'password', 'is_active'];
    protected $hidden = ['password', 'remember_token'];
    protected $casts = [
        'email_verified_at' => 'datetime',
        'last_login'       => 'datetime',
        'is_active'        => 'boolean',
        'password'         => 'hashed',
    ];

    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class)->withTimestamps();
    }

    public function capabilities(): BelongsToMany
    {
        return $this->belongsToMany(Capability::class, 'role_capability', 'role_id', 'capability_id')
            ->using(RoleCapabilityPivot::class);
    }
}
```

```php
// app/Models/Role.php
final class Role extends Model
{
    use SoftDeletes;
    protected $fillable = ['slug', 'name'];

    public function capabilities(): BelongsToMany
    {
        return $this->belongsToMany(Capability::class, 'role_capability');
    }

    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class);
    }
}
```

Sugestão de **enums**:

```php
enum PermissionAction: string
{
    case Index     = 'index';
    case Create    = 'create';
    case Edit      = 'edit';
    case Delete    = 'delete';
    case Inactive  = 'inactive';
    case Inactivate= 'inactivate';
    case Restore   = 'restore';
    case Show      = 'show';
    case Publish   = 'publish';
}
```

> O enum **não** pode ser a fonte de verdade no banco: o catálogo `permissions`
> é administrável pelo CMS (`/cms/permissao`). O enum serve para o código
> (evita *typos* em `has_access`).

---

## 4. Autorização: substituir `has_access()` e `check.route.permission`

### 4.1 Gate único

```php
// app/Providers/AuthServiceProvider.php
Gate::before(fn (User $user) => $user->hasRole('super-admin') ? true : null);

Gate::define('capability', fn (User $user, string $key) => $user->hasCapability($key));
```

### 4.2 Helper compatível (para migrar gradually)

```php
if (!function_exists('has_access')) {
    function has_access(string|array|null $access = null): bool
    {
        $user = auth()->user();
        if (!$user) return false;
        if ($access === null) return true;              // comportamento legado (item 3.11 do 05)
        if ($user->hasRole('super-admin')) return true;  // comportamento legado
        $keys = (array) $access;
        return $user->capabilities->contains(
            fn (Capability $c) => in_array($c->key, $keys, true)
        );
    }
}
```

### 4.3 Policies

```php
final class UserPolicy
{
    public function viewAny(User $u): bool   { return $u->hasCapability('usuario.index'); }
    public function create(User $u): bool    { return $u->hasCapability('usuario.create'); }
    public function update(User $u, User $t): bool
    {
        if ($t->roles->contains('slug', 'super-admin') && !$u->hasRole('super-admin')) return false;
        return $u->hasCapability('usuario.edit') || $u->is($t);
    }
    public function delete(User $u, User $t): bool
    {
        return $u->hasCapability('usuario.delete') && !$t->is($u);
    }
    public function restore(User $u, User $t): bool { return $u->hasCapability('usuario.restore'); }
    public function inactivate(User $u, User $t): bool
    {
        if ($t->roles->contains('slug', 'super-admin') && !$u->hasRole('super-admin')) return false;
        return $u->hasCapability('usuario.inactivate');
    }
}
```

> A regra "não-super-admin não edita quem tem `super-admin`" **tem** de
> estar na policy — no legado ela é replicada em 4 lugares do controller.

### 4.4 Middleware

Substituir `check.route.permission` (que tem a falha do item 3.1 do dossiê
`05-REGRAS-E-PORTABILIDADE.md`) por algo determinístico:

```php
final class EnsureRouteCapability
{
    public function handle(Request $request, Closure $next): Response
    {
        if (!auth()->check()) return redirect()->route('login');

        $routeName = $request->route()?->getName() ?? '';
        if ($routeName === '' || str_starts_with($routeName, 'datatable.')) {
            return $next($request);
        }

        // único formato aceito: recurso.acao
        if (!preg_match('/^[a-z0-9\-]+\.[a-z\-]+$/', $routeName)) {
            abort(403);
        }

        abort_unless(Gate::allows('capability', $routeName), 403);

        return $next($request);
    }
}
```

Alternativa (preferida na v7): **não usar middleware** para autorização e
fazer a checagem por Policy em cada action, com `authorize()` no Form
Request. O middleware deixa de ser necessário.

---

## 5. Form Requests

```php
final class StoreUserRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', User::class);
    }

    public function rules(): array
    {
        return [
            'name'     => ['required', 'string', 'max:255'],
            'email'    => ['required', 'email', 'max:255', Rule::unique('users', 'email')],
            'password' => ['required', 'string', 'min:6', 'confirmed'],
            'roles'    => ['required', 'array', 'min:1'],
            'roles.*'  => ['integer', 'exists:roles,id', Rule::notIn([$this->forbiddenRoleId()])],
        ];
    }
}
```

```php
final class UpdateUserRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('update', $this->route('usuario'));
    }

    public function rules(): array
    {
        return [
            'name'     => ['required', 'string', 'max:255'],
            'email'    => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($this->route('usuario'))],
            'password' => ['nullable', 'string', 'min:6', 'confirmed'],
            'roles'    => ['required', 'array', 'min:1'],
            'roles.*'  => ['integer', 'exists:roles,id'],
        ];
    }
}
```

```php
final class StoreRoleRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Role::class);
    }

    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255', Rule::unique('roles', 'slug')],
            'capabilities' => ['array'],
            'capabilities.*' => ['integer', 'exists:capabilities,id'],
        ];
    }
}
```

> O `Rule::notIn` com o id do `super-admin` implementa de forma declarativa
> a proteção que o legado faz em 4 pontos do `UserController`.

---

---

*Continua em `09-API-VIEWS-E-INFRA.md` (endpoints REST, API Resources, views Blade modernas, fila/cache/storage e riscos de equivalência).*
