Files
kite/backend/api/settings.py
T
curo1305 1882edfff6 feat(02-02): auth API endpoints + security hardening + Python 3.9 compat
- backend/api/auth.py: register, login (TOTP+backup), refresh, logout,
  me, change-password; per-account Redis rate limit; HIBP check
- backend/main.py: Origin validation middleware, CSP headers middleware,
  CORS locked to settings.cors_origins, Redis lifespan (app.state.redis),
  admin bootstrap, auth router included, slowapi SlowAPIMiddleware
- backend/services/email.py: already created in Plan 01 (verified exists)
- Python 3.9 compat: fixed match statement in ai/__init__.py,
  str|None union syntax in openai_provider.py, api/documents.py,
  api/topics.py, api/settings.py, services/classifier.py

All 17 tests in test_auth_api.py pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:35:38 +02:00

87 lines
2.6 KiB
Python

import time
from typing import Optional
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from services import storage
from config import DEFAULT_SYSTEM_PROMPT
from ai import get_provider
router = APIRouter(prefix="/api/settings", tags=["settings"])
class SettingsPatch(BaseModel):
system_prompt: Optional[str] = None
active_provider: Optional[str] = None
providers: Optional[dict] = None
class TestProviderRequest(BaseModel):
provider: str
@router.get("")
async def get_settings():
settings = storage.load_settings()
return storage.settings_masked(settings)
@router.patch("")
async def patch_settings(body: SettingsPatch):
settings = storage.load_settings()
if body.system_prompt is not None:
settings["system_prompt"] = body.system_prompt
if body.active_provider is not None:
valid = {"anthropic", "openai", "ollama", "lmstudio"}
if body.active_provider not in valid:
raise HTTPException(400, f"Invalid provider. Must be one of: {valid}")
settings["active_provider"] = body.active_provider
if body.providers is not None:
# Deep merge per-provider config
for prov_name, prov_cfg in body.providers.items():
if prov_name not in settings.get("providers", {}):
settings.setdefault("providers", {})[prov_name] = {}
existing = settings["providers"][prov_name]
for key, val in prov_cfg.items():
# Don't overwrite api_key if it comes in masked (contains ****)
if key == "api_key" and val and "****" in str(val):
continue
existing[key] = val
storage.save_settings(settings)
return storage.settings_masked(settings)
@router.post("/test-provider")
async def test_provider(body: TestProviderRequest):
settings = storage.load_settings()
# Temporarily switch active provider for the test
test_settings = dict(settings)
test_settings["active_provider"] = body.provider
try:
provider = get_provider(test_settings)
except ValueError as e:
raise HTTPException(400, str(e))
start = time.monotonic()
try:
ok = await provider.health_check()
except Exception as e:
return {"ok": False, "message": str(e), "latency_ms": 0}
latency_ms = int((time.monotonic() - start) * 1000)
return {
"ok": ok,
"message": "Connection successful" if ok else "Health check failed",
"latency_ms": latency_ms,
}
@router.get("/default-prompt")
async def get_default_prompt():
return {"system_prompt": DEFAULT_SYSTEM_PROMPT}