/** * Nexus One AI — Shared Auth Guard * Include in every portal page. Checks session, injects user info into nav. * * Usage: (place just before ) * * Pages that do NOT require auth: login.html * Admin-only pages: set data-role="admin" on the tag. */ // ── Session timeout constants ──────────────────────────────────────────────── const SESSION_HOURS = 8; // must match JWT_EXPIRE_HRS in main.py const WARN_BEFORE_MIN = 10; // show warning this many minutes before expiry const EXTEND_INTERVAL = 60000; // poll session state every 60 seconds (async function () { // Don't guard the login page itself if (window.location.pathname.endsWith('login.html')) return; let user = null; try { const res = await fetch('/api/auth/me', { credentials: 'include' }); if (!res.ok) throw new Error('unauthenticated'); user = await res.json(); } catch { window.location.href = '/login.html?next=' + encodeURIComponent(window.location.pathname); return; } // Force password change if (user.must_change_password && !window.location.pathname.endsWith('change-password.html')) { window.location.href = '/change-password.html'; return; } // Admin-only pages if (document.body.dataset.role === 'admin' && user.role !== 'admin') { window.location.href = '/index.html'; return; } // Keep the streamlined prototype navigation consistent on every legacy // page while features are progressively enabled. function applyPrototypeNavigation() { const topnav = document.querySelector('.topnav'); const nav = topnav && topnav.querySelector('nav'); if (!topnav || !nav) return; const directUseCases = nav.querySelector('a[href="usecases.html"]'); if (directUseCases) directUseCases.remove(); const dropdowns = Array.from(nav.querySelectorAll('.nav-dropdown')); const helpDropdown = dropdowns.find(el => { const button = el.querySelector('.nav-drop-btn'); return button && button.textContent.trim().startsWith('Help'); }); if (helpDropdown) helpDropdown.remove(); nav.querySelectorAll('.nav-sep').forEach(el => el.remove()); const aiDropdown = Array.from(nav.querySelectorAll('.nav-dropdown')).find(el => { const button = el.querySelector('.nav-drop-btn'); return button && button.textContent.trim().startsWith('AI Tools'); }); if (aiDropdown) { const menu = aiDropdown.querySelector('.nav-drop-menu'); if (menu) { menu.innerHTML = ` BUILD / App Studio Applications Dataset Creation Model Training WORK / Nexus Prompt Studio Open WebUI Nexus Notebook AUTOMATE / Agent Studio QUALITY / AI Evaluation DEVELOPER / Jupyter Notebook `; } } const adminDropdown = Array.from(nav.querySelectorAll('.nav-dropdown')).find(el => { const button = el.querySelector('.nav-drop-btn'); return button && button.textContent.trim().startsWith('Admin'); }); let navRight = topnav.querySelector('.nav-right'); if (!navRight) { navRight = document.createElement('div'); navRight.className = 'nav-right'; topnav.appendChild(navRight); } if (adminDropdown) navRight.insertBefore(adminDropdown, navRight.firstChild); const currentPage = window.location.pathname.split('/').pop() || 'index.html'; nav.querySelectorAll('a, .nav-drop-btn').forEach(el => el.classList.remove('active')); const currentLink = nav.querySelector(`a[href="${currentPage}"]`); if (currentLink) currentLink.classList.add('active'); if (aiDropdown && (currentPage === 'app-studio.html' || currentPage === 'applications.html' || currentPage === 'dataset-creation.html' || currentPage === 'training.html' || currentPage === 'prompt-studio.html' || currentPage === 'tool-openwebui.html' || currentPage === 'nexus-notebook.html' || currentPage === 'meeting.html' || currentPage === 'agents.html' || currentPage === 'workflows.html' || currentPage === 'schedules.html' || currentPage === 'evals.html' || currentPage === 'nexus-compare.html' || currentPage === 'rag-quality.html' || currentPage === 'feedback.html' || currentPage === 'tool-jupyter.html')) { aiDropdown.querySelector('.nav-drop-btn')?.classList.add('active'); } } applyPrototypeNavigation(); // Consolidate overlapping tools under three product-level workspaces while // preserving their existing routes and functionality. (function applyProductModeNavigation() { const page = window.location.pathname.split('/').pop() || 'index.html'; const products = [ { name: 'Nexus Notebook', pages: ['nexus-notebook.html','meeting.html'], modes: [['Notebook','nexus-notebook.html'],['Meetings','meeting.html']] }, { name: 'Agent Studio', pages: ['agents.html','workflows.html','schedules.html'], modes: [['Agents','agents.html'],['Workflows','workflows.html'],['Scheduled Jobs','schedules.html']] }, { name: 'AI Evaluation', pages: ['evals.html','nexus-compare.html','rag-quality.html','feedback.html','model-compare.html'], modes: [['Eval Suites','evals.html'],['Model Compare','nexus-compare.html'],['RAG Quality','rag-quality.html'],['Feedback','feedback.html']] } ]; const product = products.find(item => item.pages.includes(page)); if (!product || document.querySelector('.product-mode-nav')) return; const style = document.createElement('style'); style.textContent = '.product-mode-nav{height:44px;display:flex;align-items:center;gap:6px;padding:0 28px;border-bottom:1px solid var(--bdr,#e2e8f0);background:var(--navy2,#fff);position:relative;z-index:20}.product-mode-name{font-weight:700;font-size:13px;color:var(--ink,#172033);margin-right:12px}.product-mode-nav a{padding:7px 11px;border-radius:7px;color:var(--med,#64748b);font-size:12px;text-decoration:none}.product-mode-nav a:hover{background:rgba(124,58,237,.08);color:var(--purple,#7c3aed)}.product-mode-nav a.active{background:rgba(124,58,237,.12);color:var(--purple,#7c3aed);font-weight:700}@media(max-width:720px){.product-mode-nav{padding:0 12px;overflow-x:auto}.product-mode-name{display:none}.product-mode-nav a{white-space:nowrap}}'; document.head.appendChild(style); const bar = document.createElement('div'); bar.className = 'product-mode-nav'; bar.setAttribute('aria-label', product.name + ' modes'); bar.innerHTML = '' + product.name + '' + product.modes.map(mode => '' + mode[0] + '').join(''); document.querySelector('.topnav')?.insertAdjacentElement('afterend', bar); })(); // ── Inject user chip + logout into nav ───────────────────────────────────── const topnav = document.querySelector('.topnav'); if (topnav) { document.querySelectorAll('[data-admin-only]').forEach(el => { if (user.role !== 'admin') el.style.display = 'none'; }); const chip = document.createElement('div'); chip.className = 'nav-user-chip'; chip.innerHTML = ` ${user.username.charAt(0).toUpperCase()} ${user.username} ${user.role === 'admin' ? 'Administrator' : 'User'} `; topnav.appendChild(chip); document.getElementById('cezen-logout').addEventListener('click', async () => { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); window.location.href = '/login.html'; }); } // Expose user globally for page scripts window.cezenUser = user; document.dispatchEvent(new CustomEvent('cezenAuthReady', { detail: user })); // ── Online users pill in navbar (admin only) ──────────────────────────────── if (user.role === 'admin' && topnav) { const pill = document.createElement('a'); pill.id = 'cezen-online-pill'; pill.href = '/dashboard.html#sessions'; pill.title = 'View active sessions'; pill.style.cssText = [ 'display:inline-flex;align-items:center;gap:5px;', 'background:rgba(21,128,61,.08);border:1px solid rgba(21,128,61,.2);', 'color:#15803D;border-radius:20px;padding:3px 10px 3px 8px;', 'font-size:11.5px;font-weight:600;text-decoration:none;', 'margin-right:2px;cursor:pointer;transition:.15s;flex-shrink:0;', ].join(''); pill.innerHTML = '… online'; // Insert before user chip (last child) topnav.insertBefore(pill, topnav.lastElementChild); async function refreshOnlineCount() { try { const d = await fetch('/api/users/sessions', { credentials: 'include' }).then(r => r.json()); const n = (d.sessions || []).length; document.getElementById('cezen-online-count').textContent = n; pill.title = (d.sessions || []).map(s => s.username).join(', ') || 'No active sessions'; } catch { /* silently ignore */ } } refreshOnlineCount(); setInterval(refreshOnlineCount, 30000); } // ── Session timeout banner ────────────────────────────────────────────────── // Record login time in sessionStorage so we can track elapsed time const loginKey = 'cezen_login_ts'; if (!sessionStorage.getItem(loginKey)) { sessionStorage.setItem(loginKey, Date.now().toString()); } // Inject banner CSS + element const style = document.createElement('style'); style.textContent = ` #cezen-timeout-banner { display: none; position: fixed; bottom: 20px; right: 20px; background: var(--brand-hover, #6D28D9); color: white; border-radius: 12px; padding: 14px 18px; font-size: 13px; font-family: inherit; z-index: 9999; box-shadow: 0 8px 32px rgba(0,0,0,.35); max-width: 320px; border-left: 4px solid var(--amber, #D97706); animation: cezenFadeIn .3s ease; } @keyframes cezenFadeIn { from { opacity:0; transform:translateY(10px); } to { opacity:1; transform:none; } } #cezen-timeout-banner.expired { border-color: var(--red, #DC2626); } #cezen-timeout-banner strong { display:flex; align-items:center; gap:6px; margin-bottom:4px; font-size:14px; } #cezen-timeout-banner .tb-actions { display:flex; gap:8px; margin-top:10px; } #cezen-timeout-banner .tb-btn { flex:1; padding:6px 0; border:none; border-radius:7px; font-size:12px; font-weight:700; cursor:pointer; font-family:inherit; } #cezen-timeout-banner .tb-extend { background:var(--green, #16A34A); color:white; } #cezen-timeout-banner .tb-logout { background:rgba(255,255,255,.15); color:white; } `; document.head.appendChild(style); const banner = document.createElement('div'); banner.id = 'cezen-timeout-banner'; banner.innerHTML = ` Session expiring soon Your session expires in
`; document.body.appendChild(banner); document.getElementById('cezen-extend').addEventListener('click', async () => { // Touching /api/auth/me re-validates the cookie; for a true refresh we'd // need a /api/auth/refresh endpoint — for now just reset the local timer. sessionStorage.setItem(loginKey, Date.now().toString()); banner.style.display = 'none'; }); document.getElementById('cezen-tb-logout').addEventListener('click', async () => { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); window.location.href = '/login.html'; }); function fmt(ms) { const m = Math.floor(ms / 60000); const s = Math.floor((ms % 60000) / 1000); if (m > 0) return `${m}m ${s}s`; return `${s}s`; } function checkTimeout() { const loginTs = parseInt(sessionStorage.getItem(loginKey) || Date.now(), 10); const expireMs = SESSION_HOURS * 3600 * 1000; const elapsed = Date.now() - loginTs; const remaining = expireMs - elapsed; const warnMs = WARN_BEFORE_MIN * 60 * 1000; if (remaining <= 0) { // Expired — redirect to login banner.classList.add('expired'); banner.style.display = 'block'; document.getElementById('cezen-timeout-msg').innerHTML = 'Your session has expired.'; document.getElementById('cezen-extend').style.display = 'none'; setTimeout(() => { window.location.href = '/login.html'; }, 3000); return; } if (remaining <= warnMs) { banner.style.display = 'block'; const cd = document.getElementById('cezen-countdown'); if (cd) cd.textContent = fmt(remaining); } else { banner.style.display = 'none'; } } // Run every second for accurate countdown; heavier session validation every minute setInterval(checkTimeout, 1000); checkTimeout(); })();