guardia-itsm/routers/approvals.py
DESKTOP-TKLFCPRython bc85c5228a feat(itsm): Jira-like ITSM 시스템 구현
- FastAPI + SQLAlchemy(aiosqlite) 기반 SR 상태 머신
  (RECEIVED → PARSED → PENDING_APPROVAL → APPROVED → IN_PROGRESS
   → PENDING_PM_VALIDATION → COMPLETED / FAILED_ROLLBACK)
- PM 승인 워크플로우 (ApprovalFlow 테이블)
- SHA-256 해시 체인 감사 로그 (위변조 방지)
- AES-256-GCM 서버 자격증명 암호화 (IP/PW API 미노출)
- CMDB: 기관(MOF/MOIS/MSS) + 서버 정보 관리
- 더미 데이터 자동 시딩 (6개 SR, 3개 기관, 6개 서버)
- Dark-theme SPA: 대시보드 / 칸반 보드 / SR 목록 / 감사 로그 / CMDB

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:31:09 +09:00

73 lines
2.5 KiB
Python

"""PM Approval workflow endpoints."""
from datetime import datetime
from typing import List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_db
from models import (
ApprovalCreate, ApprovalFlow, ApprovalOut, ApprovalResult,
SRRequest, SRStatus, AuditLog, compute_log_hash
)
router = APIRouter(prefix="/api/approvals", tags=["approvals"])
async def _write_audit(db: AsyncSession, sr_id: str, actor: str, action: str, detail: str) -> None:
result = await db.execute(
select(AuditLog).where(AuditLog.sr_id == sr_id)
.order_by(AuditLog.id.desc()).limit(1)
)
last = result.scalars().first()
prev_hash = last.log_hash if last else None
ts = datetime.now().isoformat()
log_hash = compute_log_hash(prev_hash, actor, action, detail, ts)
db.add(AuditLog(
sr_id=sr_id, actor=actor, action=action, detail=detail,
prev_hash=prev_hash, log_hash=log_hash
))
@router.get("/{sr_id}", response_model=List[ApprovalOut])
async def list_approvals(sr_id: str, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(ApprovalFlow).where(ApprovalFlow.sr_id == sr_id)
.order_by(ApprovalFlow.created_at)
)
return result.scalars().all()
@router.post("/{sr_id}", response_model=ApprovalOut, status_code=201)
async def decide_approval(sr_id: str, payload: ApprovalCreate,
db: AsyncSession = Depends(get_db)):
r = await db.execute(select(SRRequest).where(SRRequest.sr_id == sr_id))
sr = r.scalars().first()
if not sr:
raise HTTPException(404, detail="SR을 찾을 수 없습니다.")
if sr.status != SRStatus.PENDING_APPROVAL:
raise HTTPException(400, detail="승인 대기 상태의 SR만 처리할 수 있습니다.")
apv = ApprovalFlow(
sr_id=sr_id,
approver=payload.approver,
result=payload.result,
comment=payload.comment,
decided_at=datetime.now(),
)
db.add(apv)
if payload.result == ApprovalResult.APPROVED:
sr.status = SRStatus.APPROVED
action, detail = "SR_APPROVED", f"{payload.approver} 승인"
else:
sr.status = SRStatus.REJECTED
action, detail = "SR_REJECTED", f"{payload.approver} 반려: {payload.comment or ''}"
sr.updated_at = datetime.now()
await _write_audit(db, sr_id, payload.approver, action, detail)
await db.commit()
await db.refresh(apv)
return apv