# User Roles & Permissions

## Membership vs. Role — kept separate (spec section 7)

- `members.membership_category_id` → General / Prime / Pioneer. Describes a
  person's relationship with the development.
- `users.role_id` (nullable) → an organizational role. Describes what a
  person may *do* in the system. NULL = plain member, no staff role.

A user can have a membership with no role, a role with no membership
(pure staff), or both.

## Roles seeded

Super Admin, System Administrator, Director, Manager, Finance Manager,
Accountant, Project Manager, Property Manager, Sales Manager, Content
Manager, HR Manager, Procurement Officer, Agent, Staff — all rows in the
`roles` table, not hard-coded in code. New roles can be inserted without
touching a single controller.

## Permission check

```php
if (can('finance.approve')) { ... }
```

`can()` (app/Helpers/permission_helper.php) resolves the current session's
`role_id` → its granted permission slugs (module.action) via
`role_permissions`. `super-admin` always passes. Result is memoized per
request.

## Access boundary

- `/adm/*` — requires `session('role_id')` to be set (any staff role).
  Plain members are bounced back to `/adm/login`.
- `/portal/*` — requires only a logged-in session (`auth` filter, no role
  arg) — for members.
- A specific action can further restrict by role slug:
  `$routes->get('finance', 'Adm\Finance::index', ['filter' => 'auth:finance-manager,accountant'])`.

## Still to build

An `/adm/roles` UI to edit role→permission grants and `/adm/users` to
assign roles — currently seed-data only, no admin UI yet (placeholder
pages exist at those routes).
