feat(config): DB-backed system_config infrastructure + SMTP group
Add a generic admin-editable config layer (epic 4a): system_config KV table (migration 0021), a sysconfig registry that is the single source of truth for DB-overridable keys (type/group/default/bounds/secret) + a DB-override-or-env resolver, and admin endpoints (GET/PATCH/DELETE /api/admin/config + a test-email probe). Secrets are Fernet-encrypted at rest (TOKEN_ENCRYPTION_KEY); without it they stay env-only. First group wired end-to-end: Email/SMTP — email.py now reads host/port/user/from/password via the resolver (own session, since email is sent from background tasks).
This commit is contained in:
parent
46de58082c
commit
8e4754b0b0
6 changed files with 314 additions and 5 deletions
68
backend/app/routes/config.py
Normal file
68
backend/app/routes/config.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
"""Admin Configuration page API: read/edit the DB-overridable operational settings defined in
|
||||
app.sysconfig. Secret values (e.g. SMTP password) are write-only — never returned."""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import email as email_mod
|
||||
from app import sysconfig
|
||||
from app.db import get_db
|
||||
from app.models import User
|
||||
from app.routes.admin import admin_user
|
||||
|
||||
router = APIRouter(prefix="/api/admin/config", tags=["admin"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_config(_: User = Depends(admin_user), db: Session = Depends(get_db)) -> dict:
|
||||
return sysconfig.describe(db)
|
||||
|
||||
|
||||
@router.patch("/{key}")
|
||||
def set_config(
|
||||
key: str,
|
||||
payload: dict,
|
||||
_: User = Depends(admin_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
s = sysconfig.spec(key)
|
||||
if s is None:
|
||||
raise HTTPException(status_code=404, detail="Unknown config key")
|
||||
if "value" not in payload:
|
||||
raise HTTPException(status_code=400, detail="Missing 'value'")
|
||||
if s.secret and not sysconfig.secrets_manageable():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Set TOKEN_ENCRYPTION_KEY to store secrets in the database.",
|
||||
)
|
||||
try:
|
||||
sysconfig.set_value(db, key, payload["value"])
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc) or "Invalid value")
|
||||
return sysconfig.describe(db)
|
||||
|
||||
|
||||
@router.delete("/{key}")
|
||||
def reset_config(
|
||||
key: str,
|
||||
_: User = Depends(admin_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
if sysconfig.spec(key) is None:
|
||||
raise HTTPException(status_code=404, detail="Unknown config key")
|
||||
sysconfig.reset(db, key)
|
||||
return sysconfig.describe(db)
|
||||
|
||||
|
||||
@router.post("/test-email")
|
||||
def send_test_email(
|
||||
user: User = Depends(admin_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""Send a test email to the requesting admin using the current (DB or env) SMTP config —
|
||||
lets the admin verify the settings actually work."""
|
||||
if not email_mod.email_enabled():
|
||||
raise HTTPException(status_code=400, detail="SMTP is not configured.")
|
||||
sent = email_mod.send_test(user.email)
|
||||
if not sent:
|
||||
raise HTTPException(status_code=422, detail="SMTP send failed — check the settings.")
|
||||
return {"sent": True, "to": user.email}
|
||||
Loading…
Add table
Add a link
Reference in a new issue