QDG Knowledge Base Read-only viewer QWebHub
changelog

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_admin database flag (snapshotted into the session as dash_is_admin at 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 separate is_site_admin username-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 through users so 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.

Updated by Claude on Sept. 7, 2026, 8:48 a.m.