// Copyright 2026 Versity Software // This file is licensed under the Apache License, Version 2.0 // (the "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. /** * VersityGW Admin - Application Utilities */ // ============================================ // Navigation & Auth Guards // ============================================ /** * Check if user is authenticated, redirect to login if not * Also loads user context (user type and accessible gateways) */ function requireAuth() { if (!api.loadCredentials()) { window.location.href = 'index.html'; return false; } api.loadUserContext(); return true; } /** * The page a session belongs on when it has no business being where it is. * Three deployments share this UI - S3 only, IAM only, and both - so there is * no single fixed landing page. */ function defaultLandingPage() { if (api.hasManagement()) return 'dashboard.html'; if (api.hasS3()) return 'explorer.html'; if (api.hasIAM()) return 'iam.html'; return 'index.html'; } /** * Require admin role, redirect non-admins to where they do belong. * Call this on Admin-API-only pages, whose every action is gated by the admin * role server-side. Also loads user context. */ function requireAdmin() { if (!api.loadCredentials()) { window.location.href = 'index.html'; return false; } api.loadUserContext(); if (!api.isAdmin()) { window.location.href = defaultLandingPage(); return false; } return true; } /** * Require management-page access (dashboard, buckets): a classic admin * session, or any S3 session in a standalone-IAM deployment, where the pages * run on the S3 and IAM APIs alone and surface each denial per action rather * than redirecting. */ function requireManagement() { if (!api.loadCredentials()) { window.location.href = 'index.html'; return false; } api.loadUserContext(); if (!api.hasManagement()) { window.location.href = defaultLandingPage(); return false; } return true; } /** * Require the gateway's own account store to be this deployment's user * directory. Call this on users.html, which manages that store through the * Admin API; a configured standalone IAM service takes that job over instead. * * The IAM check runs first because a standalone-IAM session is never an admin, * so testing admin first would bounce it to the landing page rather than to * this page's IAM counterpart. */ function requireGatewayUsers() { if (!api.loadCredentials()) { window.location.href = 'index.html'; return false; } api.loadUserContext(); if (api.hasIAM()) { window.location.href = 'iam-users.html'; return false; } return requireAdmin(); } /** * Require IAM service access, redirect sessions without it away. * Call this on the IAM pages (iam, iam-users, iam-roles, iam-oidc). */ function requireIAM() { if (!api.loadCredentials()) { window.location.href = 'index.html'; return false; } api.loadUserContext(); if (!api.hasIAM()) { window.location.href = defaultLandingPage(); return false; } return true; } /** * Require S3 data-plane access, redirect sessions without it away. * Call this on the S3-backed pages (explorer). */ function requireS3() { if (!api.loadCredentials()) { window.location.href = 'index.html'; return false; } api.loadUserContext(); if (!api.hasS3()) { window.location.href = defaultLandingPage(); return false; } return true; } /** * Redirect to appropriate page if already authenticated */ function redirectIfAuthenticated() { if (api.loadCredentials()) { window.location.href = defaultLandingPage(); return true; } return false; } // ============================================ // Toast Notifications // ============================================ let toastContainer = null; function initToasts() { if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.id = 'toast-container'; toastContainer.className = 'fixed top-4 right-4 z-50 flex flex-col gap-2'; document.body.appendChild(toastContainer); } } function showToast(message, type = 'info') { initToasts(); const toast = document.createElement('div'); const bgColors = { success: 'bg-green-50 border-green-500 text-green-800', error: 'bg-red-50 border-red-500 text-red-800', warning: 'bg-yellow-50 border-yellow-500 text-yellow-800', info: 'bg-blue-50 border-blue-500 text-blue-800' }; const icons = { success: ``, error: ``, warning: ``, info: `` }; toast.className = `flex items-center gap-3 px-4 py-3 rounded-lg border-l-4 shadow-lg max-w-sm animate-slide-in ${bgColors[type]}`; toast.innerHTML = ` ${icons[type]}
${escapeHtml(message)}
`; toastContainer.appendChild(toast); // Auto-remove after 5 seconds setTimeout(() => { toast.classList.add('animate-fade-out'); setTimeout(() => toast.remove(), 300); }, 5000); } // ============================================ // Modal Utilities // ============================================ function openModal(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.classList.remove('hidden'); // Focus first input const firstInput = modal.querySelector('input:not([readonly]), select'); if (firstInput) setTimeout(() => firstInput.focus(), 100); // Push to history state so the back button can close the modal history.pushState({ modal: true }, ''); } } let navigatingBack = false // Close the currently opened modal, manually popping the modal // history state if the modal was closed manually (default). In the // case where the modal was closed due to navigating back, the state // is already popped and we can skip it. function closeModal(modalId, popState = true) { const modal = document.getElementById(modalId); if (modal) { modal.classList.add('hidden'); if (popState && history.state?.modal) { navigatingBack = true; history.back(); } } } function closeAllModals() { document.querySelectorAll('[id$="-modal"]').forEach(modal => { modal.classList.add('hidden'); }); } function closeModalsOnNavigation() { Array.from(document.getElementsByClassName('modal')).forEach((modal) => { if (!modal.classList.contains('hidden')) { closeModal(modal.getAttribute('id'), false); } }) } // Catch the back button to close the open modal, if any is open. window.addEventListener('popstate', (e) => { if (navigatingBack) { navigatingBack = false; return; } closeModalsOnNavigation(); }); // Close modals on Escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeAllModals(); }); // ============================================ // Loading States // ============================================ function setLoading(element, loading) { if (loading) { element.disabled = true; element.dataset.originalText = element.innerHTML; element.innerHTML = ` `; } else { element.disabled = false; if (element.dataset.originalText) { element.innerHTML = element.dataset.originalText; } } } function showTableLoading(tableBodyId, columns) { const tbody = document.getElementById(tableBodyId); if (!tbody) return; tbody.innerHTML = ''; for (let i = 0; i < 5; i++) { const row = document.createElement('tr'); row.className = 'border-b border-gray-50'; for (let j = 0; j < columns; j++) { row.innerHTML += `${escapeHtml(message)}
${escapeHtml(accessKeyShort)}
${roleLabel}
${escapeHtml(message)}