feat(security): JWT required on every API route, gated dev fallback, role checks
- main.py: global get_current_user dependency on all routers except /auth/login and /health; prod fail-fast startup guard rejects wildcard CORS, default credentials, DEV_LOGIN_ENABLED - inventory/pm: X-User-Role header identity fallback now only active when PROCUREMENT_DEV_LOGIN_ENABLED=true (was unconditional in production) - item_decisions writes + attachment deletes: require_role( procurement_manager, admin) — require_role was defined but never used - config.py: ENVIRONMENT flag, comma-separated CORS_ORIGINS, no default Casdoor client secret/admin password, DEV_LOGIN_ENABLED=false - frontend: authenticated file downloads (Bearer + blob) — <a href> links cannot send the token; FileList/QuoteDetail/AcceptModal/EditModal/ PurchaseHistory converted; DEV_LOGIN_ENABLED build arg defaults false - smoke tests authenticate via POST /auth/login (SMOKE_USER/SMOKE_PASSWORD) - traefik: security headers, API rate limit, 12MB body cap (prod overlay) - nginx client_max_body_size aligned to 12MB
This commit is contained in:
@@ -20,6 +20,7 @@ from urllib.parse import quote
|
||||
from fastapi import APIRouter, Depends, File, Response, UploadFile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.auth import require_role
|
||||
from app.db.engine import get_db
|
||||
from app.models.procurement import Quote
|
||||
from app.services import file_service
|
||||
@@ -27,6 +28,10 @@ from app.utils.errors import NotFoundError, ValidationError
|
||||
|
||||
router = APIRouter(prefix="/quotes", tags=["files"])
|
||||
|
||||
# حذف پیوستها = عملیات تدارکات (ادمین / مدیر تدارکات)؛ بارگذاری و دانلود
|
||||
# برای هر کاربر واردشده آزاد است.
|
||||
_attachment_deleter = require_role("procurement_manager", "admin")
|
||||
|
||||
ALLOWED_MIME_TYPES = {
|
||||
"image/png", "image/jpeg", "image/webp", "image/gif", "application/pdf",
|
||||
}
|
||||
@@ -120,7 +125,7 @@ async def download_invoice(quote_id: uuid.UUID, attachment_id: uuid.UUID, db: As
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{quote_id}/invoice/{attachment_id}")
|
||||
@router.delete("/{quote_id}/invoice/{attachment_id}", dependencies=[Depends(_attachment_deleter)])
|
||||
async def delete_invoice(quote_id: uuid.UUID, attachment_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
||||
await _quote_or_404(db, quote_id)
|
||||
attachment = await _attachment_or_404(db, quote_id, attachment_id, "invoice")
|
||||
@@ -165,7 +170,7 @@ async def download_proforma(quote_id: uuid.UUID, attachment_id: uuid.UUID, db: A
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{quote_id}/proforma/{attachment_id}")
|
||||
@router.delete("/{quote_id}/proforma/{attachment_id}", dependencies=[Depends(_attachment_deleter)])
|
||||
async def delete_proforma(quote_id: uuid.UUID, attachment_id: uuid.UUID, db: AsyncSession = Depends(get_db)):
|
||||
await _quote_or_404(db, quote_id)
|
||||
attachment = await _attachment_or_404(db, quote_id, attachment_id, "proforma")
|
||||
|
||||
Reference in New Issue
Block a user