refactor(admin): migrate invites + users to createApiClient (#465) #623
Open
marcel
wants to merge 6 commits from
feat/issue-465-event-fetch-refactor into main
pull from: feat/issue-465-event-fetch-refactor
merge into: marcel:main
marcel:main
marcel:feat/issue-580-sentry-backend
marcel:fix/issue-593-management-port-zero
marcel:worktree-feat+issue-557-upload-artifact-v3-pin
marcel:worktree-chore+issue-556-drop-client-branches-coverage-gate
marcel:fix/issue-514-prerender-crawl-bakes-redirects
marcel:fix/issue-472-prerender-entries
marcel:feat/issue-395-readme
marcel:feat/issue-345-bulk-mark-reviewed
marcel:feat/issue-344-bell-tooltip
marcel:feat/issue-341-annotieren-contrast
marcel:feat/issue-225-bulk-metadata-edit
marcel:feat/issue-317-bulk-upload
marcel:feat/issue-271-dashboard-redesign
marcel:docs/issue-240-mission-control-spec
marcel:refactor/issues-193-200
marcel:feat/issue-162-korrespondenz-redesign
marcel:feature/68-new-document-file-first
marcel:feat/81-discussion-discoverability
marcel:feat/62-document-bottom-panel
marcel:feature/56-backfill-file-hashes
marcel:feat/38-document-edit-history
marcel:fix/svelte5-test-delegation-and-login-auth
No Reviewers
Labels
Clear labels
P0-critical
P1-high
P2-medium
P3-later
audit
bug
cleanup
collaboration
conversation
descoped
devops
documentation
epic
feature
file-upload
legibility
notification
person
phase-1: security
phase-2: container-images
phase-3: prod-compose
phase-4: spring-prod-profile
phase-5: backups
phase-6: deployment-docs
phase-7: monitoring
refactor
security
test
ui
user
Blocks a core user journey, causes data loss, or is a security/accessibility barrier. Work on this before P1+.
Significant friction on a main user journey; workaround exists but is non-obvious. Next up after P0.
Noticeable improvement; doesn't block anything; schedule opportunistically.
Cosmetic or parking-lot work; fine to stay open indefinitely.
Read-only audit / assessment work; produces a report rather than changing code
Something isn't working
Removal of dead code, vague comments, naming violations, and scratch artifacts
We want to extend the family archive to have more collaboration tools
We will do that later
README, ARCHITECTURE, GLOSSARY, CONTRIBUTING, per-domain docs
Marker for epic-level issues that group multiple child issues
Codebase Legibility Refactor — preparing the codebase for human evaluation and bus-factor reduction
Security hygiene — must be done first
Production-ready multi-stage Docker images
Production compose overlay + Caddy reverse proxy
Spring Boot production configuration profile
Database and object storage backup strategy
.env.example, DEPLOYMENT.md, runbook
Prometheus, Loki, Grafana, Alertmanager
Code restructuring without behaviour change
UI/UX and accessibility issues
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: marcel/familienarchiv#623
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "feat/issue-465-event-fetch-refactor"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
fetch(${apiUrl}/api/...)calls inadmin/invites/+page.server.ts(load, create, revoke) withcreateApiClient(fetch)using typed GET/POST/DELETE — removes theenvimport entirelyfetch('/api/users/...')+ try/catch JSON parsing inadmin/users/[id]/+page.server.tsupdateaction withcreateApiClient(fetch).PUT(...)per project patterncreate/revokeinvites actions and a newpage.server.test.tsfor the users update action (5 tests)*.server.spec.tsfiles caused by@sentry/sveltekitwrapping load functions and readingevent.request.method/event.url.pathnameon mock events that lacked those fieldstest:coveragesemicolon (;→&&) so a failing server run no longer gets silently swallowed by the subsequent client run — CI was reporting green despite 86 failuresCONTRIBUTING.mdthat multipart uploads must useevent.fetch, never globalfetchSpring Sessions note: The auth bypass described in the issue was already resolved by #523/#524 —
event.fetchwas already used everywhere in these files. This PR completes the remaining cleanup: typed client adoption, test coverage, and surfacing hidden test failures.Test plan
npx vitest run --project=server— 576/576 passing (was 490/576 on main)grep -rE "fetch\(\?${.*API|fetch(env." frontend/src/routes— no matches outside oflogin/` (intentional)npm run check— all errors are pre-existing onmain, none in touched filesnpm run lint— cleanCloses #465
🤖 Generated with Claude Code
Replace manual fetch(${apiUrl}/api/...) calls in load, create, and revoke with createApiClient(fetch) so auth injection is handled by handleFetch and the typed API contract is enforced at compile time. Also fix pre-existing load test failures caused by Sentry's load wrapper reading event.request.method (add request to the mock event object). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>Replace fetch('/api/users/${id}', { method: 'PUT', ... }) + inline JSON error parsing with createApiClient(fetch).PUT('/api/users/{id}', ...) and the standard result.error cast pattern. Also fix pre-existing Sentry mock event failures in layout.server.spec.ts by adding request and url to the test event object. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ✅ Approved
This is clean, disciplined refactoring. The
createApiClientmigration is consistent, error extraction follows the project pattern exactly (result.error as unknown as { code?: string }), and removingenv.API_INTERNAL_URLfrom load functions/actions is the right call — those import paths were fragile.What I looked for and found correct
!result.response.okbranches use the correct idiom (notresult.error, which breaks when the spec has no error response defined). The fallback to'INTERNAL_ERROR'is consistent acrossload,create, andrevoke.as unknown as InviteListItemin invites: the comment explains why — the spec marksshareableUrloptional but the backend always populates it. Acceptable cast with documented intent.body as components['schemas']['AdminUpdateUserRequest']in users update: commented clearly — null fields are intentional signals to clear data. The cast satisfies the optional-only spec type without lying.test:coveragesemicolon fix (; → &&): this was a genuine CI bug — a failing server run was silently swallowed, making CI appear green with 86 hidden failures. Excellent catch; fixing it in the same PR that adds the tests is the right scope.CONTRIBUTING.mdclarification: the new sentence aboutevent.fetch(not globalfetch) for multipart uploads is precise and directly actionable.Suggestions (non-blocking)
as anyin test files: The 14 spec files useas anyto avoid typing the fullRequestEventshape. Given the test-only scope and the// eslint-disable-next-linecomments, this is fine, but a sharedmakeEvent(url: string)factory could eliminate most of the repetition if the test suite grows further.page.server.test.tsvspage.server.spec.ts: The new users file uses.test.tswhile the rest of the suite uses.spec.ts. No functional difference with Vitest but worth aligning for discoverability.invites load()test: The test asserts thatmockFetchwas called twice and checks the URL strings, but doesn't verify thestatusquery param is forwarded (i.e.status=activein the URL). Low-priority gap since it's a pre-existing test that was updated rather than a new one.Excellent work. The PR description is thorough — the 86 pre-existing failures context is exactly what reviewers need.
🏛️ Markus Keller — Application Architect
Verdict: ✅ Approved
Architecturally clean. This PR enforces a consistent data-access boundary — all server-side API calls flow through
createApiClient(fetch)instead of rawfetchwith ad-hoc URL string construction. That's the right direction for the SvelteKit server layer.What I checked
Layer boundary compliance:
+page.server.tsfiles callingcreateApiClientis the correct pattern per the project architecture. No controller-level logic is leaking into components. No direct repository access anywhere in scope.env.API_INTERNAL_URLremoval: This is a meaningful improvement. The old pattern scatteredenv.API_INTERNAL_URL || 'http://localhost:8080'fallbacks across multiple files. Moving this knowledge intocreateApiClientcentralizes the concern. The architecture is now: all server-side fetch calls → one shared client → one URL resolution point.test:coveragesemicolon fix: This is the most architecturally significant fix in the PR, even though it looks trivial.;between CI steps means failures are silent.&&means failures are surfaced. This was a systemic quality-gate defect.CONTRIBUTING.md clarification: The multipart/
event.fetchnote is accurate and correctly positioned. This is architecture documentation in the right place.No documentation update required
The CLAUDE.md architecture doc table checks:
The
createApiClientpattern is already the documented approach. This PR migrates remaining files to that pattern — no new architectural decisions, no ADR needed.One observation (non-blocking)
The
as unknown as InviteListItemdouble-cast ininvites/+page.server.tsis a symptom of a mismatch between the OpenAPI-generated types and the actual backend contract (shareableUrloptional in spec, always populated in practice). The comment documents this correctly. The real fix belongs in the backend's@Schema(requiredMode = REQUIRED)annotation on that field — but that's a separate PR and a backend concern, not something to block this refactor on.🔒 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
No new vulnerabilities introduced. The migration improves the security posture in one concrete way and removes a class of latent risk.
What improved
env.API_INTERNAL_URL || 'http://localhost:8080'removed from action handlers: The old pattern had a silent fallback tolocalhost:8080in every action. IfAPI_INTERNAL_URLwas misconfigured in production, the app would silently fall back to localhost with no error. While this is more of a reliability concern than a security one, it does eliminate a class of SSRF-adjacent misconfiguration: a misconfigured internal URL could redirect server-side requests to an unexpected host. The newcreateApiClientcentralizes this to a single point.Error code extraction:
(result.error as unknown as { code?: string })?.code ?? 'INTERNAL_ERROR'is correct. No raw exception messages, no stack traces, no internal details are forwarded to the client. ThegetErrorMessage(code)call in the users update action maps to i18n strings, which is the right pattern.Items I checked and found clean
SecurityConfigor CSRF configuration.event.fetchcorrectly inherits the session cookie — this is the documented safe pattern.@CrossOriginchanges: No new CORS exposure.getErrorMessage(), not raw reflection.idin DELETE path:api.DELETE('/api/invites/{id}', { params: { path: { id } } })— the typed client handles URL encoding. No injection risk.One observation (non-blocking)
The
invites/+page.server.tsload function usesevent.url.searchParams.get('status')to pass astatusquery param to the backend without validation. Thestatusvalue goes directly intoapi.GET('/api/invites', { params: { query: { status } } }). Since this is a server-side call with the typed client and the backend validates the enum, the injection surface is minimal — but a quick allowlist (['active', 'revoked', 'expired'].includes(status)) before forwarding would be a clean defense-in-depth improvement. Not a blocker.🧪 Sara Holt — QA Engineer & Test Strategist
Verdict: ✅ Approved
The test work in this PR is solid and the
test:coveragesemicolon fix is a critical quality-gate repair. 86 hidden test failures in CI is a significant regression — surfacing them is exactly the right move.What I verified
New tests —
admin/invites/page.server.test.ts:createaction: 3 new tests — success, backend error with code, backend error without code (fallback). All three cases covered. ✅revokeaction: 3 new tests — HTTP method + URL assertion, success return shape, error forwarding. ✅mockResponse()is reused across describe blocks. ✅New tests —
admin/users/[id]/page.server.test.ts:updateaction: method/URL assertion, success, 403 error, 500 without code field, password mismatch guard (short-circuit without backend call). All meaningful boundary conditions are covered. ✅makeUpdateRequest()factory function is a good pattern — overridable defaults, readable in test bodies. ✅test:coveragesemicolon →&&fix: This is the most important fix in the PR from a QA perspective. With;, a server-side test run failure exited non-zero but the shell proceeded to the client run, which passed — so CI showed green. With&&, the failure is properly propagated. This was masking 86 failures across 14 spec files. ✅14 existing spec files updated with
request/urlfields: The@sentry/sveltekitwrapper introduced aevent.request.method/event.url.pathnameaccess that broke mocks that only provided{ fetch, url }. Addingrequest: new Request(...)to each mock event is the correct fix. All 14 files updated consistently. ✅Observations (non-blocking)
.test.tsvs.spec.tsnaming: The newpage.server.test.tsfiles use.test.tswhile the rest of the suite uses.spec.ts. Vitest runs both, so no functional issue — but if the Vitest config has a glob that only picks up one extension, this could cause a miss. Worth verifying theincludepattern invitest.config.ts.statusquery param forwarding not tested in invitesload(): The test callsevent('active')but doesn't assert thatmockFetchwas called with?status=activein the URL. The existing test only asserts that/api/invitesand/api/groupswere called. Low-priority gap — happy path coverage could include this.admin/users/[id]/page.server.test.tsmissingload()tests: The new test file covers only theupdateaction. Theload()function (which fetches user + groups in parallel and enforces ADMIN permission) has no server-side unit tests. The existinglayout.server.spec.tscovers the layout but the page-level load is untested. Recommend a follow-up issue.page.server.spec.tsvspage.server.test.ts: The invites file keeps the original namepage.server.test.ts(Gitea shows it existed before). The inconsistency predates this PR, so no action required here.🚀 Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
This PR doesn't touch Docker Compose, CI workflows, or infrastructure configuration — which is the right scope for a frontend refactor. The one DevOps-adjacent change is the
test:coveragescript fix, and it's correct.What I checked
package.json—test:coveragescript:The
;allowed the pipeline to continue even on non-zero exit from the server run. With&&, a failing server coverage run stops the script and exits non-zero — CI correctly fails. This is standard shell composition; the fix is minimal and correct.No CI workflow changes: No
.gitea/workflows/*.ymlfiles touched. No new jobs, no new artifact uploads, no dependency changes in the CI pipeline. Clean.No infrastructure changes: No Docker Compose modifications, no new services, no port exposures, no secret additions.
env.API_INTERNAL_URLremoval is a frontend concern that was never in the Compose file's service definitions.CONTRIBUTING.md—event.fetchdocumentation: Correctly documents that server-side multipart uploads must useevent.fetch, not globalfetch. This is relevant to platform engineering because the session cookie injection depends onhandleFetchbeing in the chain — a globalfetchwould silently bypass auth on multipart routes. Good documentation catch.One observation (non-blocking)
The PR description mentions 576/576 passing with
npx vitest run --project=server. If CI runs this script, the coverage threshold gates should now catch regressions that were previously invisible. Worth verifying that the CI workflow invokestest:coverage(not justtest) to pick up the&&fix in the automated pipeline.📋 Elicit — Requirements Engineer
Verdict: ✅ Approved
This PR maps cleanly to the stated intent of issue #465. No scope creep detected. The additional work (86 hidden test failures, coverage script fix) is directly enabling work — it surfaces the actual health of what was being tested.
Requirements traceability
Issue #465 intent (as stated in PR description): Replace manual
fetch()calls withcreateApiClientin admin pages; removeenvimports; adopt typed endpoints.Delivered:
admin/invites/+page.server.ts:load,create,revokeall migrated to typed clientadmin/users/[id]/+page.server.ts:updateaction migrated;loadanddeletewere already usingcreateApiClientenvimport removed from invites file entirelyevent.fetchconstraint for multipart uploadsScope boundary respected:
The PR description explicitly notes: "The auth bypass described in the issue was already resolved by #523/#524 —
event.fetchwas already used everywhere in these files." This is correct scoping — the PR delivers the remaining cleanup without re-doing closed work.Bundled fixes (enabling work, not scope creep):
@sentry/sveltekitwrapping. They blocked validating the correctness of the refactor. Fixing them in the same PR is the right call.test:coveragesemicolon fix: this was silently masking the 86 failures. Fixing it here ensures the test suite is trustworthy for future work.One gap to note (non-blocking)
The
admin/users/[id]/+page.server.tsload()function has no new unit test coverage added (see Sara's comment). Theupdateaction is now well-tested. A follow-up issue to addload()tests for this page would complete the coverage story for this route.🎨 Leonie Voss — UX Designer & Accessibility Strategist
Verdict: ✅ Approved
This PR is pure server-side refactoring — no Svelte components, no CSS, no markup, no i18n strings, no user-visible changes. No UX or accessibility surface area was modified.
What I checked
User-visible behavior: The
load,create,revoke, andupdateserver actions now callcreateApiClientinstead of rawfetch. From the user's perspective, the invite management page and the user edit page behave identically — same fields, same error messages, same redirect behavior.Error message handling: The users
updateaction correctly routes throughgetErrorMessage(code), which feeds Paraglide i18n strings to the UI. Error messages remain user-facing localized strings, not raw backend output. ✅CONTRIBUTING.mdchange: Developer documentation only — no user-facing copy changed.No component changes: No
.sveltefiles touched. Touch targets, contrast ratios, focus management, and ARIA labels are all out of scope for this PR.Nothing to flag from a UX or accessibility perspective.
👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ✅ Approved
This is a solid, principled refactor. The migration to
createApiClientis applied consistently, error handling follows the established project pattern, and the test additions are thorough. A few nits worth addressing.Blockers
None.
Suggestions
+page.server.ts(invites) —as unknown as InviteListItemdouble cast is a smellThe comment in the code explains why (
shareableUrlis now@Schema(requiredMode = REQUIRED)on the backend but the generated type might be lagging), and the backend fix inInviteListItemDTO.javais correct. The double castas unknown as InviteListItem[]at line ~58 should be a temporary measure — consider leaving a// TODO: remove cast after next npm run generate:apinote, or confirm the types have been regenerated and collapse this. Right now the comment says "keeping the required shape here avoids null-guarding" but the generated type still requires the cast, which signals the regeneration may not have happened yet. Low priority but worth confirming.admin/users/[id]/+page.server.ts—body as components['schemas']['AdminUpdateUserRequest']castThe comment explains the intent ("backend treats null as clear this field") but the spec type marks those fields as optional, not
null-accepting. If the backend's actual behaviour diverges from the spec here, the spec should be corrected upstream, not cast around. Worth filing a follow-up issue.Test file naming —
page.server.spec.tsvspage.server.test.tsThe rename from
page.server.test.ts→page.server.spec.tsin the invites folder is good for consistency. However the test plan claims 576/576 passing — it would be worth checking that no other*.server.test.tsfiles still exist un-renamed to avoid confusion about the naming convention.VALID_STATUSESwhitelist in invites load — nice additionThe
VALID_STATUSESguard with proper type narrowing ((typeof VALID_STATUSES)[number]) is exactly the right pattern. The fallback to'active'(lowercase) is intentional but note that the backend likely accepts uppercase'ACTIVE'— worth confirming the filter actually works end-to-end, since a mismatch would silently default to the wrong status with no error.test:coveragesemicolon fix — importantThe
;→&&fix is significant. Silent test failures in CI are a real risk. Good catch.🏛️ Markus Keller — Senior Application Architect
Verdict: ✅ Approved
The architectural intent of this PR is correct and aligns with the project's layering conventions. All server-side data fetching now flows through the single
createApiClientabstraction, which is the right boundary. The CONTRIBUTING.md clarification aboutevent.fetchvs globalfetchfor multipart uploads fills a real documentation gap.Blockers
None.
Observations
Consistency of the data-access boundary is now enforced in the admin module — good
Previously,
admin/inviteswas the last holdout using rawfetchwith manual URL construction andenv.API_INTERNAL_URL. Having this consolidated means any change to the API base URL, headers, or auth injection inhandleFetchis automatically picked up everywhere — no more separate branches of the fetch logic to maintain.The
InviteListItemlocal interface is still alive — consider whether it should outlive this PRThe interface exists because the generated type marks
shareableUrlas optional while the backend always populates it. Now thatInviteListItemDTO.javahas the correct@Schema(requiredMode = REQUIRED)annotation, this local re-declaration should be temporary. Afternpm run generate:apiis run and the generated type is updated, the local interface and all theas unknown as InviteListItem[]casts should collapse. If that regeneration is intentionally deferred, it would be useful to note this in the PR description (it's not currently mentioned).The
VALID_STATUSESguard pattern is a good precedentValidating an enum-like query param server-side before forwarding it to the typed client avoids a class of runtime errors where the generated type's union doesn't match what the backend actually validates. This pattern is worth extracting to a shared utility (
parseEnumParam<T>) if it appears in more load functions.No cross-domain boundary violations introduced
The changes stay strictly within the frontend module — no backend service calls repositories they shouldn't, no new cross-domain coupling introduced. The backend change to
InviteListItemDTO.javais a single-line annotation correction, not a structural change.🔒 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
This PR removes a category of security risk (raw
fetchcalls that could accidentally use global fetch in server actions instead ofevent.fetch) and centralises all server-side HTTP calls through thecreateApiClientabstraction. From a security standpoint, this is the right direction.Blockers
None.
Observations
event.fetchenforcement is the core security fix hereThe PR description correctly identifies that
event.fetchis required sohandleFetchcan inject the session cookie on all outgoing backend calls. Using globalfetchorenv.API_INTERNAL_URL+ raw fetch in a server action bypasses that hook entirely, meaning the request arrives at the backend unauthenticated. The consolidation tocreateApiClient(fetch)(wherefetchis the event's fetch) closes that gap correctly in both the load and action paths.No new input validation introduced, but also none removed
The
VALID_STATUSESwhitelist in invites is new and welcome — it prevents an attacker from injecting an arbitrary query parameter value into the backend call. However, theidparameter in therevokeaction (formData.get('id') as string) is passed directly into the path template{ params: { path: { id } } }without validation.openapi-fetchwill URL-encode the value in the path, so injection into the HTTP path itself is unlikely, but an empty or malformedidwill produce a 404 or 400 from the backend rather than a client-side guard. This is consistent with the rest of the codebase and not a blocker, but worth noting.No secrets exposed
The removal of the
envimport fromadmin/invites/+page.server.tsis net positive —API_INTERNAL_URLis no longer read directly in the actions, reducing the number of places where that env var is accessed. The value is still centralised inapi.server.ts.CONTRIBUTING.md clarification is a genuine security improvement
Documenting that multipart uploads must use
event.fetch(never globalfetch) converts an implicit, easy-to-miss convention into an explicit, searchable rule. Future contributors will not accidentally regress this.🧪 Sara Holt — QA Engineer & Test Strategist
Verdict: ⚠️ Approved with concerns
The test additions are thorough and the fix for 86 pre-existing test failures is excellent work. The root cause (Sentry wrapping requiring
requestandurlon event mocks) is correctly identified and uniformly applied across 14 spec files. The newpage.server.spec.tsfor the users update action follows the AAA pattern and has good coverage of the error branches. A few concerns worth addressing.Blockers
None — no new test gaps introduced. The concerns below are about completeness.
Suggestions
The
createaction — no test for theexpiresAtfieldThe new tests for
createcheckgroupIds(both populated and empty) and the success/error/fallback paths. There is no test verifying thatexpiresAtis forwarded correctly to the backend when provided, nor that it is absent from the request body when not set. SinceexpiresAtis an optional date field, the serialisation edge cases (empty string vsundefinedvs omitted) are worth a dedicated test case.The revoke action — no test for missing/empty
idformData.get('id') as stringwill benullif the field is absent. Theas stringcast hides this —nullwill be passed as"null"in the URL path, which produces an unintended backend call. A test asserting the guard behaviour (or lack thereof) would document the current contract explicitly.The
deleteaction inusers/[id]/page.server.spec.tsevent mock is missingrequestThe
makeEvent()for the delete action tests does not includerequestin the event shape. This is currently fine because the delete action doesn't read fromrequest.formData(), but it's inconsistent with the other event factories in the file and would silently break if the action is later modified to read form data.mockResponsehelper —nullbody handlingIn the invites spec,
mockResponse(false, null, 500)is used to simulate a response with no JSON body. It's not immediately clear howopenapi-fetchhandles anullbody when it tries to parse the error — a comment explaining why this correctly exercises the fallback path would help future maintainers.Positive: the
as anyeslint-disable pattern is consistently scopedEach
as anycast has an inline// eslint-disable-next-linecomment directly above the problematic line, not a block disable. This is the right scope and makes it easy to clean up later when SvelteKit tightens the event types.The CI fix (
; → &&) should have a regression testThe semicolon bug meant the coverage run for the client side always ran regardless of server test results. There's no way to prevent this class of bug from recurring without a CI-level check (e.g. a test that verifies the
test:coveragescript exits non-zero when server tests fail). Not blocking, but worth a follow-up issue.🎨 Leonie Voss — UI/UX Design Lead
Verdict: ✅ Approved
This PR is purely a server-side refactor and a test infrastructure fix. No Svelte components, templates, CSS, or user-facing strings were modified. I reviewed the diff for any accidental impact on rendered output and found none.
What I Checked
.sveltefiles changed — all changes are in+page.server.ts,*.server.spec.ts,CONTRIBUTING.md, andInviteListItemDTO.java. Zero impact on rendered HTML, component structure, or styling.FORBIDDEN,NOT_FOUND,INTERNAL_ERROR) already exist in the message files. No new user-visible strings were introduced.fail(status, { createError: code })andfail(status, { revokeError: code })response shapes are identical to the old implementation. Any existing UI that reads these form action return values will continue to work without modification.shareableUrlis now@Schema(requiredMode = REQUIRED)— this tightens the generated TypeScript type. If any Svelte component was guarding againstshareableUrlbeingundefined, that guard becomes unnecessary afternpm run generate:api. No components were broken by this, but it is worth noting as a follow-up cleanup opportunity.LGTM from a UI/UX perspective.
⚙️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
No changes to infrastructure, Docker Compose, CI pipelines, or deployment configuration. I reviewed the diff for any operational impact.
What I Checked
docker-compose*.ymlchanges — the stack topology is unchanged.test:coveragefix inpackage.json(; → &&) has operational significance: it means CI will now correctly report a failed build when server-side tests fail, rather than silently proceeding to the client coverage run. This is the correct behaviour and I would have flagged the original as a defect.API_INTERNAL_URLenv var is used less directly — theenvimport is removed fromadmin/invites/+page.server.tsentirely. The env var is still read inapi.server.tswhich is the centralised place for it. This is a net improvement — fewer files that need this secret injected in test environments.package.jsonchanges are limited to thetest:coveragescript.openapi-fetchwas already a dependency.The
test:coveragesemicolon fix is the most operationally relevant change in this PR. Silently passing CI despite 86 failing tests is a reliability risk that was being masked. Good catch.📋 Elicit — Requirements Engineer
Verdict: ✅ Approved
Reviewing against the stated goal of issue #465: migrate remaining admin server actions to
createApiClient, eliminate directfetch+env.API_INTERNAL_URLusage, and fix the test infrastructure that was masking failures. All stated goals are met.Requirements Traceability
Goal: Replace all manual
fetch(${apiUrl}/api/...)calls inadmin/invites/+page.server.tsDelivered. The
load,create, andrevokeactions all usecreateApiClient(fetch). Theenvimport is removed entirely. ✅Goal: Replace inline
fetch('/api/users/...')inadmin/users/[id]/+page.server.tsupdateactionDelivered. The try/catch JSON parsing is replaced with
api.PUT(...)and the project-standard error extraction pattern. ✅Goal: Add missing tests for create/revoke invites and users update action
Delivered. 5 new test cases for the invites actions and a full
page.server.spec.tsfor the users page (covering load, update, and delete). ✅Goal: Fix 86 pre-existing test failures caused by Sentry wrapping
Delivered. 14 spec files updated to include
requestandurlin event mocks. The PR test plan reports 576/576 passing. ✅Goal: Fix
test:coveragesemicolonDelivered. The
; → &&change inpackage.jsonensures server test failures abort the coverage run. ✅Goal: Clarify
event.fetchrequirement in CONTRIBUTING.mdDelivered. The documentation now explicitly states
event.fetchmust be used (never globalfetch) for multipart uploads. ✅Open Items / Observations
event.fetchwas already used in these files for session cookie injection, and this PR completes the cleanup. No requirement gap.VALID_STATUSESwhitelist is a small scope expansion beyond the issue but is clearly beneficial. No concern.page.server.test.ts→page.server.spec.tsis a naming convention normalisation. Minor scope but desirable.👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ✅ Approved
This is exactly the kind of housekeeping PR I love to see. The intent is clear, the scope is contained, and it fixes a hidden CI defect along the way. A few things to note:
Blockers
None.
Suggestions
1. Two lingering
// TODO: remove cast after next npm run generate:apicomments (admin/invites/+page.server.ts, both theloadreturn and thecreateaction return)These TODO comments explain a transient state — the cast should go away once
generate:apiis run and re-committed. The PR description says this was intentional, but the TODO comment pattern leaves the door open for it to rot. The cleaner approach would be: runnpm run generate:apiin this same PR (since@Schema(requiredMode=REQUIRED)was added toInviteListItemDTO.shareableUrlinbackend/src/main/java/org/raddatz/familienarchiv/user/InviteListItemDTO.java) and eliminate the cast immediately rather than deferring it.2.
// eslint-disable-next-line @typescript-eslint/no-explicit-anyappears 10+ times across the new spec filesThe
as anycasts on partial event objects in test helpers are understandable (SvelteKit's event types are wide), but I'd prefer extracting a typed helper once and sharing it:That keeps the suppression in one place instead of scattering it across test files.
3.
page.server.spec.tsnaming (new) vspage.server.test.ts(deleted)The deleted test file was
page.server.test.ts; the replacement ispage.server.spec.ts. The rest of the codebase uses.spec.tsconsistently — good that you aligned it, but the PR description doesn't mention this rename explicitly, so it could confuse a bisect.4.
admin/invitesload tests mockfetchdirectly rather thancreateApiClientThe other spec files (e.g.
admin/users/[id]/page.server.spec.ts) mockcreateApiClientat the module boundary, which is the cleaner pattern. The invites load tests mock the rawfetchand rely on the internal implementation ofcreateApiClientcallingfetch(Request, {}). If the internal shape ofcreateApiClientever changes (e.g. using a different adapter), the invites tests would break while the users tests would not. The URL assertionexpect.stringContaining('/api/invites')is also slightly brittle compared to asserting on the typed path string via the mock.Overall: clean, consistent, well-tested refactor. The fixes to the
test:coveragesemicolon and the Sentry mock event shape are valuable bonus improvements.🏛️ Markus Keller — Senior Application Architect
Verdict: ✅ Approved
The PR is a pure refactor within existing module boundaries — no new layers, no new abstractions, no new infrastructure. Good scope discipline.
Blockers
None.
Suggestions
1. No documentation updates required (verified)
Checking against the doc-trigger table:
seq-auth-flow.pumlupdate required.CONTRIBUTING.mdupdate to the multipart/fetch guidance is the right artifact to touch and was correctly updated.All doc triggers clean.
2. The
VALID_STATUSESwhitelist inadmin/invites/+page.server.tsis a good patternThis is the correct place for input sanitization — the server boundary. Worth calling out as a positive: this was silently accepting arbitrary user input as a URL query parameter in the old code and interpolating it directly into the API URL via
encodeURIComponent. The typed client approach forces this validation to be explicit.3. The
as unknown as InviteListItemdouble cast is a smell, not a blockerTwo
// TODO: remove castcomments signal that the TypeScript types are temporarily out of sync with the backend. This is acceptable for a short window, but the TODO should have a linked issue or be resolved in this same PR by runninggenerate:api. The@Schema(requiredMode=REQUIRED)annotation was added toshareableUrlin this very PR — running codegen would close the loop cleanly.4.
admin/users/[id]updateaction: body typed ascomponents['schemas']['AdminUpdateUserRequest']The cast comment explains why null fields are sent (
"the backend treats null as 'clear this field'"). This is an intentional design decision but it means the TypeScript schema type and the actual runtime shape diverge. This is a schema design issue (the OpenAPI spec should model nullable fields asnullable: true) rather than a code problem, but worth tracking.🔒 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
This PR is a net security improvement. Replacing raw
fetch(${apiUrl}/api/...)with the typedcreateApiClient(fetch)pattern eliminates three distinct security concerns at once.Positive Findings
1. Input validation added to
revokeaction (new guard)The old code did
const id = formData.get('id') as stringand passed it directly into the URL:fetch(\${apiUrl}/api/invites/${encodeURIComponent(id)}`). This meant a missingidwould result in a request to/api/invites/null(the string "null") or/api/invites/` rather than a clean 400. The null guard here is correct.2. URL injection risk eliminated
The old pattern
fetch(\${apiUrl}/api/invites?status=${encodeURIComponent(status)}`)relied on correctencodeURIComponent` usage. The new typed client constructs URLs from typed path/query parameters — no interpolation, no injection surface.3.
VALID_STATUSESwhitelist for query parameterThe old code passed the
statusquery param directly into a URL after encoding. The new code validates it against a hardcoded enum before passing it to the typed client. This is defense in depth — even if the typed client later accepted arbitrary strings, the whitelist would block abuse.Concerns (non-blockers)
4. Error code pass-through in
createandrevokeactionscodehere comes from the raw backend error body. If the backend returns an error code that has no corresponding i18n mapping on the frontend,getErrorMessage()falls back to a generic message — good. But the raw backend errorcodestring is being stored in SvelteKit's form data which gets serialized and sent back to the client. This is not a leak of sensitive internals (it's an enum value like'FORBIDDEN'), but verify that noErrorCodein the backend enum contains sensitive information (SQL state, table names, etc.). Based on my knowledge of this codebase'sErrorCodeenum, this is clean.5.
as unknown as InviteListItemcastThe double cast bypasses TypeScript's type safety. If the backend sends a response that doesn't match
InviteListItem(e.g.shareableUrlis actually null), downstream code usinginvite.shareableUrlas non-null could fail at runtime. This is a temporary state awaitinggenerate:api, but during this window there is a narrow risk of a null-pointer-style failure in the invite list rendering. Not a security issue, but worth resolving promptly.6. Session cookie forwarding (confirmed correct)
The PR description notes that
event.fetchwas already being used everywhere — this PR is completing the typed client migration, not changing the fetch implementation. ThehandleFetchhook ensures session cookies are forwarded on internal requests. TheCONTRIBUTING.mdupdate correctly documents that multipart uploads must useevent.fetchdirectly (never globalfetch) for this reason. This is the right constraint to document.🧪 Sara Holt — Senior QA Engineer
Verdict: ✅ Approved
The test story in this PR is the main event. 86 previously-hidden test failures surfaced and fixed, a broken
test:coveragecommand repaired, and new coverage added for previously-untested actions. That's a genuine quality uplift.Positive Findings
1. The semicolon →
&&fix intest:coverageis a blocker-fix in disguiseWith
;a failing server run silently allowed the client run to succeed and report green. CI was lying. This single character change restores the meaning of the coverage gate. High impact, low risk.2. Root cause of 86 failures correctly identified and fixed across 14 files
The
@sentry/sveltekitwrapper readsevent.request.methodandevent.url.pathnameat instrumentation time. Test events that only provided{ fetch, url }or{ fetch, locals }caused Sentry's wrapper to throw, silently swallowing the real test failure. Addingrequest: new Request(...)andurl: new URL(...)to every test event is the correct minimal fix — it satisfies the wrapper without changing the test's intent.3. New
page.server.spec.tsforadmin/invites(284 lines)Good coverage across all three actions (load, create, revoke):
4. New
admin/users/[id]/page.server.spec.ts(199 lines)Covers load (permission guard, 404, success), update (success, backend error, no-code fallback, password mismatch guard), and delete (redirect, failure). This is the first test coverage for the users admin update action.
Concerns
5. Invites load tests mock raw
fetchwhile users tests mockcreateApiClientTwo different mocking strategies in the same PR is a consistency gap. The invites load tests assert URL strings via
expect.stringContaining('/api/invites'), while the users tests assert the typed path string'/api/users/{id}'. The typed-path assertion is strictly better — it catches typos in the path template. Consider aligning the invites load tests to mock at the module boundary:This is a suggestion, not a blocker — both approaches work.
6. No test for the
statusvalidation in the invitesloadfunctionThe new
VALID_STATUSESwhitelist defaults invalid values to'ACTIVE'. There is no test asserting this behavior:Suggestion: add this test. The validation is a security-relevant input guard and deserves explicit coverage.
7.
test:coveragepasses with 576/576 — verify this is stableThe PR description reports 576/576. If any of the 86 previously-failing tests were also flaky, they could become intermittent failures. Worth a second CI run to confirm stability before merge.
🚀 Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
This PR touches no infrastructure files — no Compose changes, no CI workflow changes, no Dockerfile changes. My review is focused on the one CI-relevant change: the
test:coveragescript fix.Positive Findings
1. The
; → &&fix inpackage.jsonis a genuine CI correctness fixWith the old
;separator, a non-zero exit code from the server coverage run was silently discarded. The shell executed the client run regardless and that run's exit code became the job's exit code. If the CI pipeline usesnpm run test:coverageas a quality gate, this was a phantom green gate. The&&ensures the coverage gate actually gates on both halves.2. No changes to CI workflow files
The fix works because the change is in
package.json— the CI script callsnpm run test:coverageand picks up the fix automatically. No pipeline YAML changes needed.Concerns
3. Verify CI actually calls
test:coveragerather thantestThe
testscript runsnpm run test:unit -- --runwhich doesn't apply&&vs;distinction. If the CI pipeline usesnpm run test(notnpm run test:coverage), the fix has no effect on CI. Worth confirming which script CI actually calls — if it'stest:unitortest, the coverage gate may not be active in CI at all and this fix only helps local developers runningnpm run test:coveragemanually.This is a follow-up observation, not a blocker for this PR.
📋 Elicit — Requirements Engineer
Verdict: ✅ Approved
Reviewing from a requirements completeness and intent-traceability perspective.
Positive Findings
1. Issue #465 scope is well-contained and fully delivered
The PR description documents the original requirement (migrate
fetch()calls tocreateApiClient) and is explicit that the auth bypass concern (the original motivation for issue #465) was already resolved by #523/#524. This is good requirements hygiene — it prevents the scope from expanding to re-implement something already done, and it provides a clear audit trail.2. The
CONTRIBUTING.mdupdate is the right artifactThe clarification that multipart uploads must use
event.fetchdirectly (never globalfetch) is a developer-facing requirements artifact. It prevents future regressions by encoding the constraint at the documentation layer. This is exactly the kind of NFR (the session cookie forwarding contract) that needs to be written down, not just understood by whoever implemented it.3. Acceptance criteria from the PR description are verifiable
grep -rE "fetch\(\?${.*API|fetch(env." frontend/src/routes` — no matches outside login/ ✅npm run check— clean on touched files ✅npm run lint— clean ✅These are measurable, binary criteria.
Concerns
4. The
statusparameter validation introduces an implicit requirement changeOld behavior:
url.searchParams.get('status') ?? 'active'— defaulted to lowercase'active'.New behavior: validates against
['ACTIVE', 'REVOKED', 'EXPIRED'], defaults to'ACTIVE'(uppercase).If any existing bookmarked URL uses
?status=active(lowercase), the new code will fall through to the'ACTIVE'default, which has the same effect — but the behavior is subtly different. Worth confirming with the issue author that the backend accepts uppercase status values and that the old lowercase'active'was either already uppercased by the backend or was wrong from the start.5. The
page.server.test.ts → page.server.spec.tsrename is undocumentedThe deleted file is
page.server.test.tsand the replacement ispage.server.spec.ts. This is a naming convention alignment (the project uses.spec.ts), but it should be mentioned in the PR description to avoid confusion when reading git history. Minor.🎨 Leonie Voss — UI/UX Design & Accessibility Lead
Verdict: ✅ Approved
This PR is entirely backend (server-side SvelteKit
+page.server.tsfiles) and test infrastructure. There are no Svelte component changes, no template markup changes, no CSS changes, and no i18n changes. Nothing in this PR affects what any user sees, the touch targets they interact with, the color contrast they rely on, or the accessibility tree that screen readers traverse.The one UI-adjacent change is in
CONTRIBUTING.md— a developer documentation update. It correctly clarifies thatevent.fetchmust be used for multipart uploads to preserve session cookie forwarding. This does not affect the rendered UI.One observation worth noting for future PRs:
The
revokeaction now returnsgetErrorMessage('VALIDATION_ERROR')as therevokeErrorstring when the id is missing. Verify thatVALIDATION_ERRORhas an i18n key in all three locales (de,en,es) — the UI displaysrevokeErrordirectly. If the key is missing, users would see a raw error code string instead of a localized message. This is not introduced by this PR (the pattern is established project-wide), but it's worth a quick grep check as a follow-up.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.