Files
home-automation/app/api/routes/api/config.py
T
tliu93 2bc5d6ea9a M2-T05: add SMTP test action API (POST /api/config/smtp/test)
- 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)
2026-06-13 15:20:50 +02:00

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."},
)