feat(settings): Settings page with email, webhooks, notifications, profile [FASE-8]
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
- SystemConfig model + migration b033 for runtime key-value config - GET/PATCH /system/email-config + POST /system/email-test (admin only) - email_service reads SMTP config from DB (overrides .env) - Webhooks now accessible to red_lead/blue_lead + admin - GET /users/me already existed; /users/me/preferences already working - SettingsPage with 4 role-aware tabs: * Profile & Jira: jira_account_id, user info * Notifications: role-specific email/in-app toggles (12 prefs) * Webhooks: full CRUD + test ping (leads + admin) * Email/SMTP: enable toggle, server config, test email (admin only) - Added /settings route (all authenticated users) - Settings link added to Sidebar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,16 @@
|
||||
Provides manual triggers for background operations such as the MITRE
|
||||
ATT&CK synchronisation, intel scanning, Atomic Red Team import, and
|
||||
scheduler health introspection.
|
||||
|
||||
Also exposes email configuration CRUD (admin only) that writes to the
|
||||
system_configs table so settings survive container restarts.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Request
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import SessionLocal, get_db
|
||||
@@ -24,6 +29,68 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/system", tags=["system"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pydantic schemas for email config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class EmailConfigOut(BaseModel):
|
||||
enabled: bool
|
||||
host: str
|
||||
port: int
|
||||
username: str
|
||||
from_email: str
|
||||
use_tls: bool
|
||||
# password is never returned
|
||||
|
||||
|
||||
class EmailConfigUpdate(BaseModel):
|
||||
enabled: Optional[bool] = None
|
||||
host: Optional[str] = None
|
||||
port: Optional[int] = None
|
||||
username: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
from_email: Optional[str] = None
|
||||
use_tls: Optional[bool] = None
|
||||
|
||||
|
||||
class EmailTestRequest(BaseModel):
|
||||
to: str
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers for system_configs CRUD
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_SMTP_KEYS = {
|
||||
"enabled": "smtp.enabled",
|
||||
"host": "smtp.host",
|
||||
"port": "smtp.port",
|
||||
"username": "smtp.username",
|
||||
"password": "smtp.password",
|
||||
"from_email": "smtp.from_email",
|
||||
"use_tls": "smtp.use_tls",
|
||||
}
|
||||
|
||||
|
||||
def _upsert_config(db: Session, key: str, value: str) -> None:
|
||||
from app.models.system_config import SystemConfig # lazy import avoids circular
|
||||
|
||||
row = db.query(SystemConfig).filter(SystemConfig.key == key).first()
|
||||
if row:
|
||||
row.value = value
|
||||
else:
|
||||
row = SystemConfig(key=key, value=value)
|
||||
db.add(row)
|
||||
|
||||
|
||||
def _read_email_config_from_db(db: Session) -> dict:
|
||||
"""Return a dict with resolved email settings (DB overrides env)."""
|
||||
from app.services.email_service import _get_smtp_config
|
||||
|
||||
return _get_smtp_config(db)
|
||||
|
||||
|
||||
def _bg_mitre_sync() -> None:
|
||||
"""Run MITRE sync in a background task with its own DB session."""
|
||||
logger.info("Background MITRE sync task starting...")
|
||||
@@ -132,3 +199,89 @@ def scheduler_status(
|
||||
for job in jobs
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /system/email-config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("/email-config", response_model=EmailConfigOut)
|
||||
def get_email_config(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""Return current SMTP email configuration (merged DB + env).
|
||||
|
||||
**Requires** the ``admin`` role. Password is never returned.
|
||||
"""
|
||||
cfg = _read_email_config_from_db(db)
|
||||
return EmailConfigOut(
|
||||
enabled=cfg["enabled"],
|
||||
host=cfg["host"],
|
||||
port=cfg["port"],
|
||||
username=cfg["username"],
|
||||
from_email=cfg["from_email"],
|
||||
use_tls=cfg["use_tls"],
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PATCH /system/email-config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.patch("/email-config", response_model=EmailConfigOut)
|
||||
def update_email_config(
|
||||
payload: EmailConfigUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""Update SMTP email configuration and persist to DB.
|
||||
|
||||
**Requires** the ``admin`` role.
|
||||
Only provided fields are updated (partial update).
|
||||
"""
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
for field, val in update_data.items():
|
||||
db_key = _SMTP_KEYS.get(field)
|
||||
if db_key:
|
||||
_upsert_config(db, db_key, str(val))
|
||||
db.commit()
|
||||
|
||||
cfg = _read_email_config_from_db(db)
|
||||
return EmailConfigOut(
|
||||
enabled=cfg["enabled"],
|
||||
host=cfg["host"],
|
||||
port=cfg["port"],
|
||||
username=cfg["username"],
|
||||
from_email=cfg["from_email"],
|
||||
use_tls=cfg["use_tls"],
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /system/email-test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.post("/email-test")
|
||||
def send_test_email(
|
||||
payload: EmailTestRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""Send a test email to verify SMTP configuration.
|
||||
|
||||
**Requires** the ``admin`` role.
|
||||
Returns 200 on success, 502 if sending fails.
|
||||
"""
|
||||
from app.services.email_service import send_test_email as _send_test
|
||||
|
||||
ok = _send_test(payload.to, db=db)
|
||||
if not ok:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="Failed to send test email. Check SMTP configuration and server logs.",
|
||||
)
|
||||
return {"detail": f"Test email sent to {payload.to}"}
|
||||
|
||||
Reference in New Issue
Block a user