Files
curo1305 cfec3bb906 feat: Phase 4+5 — admin storage UI, backend proxy, CLAUDE.md enforcement
- backend/app/routers/storage_config.py: 5 admin-only endpoints proxying
  storage-service config + migration API (GET/PATCH/POST/DELETE)
- backend/app/main.py: register storage_config router
- frontend/src/api/client.ts: StorageStatus, MigrationStatus,
  StorageBackendConfig interfaces + 5 API functions
- frontend/src/pages/StorageAdminPage.tsx: full admin UI — backend health
  dot, driver selector (local/S3/WebDAV), conditional credential fields,
  Test & Migrate button, live 2s-poll migration progress bar, Cancel
- frontend/src/App.tsx: /admin/storage route (AdminRoute guard)
- CLAUDE.md: storage enforcement rule, updated Docker tables (6 services,
  3 volumes), §20 in merge checklist
- backend/CLAUDE.md, frontend/CLAUDE.md, doc-service/CLAUDE.md,
  ai-service/CLAUDE.md: updated to reflect storage-service integration
- tests/ALL_TESTS.md + tests/storage-service_tests.md: §20 (20 tests)
- backend/STATUS.md, frontend/STATUS.md: updated with new endpoints/routes
- changelog/2026-04-20_storage-service.md: full change log

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 16:13:05 +02:00

184 lines
9.0 KiB
Markdown

# Backend — Status
## What it is
Central FastAPI gateway. Handles authentication, user management, admin settings, and proxies feature-service traffic. It is the only container that has host-level port exposure (`8000`, internal) — all browser traffic arrives via the Vite/nginx frontend proxy.
Port: `8000` (on `backend-net`, no direct host binding in prod).
Database: PostgreSQL 16 (`postgres_data` named volume).
---
## Current functionality
### Auth (`/api/auth`)
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/api/auth/register` | Create account; password policy enforced (uppercase, special char, no "test") |
| `POST` | `/api/auth/login` | OAuth2 password flow; returns RS256 JWT (8-hour expiry) |
JWT signing uses a 4096-bit RSA key pair (`RS256`). Keys are generated by `scripts/generate_jwt_keys.py` and stored in `backend/.env` (gitignored). Token stored in `localStorage` on the client.
### Users (`/api/users`)
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/users/me` | Current user info |
| `GET` | `/api/users/me/preferences` | User's dashboard preferences (`app_ids` list) |
| `PATCH` | `/api/users/me/preferences` | Update pinned app IDs (max 50; validated as safe slugs) |
| `GET` | `/api/users/me/groups` | List groups the current user belongs to (for share picker) |
### Profile (`/api/profile`)
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/profile` | Fetch profile (separate `profiles` table) |
| `PUT` | `/api/profile` | Update profile fields |
### Admin (`/api/admin`)
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/admin/users` | List all users (admin only) |
| `PATCH` | `/api/admin/users/{id}` | Update user (role, active flag) |
### Groups (`/api/admin/groups`)
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/admin/groups` | List all groups with member count |
| `POST` | `/api/admin/groups` | Create a new group |
| `GET` | `/api/admin/groups/{id}` | Get group detail with member list |
| `PATCH` | `/api/admin/groups/{id}` | Update group name / description |
| `DELETE` | `/api/admin/groups/{id}` | Delete group (cascades memberships) |
| `POST` | `/api/admin/groups/{id}/members/{user_id}` | Add user to group |
| `DELETE` | `/api/admin/groups/{id}/members/{user_id}` | Remove user from group |
### Services (`/api/services`)
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/services` | Returns health status of all registered feature services |
A background task (`service_health.py`) polls each service's `/health` endpoint every 30 s and stores the result in memory. The first check runs immediately on startup. Any authenticated user may call `GET /api/services`; the frontend uses it to drive app card visibility.
### Settings (`/api/settings`)
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/settings/ai` | AI service config (masked) — superuser OR `ai-service-admin` member |
| `PATCH` | `/api/settings/ai` | Update AI provider / credentials — same access |
| `POST` | `/api/settings/ai/test` | Test AI connection — same access |
| `GET` | `/api/settings/documents/limits` | Doc service upload limits — superuser OR `doc-service-admin` member |
| `PATCH` | `/api/settings/documents/limits` | Update max PDF size — same access |
| `GET` | `/api/settings/system-prompts` | All editable system prompts — superuser OR `ai-service-admin` member |
| `PATCH` | `/api/settings/system-prompts/{id}` | Update system prompt — same access |
Settings are persisted to the `config` bucket of `storage-service:8020` via `core/config_storage.py`. All config I/O is async HTTP; no filesystem volumes are used.
Access to service-specific settings endpoints is enforced by `get_service_admin(service_id)` in `deps.py` — grants access to superusers OR members of the `{service_id}-admin` group.
### Storage config (`/api/admin`)
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/admin/storage-config` | Current backend driver + health (proxied from storage-service) |
| `PATCH` | `/api/admin/storage-config` | Reconfigure backend without migration |
| `POST` | `/api/admin/storage-config/migrate` | Start async migration to a new backend |
| `GET` | `/api/admin/storage-config/migrate/status` | Poll migration progress |
| `DELETE` | `/api/admin/storage-config/migrate` | Cancel running migration |
### Feature proxies
All `/api/documents/*` and `/api/documents/categories/*` requests are transparently proxied to `doc-service:8001` via `httpx.AsyncClient`. The proxy:
- Validates the JWT (`get_current_user`)
- Injects `x-user-id` header (UUID from `users.id`)
- Strips hop-by-hop headers + `content-length`, `accept-encoding`, `content-type`
- Returns `Response` (not `StreamingResponse`) to avoid content-length/chunked conflicts
### Plugin system (`/api/plugins`)
Generic extension/plugin infrastructure — **zero feature-specific code in backend**. Feature containers self-describe via `GET /plugin/manifest`.
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| `GET` | `/api/plugins` | user | List plugins accessible to current user |
| `GET` | `/api/plugins/{id}/manifest` | user | Cached manifest for a plugin (404 if not accessible) |
| `GET` | `/api/plugins/{id}/settings` | user | Proxy to feature `GET /plugin/settings` |
| `PATCH` | `/api/plugins/{id}/settings` | user | Proxy to feature `PATCH /plugin/settings` |
Access is controlled by the manifest: `allow_superuser` for admins; `required_groups` for group members. `check_plugin_access(plugin_id, user, db)` in `deps.py` enforces this.
During each health poll, `service_health.py` also fetches `GET /plugin/manifest` from healthy services and caches it. New feature containers that expose `/plugin/manifest` automatically appear in the plugin list — no backend code changes required.
**Service admin group bootstrap:** On every startup, `group_bootstrap.py` creates a `{service-id}-admin` group for every registered service (idempotent). Admins add users to these groups via the Admin → Groups UI to delegate service-level administration.
### Database models
| Model | Table | Notes |
|-------|-------|-------|
| `User` | `users` | email, hashed_password, role (`user`\|`admin`), is_active, dashboard_app_ids (JSON) |
| `Profile` | `profiles` | one-to-one with User; full_name, phone, etc. |
| `Group` | `groups` | name (unique), description, created_at |
| `GroupMembership` | `group_memberships` | group_id + user_id (unique pair); joined_at |
Alembic migrations in `backend/alembic/versions/` — version table: `alembic_version`.
---
## Architecture
```
Browser (port 5173 dev / 80 prod)
└── Vite dev proxy / nginx
└── /api/* → backend:8000 (FastAPI)
┌───────────┼────────────┬──────────────┐
/auth /settings /documents/* /services
/users (JSON │ │
/admin /storage- └── proxy → health-check loop
/profile config doc-service:8001 (30s poll)
(proxy)
storage-service:8020
```
---
## Security notes
- JWT stored in `localStorage` — XSS risk. Migration to `httpOnly` cookie planned.
- No refresh token — after 8h the user must log in again.
- Admin routes use `get_current_admin` dependency (checks `role == "admin"`).
- All backend routes require authentication except `/api/auth/*`.
- `backend-net` is marked `internal: true` — containers on it cannot reach the internet directly.
---
## Known limitations / not implemented
- **No refresh tokens** — 8h hard expiry; adding refresh requires `httpOnly` cookie + rotation
- **No `httpOnly` cookie** — JWT in `localStorage` is XSS-exposed
- **App permissions** — no per-user, per-app access control. Currently all authenticated users can use all apps. Planned: `user_app_permissions` table, admin UI to grant/revoke
- **Groups / sharing** — groups + memberships exist; app permission hooks not yet wired up
- **Email verification** — accounts are active immediately after registration
- **Password reset** — no flow implemented
---
## Future work
- [x] Groups system: `groups`, `group_memberships` tables; admin CRUD; add/remove members
- [x] Generic plugin infrastructure: manifest contract, `/api/plugins` proxy router, `check_plugin_access`
- [ ] App permissions registry: `group_app_permissions` table; AppsPage filtered by group grants
- [ ] Doc sharing via group membership
- [ ] App permissions registry: `user_app_permissions (user_id, app_key)`; AppsPage filtered by grants
- [ ] `httpOnly` cookie migration for JWT
- [ ] Refresh token flow (paired with cookie migration)
- [ ] Email verification on registration
- [ ] Password reset flow
- [ ] Rate limiting on auth endpoints