User accounts lived in the Admin Console, which is admins-only. Project admins
need to create the accounts on their own jobs without an app admin on the phone,
so accounts move to a new User Directory page and a new role carries the right.
server/auth.py, server/app.py
New permissions role `project_super_user`, between admin and project_admin:
everything a project admin may do, plus user administration SCOPED to the
projects they hold the role on. Four limits make it safe to hand out, all
enforced server-side:
* Scope comes from projects, not the job title. It resolves per membership
(managed_project_ids), so an ordinary account can hold it on one job via
ProjectMember.role, and a super user demoted on one job administers
nobody there. No projects, no authority.
* Account-level changes (password, disable, rename, permissions, delete)
require EXCLUSIVE scope: refused when the target is also on a project the
caller does not administer, because those changes are global. The
directory renders such rows read-only with the reason.
* No admin or super-user targets, and neither role can be granted by a
super user -- that is the line that stops it becoming app-wide control.
* PUT .../projects rebuilds only the caller's own slice; memberships on
projects they do not administer are left untouched. A payload that simply
omits them must not cut someone off a job the caller cannot see.
Creating requires naming at least one of your own projects: an account with
none would be one the creator instantly cannot manage.
/api/auth/users is now scoped rather than admin-only, and carries a per-row
`manageable` verdict plus the reason. Non-managers get a contact card only --
a project user has no business reading colleagues' login history. New
/api/auth/user-scope tells the page what it may offer. Administrative
password resets are now audited; they were the one account change that left
no trace. Settings, feature flags and the auto-add rule stay admin-only.
While here: one definition of "is a user manager", derived from the managed
set. An account-role-only version disagreed with the scoped one and locked
per-project super users out of routes they were entitled to.
html/users.html, html/users.js
The directory: three renderings from one page -- admin (everything), super
user (controls per row, read-only where scope is shared), everyone else (a
read-only directory of the people on their own projects).
html/console.css, html/console-util.js
Extracted from admin.html/admin.js so both console pages share them. A
divergent jsq() is an XSS and a divergent role list offers permissions the
server refuses, so neither may exist twice.
html/wp-sidenav.{js,css}
Global nav drawer, role-gated, carrying ?project= across links. Mounted on
the field view (which had no way to anywhere) plus both console pages.
No migration: users.role is already String(20) and the new value fits.
Verified: 93 scope/gate tests, 29 live HTTP tests through the real dependency
stack, 33 static JS checks. Not verified in a browser -- no JS engine on this
machine -- so users.html and field.html want one manual load.
server/smoketest.py still fails with 401s. Pre-existing: it has no login code,
so auth_gate refuses it. Confirmed unchanged by stashing this work.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
107 lines
4.6 KiB
HTML
107 lines
4.6 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>User Directory — Work Package Suite</title>
|
|
<script src="auth-guard.js"></script>
|
|
<!-- Date/number formatting. Must parse BEFORE the app scripts: they format
|
|
timestamps during their own boot. -->
|
|
<script src="wp-format.js"></script>
|
|
<link rel="icon" href="favicon.ico" sizes="any">
|
|
<link rel="manifest" href="manifest.webmanifest">
|
|
<meta name="theme-color" content="#161616">
|
|
<link rel="stylesheet" href="theme-light.css">
|
|
<link rel="stylesheet" href="wp-chrome.css">
|
|
<link rel="stylesheet" href="console.css">
|
|
<link rel="stylesheet" href="wp-sidenav.css">
|
|
<style>
|
|
/* Page-specific only — everything structural is in console.css.
|
|
|
|
The directory is one wide table, so the column exceptions live here: email is
|
|
the one cell long enough to stretch a row, and the two role dropdowns need
|
|
room for "Assistant Project Manager" without pushing Actions off screen. */
|
|
#users-table table td:nth-child(3){ max-width:230px; overflow:hidden; text-overflow:ellipsis; }
|
|
#users-banner:not(:empty), #scope-banner:not(:empty){ margin-bottom:var(--s3); }
|
|
/* The create form is a lot of fields; give the password one room to breathe and
|
|
let the project picker take a full row of its own. */
|
|
#nu-password{ flex:1 1 200px; }
|
|
#nu-projects{ margin-top:var(--s2); }
|
|
#nu-projects .pickrow{ padding:var(--s1) var(--s1); }
|
|
/* A manager with one project doesn't need a scrolling picker; a manager with
|
|
thirty does, and it must not push the Create button below the fold. */
|
|
#nu-project-list{ max-height:200px; overflow:auto; border:1px solid var(--border); }
|
|
.whoami-chip{ font-size:12px; color:var(--muted); }
|
|
.whoami-chip strong{ color:var(--text); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- SHARED DARK APP BAR -->
|
|
<header class="wp-appbar">
|
|
<a href="index.html" class="wp-appbar-brand" title="Back to site">
|
|
<span class="wp-logo-chip"><img src="prime-controls-logo.jpg" alt="Prime Controls"></span>
|
|
<span class="wp-appbar-title">Work Package Suite <span class="wp-appbar-sub">| User Directory</span></span>
|
|
</a>
|
|
</header>
|
|
|
|
<div class="wrap" id="users-main" style="display:none">
|
|
<div class="row" style="justify-content:space-between; margin-bottom:var(--s5)">
|
|
<div>
|
|
<h1>User Directory</h1>
|
|
<div class="sub" style="margin:0" id="dir-sub">The people on your projects — who they are, and how to reach them.</div>
|
|
</div>
|
|
<div class="row"><a class="home" href="index.html">← Site</a></div>
|
|
</div>
|
|
|
|
<!-- WHAT YOU MAY DO HERE (rendered from GET /api/auth/user-scope) -->
|
|
<div id="scope-banner"></div>
|
|
|
|
<!-- THE DIRECTORY -->
|
|
<div class="card">
|
|
<h2>People</h2>
|
|
<div class="sub" id="people-sub"></div>
|
|
<div class="toolbar">
|
|
<button onclick="loadUsers()">Refresh</button>
|
|
<input id="user-search" placeholder="Search name / username / email / job function…" oninput="renderUsers()">
|
|
<select id="user-filter" onchange="renderUsers()">
|
|
<option value="">Everyone</option>
|
|
<option value="active">Active only</option>
|
|
<option value="disabled">Disabled only</option>
|
|
<option value="mine">Accounts I manage</option>
|
|
</select>
|
|
</div>
|
|
<div id="users-banner"></div>
|
|
<div id="users-table"><div class="note">Loading…</div></div>
|
|
</div>
|
|
|
|
<!-- ADD A USER (managers only; hidden otherwise) -->
|
|
<div class="card" id="create-card" style="display:none">
|
|
<h2>Add a user</h2>
|
|
<div class="sub" id="create-sub"></div>
|
|
<div class="urow">
|
|
<input id="nu-username" placeholder="Username *" autocomplete="off">
|
|
<input id="nu-fullname" placeholder="Full name" autocomplete="off">
|
|
<input id="nu-email" placeholder="Email" autocomplete="off">
|
|
<select id="nu-role" title="Permissions — what this account may do"></select>
|
|
<select id="nu-project-role" title="Job function on the project"></select>
|
|
<input id="nu-password" type="password" placeholder="Password (min 12)" autocomplete="new-password">
|
|
</div>
|
|
<div id="nu-projects">
|
|
<div class="note" id="nu-projects-label" style="margin-bottom:var(--s1)"></div>
|
|
<div id="nu-project-list"></div>
|
|
</div>
|
|
<div class="row" style="margin-top:var(--s3)">
|
|
<button class="primary" onclick="createUser()">Create user</button>
|
|
<span id="users-create-msg" class="note" style="margin:0"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="console-util.js"></script>
|
|
<script src="users.js"></script>
|
|
<script src="wp-chrome.js"></script>
|
|
<script src="wp-sidenav.js"></script>
|
|
</body>
|
|
</html>
|