- session: set_tenant use bound param (SQL injection fix)
- health: text('SELECT 1'), REDIS_URL from config
- deps: re-export get_db from session, use settings.ENABLE_DEV_AUTH (default False)
- routes: all get_db from app.api.deps; conftest overrides deps.get_db
- main: register exception handlers from app.api.exceptions
- next.config: enable ESLint and TypeScript checks
- .eslintrc: drop @typescript-eslint/recommended; fix no-console (logger, ws-client, regulations)
- backend/.env.example added
- frontend: export apiFetch; dashboard, profile, settings, risks use api-client
- docs/ANALYSIS_AND_RECOMMENDATIONS.md
Co-authored-by: Cursor <cursoragent@cursor.com>
167 lines
8.4 KiB
Python
167 lines
8.4 KiB
Python
"""Cert Applications API — refactored: pagination, audit, WebSocket notifications."""
|
|
from __future__ import annotations
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.deps import get_current_user, require_roles
|
|
from app.services.email_service import email_service
|
|
from app.api.helpers import audit, is_authority, get_org_name, paginate_query
|
|
from app.api.deps import get_db
|
|
from app.integration.piv import push_event
|
|
from app.models import CertApplication, ApplicationRemark, CertApplicationStatus
|
|
from app.models.organization import Organization
|
|
from app.schemas.cert_application import CertApplicationCreate, CertApplicationOut, RemarkCreate, RemarkOut
|
|
from app.services.notifications import notify
|
|
from app.services.ws_manager import ws_manager, make_notification
|
|
|
|
router = APIRouter(tags=["cert_applications"])
|
|
|
|
REMARK_DEADLINE_DAYS = 30
|
|
|
|
|
|
def _next_number(db: Session) -> str:
|
|
today = datetime.now(timezone.utc).strftime("%Y%m%d")
|
|
prefix = f"KLG-{today}-"
|
|
cnt = db.query(CertApplication).filter(CertApplication.number.like(prefix + "%")).count()
|
|
return prefix + str(cnt + 1).zfill(4)
|
|
|
|
|
|
def _serialize(app, db: Session) -> CertApplicationOut:
|
|
return CertApplicationOut.model_validate({
|
|
"id": app.id, "number": app.number,
|
|
"status": app.status.value if hasattr(app.status, 'value') else str(app.status),
|
|
"applicant_org_id": app.applicant_org_id,
|
|
"applicant_org_name": get_org_name(db, app.applicant_org_id),
|
|
"created_by_user_id": app.created_by_user_id,
|
|
"submitted_at": app.submitted_at, "remarks_deadline_at": app.remarks_deadline_at,
|
|
"subject": app.subject, "description": app.description,
|
|
"created_at": app.created_at, "updated_at": app.updated_at,
|
|
})
|
|
|
|
|
|
def _get_app(db, app_id, user) -> CertApplication:
|
|
app = db.query(CertApplication).filter(CertApplication.id == app_id).first()
|
|
if not app: raise HTTPException(404, "Not found")
|
|
if not is_authority(user) and user.organization_id != app.applicant_org_id:
|
|
raise HTTPException(403, "Forbidden")
|
|
return app
|
|
|
|
|
|
# --- LIST ---
|
|
@router.get("/cert-applications")
|
|
def list_apps(
|
|
status_filter: str | None = Query(None, alias="status"),
|
|
page: int = Query(1, ge=1), per_page: int = Query(25, ge=1, le=100),
|
|
db: Session = Depends(get_db), user=Depends(get_current_user),
|
|
):
|
|
q = db.query(CertApplication)
|
|
if not is_authority(user):
|
|
if user.organization_id:
|
|
q = q.filter(CertApplication.applicant_org_id == user.organization_id)
|
|
else:
|
|
q = q.filter(False)
|
|
if status_filter:
|
|
q = q.filter(CertApplication.status == status_filter)
|
|
q = q.order_by(CertApplication.created_at.desc())
|
|
result = paginate_query(q, page, per_page)
|
|
result["items"] = [_serialize(a, db) for a in result["items"]]
|
|
return result
|
|
|
|
|
|
# --- CRUD ---
|
|
@router.post("/cert-applications", response_model=CertApplicationOut, status_code=201,
|
|
dependencies=[Depends(require_roles("admin", "operator_user", "operator_manager", "mro_user", "mro_manager"))])
|
|
def create_app(payload: CertApplicationCreate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
|
if not user.organization_id: raise HTTPException(400, "User has no organization_id")
|
|
app = CertApplication(
|
|
applicant_org_id=user.organization_id, created_by_user_id=user.id,
|
|
number=_next_number(db), status=CertApplicationStatus.DRAFT,
|
|
subject=payload.subject, description=payload.description,
|
|
)
|
|
db.add(app)
|
|
audit(db, user, "create", "cert_application", description=f"Created {app.number}")
|
|
db.commit(); db.refresh(app)
|
|
return _serialize(app, db)
|
|
|
|
|
|
@router.get("/cert-applications/{app_id}", response_model=CertApplicationOut)
|
|
def get_app_detail(app_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
|
return _serialize(_get_app(db, app_id, user), db)
|
|
|
|
|
|
# --- WORKFLOW ---
|
|
@router.post("/cert-applications/{app_id}/submit", response_model=CertApplicationOut,
|
|
dependencies=[Depends(require_roles("admin", "operator_user", "operator_manager", "mro_user", "mro_manager"))])
|
|
async def submit_app(app_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
|
app = _get_app(db, app_id, user)
|
|
if app.status not in {CertApplicationStatus.DRAFT, CertApplicationStatus.REMARKS}:
|
|
raise HTTPException(409, "Invalid status")
|
|
app.status = CertApplicationStatus.SUBMITTED
|
|
app.submitted_at = datetime.now(timezone.utc)
|
|
app.remarks_deadline_at = None
|
|
audit(db, user, "update", "cert_application", app_id, description=f"Submitted {app.number}")
|
|
db.commit(); db.refresh(app)
|
|
await push_event("cert_application_submitted", {"number": app.number, "app_id": app.id})
|
|
await ws_manager.broadcast(make_notification("submitted", "cert_application", app.id, number=app.number))
|
|
return _serialize(app, db)
|
|
|
|
|
|
@router.post("/cert-applications/{app_id}/start-review", response_model=CertApplicationOut,
|
|
dependencies=[Depends(require_roles("admin", "authority_inspector"))])
|
|
def start_review(app_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
|
app = _get_app(db, app_id, user)
|
|
if app.status != CertApplicationStatus.SUBMITTED: raise HTTPException(409, "Invalid status")
|
|
app.status = CertApplicationStatus.UNDER_REVIEW
|
|
audit(db, user, "update", "cert_application", app_id, description=f"Review started {app.number}")
|
|
db.commit(); db.refresh(app)
|
|
return _serialize(app, db)
|
|
|
|
|
|
@router.post("/cert-applications/{app_id}/remarks", response_model=RemarkOut,
|
|
dependencies=[Depends(require_roles("admin", "authority_inspector"))])
|
|
def add_remark(app_id: str, payload: RemarkCreate, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
|
app = _get_app(db, app_id, user)
|
|
app.status = CertApplicationStatus.REMARKS
|
|
app.remarks_deadline_at = datetime.now(timezone.utc) + timedelta(days=REMARK_DEADLINE_DAYS)
|
|
r = ApplicationRemark(application_id=app_id, author_user_id=user.id, text=payload.text)
|
|
db.add(r)
|
|
audit(db, user, "update", "cert_application", app_id, description=f"Remark added to {app.number}")
|
|
db.commit(); db.refresh(r)
|
|
notify(db, app.created_by_user_id, f"Заявка {app.number}: замечания",
|
|
f"Срок: {REMARK_DEADLINE_DAYS} дн. (до {app.remarks_deadline_at.isoformat()})")
|
|
return r
|
|
|
|
|
|
@router.get("/cert-applications/{app_id}/remarks", response_model=list[RemarkOut])
|
|
def list_remarks(app_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
|
_get_app(db, app_id, user) # access check
|
|
return db.query(ApplicationRemark).filter(ApplicationRemark.application_id == app_id).order_by(ApplicationRemark.created_at.desc()).all()
|
|
|
|
|
|
@router.post("/cert-applications/{app_id}/approve", response_model=CertApplicationOut,
|
|
dependencies=[Depends(require_roles("admin", "authority_inspector"))])
|
|
async def approve(app_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
|
app = _get_app(db, app_id, user)
|
|
if app.status not in {CertApplicationStatus.UNDER_REVIEW, CertApplicationStatus.SUBMITTED}:
|
|
raise HTTPException(409, "Invalid status")
|
|
app.status = CertApplicationStatus.APPROVED; app.remarks_deadline_at = None
|
|
audit(db, user, "update", "cert_application", app_id, description=f"Approved {app.number}")
|
|
db.commit(); db.refresh(app)
|
|
await ws_manager.send_to_org(app.applicant_org_id, make_notification("approved", "cert_application", app.id, number=app.number))
|
|
return _serialize(app, db)
|
|
|
|
|
|
@router.post("/cert-applications/{app_id}/reject", response_model=CertApplicationOut,
|
|
dependencies=[Depends(require_roles("admin", "authority_inspector"))])
|
|
async def reject(app_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
|
|
app = _get_app(db, app_id, user)
|
|
if app.status not in {CertApplicationStatus.UNDER_REVIEW, CertApplicationStatus.SUBMITTED, CertApplicationStatus.REMARKS}:
|
|
raise HTTPException(409, "Invalid status")
|
|
app.status = CertApplicationStatus.REJECTED; app.remarks_deadline_at = None
|
|
audit(db, user, "update", "cert_application", app_id, description=f"Rejected {app.number}")
|
|
db.commit(); db.refresh(app)
|
|
await ws_manager.send_to_org(app.applicant_org_id, make_notification("rejected", "cert_application", app.id, number=app.number))
|
|
return _serialize(app, db)
|