- 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>
73 lines
2.5 KiB
Python
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
|