Gate the suite behind a self-contained login (no external IdP): - User model with bcrypt-hashed passwords; admin/user roles - /api/auth endpoints: login, logout, me, change-password, and admin-only user management (list/create/delete/reset/enable) - Stateless JWT session in an HttpOnly, SameSite=Lax, auto-Secure cookie; middleware refuses every /api data route without a session - login.html + auth-guard.js: login page and per-page guard with a top-right "name / Admin / Sign out" pill - Admin Console now gated on admin role (passphrase gate removed) with a User administration card - manage_users.py CLI to bootstrap the first admin - Rebuilt help.js into a searchable, multi-topic help center - Local-dev convenience: app serves html/ so the site + API share one origin under uvicorn (inactive in the prod container) - Docs/env: AUTH_SECRET_KEY, requirements (bcrypt, PyJWT), README Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
4.3 KiB
JavaScript
91 lines
4.3 KiB
JavaScript
/* Auth guard for the Work Package Suite.
|
|
Included in the <head> 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; } })();
|
|
|
|
// 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 () {
|
|
fetch('/api/auth/logout', { method: 'POST' })
|
|
.catch(function () {})
|
|
.then(function () { window.location.replace('login.html'); });
|
|
};
|
|
|
|
function addLogoutPill(user) {
|
|
if (inIframe) return; // the parent page already shows it
|
|
if (document.getElementById('wp-logout-pill')) return;
|
|
var pill = document.createElement('div');
|
|
pill.id = 'wp-logout-pill';
|
|
pill.style.cssText = 'position:fixed;top:12px;right:12px;z-index:10001;' +
|
|
'display:flex;align-items:center;gap:8px;background:#fff;border:1px solid #e0e0e0;' +
|
|
'box-shadow:0 1px 4px rgba(0,0,0,.16);border-radius:16px;padding:5px 12px;' +
|
|
'font:500 12px/1.2 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:#525252;';
|
|
function sep() { var s = document.createElement('span'); s.textContent = '·'; s.style.color = '#a8a8a8'; return s; }
|
|
|
|
var who = document.createElement('span');
|
|
who.textContent = user.full_name || user.username;
|
|
pill.appendChild(who);
|
|
|
|
// Admins get a link to the Admin Console (hidden when already on it).
|
|
var onAdmin = /(^|\/)admin\.html$/.test(location.pathname);
|
|
if (user.role === 'admin' && !onAdmin) {
|
|
var adm = document.createElement('a');
|
|
adm.href = 'admin.html'; adm.textContent = 'Admin';
|
|
adm.style.cssText = 'color:#0f62fe;text-decoration:none;font-weight:600;';
|
|
pill.appendChild(sep()); pill.appendChild(adm);
|
|
}
|
|
|
|
var out = document.createElement('a');
|
|
out.href = '#'; out.textContent = 'Sign out';
|
|
out.style.cssText = 'color:#0f62fe;text-decoration:none;font-weight:600;';
|
|
out.addEventListener('click', function (e) { e.preventDefault(); window.wpLogout(); });
|
|
pill.appendChild(sep()); pill.appendChild(out);
|
|
document.body.appendChild(pill);
|
|
}
|
|
|
|
fetch('/api/auth/me', { headers: { 'Accept': 'application/json' } })
|
|
.then(function (r) {
|
|
if (r.status === 401 || r.status === 403) { goToLogin(); return; }
|
|
if (!r.ok) { reveal(); clearTimeout(safety); return; } // unexpected; show page rather than trap
|
|
return r.json().then(function (data) {
|
|
clearTimeout(safety);
|
|
window.WP_USER = data && data.user;
|
|
reveal();
|
|
if (window.WP_USER) {
|
|
try { document.dispatchEvent(new CustomEvent('wp-auth-ready', { detail: window.WP_USER })); } catch (e) {}
|
|
if (document.body) addLogoutPill(window.WP_USER);
|
|
else document.addEventListener('DOMContentLoaded', function () { addLogoutPill(window.WP_USER); });
|
|
}
|
|
});
|
|
})
|
|
.catch(function () { goToLogin(); }); // API unreachable → send to login
|
|
})();
|