# Phase 1 — Module 3: Roles & Permissions

Everything in the app is now permission-based, exactly as specced. Adds Role
CRUD, a Permission catalog (grouped by module), a visual permission matrix
(module × view/create/edit/delete/export/print/approve), and per-route
enforcement via a new `permission:` filter.

## What's included

| Layer | Files |
|---|---|
| Migrations | `permissions` (module-grouped, `is_system` protected), `role_permissions` (pivot) |
| Seeders | `PermissionSeeder` (system catalog for dashboard/users/roles/permissions), `RolePermissionSeeder` (grants every permission to Administrator, idempotent/re-runnable as new modules add permissions) |
| Models | `App\Models\{PermissionModel,RolePermissionModel}`, extended `RoleModel` (validation, `isInUse()`, `generateSlug()`) |
| Services | `Modules\Roles\Services\{RoleService,PermissionService}` |
| Controllers | `Modules\Roles\Controllers\{RoleController,PermissionController}` |
| Filter | `Modules\Auth\Filters\PermissionFilter`, registered as `permission` in `Config/Filters.php` — apply per-route: `['filter' => 'permission:users.view']` |
| Views | `Modules/Roles/Views/{roles_index,role_create,role_edit,permissions_index}.php` |
| Routes | `Modules/Roles/Config/Routes.php` |

## How permission enforcement works

1. Every protected route still runs `auth` at the group level (login + idle timeout + session-revocation checks).
2. Routes that need a specific capability *also* declare `['filter' => 'permission:{module}.{action}']` at the individual-route level — CI4 runs both filters.
3. `PermissionFilter` reads `role_id` out of the session (set at login by `AuthService::establishSession()`) and checks `role_permissions` for that role + the required slug.
4. Administrator gets every permission via `RolePermissionSeeder` — no special-cased bypass code, so the same mechanism that protects a route also governs what Administrator can do (transparent, auditable).

**Retrofit note:** the User Management module's routes were updated to use
real `permission:users.*` checks instead of the placeholder blanket `auth`
filter from Module 2 — this is now the pattern for every future module.

## Setup (adds to previous steps)

```bash
php spark migrate       # picks up permissions + role_permissions
php spark db:seed DatabaseSeeder   # now also seeds permissions + grants them to Administrator
```

If you're upgrading an existing install rather than starting fresh, re-run
`php spark db:seed DatabaseSeeder` any time — every seeder here is
idempotent (checks for existing rows before inserting).

## Adding permissions for future modules

1. Add the module and its actions to `PermissionSeeder::$map`.
2. Re-run `php spark db:seed PermissionSeeder` then `php spark db:seed RolePermissionSeeder` (the latter re-grants everything to Administrator, including the new ones).
3. Add `['filter' => 'permission:{module}.{action}']` to the new module's routes.
4. Non-administrator roles need those permissions checked manually in the matrix (`/roles/{id}/edit`) — they don't get new permissions automatically, by design.

## Known placeholders

- No UI yet to bulk-copy a permission set from one role to another — copy/paste in the matrix for now, or seed it directly.
- `permissions.edit` isn't a real capability (permissions are created/deleted, not edited, to keep slugs stable) — this is intentional, not missing.

## Testing checklist

1. Log in as the seeded admin, confirm `/roles` and `/permissions` both load.
2. Create a new role, leave every permission unchecked, save the matrix.
3. Create a second user, assign them the new role, log in as them — confirm they're bounced back to `/dashboard` with a "you do not have permission" message when hitting `/users`.
4. Check only `users.view` for that role, confirm the same user can now browse `/users` but posting to create/edit/delete still redirects away.
5. Attempt to delete the Administrator role or a role currently assigned to a user — both should be blocked with a clear error.
