zioinfo-mail/workspace/guardia-itsm/routers/push.py
DESKTOP-TKLFCPR\ython cfe2901a55 refactor(structure): consolidate all projects under workspace/
- itsm/    -> workspace/guardia-itsm/
- manager/ -> workspace/guardia-manager/
- app/     -> workspace/guardia-messenger/
- manual/  -> workspace/guardia-docs/

workspace/zioinfo-web/ unchanged.
git mv preserves full commit history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:50:56 +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),
}