Login is now by email, not username. username still exists internally
(JWT sub claim, audit logs, Jira actor attribution, SSO provisioning all
still key off it) but is now always kept equal to email everywhere a user
is created or their email changes — never a separately-chosen value.
- User.email is now unique + NOT NULL (migration b067 backfills any
missing/blank email from username first, so existing rows — notably
the seeded admin, which historically had none — never violate it).
- /auth/login and the (unused but updated for consistency)
authenticate_user() now query by email.
- create_user (legacy, unreferenced but kept) and
create_user_without_password both derive username from email.
- update_user keeps username in sync when email changes, and rejects
duplicate emails.
- seed.py reads ADMIN_EMAIL (new env var, wired through install.sh and
docker-compose.prod.yml) for the initial admin; falls back to an
email-shaped ADMIN_USERNAME or a placeholder that's flagged for the
operator to fix.
- admin_config.py's import bundle now matches/creates users by email,
skipping (not crashing on) entries with no email.
- sso_service.py always sets username = email for SSO-provisioned users.
- LoginPage/auth.ts updated to email input/copy (wire field name stays
'username' — that's the OAuth2PasswordRequestForm spec, not the value).
- New DELETE /users/{id}: hard-deletes a user only if they have zero
activity footprint (no tests, evidence, worklogs, audit entries, Jira
links, procedure/template suggestions, reviewed imports) — otherwise
rejects with a clear message to deactivate instead, to keep the audit
trail intact. Cleans up the user's own notifications/password-setup
tokens as part of deletion. Self-delete is blocked. Wired to a trash
icon in the Users page with a confirm dialog.
- Registered the EvaluationImport model in app/models/__init__.py — it
was missing, so its table never existed in the SQLite test DB; only
surfaced once the new delete-user footprint check queried it.
- Fixed a redirect bug: the axios response interceptor's on-401 redirect
to /login didn't exempt /set-password, so the auth provider's routine
mount-time /auth/me check (401 for a logged-out visitor) bounced anyone
opening their emailed set-password link away before they could use it.
- A lead opening a test with a pending procedure suggestion awaiting
their review now gets a blocking popup (approve/reject only) instead
of discovering it later in a separate queue.
- Users can be granted more than one role via extra_roles; only one is
ever active at a time (no permission mixing) and a top-bar switcher
lets the user swap which one, taking effect immediately since role
is read fresh from the DB on every request.
- The password-setup/reset webhook now posts the agreed Power Automate
contract ({to, subject, body} with the platform's standard greeting
and signature template) and supports an admin-configured API key
sent as an x-api-key header.
Admins now create users with a name + email (no password) and role;
a Send Email action (per-user, also reusable for resets) issues a
one-time token and POSTs it to an admin-configurable webhook URL —
intended for a Power Automate flow that delivers the actual email.
The old admin-sets-the-password flow is kept server-side
(LegacyUserCreateWithPassword) but no longer wired into the UI.
Every user-facing surface (top bar, sidebar, user management, audit
log, assignee pickers, dispute notifications) now shows full_name
instead of username, falling back to username when unset.
Also fixes long attack_procedure/expected_detection/suggested_text
text overflowing its container in the template review panels and
Red/Blue team fields (missing break-words on whitespace-pre-wrap
blocks).
'manager' has been a fully-functional role throughout the app —
notifications, operator assignment, dispute mediation — but was
missing from both the user creation/update whitelist and the SSO
auto-provisioning role list, so a manager account could never
actually be created.
Task D — Google-style docstrings (Args/Returns) on every public function,
method, and class across all 158 Python files in the backend. Zero ruff D
violations (pydocstyle Google convention).
Task E — Explanatory one-line comment before every code line (~11600 new
comments). ruff check passes clean after isort re-sort.