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>
126 lines
3.5 KiB
Python
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),
|
|
}
|