dorfteich/apps/web/e2e/admin-users.spec.ts
Claude Opus 4.8 42e97b9df2
Some checks failed
CD / Build and push images (push) Successful in 3m12s
CI / Lint, typecheck, test (push) Failing after 2m29s
CI / Auth e2e pack (push) Successful in 3m32s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m11s
CD / Promote to Int (push) Successful in 11s
Add Site-Admin user management (#59)
Instance operators get basic user administration for support, abuse handling,
and GDPR groundwork (security.md §Privacy).

- api `admin/`: Site-Admin-gated `/admin/users` — a searchable, paginated list
  (username, e-mail, status, role, pond count, last login) plus lifecycle
  actions: disable/enable (a disabled user is logged out everywhere and login
  is refused with the distinct `account_disabled`), resend verification, delete,
  and grant/revoke Site Admin. Guards: you cannot act on your own account
  (`cannot_modify_self`) and the last Site Admin cannot be dropped
  (`last_site_admin`). Every action is audit-logged with the actor.
- `PseudonymizationService`: account deletion scrubs the PII, removes all login
  identities + sessions, and trashes the personal pond — the kept row is what
  authorship references, so shared content the user authored shows as "Deleted
  user" (no orphaned/cascaded content).
- web: the Admin area gains a 'Users' surface — search, pagination, and the
  actions (destructive ones behind an inline two-step confirm; self-actions
  hidden). New `users` i18n namespace (de+en).
- tests: `user-admin.e2e.db.test.ts` (disable → logout + login blocked; delete
  → pseudonymized authorship + personal pond trashed + credentials gone; last
  Site Admin and self protected; Site-Admin gating); a non-destructive browser
  `admin-users` pack proving disable-in-UI blocks login and enable restores it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
2026-07-10 00:31:41 +02:00

47 lines
1.8 KiB
TypeScript

import { expect, request, test } from '@playwright/test';
import { contextForUser, FIXTURE_PASSWORD } from './helpers';
const BASE_URL = process.env.E2E_BASE_URL ?? 'http://localhost:5173';
/**
* Site-Admin user management UI (issue #59): disabling a user through the admin
* list logs them out and blocks login with a distinct message; enabling
* restores access. (Delete + pseudonymization is covered thoroughly by the api
* db test; the browser pack stays non-destructive so fixtures survive.)
*/
test('disabling a user in the admin UI blocks their login, enabling restores it', async ({
browser,
}) => {
const admin = await contextForUser(browser, BASE_URL, 'fixture-admin');
const login = async (): Promise<number> => {
const ctx = await request.newContext({ baseURL: BASE_URL });
const res = await ctx.post('/api/v1/auth/login', {
data: { usernameOrEmail: 'fixture-viewer', password: FIXTURE_PASSWORD },
});
const status = res.status();
await ctx.dispose();
return status;
};
expect(await login()).toBe(200); // active to begin with
const page = await admin.newPage();
await page.goto('/admin');
await page.locator('.user-manager__search').fill('fixture-viewer');
const row = page.locator('.user-row[data-username="fixture-viewer"]');
await expect(row).toBeVisible();
try {
await row.locator('.user-row__disable').click();
await expect(row.locator('.user-row__status')).toHaveText(/disabled|deaktiviert/i);
expect(await login()).toBe(403); // account_disabled
} finally {
// Re-enable so the fixture is left intact for other packs / reruns.
await row.locator('.user-row__disable').click();
await expect(row.locator('.user-row__status')).toHaveText(/active|aktiv/i);
await admin.close();
}
});