Merge latest prod_v2 (FileList preview modal, EditModal tabs) into integration
prod_v2's inline preview modal and EditModal redesign conflicted with the hardening branch's authenticated file API (urlOf/downloadUrl were removed because <a href>/<img> cannot send the Authorization header). - FileList: kept prod_v2's design (icons, preview modal, per-row actions) and adapted it — previews now fetch the bytes with a Bearer token and use a local objectURL (api.files.blobUrl), with revoke on close/unmount; the row download uses the authenticated onDownload. - EditModal: took prod_v2's tabbed redesign wholesale (it already carried our globalSpecs, clean material name and target_project work) and converted its FileList to onDownload + loadPreview. - AcceptModal: converted the remaining urlOf sites and removed duplicated file/date blocks the earlier merge had left outside the scroll body. - api.ts: restored the global handle401 (definition + all four fetch sites) alongside the new blobUrl helper.
This commit is contained in:
+34
-1
@@ -70,6 +70,20 @@ function authHeaders(init?: RequestInit): Record<string, string> {
|
||||
return headers;
|
||||
}
|
||||
|
||||
// 401 سراسری — هر پاسخ 401 یعنی نشست منقضی/نامعتبر است (JWT روی همهی
|
||||
// مسیرها الزامی شد). نشست را پاک میکنیم و به صفحهی ورود میرویم تا بهجای
|
||||
// «برنامهی خاموش» (استور خالی، بدون هیچ خطایی) کاربر به ورود هدایت شود.
|
||||
// دخالت نمیکنیم در: خطای رمز خودِ لاگین، حالت توسعه (هدرهای X-User-*)،
|
||||
// و وقتی از قبل روی /login هستیم (جلوگیری از حلقهی ریدایرکت).
|
||||
function handle401(path: string, res: Response): void {
|
||||
if (res.status !== 401) return;
|
||||
if (path.startsWith("/auth/login")) return;
|
||||
if (useAuthStore.getState().isDev) return;
|
||||
if (window.location.pathname === "/login") return;
|
||||
useAuthStore.getState().logout();
|
||||
window.location.replace("/login");
|
||||
}
|
||||
|
||||
// پروژهی فعالِ مدیر پروژه (چند-پروژهای، 1:N): برای همهی مسیرهای پنل انبار
|
||||
// صریحاً ارسال میشود تا بکاند دادههای همان پروژه را برگرداند/اعتبارسنجی کند.
|
||||
// ادمین/مدیر تدارکات بدون پارامتر (همهی پروژهها) باقی میماند.
|
||||
@@ -186,6 +200,7 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
} catch {
|
||||
throw new ApiError("اتصال به سرور برقرار نشد — آیا بکاند در دسترس است؟", 0);
|
||||
}
|
||||
handle401(path, res);
|
||||
if (!res.ok) {
|
||||
let msg = `خطای سرور (${res.status})`;
|
||||
let errCode: string | undefined;
|
||||
@@ -261,6 +276,7 @@ async function uploadFile(path: string, file: File): Promise<Attachment> {
|
||||
} catch {
|
||||
throw new Error("اتصال به سرور برقرار نشد — آیا بکاند در دسترس است؟");
|
||||
}
|
||||
handle401(path, res);
|
||||
if (!res.ok) {
|
||||
let msg = `خطای سرور (${res.status})`;
|
||||
try {
|
||||
@@ -1279,13 +1295,15 @@ export const api = {
|
||||
// را با توکن میگیریم و به شکل blob در مرورگر ذخیره میکنیم.
|
||||
download: async (quoteId: string, kind: FileKind, id: string, fileName: string): Promise<void> => {
|
||||
let res: Response;
|
||||
const dlPath = `/quotes/${quoteId}/${kind}/${id}`;
|
||||
try {
|
||||
res = await fetch(`${BASE_URL}/quotes/${quoteId}/${kind}/${id}`, {
|
||||
res = await fetch(BASE_URL + dlPath, {
|
||||
headers: authHeaders(),
|
||||
});
|
||||
} catch {
|
||||
throw new ApiError("اتصال به سرور برقرار نشد — آیا بکاند در دسترس است؟", 0);
|
||||
}
|
||||
handle401(dlPath, res);
|
||||
if (!res.ok) throw new ApiError(faHttpError(res.status, "خطای سرور"), res.status);
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -1300,6 +1318,21 @@ export const api = {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
},
|
||||
// آدرس blob احراز هویتشده برای پیشنمایش (تصویر/PDF در مودال).
|
||||
// تگهای <img>/<iframe>/<a href> توکن نمیفرستند، پس بایتها را با توکن
|
||||
// میگیریم و یک objectURL محلی برمیگردانیم. آزادسازی با revokeBlobUrl.
|
||||
blobUrl: async (quoteId: string, kind: FileKind, id: string): Promise<string> => {
|
||||
let res: Response;
|
||||
const path = `/quotes/${quoteId}/${kind}/${id}`;
|
||||
try {
|
||||
res = await fetch(BASE_URL + path, { headers: authHeaders() });
|
||||
} catch {
|
||||
throw new ApiError("اتصال به سرور برقرار نشد — آیا بکاند در دسترس است؟", 0);
|
||||
}
|
||||
handle401(path, res);
|
||||
if (!res.ok) throw new ApiError(faHttpError(res.status, "خطای سرور"), res.status);
|
||||
return URL.createObjectURL(await res.blob());
|
||||
},
|
||||
},
|
||||
|
||||
// ---------- برندها (از بکاند) ----------
|
||||
|
||||
Reference in New Issue
Block a user