# Page Permission – Route and Test Coverage

This document lists all protected routes, which permission set may access them, and how they are covered by tests.

## Protected Routes (Router scope with CheckPagePermission in :browser)

| Route | own_data | read_only | normal_user | admin |
|-------|----------|-----------|-------------|-------|
| `/` | ✗ | ✓ | ✓ | ✓ |
| `/members` | ✗ | ✓ | ✓ | ✓ |
| `/members/new` | ✗ | ✗ | ✓ | ✓ |
| `/members/:id` | ✓ (linked only) | ✓ | ✓ | ✓ |
| `/members/:id/edit` | ✓ (linked only) | ✗ | ✓ | ✓ |
| `/members/:id/show/edit` | ✓ (linked only) | ✗ | ✓ | ✓ |
| `/users` | ✗ | ✗ | ✗ | ✓ |
| `/users/new` | ✗ | ✗ | ✗ | ✓ |
| `/users/:id` | ✓ (own only) | ✓ (own only) | ✓ (own only) | ✓ |
| `/users/:id/edit` | ✓ (own only) | ✓ (own only) | ✓ (own only) | ✓ |
| `/users/:id/show/edit` | ✓ (own only) | ✓ (own only) | ✓ (own only) | ✓ |
| `/settings` | ✗ | ✗ | ✗ | ✓ |
| `/membership_fee_settings` | ✗ | ✗ | ✗ | ✓ |
| `/membership_fee_settings/new_fee_type` | ✗ | ✗ | ✗ | ✓ |
| `/membership_fee_settings/:id/edit_fee_type` | ✗ | ✗ | ✗ | ✓ |
| `/groups` | ✗ | ✓ | ✓ | ✓ |
| `/groups/new` | ✗ | ✗ | ✗ | ✓ |
| `/groups/:slug` | ✗ | ✓ | ✓ | ✓ |
| `/groups/:slug/edit` | ✗ | ✗ | ✗ | ✓ |
| `/statistics` | ✗ | ✓ | ✓ | ✓ |
| `/admin/roles` | ✗ | ✗ | ✗ | ✓ |
| `/admin/roles/new` | ✗ | ✗ | ✗ | ✓ |
| `/admin/roles/:id` | ✗ | ✗ | ✗ | ✓ |
| `/admin/roles/:id/edit` | ✗ | ✗ | ✗ | ✓ |
| `/join_requests` | ✗ | ✗ | ✓ | ✓ |
| `/join_requests/:id` | ✗ | ✗ | ✓ | ✓ |
| `/admin/datafields` | ✗ | ✗ | ✗ | ✓ |
| `/admin/import` | ✗ | ✗ | ✗ | ✓ |
| `/admin/import/template/en` | ✗ | ✗ | ✗ | ✓ |
| `/admin/import/template/de` | ✗ | ✗ | ✗ | ✓ |
| `/members/export.csv` | ✗ | ✓ | ✓ | ✓ |
| `/members/export.pdf` | ✗ | ✗ | ✗ | ✓ |

**Note:** Permission sets define `/custom_field_values` and related paths, but there are no such routes in the router; those entries are for future use. The Approval UI routes `/join_requests` and `/join_requests/:id` are implemented and routed: `normal_user` lists them explicitly in its permission set, and `admin` reaches them through the `*` wildcard.

**Note on admin-only routes:** `/admin/datafields`, `/admin/import`, `/admin/import/template/en`, `/admin/import/template/de`, and `/members/export.pdf` are not listed explicitly in any permission set; only `admin` can reach them, via the `*` wildcard. `/members/export.csv` is additionally granted explicitly to `read_only` and `normal_user`.

## Public Paths (no permission check)

- `/auth*`, `/register`, `/reset`, `/sign-in`, `/sign-out`, `/confirm*`, `/password-reset*`, `/set_locale`, **`/join`**

The public join page `GET /join` is explicitly public (Subtask 4); unauthenticated access returns 200 when join form is enabled, 404 when disabled. Unit test: `test/mv_web/plugs/check_page_permission_test.exs` (plug allows /join); integration: `test/mv_web/live/join_live_test.exs`.

The join confirmation route `GET /confirm_join/:token` is public (matched by `/confirm*`). Unit tests: `test/mv_web/controllers/join_confirm_controller_test.exs` (stubbed callback, no integration).

## Test Coverage

**File:** `test/mv_web/plugs/check_page_permission_test.exs` covers both unit tests (plug called directly with a mock conn) and full-router integration tests. The route→permission-set matrix above is the source of truth; each permission set (own_data/Mitglied, read_only, normal_user/Kassenwart, admin) is exercised there. Allowed routes return 200; denied routes return 302 → `/users/:id`. `GET /` redirects own_data to its profile. Unauthenticated access is denied and redirected to `/sign-in`; public paths (`/auth/sign-in`, `/register`) are allowed. Error cases (no role, invalid permission_set_name) deny.

Two coverage notes:

- **Linked-member routes** (`/members/:id*` for own_data) are covered by plug unit tests; full-router integration tests for the linked member are skipped due to session/LiveView constraints.
- **Join requests:** normal_user and admin are allowed `/join_requests` and `/join_requests/:id` (normal_user via its explicit permission-set pages, admin via the `*` wildcard); read_only and own_data are denied.

## Plug behaviour: reserved segments

The plug treats `"new"` as a reserved path segment so that patterns like `/members/:id` and `/groups/:slug` do not match `/members/new` or `/groups/new`. Thus `/groups/new` is only allowed when the permission set explicitly lists `/groups/new` (currently only admin).

## Role and member_id loading

The plug may reload the user's role (and optionally `member_id`) before checking page permission. Session/`load_from_session` can leave the role unloaded; the plug uses `Mv.Authorization.Actor.ensure_loaded/1` (and, when needed, loads `member_id`) so that permission checks always have the required data. No change to session loading is required; this is documented for clarity.
