# 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