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:
@@ -397,7 +397,7 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) {
|
||||
toast.error(err instanceof Error ? err.message : "حذف فاکتور ناموفق بود"),
|
||||
)
|
||||
}
|
||||
urlOf={(a) => (a.id ? api.files.downloadUrl(q.id, "invoice", a.id) : null)}
|
||||
onDownload={(a) => api.files.download(q.id, "invoice", a.id!, a.name)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -414,7 +414,7 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) {
|
||||
toast.error(err instanceof Error ? err.message : "حذف پیشفاکتور ناموفق بود"),
|
||||
)
|
||||
}
|
||||
urlOf={(a) => (a.id ? api.files.downloadUrl(q.id, "proforma", a.id) : null)}
|
||||
onDownload={(a) => api.files.download(q.id, "proforma", a.id!, a.name)}
|
||||
label="افزودن پیشفاکتور"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -720,7 +720,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
toast.error(err instanceof Error ? err.message : "حذف فاکتور ناموفق بود"),
|
||||
)
|
||||
}
|
||||
urlOf={(a) => (a.id ? api.files.downloadUrl(q.id, "invoice", a.id) : null)}
|
||||
onDownload={(a) => api.files.download(q.id, "invoice", a.id!, a.name)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -144,19 +144,31 @@ export function QuoteDetail({ store, q }: QuoteDetailProps) {
|
||||
attachments.proformas.forEach((pf, i) => {
|
||||
if (!pf.id) return;
|
||||
attachParts.push(
|
||||
<a key={"pf" + i} className="detail-link" href={api.files.downloadUrl(q.id, "proforma", pf.id)} download={pf.name}>
|
||||
<button
|
||||
key={"pf" + i}
|
||||
type="button"
|
||||
className="detail-link"
|
||||
title="دانلود پیشفاکتور"
|
||||
onClick={() => api.files.download(q.id, "proforma", pf.id!, pf.name)}
|
||||
>
|
||||
<Paperclip className="me-1 inline h-3 w-3" />
|
||||
پیشفاکتور: {pf.name}
|
||||
</a>,
|
||||
</button>,
|
||||
);
|
||||
});
|
||||
attachments.invoices.forEach((inv, i) => {
|
||||
if (!inv.id) return;
|
||||
attachParts.push(
|
||||
<a key={"inv" + i} className="detail-link" href={api.files.downloadUrl(q.id, "invoice", inv.id)} download={inv.name}>
|
||||
<button
|
||||
key={"inv" + i}
|
||||
type="button"
|
||||
className="detail-link"
|
||||
title="دانلود فاکتور"
|
||||
onClick={() => api.files.download(q.id, "invoice", inv.id!, inv.name)}
|
||||
>
|
||||
<Paperclip className="me-1 inline h-3 w-3" />
|
||||
فاکتور اصلی: {inv.name}
|
||||
</a>,
|
||||
</button>,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -112,28 +112,28 @@ export function PurchaseHistory({ supplier, quotes, materials }: PurchaseHistory
|
||||
) : (
|
||||
<>
|
||||
{profs.map((pf) => (
|
||||
<a
|
||||
<button
|
||||
key={"pf-" + pf.id}
|
||||
type="button"
|
||||
className="detail-link"
|
||||
href={api.files.downloadUrl(q.id, "proforma", pf.id!)}
|
||||
download={pf.name}
|
||||
title="دانلود پیشفاکتور"
|
||||
onClick={() => api.files.download(q.id, "proforma", pf.id!, pf.name)}
|
||||
>
|
||||
<Paperclip className="me-1 inline h-3 w-3" />
|
||||
پیشفاکتور: {pf.name}
|
||||
</a>
|
||||
</button>
|
||||
))}
|
||||
{invs.map((inv) => (
|
||||
<a
|
||||
<button
|
||||
key={"inv-" + inv.id}
|
||||
type="button"
|
||||
className="detail-link"
|
||||
href={api.files.downloadUrl(q.id, "invoice", inv.id!)}
|
||||
download={inv.name}
|
||||
title="دانلود فاکتور"
|
||||
onClick={() => api.files.download(q.id, "invoice", inv.id!, inv.name)}
|
||||
>
|
||||
<Paperclip className="me-1 inline h-3 w-3" />
|
||||
فاکتور: {inv.name}
|
||||
</a>
|
||||
</button>
|
||||
))}
|
||||
{!loading && !profs.length && !invs.length && (
|
||||
<span className="ph-attach-loading">فاکتوری ثبت نشده</span>
|
||||
|
||||
Reference in New Issue
Block a user