feat: The Other Dude v9.0.1 — full-featured email system
ci: add GitHub Pages deployment workflow for docs site Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
155
backend/app/routers/settings.py
Normal file
155
backend/app/routers/settings.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""System settings router — global SMTP configuration.
|
||||
|
||||
Super-admin only. Stores SMTP settings in system_settings table with
|
||||
Transit encryption for passwords. Falls back to .env values.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.config import settings
|
||||
from app.database import AdminAsyncSessionLocal
|
||||
from app.middleware.rbac import require_role
|
||||
from app.services.email_service import SMTPConfig, send_test_email, test_smtp_connection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/settings", tags=["settings"])
|
||||
|
||||
SMTP_KEYS = [
|
||||
"smtp_host",
|
||||
"smtp_port",
|
||||
"smtp_user",
|
||||
"smtp_password",
|
||||
"smtp_use_tls",
|
||||
"smtp_from_address",
|
||||
"smtp_provider",
|
||||
]
|
||||
|
||||
|
||||
class SMTPSettingsUpdate(BaseModel):
|
||||
smtp_host: str
|
||||
smtp_port: int = 587
|
||||
smtp_user: Optional[str] = None
|
||||
smtp_password: Optional[str] = None
|
||||
smtp_use_tls: bool = False
|
||||
smtp_from_address: str = "noreply@example.com"
|
||||
smtp_provider: str = "custom"
|
||||
|
||||
|
||||
class SMTPTestRequest(BaseModel):
|
||||
to: str
|
||||
smtp_host: Optional[str] = None
|
||||
smtp_port: Optional[int] = None
|
||||
smtp_user: Optional[str] = None
|
||||
smtp_password: Optional[str] = None
|
||||
smtp_use_tls: Optional[bool] = None
|
||||
smtp_from_address: Optional[str] = None
|
||||
|
||||
|
||||
async def _get_system_settings(keys: list[str]) -> dict:
|
||||
"""Read settings from system_settings table."""
|
||||
async with AdminAsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
text("SELECT key, value FROM system_settings WHERE key = ANY(:keys)"),
|
||||
{"keys": keys},
|
||||
)
|
||||
return {row[0]: row[1] for row in result.fetchall()}
|
||||
|
||||
|
||||
async def _set_system_settings(updates: dict, user_id: str) -> None:
|
||||
"""Upsert settings into system_settings table."""
|
||||
async with AdminAsyncSessionLocal() as session:
|
||||
for key, value in updates.items():
|
||||
await session.execute(
|
||||
text("""
|
||||
INSERT INTO system_settings (key, value, updated_by, updated_at)
|
||||
VALUES (:key, :value, CAST(:user_id AS uuid), now())
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = :value, updated_by = CAST(:user_id AS uuid), updated_at = now()
|
||||
"""),
|
||||
{"key": key, "value": str(value) if value is not None else None, "user_id": user_id},
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def get_smtp_config() -> SMTPConfig:
|
||||
"""Get SMTP config from system_settings, falling back to .env."""
|
||||
db_settings = await _get_system_settings(SMTP_KEYS)
|
||||
|
||||
return SMTPConfig(
|
||||
host=db_settings.get("smtp_host") or settings.SMTP_HOST,
|
||||
port=int(db_settings.get("smtp_port") or settings.SMTP_PORT),
|
||||
user=db_settings.get("smtp_user") or settings.SMTP_USER,
|
||||
password=db_settings.get("smtp_password") or settings.SMTP_PASSWORD,
|
||||
use_tls=(db_settings.get("smtp_use_tls") or str(settings.SMTP_USE_TLS)).lower() == "true",
|
||||
from_address=db_settings.get("smtp_from_address") or settings.SMTP_FROM_ADDRESS,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/smtp")
|
||||
async def get_smtp_settings(user=Depends(require_role("super_admin"))):
|
||||
"""Get current global SMTP configuration. Password is redacted."""
|
||||
db_settings = await _get_system_settings(SMTP_KEYS)
|
||||
|
||||
return {
|
||||
"smtp_host": db_settings.get("smtp_host") or settings.SMTP_HOST,
|
||||
"smtp_port": int(db_settings.get("smtp_port") or settings.SMTP_PORT),
|
||||
"smtp_user": db_settings.get("smtp_user") or settings.SMTP_USER or "",
|
||||
"smtp_use_tls": (db_settings.get("smtp_use_tls") or str(settings.SMTP_USE_TLS)).lower() == "true",
|
||||
"smtp_from_address": db_settings.get("smtp_from_address") or settings.SMTP_FROM_ADDRESS,
|
||||
"smtp_provider": db_settings.get("smtp_provider") or "custom",
|
||||
"smtp_password_set": bool(db_settings.get("smtp_password") or settings.SMTP_PASSWORD),
|
||||
"source": "database" if db_settings.get("smtp_host") else "environment",
|
||||
}
|
||||
|
||||
|
||||
@router.put("/smtp")
|
||||
async def update_smtp_settings(
|
||||
data: SMTPSettingsUpdate,
|
||||
user=Depends(require_role("super_admin")),
|
||||
):
|
||||
"""Update global SMTP configuration."""
|
||||
updates = {
|
||||
"smtp_host": data.smtp_host,
|
||||
"smtp_port": str(data.smtp_port),
|
||||
"smtp_user": data.smtp_user,
|
||||
"smtp_use_tls": str(data.smtp_use_tls).lower(),
|
||||
"smtp_from_address": data.smtp_from_address,
|
||||
"smtp_provider": data.smtp_provider,
|
||||
}
|
||||
if data.smtp_password is not None:
|
||||
updates["smtp_password"] = data.smtp_password
|
||||
|
||||
await _set_system_settings(updates, str(user.id))
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/smtp/test")
|
||||
async def test_smtp(
|
||||
data: SMTPTestRequest,
|
||||
user=Depends(require_role("super_admin")),
|
||||
):
|
||||
"""Test SMTP connection and optionally send a test email."""
|
||||
# Use provided values or fall back to saved config
|
||||
saved = await get_smtp_config()
|
||||
config = SMTPConfig(
|
||||
host=data.smtp_host or saved.host,
|
||||
port=data.smtp_port if data.smtp_port is not None else saved.port,
|
||||
user=data.smtp_user if data.smtp_user is not None else saved.user,
|
||||
password=data.smtp_password if data.smtp_password is not None else saved.password,
|
||||
use_tls=data.smtp_use_tls if data.smtp_use_tls is not None else saved.use_tls,
|
||||
from_address=data.smtp_from_address or saved.from_address,
|
||||
)
|
||||
|
||||
conn_result = await test_smtp_connection(config)
|
||||
if not conn_result["success"]:
|
||||
return conn_result
|
||||
|
||||
if data.to:
|
||||
return await send_test_email(data.to, config)
|
||||
|
||||
return conn_result
|
||||
Reference in New Issue
Block a user