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
Every route now declares its access rule explicitly and is enforced
through the shared resolution algorithm (permissions.md):
- PermissionGuard + decorators (@RequiresPondRole, @RequiresPagePermission,
@RequiresAttachmentPermission, @AuthenticatedOnly) applied to every
route; a route-enumeration test proves full coverage alongside
@Public()/Site-Admin-guarded routes.
- 404/403 policy (documented in README conventions): denied reads answer
404 (existence hiding), denied writes on readable things answer 403;
trash views need write capability (ADR 0013).
- PermissionService resolves page/pond questions via the shared resolver,
with an in-process pond-context cache (grants + label parents) that is
invalidated on every grant/label-tree change and TTL-bounded as a
multi-process safety net. Grant changes also fire pond_access_changed
for collab revalidation (#39/#53).
- shared: pond-scope resolution (hasPondRole, canSeePond) next to the
page resolver; grant wire schemas + GrantView.
- Owner Pond-Admin grants: migration backfill for all existing ponds,
created transactionally with every new pond (shared + personal + seed).
- Grant CRUD under /ponds/:id/grants (pond_admin-gated) with structural
and referential validation, last-admin protection, audit logs.
- InterimAccessService deleted; page lists, search, backlinks, phantom
links, and trash listings are filtered per page through the resolver;
collab tokens are now truly ro for readers.
- Fixture-matrix e2e (reader/editor/pond admin/foreign, label-deny,
authenticated-subject, revoke-then-immediate-deny cache test).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
- Pond model with pond-level trash columns (ADR 0013) and settings jsonb
holding only deviations from the defaults (sidebar sort, font slots per
ADR 0016); migration 20260705090100_ponds
- shared: pond schemas/views and slugify (German transliteration,
URL-safe, length-capped); deterministic -2/-3 suffixes for collisions
- InterimAccessService: single place answering pond access questions
until the real role model lands in M5
- POST/GET /ponds, GET /ponds/:slug, PATCH/DELETE /ponds/:id, Site-Admin
trash + restore; personal pond auto-created on e-mail verification and
for active seed fixtures; personal ponds cannot be trashed
- e2e pack covering verify-flow pond creation, slug suffixes, rename,
foreign-pond 404s, trash/restore; slugify unit tests
Closes#21
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UpQz6ypHJsLfMf4S6fyQEB
The web app grows its account surface: login (with next-redirect,
unverified-hint + resend), signup (react-hook-form + shared Zod
schemas, field-level api errors, closed-registration state fed by the
new public GET /auth/registration), e-mail verification, forgot/reset
password; a settings page with profile (locale applies immediately),
password change, and active-session management; a Site-Admin page for
instance name, default locale, and registration mode. AuthProvider
holds /auth/me, applies the profile locale, and backs route guards
(RequireAuth/RequireAnonymous/RequireSiteAdmin); the top bar gains a
user menu. All strings ship in the new auth/settings namespaces (de+
en); the exception filter now preserves handler-specific error codes.
Verified live: signup → Mailpit → verify → login → profile through
the Vite proxy.
Closes#16Closes#17Closes#18Closes#19
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Server halves of #17/#18/#19: PATCH /users/me and change-password
(verifies the current password, logs out every other session),
GET/DELETE /users/me/sessions with current-session flag and protection
against revoking oneself; InstanceSettingsService as a typed, cached,
Zod-validated registry over instance_settings (schema-default fallback
for invalid stored values, audit-logged writes) consumed by the signup
flow; /admin/settings behind the new SiteAdminGuard with strict
unknown-key rejection. SessionsService moves to its own module to keep
Auth/Users acyclic. Three new e2e suites bring the api to 42 tests.
Part of #17, #18, #19
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AuthModule implements the M1 core as one coherent unit:
Signup (#13): POST signup/verify-email/resend-verification with shared
Zod validation (field-level error details), double opt-in via hashed
single-use tokens (24h, superseding reissue), registration_mode
enforcement, and per-IP rate limits.
Sessions (#14): opaque 32-byte cookie tokens stored as SHA-256 row
ids, sliding 30-day expiry (refresh at most hourly), global AuthGuard
with @Public() opt-out attaching the user to every request, CSRF
origin check on mutating requests, per-account login backoff (5/15min,
reset on success), generic 401 for wrong-vs-unknown credentials,
logout with immediate invalidation, GET /auth/me.
Reset (#15): forgot-password without account enumeration, one-hour
single-use tokens, reset destroys all existing sessions.
A 14-case supertest e2e suite drives every flow against the test
database, reading verification/reset links from the mail outbox.
Closes#13Closes#14Closes#15
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>