← All manuals Operator Guide for this screen IM2

Users

Users
Technical Spec Ver 2 Updated 2026-09-06 Matches the live screen

Users — Technical Spec

All in app/im2.py (no separate router). Template app/templates/users.html.

Routes

MethodPathAuthNotes
GET/usersrole == 'admin' (hard-coded, 403 otherwise)select username, display_name, email, role, active from app_users order by role, display_name. Passes roles = ['admin','level1','level2','level3'].
POST/api/users/refreshadminReconciles app_users against slack_roster.
POST/api/users/addadmindisplay_name, email, role (default level3).
POST/api/users/{username}adminrole — the only editable column.
POST/api/users/{username}/pinadminpin (blank = generate). Defined far down the file, after /healthz.

Admin-only here is a fixed role test, not a role_action_perms lookup — by design (im2.py comment: "Two keys stay hard-coded admin-only on purpose (users, settings) so nobody can lock every admin out of the app"). The Users/Settings nav links in base.html are gated on user.role == 'admin' as well.

Tables

Roster refresh (/api/users/refresh)

For each slack_roster row: insert missing users as role='level3', active=true; otherwise update app_users set display_name, email, active=true. Then any app_users row not in the roster and currently active is set active=false. Returns {added:[display_name…], deactivated:[username…], roster:N}. Never changes role.

PIN handling

Known limits / defects

  1. set_pin() writes its audit_log row with sku = None and no username — the trail records that a PIN was set and by whom, but not for whom. One-line fix: pass f"USER:{username}".
  2. set_pin() never checks the user exists. POST /api/users/nosuchperson/pin updates 0 rows and returns {"ok": true} (/api/users/{username} role-set does check, with 404).
  3. set_role() has no self-guard; only the template disables the operator's own dropdown. An admin can demote themselves via a direct API call, and if they are the last admin the app has no admin (recovery = SQL).
  4. /api/users/refresh unconditionally sets active=true for every roster member, so a person deliberately switched off is silently re-enabled on the next refresh, and any manual (/api/users/add) user is deactivated by the next refresh because they are not in slack_roster. There is no per-row active toggle on this screen at all.
  5. users.html renders inactive users identically to active ones — u.active is selected but never displayed.
  6. scripts/schema.sql still has app_users.role CHECK (role IN ('viewer','warehouse','admin')) and no email / slack_id / pin_hash / phone columns. The live database has been migrated past it; the DDL in this package would not support this screen if replayed clean.
  7. Every POST here is form-encoded with cookie auth and no CSRF token. samesite=lax on the session cookie is the only thing stopping a cross-site form post.

PIN reset audit (fixed 2026-09-06)

set_pin now writes the target username into audit_log.old_value and a new_value of (set for <username>; generated|typed by admin), alongside changed_by (the admin who did it). Before this the row recorded only that some PIN was set, which made a reset untraceable. The PIN itself is still never stored or logged in clear; a generated PIN is returned once in the response so the admin can pass it on.

Shared table sort (2026-09-07)

Column sorting is one implementation in templates/base.html (Dave, 2026-09-07: "all screens that have tables like this need sorts on the appropriate columns"). A table opts in with class="sorttable"; every thead th becomes sortable except those with class="nosort"/class="toggle", an empty heading, or a checkbox in the heading. Client-side only, over the rows already rendered. Details: tech/table_sorting.md.