a8dfbd5dc6
Delete the old top-level page files whose content was migrated into service-page tabs in slices 5-9: - pages/Media.tsx, Applications.tsx (-> MediaTab) - pages/FileBrowser.tsx, FileBrowser.impl.tsx (-> FilesTab) - pages/Actions.tsx (-> ActionsTab) - pages/Users.tsx, UsersPage.impl.tsx (replaced by Authentik tabs) - components/BackupsPage.tsx (-> JobsTab) - components/ObservabilityPage.tsx (split into Alerts/Links/Metrics tabs) - hooks/useUsers.ts (orphaned after Users page deletion) - the corresponding page test files (Media, FileBrowser, Applications, Actions, UsersPage) that tested the deleted pages directly. The service-tab components are the live implementations; ServicePage renders them. No live code references the deleted files. Docs: append an Information Architecture section to REQUIREMENTS.md documenting the services-as-hub model (nav shape, service-page tabs, service type registry, Users->Authentik, Observability split, legacy route 404s, empty state). Add a CHANGELOG entry under [Unreleased]. 92 frontend tests pass (was 112; -20 deleted page tests); 271 backend tests pass; lint/build green. Refs openspec/changes/services-as-hub-ia/ (tasks slice 11).
144 lines
7.1 KiB
Markdown
144 lines
7.1 KiB
Markdown
# Slice 2 — Authentik directory client + endpoint (worker output)
|
|
|
|
## Files changed
|
|
|
|
| File | Status | Lines |
|
|
|------|--------|-------|
|
|
| `backend/src/media_library_viewer_api/clients/authentik.py` | new | 133 |
|
|
| `backend/src/media_library_viewer_api/routers/authentik_users.py` | new | 88 |
|
|
| `backend/src/media_library_viewer_api/main.py` | modified | +4 / -1 |
|
|
| `backend/tests/test_authentik_client.py` | new | 175 |
|
|
|
|
**Total: ~400 changed lines** (400 insertions, 1 deletion). At the 400-line budget.
|
|
|
|
## What was implemented
|
|
|
|
### 2.1 — AuthentikClient (`clients/authentik.py`)
|
|
|
|
- `AuthentikClient(base_url, api_token, timeout=10.0)` — mirrors the JellyseerrClient pattern.
|
|
- `requests.Session()` with `Authorization: Bearer <token>` header + `Accept: application/json`.
|
|
- base_url normalization: rstrip "/" and strip trailing `/api/v3` suffix.
|
|
- `get(path, **params)` helper — same error-logging pattern as JellyseerrClient (raise_for_status with detail text on HTTPError).
|
|
- `users(search, page, page_size)` — calls `GET /api/v3/core/users/` with query params `search`, `page`, `page_size`. Normalizes the Authentik `{pagination: {count}, results: [...]}` response shape into `{items, total, page, page_size}`. Handles empty results and non-dict payloads defensively.
|
|
- `ValueError` on empty base_url or api_token.
|
|
- Module-level logger.
|
|
|
|
### 2.2 — Directory endpoint (`routers/authentik_users.py`)
|
|
|
|
- `GET /api/services/authentik/{service_id}/users` — resolves the service record, builds an AuthentikClient from config + decrypted `api_token` secret, calls `users()`.
|
|
- Query params: `search: str | None = None`, `page: int = 1`, `page_size: int = 50`.
|
|
- Graceful error handling matching monitoring.py's pattern:
|
|
- Service not configured → `{"items": [], "total": 0, ..., "error": "Authentik service not configured"}` with 200.
|
|
- Request failure → `{"items": [], ..., "error": "Authentik is unreachable"}` with 200, logs the exception.
|
|
- `_resolve_service_record` helper copied into the new router (type-specific to `authentik`; the monitoring.py one is generic but takes `service_type` as a param — copying keeps the new router self-contained without restructuring monitoring.py).
|
|
- Router registered in `main.py`.
|
|
|
|
### Authentik API endpoint shape
|
|
|
|
```
|
|
GET /api/services/authentik/{service_id}/users?search=ali&page=1&page_size=50
|
|
|
|
Response (success):
|
|
{
|
|
"items": [{"pk": 1, "username": "alice", "email": "...", "avatar": "...", ...}],
|
|
"total": 42,
|
|
"page": 1,
|
|
"page_size": 50
|
|
}
|
|
|
|
Response (not configured / unreachable):
|
|
{
|
|
"items": [],
|
|
"total": 0,
|
|
"page": 1,
|
|
"page_size": 50,
|
|
"error": "Authentik service not configured" | "Authentik is unreachable"
|
|
}
|
|
```
|
|
|
|
## Validation
|
|
|
|
```
|
|
cd backend && .venv/bin/ruff check src/ tests/ → All checks passed!
|
|
cd backend && .venv/bin/python -m pytest tests/ → 268 passed, 2 warnings (pre-existing)
|
|
```
|
|
|
|
New tests: 12 (8 client unit tests + 3 endpoint integration tests + 1 get URL/params assertion).
|
|
|
|
## Deviations from design
|
|
|
|
1. **`_resolve_service_record` copied rather than imported.** The monitoring.py helper takes `(store, service_type, service_id)` and is tightly coupled to monitoring's imports. Copying the ~15 lines into the new router (hardcoding `service_type="authentik"`) keeps the new router self-contained. A follow-up refactor could extract a shared `resolve_service_record` utility.
|
|
|
|
2. **`timeout` config parsing is guarded.** Added a `try/except (TypeError, ValueError)` around `float(config.get("timeout_seconds") or 10)` to handle a malformed config value gracefully (falls back to 10.0). Minor defensive addition not named in the design.
|
|
|
|
## skill_resolution
|
|
|
|
`none` — no project/user SKILL.md paths were injected, and no `.atl/skill-registry.md` was found.
|
|
|
|
## Residual risks
|
|
|
|
- The Authentik directory API field coverage (`avatar`, `is_active`, `attributes`, groups, etc.) is not pinned — the client returns raw user dicts and the frontend (Slice 8 UsersTab) will pick fields. Some fields the old compose flow used (Jellyfin activity state, Jellyseerr enrichment) will not be available from Authentik.
|
|
- `_resolve_service_record` is duplicated across `monitoring.py` and the new `authentik_users.py`. A shared utility extraction is a follow-up.
|
|
|
|
## Acceptance
|
|
|
|
```acceptance-report
|
|
{
|
|
"criteriaSatisfied": [
|
|
{
|
|
"id": "criterion-1",
|
|
"status": "satisfied",
|
|
"evidence": "Slice 2 implements AuthentikClient + directory endpoint + tests without widening scope (only authentik.py, authentik_users.py, main.py, test file). Mirrors JellyseerrClient + monitoring.py patterns. 268 backend tests pass; ruff clean."
|
|
}
|
|
],
|
|
"changedFiles": [
|
|
"backend/src/media_library_viewer_api/clients/authentik.py",
|
|
"backend/src/media_library_viewer_api/routers/authentik_users.py",
|
|
"backend/src/media_library_viewer_api/main.py",
|
|
"backend/tests/test_authentik_client.py"
|
|
],
|
|
"testsAddedOrUpdated": [
|
|
"backend/tests/test_authentik_client.py"
|
|
],
|
|
"commandsRun": [
|
|
{
|
|
"command": "cd backend && .venv/bin/ruff check src/ tests/",
|
|
"result": "passed",
|
|
"summary": "All checks passed (after --fix import sorting)"
|
|
},
|
|
{
|
|
"command": "cd backend && .venv/bin/python -m pytest tests/test_authentik_client.py -v",
|
|
"result": "passed",
|
|
"summary": "12 passed (8 client + 4 endpoint)"
|
|
},
|
|
{
|
|
"command": "cd backend && .venv/bin/python -m pytest tests/ -q",
|
|
"result": "passed",
|
|
"summary": "268 passed, 2 warnings (pre-existing deprecation warnings)"
|
|
},
|
|
{
|
|
"command": "git diff --cached --stat",
|
|
"result": "passed",
|
|
"summary": "Empty — no staged files"
|
|
}
|
|
],
|
|
"validationOutput": [
|
|
"AuthentikClient mirrors JellyseerrClient: Session, Bearer header, base_url normalization, get() helper with raise_for_status + detail logging.",
|
|
"users() normalizes Authentik {pagination, results} into {items, total, page, page_size}; handles empty + non-dict payloads.",
|
|
"GET /api/services/authentik/{id}/users resolves service record, builds client from decrypted secret, returns graceful error dict on not-configured/unreachable (200, matching monitoring.py).",
|
|
"Router registered in main.py alongside existing routers.",
|
|
"268 backend tests pass (+12 new); ruff clean."
|
|
],
|
|
"residualRisks": [
|
|
"Authentik directory API field coverage not pinned (frontend UsersTab will pick fields in Slice 8).",
|
|
"_resolve_service_record duplicated across monitoring.py and authentik_users.py (shared utility extraction is a follow-up)."
|
|
],
|
|
"noStagedFiles": true,
|
|
"diffSummary": "Adds AuthentikClient (clients/authentik.py, 133 lines) with Bearer-auth session + users() pagination normalization, a directory endpoint (routers/authentik_users.py, 88 lines) at GET /api/services/authentik/{id}/users with graceful error handling, main.py router registration (+4 lines), and 12 new tests (175 lines). 400 lines total, at budget.",
|
|
"reviewFindings": [
|
|
"no blockers"
|
|
],
|
|
"manualNotes": "git status confirms nothing is staged; all changes are unstaged/untracked, ready for the parent to review and commit. The _resolve_service_record helper was copied (not imported) to keep the new router self-contained; monitoring.py was not modified."
|
|
}
|
|
```
|