/* Shared helpers for the suite's admin pages (Admin Console, User Directory). These used to live in admin.js. They are here because the User Directory needs the same escaping and the same role vocabulary, and a second copy of either is a liability: a divergent jsq() is an XSS, and a divergent role list quietly offers a permission the server will refuse. Loaded as plain globals (no modules) to match the rest of the suite. */ // ── api ────────────────────────────────────────────────────────────────────── // Never throws: returns {status, json} with status 0 when the request itself // failed, so every caller can branch on one shape. async function api(method, path, body){ const opt = { method, headers:{ 'Accept':'application/json' } }; if(body !== undefined){ opt.headers['Content-Type']='application/json'; opt.body=JSON.stringify(body); } try { const r = await fetch(path, opt); const t = await r.text(); let json; try { json = t ? JSON.parse(t) : null; } catch(_){ json = t; } return { status:r.status, json }; } catch(e){ return { status:0, json:String(e) }; } } // The message to show for a failed call, preferring the server's own words. function apiError(status, json, fallback){ if(json && json.detail) return json.detail; if(status === 0) return 'Could not reach the server.'; if(status === 401) return 'Not signed in. Reload and log in again.'; return (fallback || 'Request failed') + ' (HTTP ' + status + ').'; } // ── escaping ───────────────────────────────────────────────────────────────── function uesc(v){ return v==null ? '' : String(v).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } // A value bound into an inline handler — onclick="fn('…')" — is escaped TWICE: once // for the JS string literal it lands in, and again for the HTML attribute carrying // it. The order is the whole point. Escape the backslashes FIRST, then the quotes, // then hand the result to uesc: uesc leaves \ and ' alone, so the JS escaping // survives, and the browser decodes the entities before the JS parser runs. // // Doing it the other way round — uesc(v).replace(/'/g,"\\'") — silently fails on a // value containing a backslash: the \ we add is itself escaped by the stored one, // the quote closes the literal, and everything after it runs as code. Project names, // full names and usernames are free text that a signed-in user can write, so that is // a real path from a project_user to whatever an admin's session can do. Use jsq() // for EVERY value that lands inside an inline handler. function jsq(v){ return uesc(String(v==null ? '' : v).replace(/\\/g,'\\\\').replace(/'/g,"\\'")); } // ── role vocabulary (mirrors server/auth.py) ───────────────────────────────── // Permissions roles: what an account may DO. Ordered most- to least-privileged, // same as auth.ROLES, because that is the order the dropdowns render in. const PERM_ROLES = ['admin','project_super_user','project_admin','project_user']; const PERM_LABELS = { admin:'Administrator', project_super_user:'Project Super User', project_admin:'Project Admin', project_user:'Project User', }; // One-line description of each, used in the legends and dropdown titles. const PERM_HELP = { admin:'Manages users, app settings and every project.', project_super_user:'On their assigned projects: everything a Project Admin can do, '+ 'plus creating and managing that project\'s user accounts.', project_admin:'On their assigned projects: may delete work packages, change a completed SOP, '+ 'and delete the project.', project_user:'Creates and edits work packages and authors the SOP, but cannot delete WPs '+ 'or change the SOP once it is complete.', }; // Roles that can be held on a SINGLE project (ProjectMember.role); '' inherits the // account's own. 'admin' is app-wide by definition and never appears here. const PROJECT_SCOPED_ROLES = ['project_super_user','project_admin','project_user']; // Job functions on a project. Descriptive only — no permissions attached. const PROJECT_ROLES = ['Project Manager','Assistant Project Manager','Construction Manager', 'Quality Manager','Superintendent','General Foreman','Foreman','Planner / Scheduler', 'BIM / VDC Coordinator','Engineer','Safety (HSE)','Warehouse / Materials','Commissioning', 'Field Technician']; // Accounts created before permissions roles existed carry the legacy value 'user'. function normRole(r){ return r==='user' ? 'project_user' : (PERM_ROLES.indexOf(r)>=0 ? r : 'project_user'); } function roleLabel(r){ const n = normRole(r); return PERM_LABELS[n] || n; } // Which pill a role wears. Admin and super user each get their own colour because // "can reach every project" and "can create users here" are the two facts you scan // this column for. function roleTagClass(r){ const n = normRole(r); return n==='admin' ? 'admin' : n==='project_super_user' ? 'super' : 'user'; }