Changelog
Version 2 · Added 2026-09-06 and 2026-09-07 entries: role-based dashboard access, self-service account page, 8 QA bug fixes, Site Clients, and per-site role management
2026-09-07 — Site Clients + per-site role management
Added apps/auth/site_clients.py + db/create_site_clients_table.sql (new
site_clients table): each registered site's own clients/customers —
previously managed outside QDBAuth — now get a username/password/TOTP login
created and managed from the dashboard (/dashboard/site-clients/), scoped
to the site they belong to. Reuses the exact same 2FA and SSO-token
machinery as staff: login_view tries site_clients.authenticate() as a
fallback once a users-table lookup misses and a return_url resolves to a
real site; verify_2fa_view delegates to a new _verify_2fa_client helper.
A client is username-login with no real email (same as a site-admin staff
account), so 2FA setup happens on-screen via a QR code
(site_client_totp_setup_page) rather than an emailed link, and a client
can never reach the QDBAuth dashboard itself — only ever redirected back to
their own site with an SSO token. Clients hold a single role, no tiers.
Added apps/auth/site_roles.py + db/create_site_roles_table.sql (new
site_roles table), replacing the hardcoded ROLE_GROUPS dict in
apps/auth/users.py that the 2026-07-30 entry below introduced. Every site
can now define its own role list — with one role flagged is_default and
one flagged is_admin_equivalent — through a new dashboard page
(/dashboard/site-roles/), shared by both staff user creation
(users.role_group_for_sites now delegates to site_roles) and site client
creation. A site with no roles of its own falls back to a shared default
list (site_id IS NULL rows). The migration seeds the previous default list
and TD's existing custom list verbatim, so behavior is unchanged until an
admin adds roles for another site (e.g. MTC) through the new page.
Scope note: the originating request also asked to rename site →
Organization throughout. That rename was dropped after review — it
would have touched roughly 450 references across a live production system
(the site table plus 3+ dependent tables, ~15 Python files, 10 templates),
for a purely cosmetic change, with no migration framework or rollback
safety net. site/site_id/site_name remain exactly as they were.
2026-09-06 — Role-based dashboard access + self-service account page
Introduced a 5-tier role model — Super Admin, Admin, Developer, Support Lead, User — replacing the old flat admin/developer/user list documented below (2026-07-30). See Roles, Permissions & Multi-Tenancy for the full model; in short:
- Super Admin: full, unrestricted dashboard access. Checked via the
account's actual
is_admindatabase flag (snapshotted into the session asdash_is_adminat login) rather than parsing the role string, so accounts created before this change (role literally"admin") keep full access with no data migration needed. - Admin: scoped to the account's own
approved_sites— formalizes what was previously only reachable via the separateis_site_adminusername-login account type. - Developer / Support Lead / User: no admin screens at all — login now
redirects these roles to a new self-service My Account page
(
/dashboard/account/): change own password, reset own authenticator, change/remove own profile picture.
Heads-up logged for consuming apps: the same role string is also sent as
the SSO token's user_permissions value — an app that checks for the
literal string "ADMIN" may need updating once accounts start using the
new role names (TD accounts already sent non-"ADMIN" strings before this
change, so this isn't a new category of risk, just wider now).
Also fixed 8 QA-reported bugs in the same delivery:
- Manage Sites / Users search boxes: clearing the field now auto-restores the full list instead of requiring a manual re-search.
- Added a "Remove" action for a user's profile picture (previously upload-only, no way to clear one).
- Users search now matches the full "First Last" name together, not just each field separately.
- Manage Sites and Users both gained multi-select checkboxes + bulk delete.
- New users are now auto-enrolled into an auto-created "Default" group.
- Groups gained a "Role Mapping" tab (reference-only record, mirrors Keycloak's group role mapping UI — does not itself grant permissions).
- Activity Logs split into "Admin Events" / "User Events" tabs; added audit logging for login/logout/failed-login/self-service password reset, none of which were being recorded before this change.
- Fixed a stale group member-count:
user_groups.list_groups()was counting membership rows left behind by a deleted user (no FK by design) — now joins throughusersso the count matches what the member list actually shows.
2026-07-31 — Production hardening groundwork
Added /health/ endpoint (apps/auth/views.py health_view, checks app + DB
connectivity) and serve_waitress.py (production WSGI launcher, replacing
manage.py runserver). Groundwork for a broader server-reliability plan —
recommended direction is cloud hosting (AWS/Azure) with provider-level
auto-recovery for a single instance, plus a managed database with automatic
failover, rather than the team manually operating two servers with MySQL
replication.
2026-07-30 — TD site-specific roles
Added ROLE_GROUPS in apps/auth/users.py — the TD (Troyen Data) site gets its
own 5 roles (Developer, Support, Dev Lead, Support Lead, Super Admin) instead of
the global admin/developer/user list. role_group_for_sites() resolves which
group applies; create_user_direct, update_user, and
bulk_users.create_users_from_rows all validate/default against the correct
group. The Create/Edit User dashboard forms swap the Role dropdown's options live
via JS based on which site checkbox is checked (role_groups passed to the
template via json_script).
Superseded 2026-09-07: this hardcoded dict was replaced by the
database-backed site_roles table — see above.
2026-07-28 — Per-site credentials
Added apps/auth/site_credentials.py and
db/create_user_site_credentials_table.sql (new user_site_credentials table,
deliberately with no FK constraints to users/site, matching this codebase's
existing convention of not hard-linking auxiliary tables so delete_user_api and
site deletion keep working unmodified).
Accounts created going forward get one password + TOTP secret per granted site
(or one "siteless" row if granted none), instead of one shared credential across
every site. site_credentials.has_any(user_id) is the legacy/new-style switch.
Fixes: previously, granting one person 2-3 sites sent 2-3 separate verification
emails that all pointed at the same shared credential, so completing setup via
the first email made every other site's link say "already verified" immediately.
Existing accounts are explicitly NOT migrated — a deliberate decision, not an oversight, to avoid touching already-working logins.
Also touched: tokens.py (signup/pending-login tokens now carry an optional
credential_id), emailer.py, dashboard.py, bulk_users.py, views.py
(login_view, verify_2fa_view, verify_signup_view, verify_signup_qr_view,
api_login_view, api_verify_2fa_view, api_totp_qr_view), sites.py (added
site_name_for_url, get_site_id_by_name).
2026-07-28 — HTTP 400 for auth failures
/api/login/ and /api/verify-2fa/ previously returned HTTP 200 with
success:false for wrong password / wrong authenticator code — now return HTTP
400 for these business-logic failures. HTTP 401 stays reserved for a
missing/wrong X-QDB-Client-Secret. /api/verify-2fa/ and /api/token/refresh/
responses also gained refresh_token_expires_at; /api/verify-2fa/ additionally
returns a user object (id, email, first_name, last_name, role) so
consuming apps (e.g. the CMS) don't need a separate call to get basic user info
after login.