356 lines
11 KiB
Python
356 lines
11 KiB
Python
"""Effective notification channel config (DB overrides env)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.config import get_settings
|
|
from app.models.notification_channel import NotificationChannel
|
|
|
|
VALID_SEVERITIES = frozenset({"info", "warning", "high", "critical"})
|
|
CHANNEL_TELEGRAM = "telegram"
|
|
CHANNEL_WEBHOOK = "webhook"
|
|
CHANNEL_EMAIL = "email"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TelegramConfig:
|
|
enabled: bool
|
|
bot_token: str
|
|
chat_id: str
|
|
min_severity: str
|
|
source: str # env | db
|
|
|
|
@property
|
|
def configured(self) -> bool:
|
|
return bool(self.bot_token.strip() and self.chat_id.strip())
|
|
|
|
@property
|
|
def active(self) -> bool:
|
|
return self.enabled and self.configured
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WebhookConfig:
|
|
enabled: bool
|
|
url: str
|
|
secret_header: str
|
|
secret: str
|
|
min_severity: str
|
|
source: str # env | db
|
|
|
|
@property
|
|
def configured(self) -> bool:
|
|
return bool(self.url.strip())
|
|
|
|
@property
|
|
def active(self) -> bool:
|
|
return self.enabled and self.configured
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class EmailConfig:
|
|
enabled: bool
|
|
smtp_host: str
|
|
smtp_port: int
|
|
smtp_user: str
|
|
smtp_password: str
|
|
mail_from: str
|
|
mail_to: str
|
|
smtp_starttls: bool
|
|
smtp_ssl: bool
|
|
min_severity: str
|
|
source: str # env | db
|
|
|
|
@property
|
|
def configured(self) -> bool:
|
|
return bool(self.smtp_host.strip() and self.mail_from.strip() and self.mail_to.strip())
|
|
|
|
@property
|
|
def active(self) -> bool:
|
|
return self.enabled and self.configured
|
|
|
|
|
|
def _telegram_from_env() -> TelegramConfig:
|
|
settings = get_settings()
|
|
return TelegramConfig(
|
|
enabled=bool(settings.telegram_enabled),
|
|
bot_token=settings.telegram_bot_token.strip(),
|
|
chat_id=settings.telegram_chat_id.strip(),
|
|
min_severity=settings.telegram_min_severity.strip() or "high",
|
|
source="env",
|
|
)
|
|
|
|
|
|
def get_effective_telegram_config(db: Session | None = None) -> TelegramConfig:
|
|
if db is None:
|
|
from app.database import SessionLocal
|
|
|
|
session = SessionLocal()
|
|
try:
|
|
return get_effective_telegram_config(session)
|
|
finally:
|
|
session.close()
|
|
|
|
row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_TELEGRAM))
|
|
env_cfg = _telegram_from_env()
|
|
if row is None:
|
|
return env_cfg
|
|
|
|
token = (row.bot_token or "").strip() or env_cfg.bot_token
|
|
chat_id = (row.chat_id or "").strip() or env_cfg.chat_id
|
|
min_sev = (row.min_severity or "").strip() or env_cfg.min_severity
|
|
if min_sev not in VALID_SEVERITIES:
|
|
min_sev = env_cfg.min_severity
|
|
|
|
return TelegramConfig(
|
|
enabled=bool(row.enabled),
|
|
bot_token=token,
|
|
chat_id=chat_id,
|
|
min_severity=min_sev,
|
|
source="db",
|
|
)
|
|
|
|
|
|
def upsert_telegram_channel(
|
|
db: Session,
|
|
*,
|
|
enabled: bool | None = None,
|
|
bot_token: str | None = None,
|
|
chat_id: str | None = None,
|
|
min_severity: str | None = None,
|
|
) -> TelegramConfig:
|
|
row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_TELEGRAM))
|
|
env_cfg = _telegram_from_env()
|
|
|
|
if row is None:
|
|
row = NotificationChannel(
|
|
channel=CHANNEL_TELEGRAM,
|
|
enabled=env_cfg.enabled,
|
|
bot_token=env_cfg.bot_token or None,
|
|
chat_id=env_cfg.chat_id or None,
|
|
min_severity=env_cfg.min_severity,
|
|
)
|
|
db.add(row)
|
|
|
|
if enabled is not None:
|
|
row.enabled = enabled
|
|
if min_severity is not None:
|
|
if min_severity not in VALID_SEVERITIES:
|
|
raise ValueError(f"invalid min_severity: {min_severity}")
|
|
row.min_severity = min_severity
|
|
if bot_token is not None and bot_token.strip():
|
|
row.bot_token = bot_token.strip()
|
|
if chat_id is not None and chat_id.strip():
|
|
row.chat_id = chat_id.strip()
|
|
|
|
db.commit()
|
|
db.refresh(row)
|
|
return get_effective_telegram_config(db)
|
|
|
|
|
|
def _webhook_from_env() -> WebhookConfig:
|
|
settings = get_settings()
|
|
return WebhookConfig(
|
|
enabled=bool(settings.webhook_enabled),
|
|
url=settings.webhook_url.strip(),
|
|
secret_header=settings.webhook_secret_header.strip(),
|
|
secret=settings.webhook_secret.strip(),
|
|
min_severity=settings.webhook_min_severity.strip() or "high",
|
|
source="env",
|
|
)
|
|
|
|
|
|
def get_effective_webhook_config(db: Session | None = None) -> WebhookConfig:
|
|
if db is None:
|
|
from app.database import SessionLocal
|
|
|
|
session = SessionLocal()
|
|
try:
|
|
return get_effective_webhook_config(session)
|
|
finally:
|
|
session.close()
|
|
|
|
row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_WEBHOOK))
|
|
env_cfg = _webhook_from_env()
|
|
if row is None:
|
|
return env_cfg
|
|
|
|
url = (row.webhook_url or "").strip() or env_cfg.url
|
|
secret_header = (row.webhook_secret_header or "").strip() or env_cfg.secret_header
|
|
secret = (row.webhook_secret or "").strip() or env_cfg.secret
|
|
min_sev = (row.min_severity or "").strip() or env_cfg.min_severity
|
|
if min_sev not in VALID_SEVERITIES:
|
|
min_sev = env_cfg.min_severity
|
|
|
|
return WebhookConfig(
|
|
enabled=bool(row.enabled),
|
|
url=url,
|
|
secret_header=secret_header,
|
|
secret=secret,
|
|
min_severity=min_sev,
|
|
source="db",
|
|
)
|
|
|
|
|
|
def upsert_webhook_channel(
|
|
db: Session,
|
|
*,
|
|
enabled: bool | None = None,
|
|
url: str | None = None,
|
|
secret_header: str | None = None,
|
|
secret: str | None = None,
|
|
min_severity: str | None = None,
|
|
) -> WebhookConfig:
|
|
row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_WEBHOOK))
|
|
env_cfg = _webhook_from_env()
|
|
|
|
if row is None:
|
|
row = NotificationChannel(
|
|
channel=CHANNEL_WEBHOOK,
|
|
enabled=env_cfg.enabled,
|
|
min_severity=env_cfg.min_severity,
|
|
webhook_url=env_cfg.url or None,
|
|
webhook_secret_header=env_cfg.secret_header or None,
|
|
webhook_secret=env_cfg.secret or None,
|
|
)
|
|
db.add(row)
|
|
|
|
if enabled is not None:
|
|
row.enabled = enabled
|
|
if min_severity is not None:
|
|
if min_severity not in VALID_SEVERITIES:
|
|
raise ValueError(f"invalid min_severity: {min_severity}")
|
|
row.min_severity = min_severity
|
|
if url is not None and url.strip():
|
|
row.webhook_url = url.strip()
|
|
if secret_header is not None:
|
|
row.webhook_secret_header = secret_header.strip() or None
|
|
if secret is not None and secret.strip():
|
|
row.webhook_secret = secret.strip()
|
|
|
|
db.commit()
|
|
db.refresh(row)
|
|
return get_effective_webhook_config(db)
|
|
|
|
|
|
def _email_from_env() -> EmailConfig:
|
|
settings = get_settings()
|
|
return EmailConfig(
|
|
enabled=bool(settings.smtp_enabled),
|
|
smtp_host=settings.smtp_host.strip(),
|
|
smtp_port=int(settings.smtp_port or 587),
|
|
smtp_user=settings.smtp_user.strip(),
|
|
smtp_password=settings.smtp_password,
|
|
mail_from=settings.smtp_from.strip(),
|
|
mail_to=settings.smtp_to.strip(),
|
|
smtp_starttls=bool(settings.smtp_starttls),
|
|
smtp_ssl=bool(settings.smtp_ssl),
|
|
min_severity=settings.smtp_min_severity.strip() or "high",
|
|
source="env",
|
|
)
|
|
|
|
|
|
def get_effective_email_config(db: Session | None = None) -> EmailConfig:
|
|
if db is None:
|
|
from app.database import SessionLocal
|
|
|
|
session = SessionLocal()
|
|
try:
|
|
return get_effective_email_config(session)
|
|
finally:
|
|
session.close()
|
|
|
|
row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_EMAIL))
|
|
env_cfg = _email_from_env()
|
|
if row is None:
|
|
return env_cfg
|
|
|
|
host = (row.smtp_host or "").strip() or env_cfg.smtp_host
|
|
port = row.smtp_port if row.smtp_port is not None else env_cfg.smtp_port
|
|
user = (row.smtp_user or "").strip() or env_cfg.smtp_user
|
|
password = (row.smtp_password or "") or env_cfg.smtp_password
|
|
mail_from = (row.mail_from or "").strip() or env_cfg.mail_from
|
|
mail_to = (row.mail_to or "").strip() or env_cfg.mail_to
|
|
min_sev = (row.min_severity or "").strip() or env_cfg.min_severity
|
|
if min_sev not in VALID_SEVERITIES:
|
|
min_sev = env_cfg.min_severity
|
|
|
|
return EmailConfig(
|
|
enabled=bool(row.enabled),
|
|
smtp_host=host,
|
|
smtp_port=int(port or 587),
|
|
smtp_user=user,
|
|
smtp_password=password,
|
|
mail_from=mail_from,
|
|
mail_to=mail_to,
|
|
smtp_starttls=bool(row.smtp_starttls),
|
|
smtp_ssl=bool(row.smtp_ssl),
|
|
min_severity=min_sev,
|
|
source="db",
|
|
)
|
|
|
|
|
|
def upsert_email_channel(
|
|
db: Session,
|
|
*,
|
|
enabled: bool | None = None,
|
|
smtp_host: str | None = None,
|
|
smtp_port: int | None = None,
|
|
smtp_user: str | None = None,
|
|
smtp_password: str | None = None,
|
|
mail_from: str | None = None,
|
|
mail_to: str | None = None,
|
|
smtp_starttls: bool | None = None,
|
|
smtp_ssl: bool | None = None,
|
|
min_severity: str | None = None,
|
|
) -> EmailConfig:
|
|
row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_EMAIL))
|
|
env_cfg = _email_from_env()
|
|
|
|
if row is None:
|
|
row = NotificationChannel(
|
|
channel=CHANNEL_EMAIL,
|
|
enabled=env_cfg.enabled,
|
|
min_severity=env_cfg.min_severity,
|
|
smtp_host=env_cfg.smtp_host or None,
|
|
smtp_port=env_cfg.smtp_port,
|
|
smtp_user=env_cfg.smtp_user or None,
|
|
smtp_password=env_cfg.smtp_password or None,
|
|
mail_from=env_cfg.mail_from or None,
|
|
mail_to=env_cfg.mail_to or None,
|
|
smtp_starttls=env_cfg.smtp_starttls,
|
|
smtp_ssl=env_cfg.smtp_ssl,
|
|
)
|
|
db.add(row)
|
|
|
|
if enabled is not None:
|
|
row.enabled = enabled
|
|
if min_severity is not None:
|
|
if min_severity not in VALID_SEVERITIES:
|
|
raise ValueError(f"invalid min_severity: {min_severity}")
|
|
row.min_severity = min_severity
|
|
if smtp_host is not None and smtp_host.strip():
|
|
row.smtp_host = smtp_host.strip()
|
|
if smtp_port is not None:
|
|
row.smtp_port = smtp_port
|
|
if smtp_user is not None:
|
|
row.smtp_user = smtp_user.strip() or None
|
|
if smtp_password is not None and smtp_password.strip():
|
|
row.smtp_password = smtp_password
|
|
if mail_from is not None and mail_from.strip():
|
|
row.mail_from = mail_from.strip()
|
|
if mail_to is not None and mail_to.strip():
|
|
row.mail_to = mail_to.strip()
|
|
if smtp_starttls is not None:
|
|
row.smtp_starttls = smtp_starttls
|
|
if smtp_ssl is not None:
|
|
row.smtp_ssl = smtp_ssl
|
|
|
|
db.commit()
|
|
db.refresh(row)
|
|
return get_effective_email_config(db)
|