porchlight/docs/plans/2026-02-18-admin-pages-design.md
2026-04-10 11:28:51 +02:00

95 lines
3.9 KiB
Markdown

# Admin Pages Design
## Overview
Admin pages for user management in the porchlight OIDC provider. Authenticated users with the `"admin"` group can list, search, view, edit, activate/deactivate, and delete users, manage group memberships, view/delete credentials, create invite links, and re-invite existing users.
## Routing & Auth
New router at `src/porchlight/admin/routes.py`, mounted at `/admin/` in `app.py`.
**Admin guard:** Every admin route fetches the full user via `user_repo.get_by_userid()` and checks `"admin" in user.groups`. Unauthenticated users redirect to `/login`. Non-admin authenticated users get 403.
### Endpoints
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/admin/users` | User list (paginated, searchable) |
| GET | `/admin/users/{userid}` | User detail page |
| POST | `/admin/users/{userid}/profile` | Update user profile |
| POST | `/admin/users/{userid}/groups` | Update group memberships |
| POST | `/admin/users/{userid}/activate` | Activate user |
| POST | `/admin/users/{userid}/deactivate` | Deactivate user |
| DELETE | `/admin/users/{userid}/credentials/password` | Delete user's password |
| DELETE | `/admin/users/{userid}/credentials/webauthn/{cred_id}` | Delete a WebAuthn key |
| POST | `/admin/users/{userid}/invite` | Generate re-invite link |
| DELETE | `/admin/users/{userid}` | Delete user entirely |
| POST | `/admin/invite` | Create invite for new username |
## Templates & UI
### Template Structure
```
templates/admin/
base.html -- extends base.html, adds admin nav + admin label
users.html -- user list table
user_detail.html -- single-user detail with sections
```
### User List Page (`/admin/users`)
- Search input at top (HTMX GET to filter, targets table body)
- Table columns: Username, Name, Email, Groups, Status, Created
- Each row links to detail page
- Active/inactive toggle button per row (HTMX POST, swaps button)
- Pagination controls (prev/next, HTMX)
- "Create Invite" form -- enter username, generates magic link URL
### User Detail Page (`/admin/users/{userid}`)
Single page with sections, each with its own HTMX form/target:
1. **Profile** -- Same editable fields as self-service (given_name, family_name, preferred_username, email, phone_number, picture, locale). Username displayed read-only. HTMX POST.
2. **Groups** -- Current groups as removable tags/chips. Text input to add groups. HTMX POST replaces full group list.
3. **Credentials** -- Read-only list: password (exists/doesn't), WebAuthn keys (device name, created). Delete buttons per credential (HTMX DELETE with confirmation).
4. **Actions** -- Re-invite button (shows generated URL). Delete user button (with confirmation). Activate/deactivate toggle.
### New CSS
- `.admin-table` -- bordered table with hover rows
- `.group-tag` -- removable group chips
- `.status-badge` / `.status-active` / `.status-inactive` -- status indicators
## Data Layer Changes
### No Schema Changes
Existing tables cover all needs: `users`, `user_groups`, `webauthn_credentials`, `password_credentials`, `magic_links`.
### Repository Additions
Add to `UserRepository` protocol and SQLite implementation:
- `search_users(query: str, offset: int, limit: int) -> list[User]` -- SQL LIKE on username and email
- `count_users(query: str | None) -> int` -- total count for pagination
Groups update: fetch user, modify `user.groups`, call `update(user)` (existing method handles group replacement).
## Testing
### Python Unit Tests
- Admin guard (403 for non-admin, redirect for unauthenticated)
- `search_users()` and `count_users()` repository methods
- Route handlers: profile update, group update, activate/deactivate, delete, invite
### E2E Playwright Tests
- Auth guard (non-admin blocked from `/admin/`)
- User list: pagination, search, inline activate/deactivate
- User detail: edit profile, manage groups, view credentials, delete credential, re-invite, delete user
- Create invite from admin UI