/* Auth guard for the Work Package Suite. Included in the
of every protected page (before other scripts). It confirms there is a valid session by calling /api/auth/me; if not, it sends the user to the login page. The real protection is server-side (the API refuses data requests without a session) — this guard is for UX so people land on the login screen instead of an empty app. It also exposes: window.WP_USER the logged-in user object (set once verified) window.wpLogout() clears the session and returns to the login page and dispatches a 'wp-auth-ready' event on document once WP_USER is set. */ (function () { 'use strict'; var inIframe = (function () { try { return window.top !== window.self; } catch (e) { return true; } })(); // Register the PWA service worker (caches the app shell for offline use). Only // from the top window; the API and writes are never cached (see sw.js). if (!inIframe && 'serviceWorker' in navigator) { try { navigator.serviceWorker.register('/sw.js'); } catch (e) {} } // Hide the page until we know the user is allowed, to avoid a flash of the app // before a redirect. A safety timer reveals it even if the check hangs. var root = document.documentElement; var style = document.createElement('style'); style.textContent = '.wp-auth-pending body{visibility:hidden!important}'; (document.head || root).appendChild(style); root.className += ' wp-auth-pending'; function reveal() { root.className = root.className.replace(/\bwp-auth-pending\b/, ''); } var safety = setTimeout(reveal, 4000); function goToLogin() { clearTimeout(safety); var next = encodeURIComponent(location.pathname + location.search); var url = 'login.html?next=' + next; // If we're inside the WP-creator iframe, redirect the whole window. var w = inIframe ? window.top : window; try { w.location.replace(url); } catch (e) { window.location.replace(url); } } window.wpLogout = function () { try { // Clear the auth cache AND all cached project data (customer IP) from this // device on sign-out — important on shared/field tablets. The outbox // (wp_sync_outbox_v1) is left intact so unsynced writes aren't lost. // (localStorage is not a security boundary; field devices still need // full-disk encryption / MDM — see DEPLOYMENT.md.) localStorage.removeItem('wp_auth_cache'); Object.keys(localStorage).forEach(function (k) { if (/^wp_(iwp_v1|suite_sop|suite_state|projects|active_project)/.test(k)) { localStorage.removeItem(k); } }); } catch (e) {} fetch('/api/auth/logout', { method: 'POST' }) .catch(function () {}) .then(function () { window.location.replace('login.html'); }); }; // Change-password dialog (uses POST /api/auth/password, which requires the // current password). Available from the top-right pill on any page. window.wpChangePassword = function () { if (document.getElementById('wp-pw-modal')) return; var ov = document.createElement('div'); ov.id = 'wp-pw-modal'; ov.style.cssText = 'position:fixed;inset:0;background:rgba(20,30,50,.5);display:flex;align-items:center;' + 'justify-content:center;z-index:10002;padding:20px;font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;'; var inp = 'width:100%;padding:9px 10px;margin-bottom:12px;border:1px solid #8d8d8d;border-radius:4px;font-size:14px;'; var lbl = 'display:block;font-size:12px;color:#525252;margin-bottom:4px;'; ov.innerHTML = ''; function close() { var m = document.getElementById('wp-pw-modal'); if (m) m.remove(); } function msg(text, ok) { var el = document.getElementById('wp-pw-msg'); el.style.display = 'block'; el.textContent = text; el.style.background = ok ? '#defbe6' : '#fff1f1'; el.style.color = ok ? '#0e6027' : '#da1e28'; } ov.addEventListener('click', function (e) { if (e.target === ov) close(); }); document.body.appendChild(ov); document.getElementById('wp-pw-cancel').onclick = close; document.getElementById('wp-pw-cur').focus(); document.getElementById('wp-pw-save').onclick = function () { var cur = document.getElementById('wp-pw-cur').value; var n1 = document.getElementById('wp-pw-new').value; var n2 = document.getElementById('wp-pw-new2').value; if (!cur || !n1) { msg('Please fill in every field.', false); return; } if (n1.length < 12) { msg('New password must be at least 12 characters.', false); return; } if (n1 !== n2) { msg('New passwords do not match.', false); return; } fetch('/api/auth/password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current_password: cur, new_password: n1 }) }) .then(function (r) { return r.json().catch(function () { return null; }).then(function (j) { return { ok: r.ok, status: r.status, j: j }; }); }) .then(function (res) { if (res.ok) { msg('Password updated.', true); setTimeout(close, 1200); } else { msg((res.j && res.j.detail) || ('Could not update (HTTP ' + res.status + ').'), false); } }) .catch(function () { msg('Could not reach the server.', false); }); }; }; // ── permissions helpers ──────────────────────────────────────────────────── // The server enforces all of this; these are for hiding controls the signed-in // user can't use, so nobody clicks a button just to get a 403. // 'user' is the legacy value for what is now 'project_user'. window.wpRole = function () { var r = (window.WP_USER && window.WP_USER.role) || ''; return r === 'user' ? 'project_user' : r; }; window.wpIsAdmin = function () { return window.wpRole() === 'admin'; }; // A Project Super User is a Project Admin with user administration on top, so it // counts here too (server: auth.is_project_admin). window.wpIsProjectAdmin = function () { var r = window.wpRole(); return r === 'admin' || r === 'project_super_user' || r === 'project_admin'; }; // Deleting a work package, deleting a project, and editing a completed SOP are // all Project Admin actions (see server require_project_admin). window.wpCanDeleteWP = window.wpIsProjectAdmin; window.wpCanEditCompletedSOP = window.wpIsProjectAdmin; // Whether this account can administer USER accounts. The account role is only half // the answer — the role can also be held on a single project — so anything that // needs the real verdict asks GET /api/auth/user-scope (users.js does). This is the // cheap hint used to decide whether to bother offering a control. window.wpMayManageUsers = function () { var r = window.wpRole(); return r === 'admin' || r === 'project_super_user'; }; // ── app feature flags ────────────────────────────────────────────────────── // Cached per page load. Pages that must know before rendering should await // wpFlags(); anything already rendered can re-check on the 'wp-flags-ready' event. window.WP_FLAGS = null; var _flagsPromise = null; window.wpFlags = function () { if (window.WP_FLAGS) return Promise.resolve(window.WP_FLAGS); if (_flagsPromise) return _flagsPromise; _flagsPromise = fetch('/api/app-flags', { headers: { 'Accept': 'application/json' } }) .then(function (r) { return r.ok ? r.json() : {}; }) .catch(function () { return {}; }) // offline: fall through to defaults .then(function (f) { window.WP_FLAGS = f || {}; try { document.dispatchEvent(new CustomEvent('wp-flags-ready', { detail: window.WP_FLAGS })); } catch (e) {} return window.WP_FLAGS; }); return _flagsPromise; }; // BIM/VDC is off unless an admin has switched it on, so an unreachable API or a // stale cache errs toward hiding the unfinished tooling rather than showing it. window.wpBimEnabled = function () { return !!(window.WP_FLAGS && window.WP_FLAGS.bim_enabled); }; // The flat user menu that used to sit in this bar is gone (T2.2). It duplicated // Admin, Users and Sign out from the navigation drawer, and being one unbreakable // 412px run with an inline white-space:nowrap, it was what clipped the bar at 390px // and cut "Sign out" in half — F2. wp-sidenav.js now carries all of it, including // the two items that were only here: Language & time, and Password. // // Nothing replaces it. Every signed-in page mounts the drawer, so there is no page // left that would need a floating fallback pill. function proceed(user) { clearTimeout(safety); window.WP_USER = user; reveal(); if (window.WP_USER) { window.wpFlags(); // start the feature-flag fetch; pages await it as needed try { document.dispatchEvent(new CustomEvent('wp-auth-ready', { detail: window.WP_USER })); } catch (e) {} } } fetch('/api/auth/me', { headers: { 'Accept': 'application/json' } }) .then(function (r) { if (r.status === 401 || r.status === 403) { try { localStorage.removeItem('wp_auth_cache'); } catch (e) {} goToLogin(); return; } if (!r.ok) { reveal(); clearTimeout(safety); return; } // unexpected; show page rather than trap return r.json().then(function (data) { var user = data && data.user; // Remember the last good auth so the PWA can open offline. The server is // still the real gate; offline writes queue in the outbox until reconnect. try { if (user) localStorage.setItem('wp_auth_cache', JSON.stringify({ user: user, at: Date.now() })); } catch (e) {} proceed(user); }); }) .catch(function () { // Offline / API unreachable: fall back to a recent cached auth if present, // so the app (and the field view) still open without a network. try { var c = JSON.parse(localStorage.getItem('wp_auth_cache') || 'null'); if (c && c.user && (Date.now() - (c.at || 0)) < 12 * 3600 * 1000) { proceed(c.user); return; } } catch (e) {} goToLogin(); }); })();