feat: overhaul with modular API, agent endpoints, migrations, backup and tests

- Split monolithic app.py into server/api/ routers
- Add Pydantic schemas for input validation
- Add /agent/* API with separate AGENT_TOKEN auth
- Replace static schema.sql with versioned migrations (datenerhaltend)
- Add tools/backup_db.py for SQLite backups
- Add Dockerfile HEALTHCHECK and compose.yml AGENT_TOKEN env
- Add pytest tests for user and agent API
- Update README with API docs and deployment safety notes
- Bump version to 1.2.0
This commit is contained in:
Frank Schwenk
2026-09-21 12:46:44 +02:00
parent 7cec3030c1
commit 88bdf6f34b
31 changed files with 942 additions and 184 deletions
+21 -3
View File
@@ -1,11 +1,12 @@
from __future__ import annotations
import hmac
from datetime import datetime, timedelta, timezone
import secrets
from datetime import UTC, datetime, timedelta
from typing import Any
import jwt
from fastapi import Depends, HTTPException, Request
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from server.settings import settings
@@ -19,7 +20,7 @@ def verify_pin(pin: str) -> bool:
def create_token() -> str:
expire = datetime.now(timezone.utc) + timedelta(days=settings.jwt_expire_days)
expire = datetime.now(UTC) + timedelta(days=settings.jwt_expire_days)
payload = {"sub": "user", "exp": expire}
return jwt.encode(payload, settings.jwt_secret, algorithm=ALGORITHM)
@@ -37,3 +38,20 @@ async def require_auth(
if creds is None or creds.scheme.lower() != "bearer":
raise HTTPException(status_code=401, detail="Missing token")
return decode_token(creds.credentials)
async def require_agent(
creds: HTTPAuthorizationCredentials | None = Depends(_bearer),
) -> dict[str, Any]:
"""Authenticate external agents via AGENT_TOKEN (long-lived, not JWT)."""
if creds is None or creds.scheme.lower() != "bearer":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing agent token")
token = creds.credentials
if not settings.agent_token:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Agent API not configured",
)
if not secrets.compare_digest(token.strip(), settings.agent_token.strip()):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid agent token")
return {"sub": "agent"}