User Management – Technische Dokumentation¶
Sovereign MoE Orchestrator – User Management System
Version 1.0 · April 2026
Architektur-Überblick¶
graph TD
subgraph ADMIN_UI["Admin UI · Port 8088"]
USERS_ROUTE["/users — User CRUD,\nBudget, Permissions, API Keys"]
PORTAL_ROUTE["/user/* — User-Portal\n(Login, Dashboard, Usage)"]
DB_PY["database.py\nSQLite CRUD + Redis-Sync"]
end
SQLITE[("SQLite\n/opt/moe-infra/userdb/users.db\nWAL-Mode")]
subgraph ORCH["Orchestrator · Port 8002"]
VALIDATE["_validate_api_key()\nRedis-Lookup (TTL 300s)"]
LOG_USAGE["_log_usage_to_db()\nasync fire&forget"]
INC_BUDGET["_increment_user_budget()\nRedis-Counter"]
PROM_METRICS["PROM_TOKENS\nPROM_REQUESTS\n(user_id Label)"]
end
subgraph REDIS_KEYS["Redis (terra_cache · Port 6379)"]
KEY_APIKEY["user:apikey:{sha256} HASH TTL 300s"]
KEY_DAILY["user:{id}:tokens:daily:{date}"]
KEY_MONTHLY["user:{id}:tokens:monthly:{YYYY-MM}"]
KEY_TOTAL["user:{id}:tokens:total"]
end
ADMIN_UI <-->|"shared Docker volume"| SQLITE
SQLITE <--> ORCH
VALIDATE <--> KEY_APIKEY
INC_BUDGET --> KEY_DAILY & KEY_MONTHLY & KEY_TOTAL
Datenbank-Schema¶
Datei: /opt/moe-infra/userdb/users.db (SQLite, WAL-Modus)
Tabellen¶
users¶
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | TEXT PK | UUID4 hex |
| username | TEXT UNIQUE | Anmeldename |
| TEXT UNIQUE | ||
| display_name | TEXT | Anzeigename |
| hashed_password | TEXT | bcrypt-Hash (via passlib) |
| is_active | INTEGER | 1=aktiv, 0=gesperrt |
| is_admin | INTEGER | 1=Admin-Rechte |
| created_at | TEXT | ISO-8601 UTC |
| updated_at | TEXT | ISO-8601 UTC |
api_keys¶
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | TEXT PK | UUID4 hex |
| user_id | TEXT FK→users | Zugehöriger User |
| key_hash | TEXT UNIQUE | SHA-256 des raw key |
| key_prefix | TEXT | Erste 16 Zeichen (Anzeige) |
| label | TEXT | Benutzerdef. Bezeichnung |
| is_active | INTEGER | 1=aktiv |
| created_at | TEXT | ISO-8601 UTC |
| last_used_at | TEXT | Letzter Einsatz (NULL=nie) |
| expires_at | TEXT | Ablaufdatum (NULL=unbegrenzt) |
Raw API Keys werden nie gespeichert. Nur der SHA-256-Hash wird für Lookups verwendet. Das Key-Format ist
moe-sk-{48 random hex chars}.
token_budgets¶
| Spalte | Typ | Beschreibung |
|---|---|---|
| user_id | TEXT PK FK | 1:1 mit users |
| daily_limit | INTEGER | NULL = unlimitiert |
| monthly_limit | INTEGER | NULL = unlimitiert |
| total_limit | INTEGER | NULL = unlimitiert |
| updated_at | TEXT | Letztes Update |
permissions¶
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | TEXT PK | UUID4 hex |
| user_id | TEXT FK | Zugehöriger User |
| resource_type | TEXT | expert_template, cc_profile, model_endpoint, skill, mcp_tool, moe_mode |
| resource_id | TEXT | z.B. tmpl-abc123, profile-moe, qwen2.5-coder:32b@N04-RTX, native |
| granted_at | TEXT | ISO-8601 UTC |
Default: alle Zugriffe gesperrt. Nur explizit eingetragene Permissions sind erlaubt.
Permission-Typen:
| resource_type | Bedeutung | Beispiel resource_id |
|---|---|---|
expert_template |
Experten-Konfigurationspaket | tmpl-abc123 |
cc_profile |
Claude Code Integrations-Profil | profile-moe |
model_endpoint |
Native LLM via OpenAI-API | llama3.3:70b@N04-RTX |
moe_mode |
MoE-Verarbeitungsmodus | native, moe_orchestrated, moe_reasoning |
skill |
Claude Code Skill | calc |
mcp_tool |
MCP-Tool | precision-calc |
usage_log¶
| Spalte | Typ | Beschreibung |
|---|---|---|
| id | TEXT PK | UUID4 hex |
| user_id | TEXT FK | Zugehöriger User |
| api_key_id | TEXT FK | Genutzter Key |
| request_id | TEXT | chat_id vom Orchestrator |
| model | TEXT | Angefragtes Modell |
| moe_mode | TEXT | Routing-Modus |
| prompt_tokens | INTEGER | Eingabe-Tokens |
| completion_tokens | INTEGER | Ausgabe-Tokens |
| total_tokens | INTEGER | Summe |
| status | TEXT | ok, budget_exceeded, error |
| requested_at | TEXT | ISO-8601 UTC |
API-Kompatibilität¶
Der Orchestrator (Port 8002) bietet zwei kompatible Schnittstellen:
| Endpunkt | Format | Primäre Nutzung |
|---|---|---|
/v1/messages |
Anthropic Messages API | Claude Code (ANTHROPIC_BASE_URL), Anthropic SDK |
/v1/chat/completions |
OpenAI Chat Completions API | Open WebUI, native LLMs, OpenAI SDK |
Claude Code kommuniziert ausschließlich über /v1/messages (Anthropic Messages API). Die API-Auswahl hängt vom Client ab — der Orchestrator unterstützt beide Formate transparent.
Redis-Schlüsselstruktur¶
user:apikey:{sha256_of_key} HASH TTL 300s
Fields:
user_id → UUID hex des Users
username → Anmeldename (für Logging)
is_active → "1" oder "0"
permissions_json → '{"model_endpoint":["qwen2.5-coder:32b@N04-RTX"],...}'
budget_daily → "50000" oder "" (leer = kein Limit)
budget_monthly → "500000" oder ""
budget_total → "" oder Zahl
user:{user_id}:tokens:daily:{YYYY-MM-DD} STRING INCRBY TTL 48h
user:{user_id}:tokens:monthly:{YYYY-MM} STRING INCRBY TTL 35d
user:{user_id}:tokens:total STRING INCRBY kein TTL
Cache-Invalidierung¶
| Ereignis | Redis-Aktion |
|---|---|
| User erstellt | HSET aller Keys + TTL 300s |
| User deaktiviert | DEL aller Keys |
| API Key erstellt | HSET neuer Key |
| API Key gesperrt | DEL spezifischer Key |
| Permission geändert | HSET aller Keys (permissions_json) |
| Budget geändert | HSET aller Keys (budget_*) |
Auth-Flow (Orchestrator)¶
Client-Request
│
▼
_extract_api_key(request)
├── Authorization: Bearer moe-sk-xxx → raw_key
└── x-api-key: moe-sk-xxx → raw_key
│
▼
_validate_api_key(raw_key)
│
├── SHA-256(raw_key) → key_hash
├── Redis HGETALL user:apikey:{key_hash}
│ ├── Cache-Miss → return None (401)
│ ├── is_active != "1" → return None (401)
│ └── Budget-Check:
│ ├── daily_limit gesetzt?
│ │ Redis GET user:{id}:tokens:daily:{date}
│ │ ≥ limit → PROM_BUDGET_EXCEEDED.inc() + return None (429)
│ ├── monthly_limit gesetzt? → analog
│ └── total_limit gesetzt? → analog
│
└── return {user_id, username, permissions_json, ...}
Pipeline wird ausgeführt (AgentState.user_id gesetzt)
│
▼
Post-Request (fire-and-forget asyncio.create_task)
├── _log_usage_to_db() → SQLite usage_log INSERT
└── _increment_user_budget() → Redis INCRBY (daily, monthly, total)
Admin-UI Routen¶
User-Verwaltung (erfordert Admin-Login)¶
| Methode | Pfad | Funktion |
|---|---|---|
| GET | /users |
User-Liste (HTML) |
| GET | /api/users |
User-Liste JSON |
| POST | /api/users |
User anlegen |
| GET | /api/users/{id} |
User-Details (inkl. Keys, Perms, Usage) |
| PUT | /api/users/{id} |
User aktualisieren |
| DELETE | /api/users/{id} |
User deaktivieren (soft delete) |
| PUT | /api/users/{id}/budget |
Token-Budget setzen |
| GET | /api/users/{id}/permissions |
Freigaben auflisten |
| POST | /api/users/{id}/permissions |
Freigabe erteilen |
| DELETE | /api/users/{id}/permissions/{pid} |
Freigabe entziehen |
| GET | /api/users/{id}/keys |
API Keys auflisten |
| POST | /api/users/{id}/keys |
API Key erstellen |
| DELETE | /api/users/{id}/keys/{kid} |
API Key sperren |
| GET | /api/users/{id}/usage |
Nutzungsstatistik |
Expert Templates (erfordert Admin-Login)¶
| Methode | Pfad | Funktion |
|---|---|---|
| GET | /templates |
Templates-Übersicht (HTML) |
| GET | /api/expert-templates |
Templates auflisten (JSON) |
| POST | /api/expert-templates |
Template erstellen |
| PUT | /api/expert-templates/{id} |
Template bearbeiten |
| DELETE | /api/expert-templates/{id} |
Template löschen |
User-Portal (erfordert User-Login)¶
| Methode | Pfad | Funktion |
|---|---|---|
| GET/POST | /user/login |
User-Login |
| GET | /user/logout |
Logout |
| GET | /user/dashboard |
Dashboard |
| GET/POST | /user/profile |
Profil & Passwort |
| GET | /user/usage |
Nutzungshistorie |
| GET | /user/billing |
Abrechnung |
| GET/POST | /user/keys |
API Keys |
| POST | /user/keys/{id}/revoke |
Key sperren |
Prometheus-Metriken¶
Geänderte Metriken (neues Label user_id)¶
# Vorher:
moe_tokens_total{model, token_type, node}
moe_requests_total{mode, cache_hit}
# Nachher:
moe_tokens_total{model, token_type, node, user_id}
moe_requests_total{mode, cache_hit, user_id}
Nicht-authentifizierte Anfragen tragen user_id="anon".
Neue Metrik¶
Beispiel-PromQL-Queries¶
# Tokens pro User (letzte Stunde)
sum by (user_id) (increase(moe_tokens_total[1h]))
# Anfragen pro User heute
sum by (user_id) (increase(moe_requests_total[24h]))
# Budget-Überschreitungen dieser Woche
sum by (user_id, limit_type) (increase(moe_budget_exceeded_total[7d]))
# Top-5 User nach Verbrauch (gesamt)
topk(5, sum by (user_id) (moe_tokens_total))
Deployment¶
Neue Dienste / Volumes¶
# docker-compose.yml Ergänzungen:
volumes:
userdb_data: # Shared zwischen moe-admin und langgraph-app
driver: local
driver_opts:
type: none
o: bind
device: /opt/moe-infra/userdb
# moe-admin erhält:
environment:
- REDIS_URL=redis://terra_cache:6379
- DB_PATH=/app/userdb/users.db
volumes:
- userdb_data:/app/userdb
# langgraph-app erhält:
environment:
- DB_PATH=/app/userdb/users.db
volumes:
- userdb_data:/app/userdb
Rebuild & Deploy¶
cd /opt/deployment/moe-infra
# Admin-UI neu bauen (neue requirements + database.py)
sudo docker compose build moe-admin
# Orchestrator neu bauen (aiosqlite in requirements)
sudo docker compose build langgraph-app
# Services neu starten
sudo docker compose up -d moe-admin langgraph-app
Verifikation¶
# DB-Datei prüfen
ls -la /opt/moe-infra/userdb/
# Admin-UI Logs (DB-Init)
sudo docker logs moe-admin --tail 20
# Test: User anlegen via API
curl -s -X POST http://localhost:8088/api/users \
-H "Content-Type: application/json" \
-d '{"username":"testuser","email":"test@example.com","password":"test1234"}' \
-b "session=<admin-session-cookie>"
# Redis-Key prüfen nach Key-Erstellung:
sudo docker exec terra_cache redis-cli keys "user:apikey:*"
Sicherheitshinweise¶
- Passwort-Hashing: bcrypt via passlib (work factor 12), asyncio.to_thread für Non-Blocking
- API Key Storage: SHA-256 One-Way-Hash, raw key nie persistent gespeichert
- CSRF-Schutz: Alle POST-Formulare im User-Portal nutzen CSRF-Tokens (Starlette SessionMiddleware)
- Session-Isolation: Admin-Session (
authenticated) und User-Session (user_authenticated) sind getrennte Keys im selben Session-Store - SQLite WAL-Modus: Ermöglicht concurrent reads zwischen zwei Docker-Containern ohne Locks
- Redis-TTL: API-Key-Cache verfällt nach 300 Sekunden — bei Revoke sofortige DEL
- Principle of Least Privilege: Neuer User hat keine Permissions — alles muss explizit erteilt werden
Grafana Dashboard¶
Das Dashboard MoE – User Metriken (UID: moe-users-v1) ist unter
/opt/grafana/dashboards/moe-users.json gespeichert und wird automatisch via Provisioning geladen.
Panels: - Stat: Anfragen gesamt, Tokens gesamt, Budget-Überschreitungen (24h), Aktive User (1h) - Timeseries: Token-Verbrauch pro User (Rate 5m), Anfragen pro User, Budget-Überschreitungen - Table: Top User nach Token-Verbrauch - Bar Chart: Token-Verbrauch nach User × Modell
Template-Variable: user_id — filtert alle Panels auf ausgewählte User.
Stand: April 2026 · MoE Sovereign Orchestrator