// auth.js — authentication utilities used by every authenticated page const Auth = { getToken() { return localStorage.getItem('token'); }, setToken(token) { localStorage.setItem('token', token); }, removeToken() { localStorage.removeItem('token'); }, getUser() { const token = this.getToken(); if (!token) return null; try { const payload = JSON.parse(atob(token.split('.')[1])); if (payload.exp < Date.now() / 1000) { this.removeToken(); return null; } return payload; } catch (_) { return null; } }, requireAuth() { const user = this.getUser(); if (!user) { window.location.href = '/login'; return null; } return user; }, logout() { this.removeToken(); window.location.href = '/login'; }, }; async function returnToAdmin() { try { const data = await API.post('/api/admin/unimpersonate', {}); Auth.setToken(data.access_token); window.location.href = '/admin'; } catch (err) { Auth.logout(); } } // ── Timezone helpers ────────────────────────────────────────────────────────── function getUserTimezone() { return Auth.getUser()?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; } function buildTimezoneOptions(selected) { let allTz; try { allTz = Intl.supportedValuesOf('timeZone'); } catch (_) { // Fallback for older browsers allTz = ['UTC', 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', 'America/Anchorage', 'Pacific/Honolulu', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Asia/Tokyo', 'Asia/Shanghai', 'Australia/Sydney']; } const groups = {}; for (const tz of allTz) { const slash = tz.indexOf('/'); const group = slash === -1 ? 'Other' : tz.slice(0, slash); (groups[group] = groups[group] || []).push(tz); } return Object.keys(groups).sort().map(group => { const options = groups[group].map(tz => { const label = tz.slice(tz.indexOf('/') + 1).replace(/_/g, ' ').replace(/\//g, ' / '); return ``; }).join(''); return ``; }).join(''); } // ── Nav + settings modal ────────────────────────────────────────────────────── function _checkSessionExpiry(user) { const msLeft = (user.exp * 1000) - Date.now(); const hoursLeft = msLeft / (1000 * 60 * 60); if (hoursLeft > 24) return; const warning = document.createElement('div'); warning.id = 'session-warning'; const label = hoursLeft < 1 ? 'Your session expires in less than an hour — please log out and back in.' : `Your session expires in ${Math.floor(hoursLeft)} hours — please log out and back in.`; warning.innerHTML = `${label}`; document.querySelector('.nav')?.insertAdjacentElement('afterend', warning); } function initNav() { const user = Auth.requireAuth(); if (!user) return; _checkSessionExpiry(user); const nav = document.querySelector('.nav'); if (!nav) return; const isImpersonating = !!user.admin_id; const navUser = document.createElement('div'); navUser.className = 'nav-user'; if (isImpersonating) { navUser.innerHTML = ` `; } else { navUser.innerHTML = ` ${user.is_admin ? 'Admin' : ''} `; } nav.appendChild(navUser); if (!isImpersonating) { const tzOptions = buildTimezoneOptions(user.timezone || 'UTC'); document.body.insertAdjacentHTML('beforeend', `
`); } } function showSettingsModal() { document.getElementById('settings-modal').style.display = 'flex'; document.getElementById('settings-msg').className = 'message'; document.getElementById('pw-current').value = ''; document.getElementById('pw-new').value = ''; document.getElementById('pw-confirm').value = ''; } function hideSettingsModal() { document.getElementById('settings-modal').style.display = 'none'; } function detectTimezone() { const detected = Intl.DateTimeFormat().resolvedOptions().timeZone; const sel = document.getElementById('tz-select'); if (sel) sel.value = detected; } async function submitTimezone() { const tz = document.getElementById('tz-select').value; const msgEl = document.getElementById('settings-msg'); try { const data = await API.put('/api/auth/timezone', { timezone: tz }); Auth.setToken(data.access_token); msgEl.textContent = `Timezone saved: ${tz.replace(/_/g, ' ')}`; msgEl.className = 'message success visible'; setTimeout(() => { msgEl.className = 'message'; }, 3000); } catch (err) { msgEl.textContent = err.message; msgEl.className = 'message error visible'; } } async function submitPasswordChange() { const current = document.getElementById('pw-current').value; const newPw = document.getElementById('pw-new').value; const confirm = document.getElementById('pw-confirm').value; const msgEl = document.getElementById('settings-msg'); if (newPw !== confirm) { msgEl.textContent = 'New passwords do not match'; msgEl.className = 'message error visible'; return; } if (newPw.length < 10) { msgEl.textContent = 'Password must be at least 10 characters'; msgEl.className = 'message error visible'; return; } try { await API.post('/api/auth/change-password', { current_password: current, new_password: newPw, }); msgEl.textContent = 'Password updated!'; msgEl.className = 'message success visible'; document.getElementById('pw-current').value = ''; document.getElementById('pw-new').value = ''; document.getElementById('pw-confirm').value = ''; setTimeout(() => { msgEl.className = 'message'; }, 3000); } catch (err) { msgEl.textContent = err.message; msgEl.className = 'message error visible'; } } document.addEventListener('click', (e) => { const modal = document.getElementById('settings-modal'); if (modal && e.target === modal) hideSettingsModal(); }); document.addEventListener('DOMContentLoaded', initNav);