const StorageHelper = { get: (key) => { return localStorage.getItem(key) || sessionStorage.getItem(key); }, getCurrentStorage: () => { return localStorage.getItem("refresh_token") ? localStorage : sessionStorage; }, clearAll: () => { localStorage.removeItem("access_token"); localStorage.removeItem("refresh_token"); localStorage.removeItem("user"); sessionStorage.removeItem("access_token"); sessionStorage.removeItem("refresh_token"); sessionStorage.removeItem("user"); }, }; const Utils = { escapeHtml: (text) => { if (text === null || text === undefined) return ""; return String(text).replace( /[&<>"']/g, (m) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", })[m], ); }, showToast: (message, type = "info") => { const container = document.getElementById("toast-container"); if (!container) return; const el = document.createElement("div"); const colors = type === "error" ? "bg-red-500" : type === "success" ? "bg-green-500" : "bg-blue-500"; el.className = `${colors} text-white px-6 py-3 rounded shadow-lg transform transition-all duration-300 translate-y-10 opacity-0 mb-3`; el.textContent = message; container.appendChild(el); requestAnimationFrame(() => { el.classList.remove("translate-y-10", "opacity-0"); }); setTimeout(() => { el.classList.add("translate-y-10", "opacity-0"); setTimeout(() => el.remove(), 300); }, 3000); }, getGravatarUrl: async (email) => { if (!email) return ""; const msgBuffer = new TextEncoder().encode(email.trim().toLowerCase()); const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray .map((b) => b.toString(16).padStart(2, "0")) .join(""); return `https://www.gravatar.com/avatar/${hashHex}?d=identicon&s=200`; }, }; const Api = { getBaseUrl() { return window.location.origin; }, async request(endpoint, options = {}) { const fullUrl = this.getBaseUrl() + endpoint; const token = StorageHelper.get("access_token"); const headers = { "Content-Type": "application/json", ...options.headers, }; if (token) { headers["Authorization"] = `Bearer ${token}`; } const config = { ...options, headers }; try { const response = await fetch(fullUrl, config); const isLoginRequest = endpoint.includes("/auth/token"); if (response.status === 401 && !isLoginRequest) { const refreshed = await Auth.tryRefresh(); if (refreshed) { headers["Authorization"] = `Bearer ${StorageHelper.get("access_token")}`; const retryResponse = await fetch(fullUrl, { ...options, headers }); if (retryResponse.ok) { return retryResponse.json(); } } Auth.logout(); return null; } if (!response.ok) { const errorData = await response.json().catch(() => ({})); const error = new Error("API Error"); Object.assign(error, errorData); if (typeof errorData.detail === "string") { error.message = errorData.detail; } else if (errorData.error_description) { error.message = errorData.error_description; } else if (!errorData.detail) { error.message = `Ошибка ${response.status}`; } throw error; } return response.json(); } catch (error) { throw error; } }, get(endpoint) { return this.request(endpoint, { method: "GET" }); }, post(endpoint, body) { return this.request(endpoint, { method: "POST", body: JSON.stringify(body), }); }, put(endpoint, body) { return this.request(endpoint, { method: "PUT", body: JSON.stringify(body), }); }, delete(endpoint) { return this.request(endpoint, { method: "DELETE" }); }, postForm(endpoint, formData) { return this.request(endpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: formData.toString(), }); }, async uploadFile(endpoint, formData) { const fullUrl = this.getBaseUrl() + endpoint; const token = StorageHelper.get("access_token"); const headers = {}; if (token) { headers["Authorization"] = `Bearer ${token}`; } try { const response = await fetch(fullUrl, { method: "POST", headers: headers, body: formData, credentials: "include", }); if (response.status === 401) { const refreshed = await Auth.tryRefresh(); if (refreshed) { headers["Authorization"] = `Bearer ${StorageHelper.get("access_token")}`; const retryResponse = await fetch(fullUrl, { method: "POST", headers: headers, body: formData, credentials: "include", }); if (retryResponse.ok) { return retryResponse.json(); } } Auth.logout(); return null; } if (!response.ok) { const errorData = await response.json().catch(() => ({})); let errorMessage = `Ошибка ${response.status}`; if (typeof errorData.detail === "string") { errorMessage = errorData.detail; } else if (Array.isArray(errorData.detail)) { errorMessage = errorData.detail.map((e) => e.msg || e).join(", "); } else if (errorData.detail?.message) { errorMessage = errorData.detail.message; } else if (errorData.message) { errorMessage = errorData.message; } const error = new Error(errorMessage); error.status = response.status; throw error; } return response.json(); } catch (error) { throw error; } }, }; const Auth = { logout: () => { StorageHelper.clearAll(); window.location.href = "/"; }, tryRefresh: async () => { const refreshToken = StorageHelper.get("refresh_token"); if (!refreshToken) return false; const activeStorage = StorageHelper.getCurrentStorage(); try { const response = await fetch("/api/auth/refresh", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken }), }); if (response.ok) { const data = await response.json(); activeStorage.setItem("access_token", data.access_token); activeStorage.setItem("refresh_token", data.refresh_token); return true; } } catch (e) { console.error("Refresh failed:", e); } return false; }, init: async () => { const token = StorageHelper.get("access_token"); const refreshToken = StorageHelper.get("refresh_token"); if (!token && !refreshToken) { localStorage.removeItem("user"); sessionStorage.removeItem("user"); return null; } const activeStorage = StorageHelper.getCurrentStorage(); try { let response = await fetch("/api/auth/me", { headers: { Authorization: "Bearer " + token }, }); if (response.status === 401 && refreshToken) { const refreshed = await Auth.tryRefresh(); if (refreshed) { response = await fetch("/api/auth/me", { headers: { Authorization: "Bearer " + StorageHelper.get("access_token"), }, }); } } if (response.ok) { const user = await response.json(); activeStorage.setItem("user", JSON.stringify(user)); document.dispatchEvent(new CustomEvent("auth:login", { detail: user })); return user; } } catch (e) { console.error("Auth check failed", e); } StorageHelper.clearAll(); return null; }, }; window.getUser = function () { const userJson = StorageHelper.get("user"); if (!userJson) return null; try { return JSON.parse(userJson); } catch (e) { return null; } }; window.hasRole = function (roleName) { const user = window.getUser(); if (!user || !user.roles) { return false; } return user.roles.includes(roleName); }; window.isAdmin = function () { return window.hasRole("admin"); }; window.isLibrarian = function () { return window.hasRole("librarian") || window.hasRole("admin"); }; window.isAuthenticated = function () { return !!window.getUser(); }; window.canManage = function () { return ( (typeof window.isAdmin === "function" && window.isAdmin()) || (typeof window.isLibrarian === "function" && window.isLibrarian()) ); };