feat: 알림 채널 다중 관리 UI 추가
Some checks failed
Build and Push Images / build-backend (push) Has been cancelled
Some checks failed
Build and Push Images / build-backend (push) Has been cancelled
This commit is contained in:
169
backend/main.py
169
backend/main.py
@@ -425,59 +425,73 @@ def update_user_pages(user_id: int, data: AccessUpdate, token=Depends(require_ad
|
||||
conn.commit()
|
||||
return {"ok": True}
|
||||
|
||||
# ─── Admin: 알림 설정 ────────────────────────────────────
|
||||
class NotifyConfig(BaseModel):
|
||||
# ─── Admin: 알림 채널 관리 (다중 채널) ────────────────────
|
||||
class NotifyChannel(BaseModel):
|
||||
id: Optional[int] = None
|
||||
name: str # 채널 이름 (식별용)
|
||||
type: str # "discord" | "email"
|
||||
discord_webhook_url: Optional[str] = ""
|
||||
gmail_user: Optional[str] = ""
|
||||
gmail_app_password: Optional[str] = ""
|
||||
alert_email_to: Optional[str] = ""
|
||||
enabled: Optional[bool] = True
|
||||
|
||||
def init_notify_channels_db():
|
||||
conn = psycopg2.connect(**DB_CONFIG)
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS notify_channels (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type VARCHAR(20) NOT NULL,
|
||||
discord_webhook_url TEXT DEFAULT '',
|
||||
gmail_user VARCHAR(200) DEFAULT '',
|
||||
gmail_app_password TEXT DEFAULT '',
|
||||
alert_email_to VARCHAR(200) DEFAULT '',
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
""")
|
||||
conn.commit()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
@app.on_event("startup")
|
||||
def startup_notify_channels():
|
||||
import time
|
||||
for _ in range(10):
|
||||
try:
|
||||
init_notify_channels_db()
|
||||
print("Notify channels DB initialized")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Notify channels DB not ready... {e}")
|
||||
time.sleep(3)
|
||||
|
||||
def get_notify_config_from_db(conn) -> dict:
|
||||
"""DB에서 알림 설정 조회, 없으면 환경변수 fallback"""
|
||||
"""하위 호환: 첫 번째 활성 채널에서 설정 조회, 없으면 환경변수 fallback"""
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
cur.execute("SELECT key, value FROM notify_config")
|
||||
rows = cur.fetchall()
|
||||
db_config = {r["key"]: r["value"] for r in rows}
|
||||
try:
|
||||
cur.execute("SELECT * FROM notify_channels WHERE enabled=TRUE ORDER BY id LIMIT 1")
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return {
|
||||
"discord_webhook_url": row["discord_webhook_url"] or "",
|
||||
"gmail_user": row["gmail_user"] or "",
|
||||
"gmail_app_password": row["gmail_app_password"] or "",
|
||||
"alert_email_to": row["alert_email_to"] or "",
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"discord_webhook_url": db_config.get("discord_webhook_url", os.getenv("DISCORD_WEBHOOK_URL", "")),
|
||||
"gmail_user": db_config.get("gmail_user", os.getenv("GMAIL_USER", "")),
|
||||
"gmail_app_password": db_config.get("gmail_app_password", os.getenv("GMAIL_APP_PASSWORD", "")),
|
||||
"alert_email_to": db_config.get("alert_email_to", os.getenv("ALERT_EMAIL_TO", "")),
|
||||
"discord_webhook_url": os.getenv("DISCORD_WEBHOOK_URL", ""),
|
||||
"gmail_user": os.getenv("GMAIL_USER", ""),
|
||||
"gmail_app_password": os.getenv("GMAIL_APP_PASSWORD", ""),
|
||||
"alert_email_to": os.getenv("ALERT_EMAIL_TO", ""),
|
||||
}
|
||||
|
||||
@app.get("/api/admin/notify-config")
|
||||
def get_notify_config(token=Depends(require_admin), conn=Depends(get_db)):
|
||||
cfg = get_notify_config_from_db(conn)
|
||||
# 비밀번호는 설정 여부만 반환 (보안)
|
||||
return {
|
||||
"discord_webhook_url": cfg["discord_webhook_url"],
|
||||
"gmail_user": cfg["gmail_user"],
|
||||
"gmail_app_password": "••••••••" if cfg["gmail_app_password"] else "",
|
||||
"alert_email_to": cfg["alert_email_to"],
|
||||
}
|
||||
|
||||
@app.put("/api/admin/notify-config")
|
||||
def save_notify_config(data: NotifyConfig, token=Depends(require_admin), conn=Depends(get_db)):
|
||||
cur = conn.cursor()
|
||||
fields = {
|
||||
"discord_webhook_url": data.discord_webhook_url,
|
||||
"gmail_user": data.gmail_user,
|
||||
"alert_email_to": data.alert_email_to,
|
||||
}
|
||||
# 비밀번호가 마스킹값이면 업데이트 안 함
|
||||
if data.gmail_app_password and data.gmail_app_password != "••••••••":
|
||||
fields["gmail_app_password"] = data.gmail_app_password
|
||||
|
||||
for key, value in fields.items():
|
||||
if value is not None:
|
||||
cur.execute("""
|
||||
INSERT INTO notify_config (key, value, updated_at)
|
||||
VALUES (%s, %s, NOW())
|
||||
ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value, updated_at=NOW()
|
||||
""", (key, value))
|
||||
conn.commit()
|
||||
|
||||
# notifier 모듈 환경변수 동적 업데이트
|
||||
def refresh_notifier(conn):
|
||||
"""활성화된 모든 채널 중 첫 번째 채널로 notifier 모듈 업데이트"""
|
||||
import notifier
|
||||
cfg = get_notify_config_from_db(conn)
|
||||
notifier.DISCORD_WEBHOOK_URL = cfg["discord_webhook_url"]
|
||||
@@ -485,8 +499,77 @@ def save_notify_config(data: NotifyConfig, token=Depends(require_admin), conn=De
|
||||
notifier.GMAIL_APP_PASSWORD = cfg["gmail_app_password"]
|
||||
notifier.ALERT_EMAIL_TO = cfg["alert_email_to"]
|
||||
|
||||
@app.get("/api/admin/notify-channels")
|
||||
def list_notify_channels(token=Depends(require_admin), conn=Depends(get_db)):
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
cur.execute("SELECT id, name, type, discord_webhook_url, gmail_user, alert_email_to, enabled, created_at FROM notify_channels ORDER BY id")
|
||||
rows = cur.fetchall()
|
||||
# 비밀번호 제외하고 반환
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
@app.post("/api/admin/notify-channels")
|
||||
def create_notify_channel(data: NotifyChannel, token=Depends(require_admin), conn=Depends(get_db)):
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
cur.execute("""
|
||||
INSERT INTO notify_channels (name, type, discord_webhook_url, gmail_user, gmail_app_password, alert_email_to, enabled)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id, name, type, discord_webhook_url, gmail_user, alert_email_to, enabled
|
||||
""", (data.name, data.type, data.discord_webhook_url or "", data.gmail_user or "",
|
||||
data.gmail_app_password or "", data.alert_email_to or "", data.enabled))
|
||||
conn.commit()
|
||||
refresh_notifier(conn)
|
||||
return cur.fetchone()
|
||||
|
||||
@app.put("/api/admin/notify-channels/{channel_id}")
|
||||
def update_notify_channel(channel_id: int, data: NotifyChannel, token=Depends(require_admin), conn=Depends(get_db)):
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
# 비밀번호가 비어있으면 기존값 유지
|
||||
if data.gmail_app_password and data.gmail_app_password.strip():
|
||||
cur.execute("""
|
||||
UPDATE notify_channels SET name=%s, type=%s, discord_webhook_url=%s,
|
||||
gmail_user=%s, gmail_app_password=%s, alert_email_to=%s, enabled=%s
|
||||
WHERE id=%s RETURNING id, name, type, discord_webhook_url, gmail_user, alert_email_to, enabled
|
||||
""", (data.name, data.type, data.discord_webhook_url or "", data.gmail_user or "",
|
||||
data.gmail_app_password, data.alert_email_to or "", data.enabled, channel_id))
|
||||
else:
|
||||
cur.execute("""
|
||||
UPDATE notify_channels SET name=%s, type=%s, discord_webhook_url=%s,
|
||||
gmail_user=%s, alert_email_to=%s, enabled=%s
|
||||
WHERE id=%s RETURNING id, name, type, discord_webhook_url, gmail_user, alert_email_to, enabled
|
||||
""", (data.name, data.type, data.discord_webhook_url or "", data.gmail_user or "",
|
||||
data.alert_email_to or "", data.enabled, channel_id))
|
||||
conn.commit()
|
||||
refresh_notifier(conn)
|
||||
return cur.fetchone()
|
||||
|
||||
@app.delete("/api/admin/notify-channels/{channel_id}")
|
||||
def delete_notify_channel(channel_id: int, token=Depends(require_admin), conn=Depends(get_db)):
|
||||
cur = conn.cursor()
|
||||
cur.execute("DELETE FROM notify_channels WHERE id=%s", (channel_id,))
|
||||
conn.commit()
|
||||
refresh_notifier(conn)
|
||||
return {"ok": True}
|
||||
|
||||
# ─── 하위호환: 기존 단일 설정 API ────────────────────────
|
||||
def get_notify_config_from_db_legacy(conn) -> dict:
|
||||
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||
try:
|
||||
cur.execute("SELECT key, value FROM notify_config")
|
||||
rows = cur.fetchall()
|
||||
db_config = {r["key"]: r["value"] for r in rows}
|
||||
return {
|
||||
"discord_webhook_url": db_config.get("discord_webhook_url", os.getenv("DISCORD_WEBHOOK_URL", "")),
|
||||
"gmail_user": db_config.get("gmail_user", os.getenv("GMAIL_USER", "")),
|
||||
"gmail_app_password": db_config.get("gmail_app_password", os.getenv("GMAIL_APP_PASSWORD", "")),
|
||||
"alert_email_to": db_config.get("alert_email_to", os.getenv("ALERT_EMAIL_TO", "")),
|
||||
}
|
||||
except Exception:
|
||||
return {
|
||||
"discord_webhook_url": os.getenv("DISCORD_WEBHOOK_URL", ""),
|
||||
"gmail_user": os.getenv("GMAIL_USER", ""),
|
||||
"gmail_app_password": os.getenv("GMAIL_APP_PASSWORD", ""),
|
||||
"alert_email_to": os.getenv("ALERT_EMAIL_TO", ""),
|
||||
}
|
||||
|
||||
@app.get("/api/admin/notify-test")
|
||||
async def notify_test(token=Depends(require_admin)):
|
||||
# Discord + Gmail (Pod 이상/복구 테스트)
|
||||
|
||||
Reference in New Issue
Block a user