zioinfo-mail/itsm/routers/push.py
DESKTOP-TKLFCPR\ython e228faabf5 feat(itsm): G-1~G-12 확장 기능 + 하네스/봇/설치스크립트 구현
G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현
G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건)
G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST)
G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직
G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트
G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트
G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API
G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트
G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트
G-10: core/push_notify.py + routers/push.py + PushSubscription 모델
G-11: approvals 다중승인 (위임/서명/기한초과/마감연장)
G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh

하네스: guardia-orchestrator 확장기능 Phase 반영
봇명령어: /sr /status /license /bulk 슬래시 명령어 추가
설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:18:52 +09:00

126 lines
3.5 KiB
Python

"""G-10: PWA Push 알림 구독 관리 API."""
from __future__ import annotations
import logging
import os
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user, require_admin_role
from database import get_db
from models import PushSubscription, User
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/push", tags=["push"])
class SubscribeRequest(BaseModel):
endpoint: str
p256dh: str
auth: str
user_agent: Optional[str] = None
class PushTestRequest(BaseModel):
title: str = "GUARDiA 테스트"
body: str = "푸시 알림 테스트입니다."
url: str = "/"
@router.get("/vapid-key")
async def get_vapid_public_key():
"""VAPID 공개키 반환 (프론트엔드 subscribe 시 사용)."""
key = os.getenv("VAPID_PUBLIC_KEY", "")
return {"vapid_public_key": key, "configured": bool(key)}
@router.post("/subscribe", status_code=201)
async def subscribe(
body: SubscribeRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""PWA Push 구독 등록."""
# 기존 구독 확인 (endpoint 기준)
existing = (await db.execute(
select(PushSubscription).where(PushSubscription.endpoint == body.endpoint)
)).scalars().first()
if existing:
# 갱신
existing.user_id = current_user.id
existing.p256dh = body.p256dh
existing.auth = body.auth
existing.user_agent = body.user_agent
else:
sub = PushSubscription(
user_id = current_user.id,
endpoint = body.endpoint,
p256dh = body.p256dh,
auth = body.auth,
user_agent = body.user_agent,
)
db.add(sub)
await db.commit()
return {"message": "푸시 구독이 등록되었습니다.", "user": current_user.username}
@router.delete("/subscribe", status_code=204)
async def unsubscribe(
endpoint: str,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""PWA Push 구독 해제."""
sub = (await db.execute(
select(PushSubscription).where(
PushSubscription.endpoint == endpoint,
PushSubscription.user_id == current_user.id,
)
)).scalars().first()
if sub:
await db.delete(sub)
await db.commit()
@router.post("/test")
async def test_push(
body: PushTestRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin_role),
):
"""테스트 푸시 전송 (ADMIN 전용 — 자신의 구독에 발송)."""
subs = (await db.execute(
select(PushSubscription).where(PushSubscription.user_id == current_user.id)
)).scalars().all()
if not subs:
raise HTTPException(404, "등록된 푸시 구독이 없습니다. 먼저 구독을 등록하세요.")
from core.push_notify import send_push
sent = 0
for sub in subs:
ok = await send_push(
subscription_info={
"endpoint": sub.endpoint,
"keys": {"p256dh": sub.p256dh, "auth": sub.auth},
},
title=body.title,
body=body.body,
url=body.url,
)
if ok:
sent += 1
return {
"message": f"테스트 푸시 전송 완료 (성공: {sent}/{len(subs)})",
"sent": sent,
"total_subs": len(subs),
}