From 253e7d5839868f7fd11cf6f0e8c357a25e2411b1 Mon Sep 17 00:00:00 2001 From: TronoSfera <119615520+TronoSfera@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:37:34 +0300 Subject: [PATCH] fix UI 12 --- app/web/admin.js | 37 +++++++++++++++++++++++++++++- app/web/admin.jsx | 23 ++++++++++++++++++- app/web/admin/hooks/useAdminApi.js | 17 ++++++++++++++ app/web/admin/shared/constants.js | 1 + 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/app/web/admin.js b/app/web/admin.js index 598495e..35aaeea 100644 --- a/app/web/admin.js +++ b/app/web/admin.js @@ -2105,6 +2105,7 @@ // app/web/admin/shared/constants.js var LS_TOKEN = "admin_access_token"; + var ADMIN_AUTH_REDIRECT_REASON_KEY = "admin_auth_redirect_reason"; var PAGE_SIZE = 50; var DEFAULT_FORM_FIELD_TYPES = ["string", "text", "number", "boolean", "date"]; var ALL_OPERATORS = ["=", "!=", ">", "<", ">=", "<=", "~"]; @@ -5843,6 +5844,21 @@ } if (!response.ok) { const message = payload && (payload.detail || payload.error || payload.raw) || "HTTP " + response.status; + if (response.status === 401 && opts.auth !== false) { + try { + localStorage.removeItem(LS_TOKEN); + sessionStorage.setItem(ADMIN_AUTH_REDIRECT_REASON_KEY, "expired"); + } catch (_) { + } + if (typeof window !== "undefined") { + const target = "/admin.html"; + if (window.location.pathname !== target || window.location.search) { + window.location.replace(target); + } else { + window.location.reload(); + } + } + } const error = new Error(translateApiError(String(message))); error.httpStatus = Number(response.status || 0); throw error; @@ -7627,6 +7643,13 @@ setStatusMap((prev) => ({ ...prev, [key]: { message: message || "", kind: kind || "" } })); }, []); const getStatus = useCallback((key) => statusMap[key] || { message: "", kind: "" }, [statusMap]); + const isAdminTokenExpired = useCallback((rawToken) => { + const payload = decodeJwtPayload(rawToken || ""); + const exp = Number((payload == null ? void 0 : payload.exp) || 0); + if (!payload || !payload.role || !payload.email) return true; + if (!Number.isFinite(exp) || exp <= 0) return true; + return exp * 1e3 <= Date.now(); + }, []); const api = useAdminApi(token); const { requestModal, @@ -9657,6 +9680,7 @@ const nextToken = data.access_token; const payload = decodeJwtPayload(nextToken || ""); if (!payload || !payload.role || !payload.email) throw new Error("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0435 \u0442\u043E\u043A\u0435\u043D\u0430"); + sessionStorage.removeItem(ADMIN_AUTH_REDIRECT_REASON_KEY); localStorage.setItem(LS_TOKEN, nextToken); setToken(nextToken); setRole(payload.role); @@ -9674,18 +9698,29 @@ [api, bootstrapReferenceData, loadDashboard, loadTotpStatus, setStatus] ); useEffect(() => { + const authRedirectReason = sessionStorage.getItem(ADMIN_AUTH_REDIRECT_REASON_KEY) || ""; + if (authRedirectReason === "expired") { + setStatus("login", "\u0421\u0435\u0441\u0441\u0438\u044F \u0438\u0441\u0442\u0435\u043A\u043B\u0430. \u0412\u043E\u0439\u0434\u0438\u0442\u0435 \u0441\u043D\u043E\u0432\u0430.", "error"); + sessionStorage.removeItem(ADMIN_AUTH_REDIRECT_REASON_KEY); + } const saved = localStorage.getItem(LS_TOKEN) || ""; if (!saved) return; + if (isAdminTokenExpired(saved)) { + localStorage.removeItem(LS_TOKEN); + setStatus("login", "\u0421\u0435\u0441\u0441\u0438\u044F \u0438\u0441\u0442\u0435\u043A\u043B\u0430. \u0412\u043E\u0439\u0434\u0438\u0442\u0435 \u0441\u043D\u043E\u0432\u0430.", "error"); + return; + } const payload = decodeJwtPayload(saved); if (!payload || !payload.role || !payload.email) { localStorage.removeItem(LS_TOKEN); + setStatus("login", "\u0421\u0435\u0441\u0441\u0438\u044F \u0438\u0441\u0442\u0435\u043A\u043B\u0430. \u0412\u043E\u0439\u0434\u0438\u0442\u0435 \u0441\u043D\u043E\u0432\u0430.", "error"); return; } setToken(saved); setRole(payload.role); setEmail(payload.email); setUserId(String(payload.sub || "")); - }, []); + }, [isAdminTokenExpired, setStatus]); useEffect(() => { if (!token || !role) return; let cancelled = false; diff --git a/app/web/admin.jsx b/app/web/admin.jsx index d301859..9f4d6a2 100644 --- a/app/web/admin.jsx +++ b/app/web/admin.jsx @@ -1,5 +1,6 @@ import { DEFAULT_FORM_FIELD_TYPES, + ADMIN_AUTH_REDIRECT_REASON_KEY, INVOICE_STATUS_LABELS, LS_TOKEN, OPERATOR_LABELS, @@ -1263,6 +1264,13 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__"; }, []); const getStatus = useCallback((key) => statusMap[key] || { message: "", kind: "" }, [statusMap]); + const isAdminTokenExpired = useCallback((rawToken) => { + const payload = decodeJwtPayload(rawToken || ""); + const exp = Number(payload?.exp || 0); + if (!payload || !payload.role || !payload.email) return true; + if (!Number.isFinite(exp) || exp <= 0) return true; + return exp * 1000 <= Date.now(); + }, []); const api = useAdminApi(token); @@ -3465,6 +3473,7 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__"; const payload = decodeJwtPayload(nextToken || ""); if (!payload || !payload.role || !payload.email) throw new Error("Не удалось прочитать данные токена"); + sessionStorage.removeItem(ADMIN_AUTH_REDIRECT_REASON_KEY); localStorage.setItem(LS_TOKEN, nextToken); setToken(nextToken); setRole(payload.role); @@ -3485,18 +3494,30 @@ const NEW_REQUEST_CLIENT_OPTION = "__new_client__"; ); useEffect(() => { + const authRedirectReason = sessionStorage.getItem(ADMIN_AUTH_REDIRECT_REASON_KEY) || ""; + if (authRedirectReason === "expired") { + setStatus("login", "Сессия истекла. Войдите снова.", "error"); + sessionStorage.removeItem(ADMIN_AUTH_REDIRECT_REASON_KEY); + } + const saved = localStorage.getItem(LS_TOKEN) || ""; if (!saved) return; + if (isAdminTokenExpired(saved)) { + localStorage.removeItem(LS_TOKEN); + setStatus("login", "Сессия истекла. Войдите снова.", "error"); + return; + } const payload = decodeJwtPayload(saved); if (!payload || !payload.role || !payload.email) { localStorage.removeItem(LS_TOKEN); + setStatus("login", "Сессия истекла. Войдите снова.", "error"); return; } setToken(saved); setRole(payload.role); setEmail(payload.email); setUserId(String(payload.sub || "")); - }, []); + }, [isAdminTokenExpired, setStatus]); useEffect(() => { if (!token || !role) return; diff --git a/app/web/admin/hooks/useAdminApi.js b/app/web/admin/hooks/useAdminApi.js index 60b089a..0c1a51e 100644 --- a/app/web/admin/hooks/useAdminApi.js +++ b/app/web/admin/hooks/useAdminApi.js @@ -1,3 +1,4 @@ +import { ADMIN_AUTH_REDIRECT_REASON_KEY, LS_TOKEN } from "../shared/constants.js"; import { translateApiError } from "../shared/utils.js"; export function useAdminApi(token) { @@ -30,6 +31,22 @@ export function useAdminApi(token) { if (!response.ok) { const message = (payload && (payload.detail || payload.error || payload.raw)) || "HTTP " + response.status; + if (response.status === 401 && opts.auth !== false) { + try { + localStorage.removeItem(LS_TOKEN); + sessionStorage.setItem(ADMIN_AUTH_REDIRECT_REASON_KEY, "expired"); + } catch (_) { + // noop + } + if (typeof window !== "undefined") { + const target = "/admin.html"; + if (window.location.pathname !== target || window.location.search) { + window.location.replace(target); + } else { + window.location.reload(); + } + } + } const error = new Error(translateApiError(String(message))); error.httpStatus = Number(response.status || 0); throw error; diff --git a/app/web/admin/shared/constants.js b/app/web/admin/shared/constants.js index a089673..141530a 100644 --- a/app/web/admin/shared/constants.js +++ b/app/web/admin/shared/constants.js @@ -1,4 +1,5 @@ export const LS_TOKEN = "admin_access_token"; +export const ADMIN_AUTH_REDIRECT_REASON_KEY = "admin_auth_redirect_reason"; export const PAGE_SIZE = 50; export const DEFAULT_FORM_FIELD_TYPES = ["string", "text", "number", "boolean", "date"]; export const ALL_OPERATORS = ["=", "!=", ">", "<", ">=", "<=", "~"];