- User-Modell (username, password_hash, role admin/user, is_active) - Standard-Admin-Benutzer wird beim ersten Start automatisch angelegt - JWT-Tokens (HS256) für Benutzer-Sessions, konfigurierbare Ablaufzeit - API-Key bleibt für service-to-service-Calls (backward-compatible) - POST /api/v1/auth/login → JWT-Token - GET /api/v1/auth/me → aktueller Benutzer - CRUD /api/v1/users/ → Benutzerverwaltung (nur Admin) - TUI zeigt Login-Screen beim Start; nach Erfolg → MainScreen - Passwort-Hashing mit bcrypt (python-jose für JWT)
126 lines
5.0 KiB
Python
126 lines
5.0 KiB
Python
"""Async HTTP-Client für die MCM REST-API.
|
|
|
|
Die TUI kommuniziert ausschließlich über diesen Client mit dem Backend.
|
|
Dadurch läuft die TUI sowohl im Terminal (python main.py) als auch im
|
|
Browser (textual serve tui_standalone.py) ohne Änderung.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
from config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MCMApiClient:
|
|
"""Thin async wrapper um die MCM REST-API."""
|
|
|
|
def __init__(self) -> None:
|
|
self._base = f"http://127.0.0.1:{settings.port}/api/v1"
|
|
self._token: str | None = None
|
|
|
|
@property
|
|
def _headers(self) -> dict[str, str]:
|
|
if self._token:
|
|
return {"Authorization": f"Bearer {self._token}"}
|
|
return {}
|
|
|
|
# ── Authentifizierung ──────────────────────────────────────────────────────
|
|
|
|
async def login(self, username: str, password: str) -> bool:
|
|
"""Meldet den Benutzer an und speichert den JWT-Token. Gibt True bei Erfolg zurück."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
resp = await client.post(
|
|
self._base + "/auth/login",
|
|
json={"username": username, "password": password},
|
|
)
|
|
if resp.status_code == 200:
|
|
self._token = resp.json()["access_token"]
|
|
return True
|
|
return False
|
|
except Exception as exc:
|
|
logger.warning("Login fehlgeschlagen: %s", exc)
|
|
return False
|
|
|
|
def logout(self) -> None:
|
|
self._token = None
|
|
|
|
@property
|
|
def is_authenticated(self) -> bool:
|
|
return self._token is not None
|
|
|
|
# ── Konversationen ─────────────────────────────────────────────────────────
|
|
|
|
async def get_conversations(self, channel: str | None = None) -> list[dict[str, Any]]:
|
|
params: dict[str, Any] = {}
|
|
if channel:
|
|
params["channel"] = channel
|
|
return await self._get("/conversations", params=params)
|
|
|
|
async def get_conversation(self, conv_id: str) -> dict[str, Any] | None:
|
|
try:
|
|
return await self._get(f"/conversations/{conv_id}")
|
|
except httpx.HTTPStatusError:
|
|
return None
|
|
|
|
async def get_messages(
|
|
self, conv_id: str, limit: int = 100, offset: int = 0
|
|
) -> list[dict[str, Any]]:
|
|
return await self._get(
|
|
f"/conversations/{conv_id}/messages",
|
|
params={"limit": limit, "offset": offset},
|
|
)
|
|
|
|
# ── Nachrichten senden ─────────────────────────────────────────────────────
|
|
|
|
async def send_message(
|
|
self,
|
|
channel: str,
|
|
text: str,
|
|
recipient_phone: str | None = None,
|
|
recipient_telegram_id: str | None = None,
|
|
reply_to_id: str | None = None,
|
|
) -> dict[str, Any]:
|
|
body: dict[str, Any] = {"channel": channel, "text": text}
|
|
if recipient_phone:
|
|
body["recipient_phone"] = recipient_phone
|
|
if recipient_telegram_id:
|
|
body["recipient_telegram_id"] = recipient_telegram_id
|
|
if reply_to_id:
|
|
body["reply_to_id"] = reply_to_id
|
|
return await self._post("/messages", body)
|
|
|
|
# ── Kontakte ───────────────────────────────────────────────────────────────
|
|
|
|
async def get_contacts(self) -> list[dict[str, Any]]:
|
|
return await self._get("/contacts")
|
|
|
|
# ── Status ─────────────────────────────────────────────────────────────────
|
|
|
|
async def get_channel_status(self) -> dict[str, Any]:
|
|
return await self._get("/channels/status")
|
|
|
|
# ── Interne Hilfsmethoden ──────────────────────────────────────────────────
|
|
|
|
async def _get(self, path: str, params: dict | None = None) -> Any:
|
|
async with httpx.AsyncClient(timeout=5.0, follow_redirects=True) as client:
|
|
resp = await client.get(
|
|
self._base + path, headers=self._headers, params=params
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
async def _post(self, path: str, body: dict) -> Any:
|
|
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client:
|
|
resp = await client.post(
|
|
self._base + path, headers=self._headers, json=body
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|