2bc5d6ea9a
- reuses send_smtp_test_email; tri-state result success(200)/config-error(400)/failed(502) - session + CSRF protected; never echoes SMTP secrets - SmtpTestResponse schema; regenerate openapi/ - extend tests/test_api_config.py (3 states + 401 + missing-CSRF 403)
120 lines
4.2 KiB
Python
120 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from fastapi.responses import JSONResponse
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.routes.api.deps import require_csrf, require_session
|
|
from app.config import Settings, get_settings
|
|
from app.dependencies import get_app_settings, get_db
|
|
from app.schemas.config import (
|
|
ConfigField,
|
|
ConfigResponse,
|
|
ConfigSection,
|
|
ConfigUpdateRequest,
|
|
ConfigUpdateResponse,
|
|
SmtpTestResponse,
|
|
)
|
|
from app.services.auth import AuthenticatedSession
|
|
from app.services.config_page import ConfigSaveError, build_config_sections, save_config_updates
|
|
from app.services.email import EmailConfigurationError, EmailDeliveryError, send_smtp_test_email
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api", tags=["api-config"])
|
|
|
|
|
|
def _sections_from_raw(sections_raw: list[dict]) -> list[ConfigSection]:
|
|
result = []
|
|
for section in sections_raw:
|
|
fields = [ConfigField(**f) for f in section["fields"]]
|
|
result.append(ConfigSection(name=section["name"], fields=fields))
|
|
return result
|
|
|
|
|
|
@router.get("/config", response_model=ConfigResponse)
|
|
def get_config(
|
|
db: Session = Depends(get_db),
|
|
settings: Settings = Depends(get_app_settings),
|
|
_auth: AuthenticatedSession = Depends(require_session),
|
|
) -> ConfigResponse:
|
|
"""Return all configuration sections. Secret field values are masked (empty string)."""
|
|
sections_raw = build_config_sections(db, settings)
|
|
return ConfigResponse(sections=_sections_from_raw(sections_raw))
|
|
|
|
|
|
@router.put("/config", response_model=ConfigUpdateResponse)
|
|
def put_config(
|
|
body: ConfigUpdateRequest,
|
|
db: Session = Depends(get_db),
|
|
settings: Settings = Depends(get_app_settings),
|
|
_auth: AuthenticatedSession = Depends(require_session),
|
|
_csrf: None = Depends(require_csrf),
|
|
) -> ConfigUpdateResponse:
|
|
"""
|
|
Save configuration updates.
|
|
|
|
- Blank secret value keeps the existing stored value (no change).
|
|
- Invalid values return 422 and nothing is written to the database.
|
|
"""
|
|
try:
|
|
save_config_updates(db, body.updates, settings)
|
|
except ConfigSaveError as exc:
|
|
logger.warning("Rejected config update via API: %s", exc)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
detail="invalid config submission",
|
|
) from exc
|
|
|
|
# Re-read settings after save (save_config_updates clears the settings cache)
|
|
refreshed_settings = get_settings()
|
|
sections_raw = build_config_sections(db, refreshed_settings)
|
|
return ConfigUpdateResponse(sections=_sections_from_raw(sections_raw))
|
|
|
|
|
|
@router.post(
|
|
"/config/smtp/test",
|
|
responses={
|
|
200: {"model": SmtpTestResponse},
|
|
400: {"model": SmtpTestResponse},
|
|
502: {"model": SmtpTestResponse},
|
|
},
|
|
)
|
|
def post_smtp_test(
|
|
settings: Settings = Depends(get_app_settings),
|
|
_auth: AuthenticatedSession = Depends(require_session),
|
|
_csrf: None = Depends(require_csrf),
|
|
) -> JSONResponse:
|
|
"""
|
|
Send a test SMTP email using the current runtime settings.
|
|
|
|
Returns a structured result indicating success or the category of failure.
|
|
Three possible outcomes:
|
|
- 200 { "result": "success", "message": ... }
|
|
- 400 { "result": "config-error", "message": ... } (EmailConfigurationError)
|
|
- 502 { "result": "failed", "message": ... } (EmailDeliveryError)
|
|
|
|
SMTP credentials are never echoed in the response.
|
|
"""
|
|
try:
|
|
send_smtp_test_email(settings)
|
|
except EmailConfigurationError as exc:
|
|
logger.warning("SMTP test rejected due to configuration: %s", exc)
|
|
return JSONResponse(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
content={"result": "config-error", "message": str(exc)},
|
|
)
|
|
except EmailDeliveryError as exc:
|
|
logger.warning("SMTP test delivery failed: %s", exc)
|
|
return JSONResponse(
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
content={"result": "failed", "message": str(exc)},
|
|
)
|
|
|
|
return JSONResponse(
|
|
status_code=status.HTTP_200_OK,
|
|
content={"result": "success", "message": "Test email sent successfully."},
|
|
)
|