From ce9bdbd974255280ec104dcbdcd68c042c93a6d8 Mon Sep 17 00:00:00 2001 From: ManikandanC Date: Tue, 27 Oct 2026 13:45:44 +0530 Subject: [PATCH] Sign-in page redesign and Portal roles Super Admin / Admin / Viewer Sign-in: brand panel, show / hide password, Caps Lock warning, moving background, tab icon. Roles: Super Admin manages Admins and Viewers, Admin manages Viewers only (and can Start / Stop, see Sign-ins), Viewer sees claims and Runs only. create_tables.sql moves an old table. 71 test files, 1220 tests. Co-Authored-By: Claude Opus 5.5 --- Context.md | 57 ++++++++++--- GLOSSARY.md | 9 +- tests/test_portal_signin.py | 95 +++++++++++++++------ tests/test_portal_users.py | 78 ++++++++++++++--- tools/add_portal_user.py | 10 ++- tools/sql/create_tables.sql | 20 ++++- web/app.py | 23 ++++-- web/templates/base.html | 1 + web/templates/signin.html | 161 ++++++++++++++++++++++++++++++++---- web/templates/users.html | 11 ++- web/users.py | 42 ++++++---- 11 files changed, 408 insertions(+), 99 deletions(-) diff --git a/Context.md b/Context.md index d3330af..dfb2e2d 100644 --- a/Context.md +++ b/Context.md @@ -87,6 +87,13 @@ before any login; **17:07 — the box came up, OK, login in 279 s (box wait included), 2FA mail in 42 s, Chrome showed only eCW tabs**, claims running. A full test suite ran beside the 16:27 and 17:07 runs without touching them (the tests' Chrome uses a free port, never 9222). +**2026-10-26 17:40 terminal run:** Chrome open box → OK, login 97 s (2FA +mail 42 s), **50/50 read, 38 Pass / 12 Fail / 0 Review, no bot error**, +~27 min. Against the 23/10 17:07 run, 49 claims gave the same comment; +the one change is eCW data, not code: a patient's sliding fee was renewed +(new period from 10/23/2026), so a claim with an earlier DOS is now Fail +"Sliding fee NOT STARTED" (§6 Waiting 6). The #25 and Portal changes +of 23–26/10 changed no other result. **Runs page (2026-10-22, Stage 1 of EEWA-31..33, `751a177`):** http://127.0.0.1:8000/runs (header link "Runs"). Every Run writes @@ -113,10 +120,16 @@ and not SQL: `docs/adr/0001-run-events-in-files.md`. Spec: page and `/api` needs a Sign-in. Setup, once: run `tools\sql\create_tables.sql` in SSMS (adds `CV_PortalUser`, `CV_PortalSignIn` and, for Stage 3, `CV_ChargeSummary.started_by`), then -`python tools\add_portal_user.py` for the first Admin (password typed hidden), -then restart `run_web.bat`. Roles: **Admin** (Users page: add / role / reset -/ disable — never delete; Sign-ins page), **Operator** (Start / Stop), -**Viewer** (see only). Passwords PBKDF2-SHA256 (600k, own salt), 10+ +`python tools\add_portal_user.py` for the first Super Admin (password typed +hidden), then restart `run_web.bat`. Roles since `f99dc17` (2026-10-27, +operator: "super admin means me"): **Super Admin** (everything; Users page +adds / changes Admins and Viewers — never deletes; nobody changes a Super +Admin from the Portal, only the PC tool makes one), **Admin** (everything: +Start / Stop, Sign-ins page; adds / changes Viewers only), **Viewer** +(Dashboard, Claim status, Runs; changes nothing, no Users or Sign-ins). +Operator is gone (folded into Admin). The live table was moved 2026-10-27 +by the operator in SSMS (the bot's SQL login cannot ALTER the table): the +one user is Super Admin; the CHECK allows only the three roles. Passwords PBKDF2-SHA256 (600k, own salt), 10+ characters; a user an Admin adds or resets must choose their own at the next Sign-in. 5 wrong passwords = locked 15 minutes; 30 minutes idle = signed out; sessions live in the server's memory (a restart signs everyone @@ -147,7 +160,7 @@ table needs an update — run create_tables.sql". The Excel Charge Summary is unchanged. Tests: `test_run_alerts.py`. **From the page (2026-10-14):** `.\run_web.bat` → http://127.0.0.1:8000 -(the Dashboard) → **Start run** (Admin / Operator, since 2026-10-22). The page starts `python run.py` itself, always **VERIFY** +(the Dashboard) → **Start run** (Super Admin / Admin; Admin / Operator 2026-10-22 to 10-27). The page starts `python run.py` itself, always **VERIFY** (`SAVE_CORRECTIONS=0` forced), information boxes go to the log (`QUIET_BOXES=1`); CAPTCHA / 2FA boxes still come up on the PC. The card shows phase, progress, Pass/Fail/Review and a live log; **Stop** kills the @@ -173,9 +186,8 @@ Get-ChildItem tests\test_*.py | ForEach-Object { "$($_.Name): $((python $_.FullN this machine, with ODBC Driver 18 for SQL Server. - Tests run offline against fake HTML pages in `tests/fixtures/` and fake DB connections; they never touch eCW or SQL Server (`test_cdp_browser.py` - starts a local Chromium on a debug port). As of 2026-10-26: 71 test - files, **1215 tests** (full run 2026-10-23: 1211/1211; the 4 new Portal - tests and every Portal test file passed 2026-10-26) (earlier flaky, each + starts a local Chromium on a debug port). As of 2026-10-27: 71 test + files, **1220 tests** (full run 2026-10-27: 1220/1220) (earlier flaky, each failed once and passed on rerun: test_verify_no_live_writes, depression_dx_pointer, progress_note_wait — watch them) (about 5 minutes for all of them). @@ -444,6 +456,14 @@ browser holds in the background must not stay faded), and off under Checked in the Browser pane against a made-up-data preview (port 8765, a scratchpad script; no SQL, no real users), desktop and phone width. The running Portal shows it after `run_web.bat` is restarted. +**Sign-in page since `61338b4` (2026-10-26, operator):** a card on a +slow navy-to-blue moving gradient — navy brand panel ("Charge +Validation" and three feature titles only, no description lines; +floating circles, pulsing CV mark) and the form (icon fields, Show / +Hide password, Caps Lock warning, idle / lockout rules read from +`web/users.py`). Every page has an inline SVG tab icon (no +`/favicon.ico` 404). Motion off under `prefers-reduced-motion`. Live on +the Portal (restarted 2026-10-26, [portal] terminal tab). - Server `MECARE331\MECARE331`, database `Charge_Validation`, table `dbo.CV_ChargeSummary` (every table of this project starts with **`CV_`**, operator 2026-10-12): the format's 24 columns + `run_at`, `run_mode` @@ -568,6 +588,7 @@ The running Portal shows it after `run_web.bat` is restarted. 3. Dental HIGH RISK / sliding EXPIRED rows say "Corrections pending (not saved)" although nothing was corrected. Needs the operator's wording (e.g. "Submit via DentaQuest portal"). 4. Demo `.env`: which sheet `TEST_CLAIM_IDS_FILE` points at (now the Dental sheet), and a shorter `MSGBOX_TIMEOUT_SECONDS`? 5. #25 mental health: G0469 or G0470 (new / established) — the client's ruling (EEWA-37). Until then Review. +6. Sliding fee renewed: a claim whose DOS falls in the patient's **previous** sliding fee period, checked after a renewal — Pass on the old period, or Fail "NOT STARTED" against the current one (as built)? Seen 2026-10-26. Billing team / client to rule. --- @@ -602,7 +623,9 @@ The running Portal shows it after `run_web.bat` is restarted. History: `git log --oneline`. -Last code commits: `a436489` (2026-10-26, Portal look: aligned header / +Last code commits: `f99dc17` (2026-10-27, Portal roles Super Admin / +Admin / Viewer — §2), `61338b4` (2026-10-26, Sign-in page redesign + tab +icon — §5), `a436489` (2026-10-26, Portal look: aligned header / footer, 7-day chart, today's reasons, pass rate — §5), `2d4d4fc` (2026-10-23, tracked files de-identified — §7), `8fea168` (2026-10-23, "Chrome open" box before the login and the empty first tab closed, in a Chrome the run opened — §2), @@ -703,7 +726,7 @@ Status lives in Jira; the open work is in §6 and §10. | EEWA-30 | medicaid_pregnancy_th (#23: TH modifier on a pregnancy visit on Medicaid) | To Do | | EEWA-29 | medicare_wellness_history (#27/#28/#29: G0402 / G0438 / G0439 billed too often → Review) | To Do | | EEWA-31 | run_events + Runs page (Stage 1) | Done (closed 2026-10-26; built `751a177`, seen on every live run since 2026-10-22) | -| EEWA-32 | portal sign-in (Stage 2: Portal Users, Roles, Started By / Stopped By); blocked by EEWA-31 | In Progress (built 2026-10-22, `c230a8e`; live Sign-in and a Portal Start with Started By 2026-10-22; Viewer not yet tried) | +| EEWA-32 | portal sign-in (Stage 2: Portal Users, Roles, Started By / Stopped By); blocked by EEWA-31 | In Progress (built 2026-10-22, `c230a8e`; live Sign-in and a Portal Start with Started By 2026-10-22; roles Super Admin / Admin / Viewer `f99dc17` 2026-10-27; Viewer not yet tried) | | EEWA-33 | portal run alerts (Stage 3: Run Error banner, `started_by` column); blocked by EEWA-32 | Done (closed 2026-10-26; built `ecbbf3a`; banner seen after the 19:42 run, `started_by` on the Portal runs' rows) | | EEWA-34 | medicare_pps_code (#25) — spec | To Do (parent of the work below) | | EEWA-35 | #25 PPS Code check — Review when missing | In Progress (built 2026-10-23, `ac25f83`; not run live) | @@ -724,6 +747,20 @@ test claim ids — new posts must not (§7). ## 10. Open items +- **Stability (review of 25 runs, 20–26/10, 2026-10-26).** After the eCW + login the run is steady: the last five full runs read 50/50 with no + bot error and the same results run to run. The weak part is getting + in: about half the runs never passed the login — "No second factor + prompted / Login not confirmed" (four on 20/10, not since), a late + 2FA mail (372 s once, one 9-minute timeout), the 19:42 restriction + (fixed by the proxy check), guard waits, and stops by hand. Two runs + stopped at "Opening landing page" (Chrome takes ~2 min there); the + watchdog came after them and has not fired live yet. Suggested, not + ticketed: a landing-page timeout with one retry, a 2FA retry, a week of + daily runs to measure login times on the Runs page. +- **Gitea:** one overall snapshot update at the end of a day, when the + operator asks (from the clone `D:\ecw-charge-validation`; operator + pushes; 2026-10-26: `ba15a57` → `120fd68`). - **Portal for the client — Gitea + Dokploy (asked 2026-10-23, not started).** Gitea `giteanew.ecareindia.com` (10.200.0.15, internal; its HTTPS certificate is not in Git Bash's curl bundle) and Dokploy diff --git a/GLOSSARY.md b/GLOSSARY.md index c6fd085..7885843 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -83,9 +83,12 @@ minutes idle. _Avoid_: login, logon **Role**: -What a Portal User may do. **Admin**: everything, including managing Portal -Users. **Operator**: see everything and start / stop a Run. **Viewer**: see -only. +What a Portal User may do. **Super Admin**: everything; adds and changes +Admins and Viewers; nobody changes a Super Admin from the Portal. **Admin**: +everything, including start / stop a Run and the Sign-in history; adds and +changes Viewers only. **Viewer**: sees the claims and Runs, changes nothing, +no Portal Users or Sign-in history. +_Avoid_: Operator (the old role, folded into Admin on 2026-10-27) **Started By**: The Portal User who started a Run from the Portal, or the Windows user for a diff --git a/tests/test_portal_signin.py b/tests/test_portal_signin.py index 09bcde9..5233d3b 100644 --- a/tests/test_portal_signin.py +++ b/tests/test_portal_signin.py @@ -12,8 +12,10 @@ user store and the fake repository / runner (no SQL Server). * Wrong password / lock / disabled read as such; 30 minutes idle signs out (and is recorded). * A user an Admin made must change the password before anything else. - * Roles: a Viewer sees no Start / Stop and is refused them; only an - Admin reaches Portal Users and the Sign-in history. + * Roles (2026-10-27): a Viewer sees no Start / Stop and is refused them; + only a Super Admin or an Admin reaches Portal Users and the Sign-in + history; an Admin adds and changes Viewers only, a Super Admin Admins + and Viewers, and nobody changes a Super Admin from the Portal. * Started By / Stopped By reach the runner as the signed-in username. * Admin changes take effect on the user's open session at once. """ @@ -47,11 +49,11 @@ def case(fn): def _portal(runner=None, clock=None): - """(app, store, runner) with users asha (Admin), olu (Operator) and + """(app, store, runner) with users sam (Super Admin), asha (Admin) and vee (Viewer), all able to sign in straight away.""" store = FakeUserStore() acc = portal_users.Accounts(store) - for name, role in (("asha", "Admin"), ("olu", "Operator"), + for name, role in (("sam", "Super Admin"), ("asha", "Admin"), ("vee", "Viewer")): acc.create_user(name, name.title(), role, PW) runner = runner or FakeRunner() @@ -136,7 +138,7 @@ def a_wrong_password_says_so_and_five_lock(): @case def sign_out_ends_the_session(): app, store, _ = _portal() - c, _ = _sign_in(app, "olu") + c, _ = _sign_in(app, "asha") assert c.post("/signout", headers=ORIGIN, data={"token": "wrong"}).status_code == 403 r = c.post("/signout", headers=ORIGIN, data=_form(app), @@ -144,7 +146,7 @@ def sign_out_ends_the_session(): assert r.status_code == 303 and r.headers["location"] == "/signin" c.cookies.clear() assert c.get("/days", follow_redirects=False).status_code == 303 - assert ("olu", "sign_out", "") in store.events + assert ("asha", "sign_out", "") in store.events @case @@ -203,24 +205,27 @@ def a_viewer_sees_no_start_and_is_refused_it(): @case def started_by_and_stopped_by_are_the_signed_in_user(): app, _, runner = _portal() - c, _ = _sign_in(app, "olu") + c, _ = _sign_in(app, "asha") assert "const CAN_RUN = true;" in c.get("/").text h = {**ORIGIN, "X-Run-Token": app.state.run_token} assert c.post("/api/bot/start", headers=h).status_code == 200 - assert runner.started_by == "olu" + assert runner.started_by == "asha" assert c.post("/api/bot/stop", headers=h).status_code == 200 - assert runner.stopped_by == "olu" - assert c.get("/admin/users").status_code == 403, "Operator is no Admin" + assert runner.stopped_by == "asha" @case def the_header_names_the_user_and_admin_links_are_admin_only(): app, _, _ = _portal() - a, _ = _sign_in(app, "asha") - page = a.get("/runs").text - assert "Asha · Admin" in page and 'href="/admin/users"' in page - o, _ = _sign_in(app, "olu") - assert 'href="/admin/users"' not in o.get("/runs").text + for name, role in (("sam", "Super Admin"), ("asha", "Admin")): + a, _ = _sign_in(app, name) + page = a.get("/runs").text + assert f"{name.title()} · {role}" in page, name + assert 'href="/admin/users"' in page and 'href="/admin/signins"' in page + v, _ = _sign_in(app, "vee") + page = v.get("/runs").text + assert 'href="/admin/users"' not in page + assert 'href="/admin/signins"' not in page @case @@ -228,9 +233,13 @@ def an_admin_adds_a_user_who_then_must_change(): app, store, _ = _portal() c, _ = _sign_in(app, "asha") r = c.post("/admin/users", headers=ORIGIN, data=_form( - app, username="Ravi", display_name="Ravi K", role="Operator", + app, username="Ravi", display_name="Ravi K", role="Viewer", password="temp-password-1")) assert r.status_code == 200 and "ravi added" in r.text, r.text[:300] + r = c.post("/admin/users", headers=ORIGIN, data=_form( + app, username="kim", display_name="", role="Admin", + password="temp-password-1")) + assert r.status_code == 400 and "An Admin can add or change Viewer only" in r.text and "kim" not in store.users assert store.users["ravi"]["must_change"] is True assert ("ravi", "user_created", "asha") in store.events r = c.post("/admin/users", headers=ORIGIN, data=_form( @@ -242,20 +251,42 @@ def an_admin_adds_a_user_who_then_must_change(): @case def admin_changes_reach_an_open_session_at_once(): app, store, _ = _portal() - admin, _ = _sign_in(app, "asha") - olu, _ = _sign_in(app, "olu") + admin, _ = _sign_in(app, "sam") + olu, _ = _sign_in(app, "asha") vee, _ = _sign_in(app, "vee") - admin.post("/admin/users/olu/role", headers=ORIGIN, + admin.post("/admin/users/asha/role", headers=ORIGIN, data=_form(app, role="Viewer")) assert "const CAN_RUN = false;" in olu.get("/").text, "role now Viewer" admin.post("/admin/users/vee/disable", headers=ORIGIN, data=_form(app)) assert vee.get("/days", follow_redirects=False).status_code == 303 _, r = _sign_in(app, "vee") assert r.status_code == 401 and "disabled" in r.text - admin.post("/admin/users/olu/reset", headers=ORIGIN, + admin.post("/admin/users/asha/reset", headers=ORIGIN, data=_form(app, password="temp-password-2")) assert olu.get("/days", follow_redirects=False).status_code == 303 - assert store.users["olu"]["must_change"] is True + assert store.users["asha"]["must_change"] is True + + +@case +def the_users_page_offers_only_what_each_role_may_change(): + """An Admin's page: add a Viewer, change Viewer rows only. A Super + Admin's: Admin or Viewer, never Super Admin; no row changes a Super + Admin.""" + app, store, _ = _portal() + a, _ = _sign_in(app, "asha") + page = a.get("/admin/users").text + assert "" in page + assert "" not in page, "an Admin adds Viewers only" + assert 'action="/admin/users/vee/reset"' in page + for name in ("sam", "asha"): + assert f'action="/admin/users/{name}/' not in page, name + s, _ = _sign_in(app, "sam") + page = s.get("/admin/users").text + assert "" in page and "Super Admin" not in page + assert 'action="/admin/users/asha/role"' in page + assert 'action="/admin/users/sam/' not in page, "nobody changes sam here" + r = a.post("/admin/users/sam/disable", headers=ORIGIN, data=_form(app)) + assert r.status_code == 400 and not store.users["sam"]["disabled"] @case @@ -271,9 +302,10 @@ def admin_forms_need_the_token(): def the_sign_in_history_lists_events_for_an_admin(): app, _, _ = _portal() _sign_in(app, "vee", "wrong-password-x") - c, _ = _sign_in(app, "asha") - page = c.get("/admin/signins").text - assert "wrong password" in page and "vee" in page + for name in ("sam", "asha"): + c, _ = _sign_in(app, name) + page = c.get("/admin/signins").text + assert "wrong password" in page and "vee" in page, name @case @@ -285,6 +317,21 @@ def with_no_users_the_sign_in_page_says_how_to_add_one(): assert "add_portal_user.py" in page +@case +def the_sign_in_page_says_what_the_portal_is_and_helps_with_the_password(): + """Operator, 2026-10-26: the Sign-in page was plain. It now introduces + the Portal, can show the typed password, warns about Caps Lock, and + says the idle and lockout rules.""" + app, _, _ = _portal() + page = TestClient(app, base_url=BASE).get("/signin").text + assert 'class="auth-brand"' in page and "Charge Validation" in page + assert 'id="pw-toggle"' in page and 'aria-controls="pw"' in page + assert 'id="caps"' in page, "a Caps Lock warning" + assert "30 minutes" in page and "15 minutes" in page + assert 'name="username"' in page and 'id="pw" name="password"' in page + assert ' int: username = argv[0] if argv else input("Username: ").strip() role = argv[1] if len(argv) > 1 else ( - input(f"Role ({' / '.join(ROLES)}) [Admin]: ").strip() or "Admin") + input(f"Role ({' / '.join(ROLES)}) [Super Admin]: ").strip() + or "Super Admin") name = argv[2] if len(argv) > 2 else input("Name shown on the Portal: ") password = getpass.getpass("Password (10+ characters, hidden): ") if getpass.getpass("Password again: ") != password: diff --git a/tools/sql/create_tables.sql b/tools/sql/create_tables.sql index 6f4db69..25407df 100644 --- a/tools/sql/create_tables.sql +++ b/tools/sql/create_tables.sql @@ -60,7 +60,7 @@ BEGIN CREATE TABLE dbo.CV_PortalUser ( username NVARCHAR(50) NOT NULL PRIMARY KEY, -- lower case display_name NVARCHAR(100) NOT NULL, - role NVARCHAR(20) NOT NULL, -- Admin / Operator / Viewer + role NVARCHAR(20) NOT NULL, -- Super Admin / Admin / Viewer pw_hash CHAR(64) NOT NULL, -- hex pw_salt CHAR(32) NOT NULL, -- hex pw_iterations INT NOT NULL, @@ -72,10 +72,24 @@ BEGIN created_by NVARCHAR(50) NOT NULL, changed_at DATETIME2 NOT NULL, CONSTRAINT CK_CV_PortalUser_role - CHECK (role IN ('Admin', 'Operator', 'Viewer')) + CHECK (role IN ('Super Admin', 'Admin', 'Viewer')) ); END GO +-- Roles from 2026-10-27: Super Admin / Admin / Viewer, no Operator. A table +-- made before then: its Admins become Super Admins and its Operators +-- Admins, in that order. Runs until there is a Super Admin; safe to run +-- again. (Not read from sys.check_constraints: the bot's login may not see +-- a constraint's definition.) +IF NOT EXISTS (SELECT 1 FROM dbo.CV_PortalUser WHERE role = 'Super Admin') +BEGIN + ALTER TABLE dbo.CV_PortalUser DROP CONSTRAINT CK_CV_PortalUser_role; + UPDATE dbo.CV_PortalUser SET role = 'Super Admin' WHERE role = 'Admin'; + UPDATE dbo.CV_PortalUser SET role = 'Admin' WHERE role = 'Operator'; + ALTER TABLE dbo.CV_PortalUser ADD CONSTRAINT CK_CV_PortalUser_role + CHECK (role IN ('Super Admin', 'Admin', 'Viewer')); +END +GO IF OBJECT_ID('dbo.CV_PortalSignIn', 'U') IS NULL BEGIN CREATE TABLE dbo.CV_PortalSignIn ( @@ -84,7 +98,7 @@ BEGIN username NVARCHAR(50) NOT NULL, -- as typed, for a wrong one event NVARCHAR(30) NOT NULL, -- sign_in, sign_out, wrong_password, -- locked, timeout, user_created, ... - by_user NVARCHAR(50) NULL -- the Admin, for an Admin change + by_user NVARCHAR(50) NULL -- the Super Admin / Admin who changed it ); CREATE INDEX IX_CV_PortalSignIn_at ON dbo.CV_PortalSignIn (at); END diff --git a/web/app.py b/web/app.py index 9403aef..0f6cf67 100644 --- a/web/app.py +++ b/web/app.py @@ -54,8 +54,9 @@ from ecw_bot.core.run_events import run_id_of # noqa: E402 from .repo import SqlResultsRepo # noqa: E402 from . import run_status # noqa: E402 from . import runs as run_view # noqa: E402 -from .users import (Accounts, CAN_RUN, ROLES, Sessions, # noqa: E402 - SqlUserStore, UserError) +from .users import (Accounts, CAN_ADMIN, CAN_RUN, # noqa: E402 + IDLE_MINUTES, LOCK_AFTER, LOCK_MINUTES, MANAGES, + Sessions, SqlUserStore, UserError) from .bot_runner import (AlreadyRunning, BotRunner, # noqa: E402 NotRunning, StopFailed) @@ -379,7 +380,7 @@ def create_app(repo, allowed_hosts=ALLOWED_HOSTS, runner=None, "run_token": app.state.run_token, "crumbs": crumbs, "categories": CATEGORIES, "user": user, "can_run": bool(user and user["role"] in CAN_RUN), - "is_admin": bool(user and user["role"] == "Admin"), + "is_admin": bool(user and user["role"] in CAN_ADMIN), **context}) @app.get("/", response_class=HTMLResponse) @@ -548,7 +549,9 @@ def create_app(repo, allowed_hosts=ALLOWED_HOSTS, runner=None, def _signin_page(request, error="", next_url="/", status_code=200): page = _page(request, "signin.html", [["Sign in", None]], error=error, next_url=_safe_next(next_url), - no_users=not _db(store.list)) + no_users=not _db(store.list), + idle_minutes=IDLE_MINUTES, lock_after=LOCK_AFTER, + lock_minutes=LOCK_MINUTES) page.status_code = status_code return page @@ -611,26 +614,28 @@ def create_app(repo, allowed_hosts=ALLOWED_HOSTS, runner=None, sessions.refresh_user(updated) return RedirectResponse("/", status_code=303) - # ── Portal Users (Admin) ───────────────────────────────────────────── + # ── Portal Users (Super Admin, Admin) ──────────────────────────────── def _users_page(request, error="", message="", status_code=200): page = _page(request, "users.html", [["Dashboard", "/"], ["Portal Users", None]], - users=_db(store.list), roles=ROLES, error=error, + users=_db(store.list), + roles=MANAGES.get(request.state.user["role"], ()), + error=error, message=message) page.status_code = status_code return page @app.get("/admin/users", response_class=HTMLResponse) def users_page(request: Request, msg: str = ""): - _need(request, ("Admin",)) + _need(request, CAN_ADMIN) return _users_page(request, message=msg[:200]) def _admin_change(request, token, done, fn, *args): """One Admin change: checked, made, then back to the list with `done` said — or the list again with the problem.""" _check_token(request, token) - me = _need(request, ("Admin",))["username"] + me = _need(request, CAN_ADMIN)["username"] try: _db(fn, *args, by=me) except UserError as exc: @@ -686,7 +691,7 @@ def create_app(repo, allowed_hosts=ALLOWED_HOSTS, runner=None, @app.get("/admin/signins", response_class=HTMLResponse) def signins_page(request: Request): - _need(request, ("Admin",)) + _need(request, CAN_ADMIN) return _page(request, "signins.html", [["Dashboard", "/"], ["Sign-in history", None]], events=_db(store.history)) diff --git a/web/templates/base.html b/web/templates/base.html index e9c2f21..e4b3819 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -5,6 +5,7 @@ {% block title %}Charge Validation{% endblock %} · eCW + {% block head %}{% endblock %}