feat(users): admin can permanently delete users, fix set-password redirect bug
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

- 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.
This commit is contained in:
kitos
2026-07-23 08:58:17 +02:00
parent 545a84b137
commit d932134975
7 changed files with 242 additions and 2 deletions
+9 -2
View File
@@ -38,6 +38,11 @@ async function tryRefresh(): Promise<boolean> {
}
}
// Routes that are public — a 401 from a background call (e.g. the auth
// provider's mount-time /auth/me check) must never bounce someone away
// from these while they're using them logged out.
const PUBLIC_PATHS = ["/login", "/set-password"];
// ── Response interceptor ────────────────────────────────────────────
client.interceptors.response.use(
(response) => response,
@@ -58,8 +63,10 @@ client.interceptors.response.use(
}
}
// Refresh failed or not applicable — redirect to login
if (window.location.pathname !== "/login") {
// Refresh failed or not applicable — redirect to login, unless
// we're already on a public page (e.g. /set-password) that a
// logged-out visitor is legitimately using.
if (!PUBLIC_PATHS.includes(window.location.pathname)) {
window.location.href = "/login";
}
}
+7
View File
@@ -61,6 +61,13 @@ export async function updateUser(userId: string, payload: UserUpdatePayload): Pr
return data;
}
/** Permanently delete a user (admin only). Fails with a 400 if the user has
* any activity footprint (tests, evidence, worklogs, audit entries, etc) —
* deactivate those instead to preserve the audit trail. */
export async function deleteUser(userId: string): Promise<void> {
await client.delete(`/users/${userId}`);
}
/** Email a one-time set-password link to a user (admin only). Also used for password resets. */
export async function sendPasswordEmail(userId: string): Promise<{ detail: string }> {
const { data } = await client.post<{ detail: string }>(`/users/${userId}/send-password-email`);