/* 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'; // Register the PWA service worker (caches the app shell for offline use). The // API and writes are never cached (see sw.js). This used to be skipped inside // an iframe so the embedded creator did not register a second time; B7/T7.1 // dissolved that frame and there is no longer a document in the app that is // not the top one. if ('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; // Was `inIframe ? window.top : window`, so an expired session inside the // embedded creator replaced the whole window rather than painting a login // page into a frame. No frame, no branch (B7/T7.1). 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'); }); }; // ── 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(); }); })();