Bug — gelöschte Liste bei direktem URL-Aufruf zeigt Login-Bildschirm statt Weiterleitung #151

Closed
opened 2026-09-01 14:13:09 +02:00 by lena · 3 comments
lena commented 2026-09-01 14:13:09 +02:00 (Migrated from git.butzei.de)

Story: Bug — gelöschte Liste bei direktem URL-Aufruf zeigt Login-Bildschirm statt Weiterleitung

As a Nutzer, der eine Liste per direktem Link/URL geöffnet hat,
I want to bei einer inzwischen gelöschten Liste automatisch auf eine andere verfügbare Liste
oder die Gesamtübersicht weitergeleitet zu werden,
so that ich nicht fälschlich den Eindruck bekomme, ausgeloggt zu sein.

Background

Gemeldet vom Nutzer: Ist man z. B. unter https://todo.moekies.de/list/6 und diese Liste wird
gelöscht (z. B. von einem anderen Gerät, oder weil man selbst löscht während man auf ihr ist),
springt die App nicht automatisch auf eine andere Liste/die Gesamtübersicht. Stattdessen erscheint
der Login-Bildschirm, weil die Liste unter der URL nicht mehr existiert — obwohl man weiterhin
eingeloggt ist. Die URL /list/6 bleibt dabei unverändert im Browser stehen.

Acceptance criteria:

  • Ruft die App eine List-ID auf, die nicht (mehr) existiert oder für die der Nutzer keinen
    Zugriff mehr hat, wird zur Gesamtübersicht bzw. zur nächsten verfügbaren Liste weitergeleitet
    — nicht zum Login-Bildschirm, solange eine gültige Session besteht.
  • Der Login-Bildschirm erscheint ausschließlich, wenn tatsächlich keine gültige Session (mehr)
    vorhanden ist.
  • Regressionstest: aktive Session, Liste wird während der Ansicht gelöscht (oder ist beim Aufruf
    per direkter URL bereits gelöscht) → Nutzer landet auf der Übersicht, bleibt eingeloggt.

Out of scope for this story:

  • Allgemeines Session-Timeout-Verhalten (das ist bereits korrekt, betrifft nur den Sonderfall "Liste
    existiert nicht mehr").
# Story: Bug — gelöschte Liste bei direktem URL-Aufruf zeigt Login-Bildschirm statt Weiterleitung **As a** Nutzer, der eine Liste per direktem Link/URL geöffnet hat, **I want to** bei einer inzwischen gelöschten Liste automatisch auf eine andere verfügbare Liste oder die Gesamtübersicht weitergeleitet zu werden, **so that** ich nicht fälschlich den Eindruck bekomme, ausgeloggt zu sein. ## Background Gemeldet vom Nutzer: Ist man z. B. unter `https://todo.moekies.de/list/6` und diese Liste wird gelöscht (z. B. von einem anderen Gerät, oder weil man selbst löscht während man auf ihr ist), springt die App nicht automatisch auf eine andere Liste/die Gesamtübersicht. Stattdessen erscheint der Login-Bildschirm, weil die Liste unter der URL nicht mehr existiert — obwohl man weiterhin eingeloggt ist. Die URL `/list/6` bleibt dabei unverändert im Browser stehen. **Acceptance criteria:** - [ ] Ruft die App eine List-ID auf, die nicht (mehr) existiert oder für die der Nutzer keinen Zugriff mehr hat, wird zur Gesamtübersicht bzw. zur nächsten verfügbaren Liste weitergeleitet — nicht zum Login-Bildschirm, solange eine gültige Session besteht. - [ ] Der Login-Bildschirm erscheint ausschließlich, wenn tatsächlich keine gültige Session (mehr) vorhanden ist. - [ ] Regressionstest: aktive Session, Liste wird während der Ansicht gelöscht (oder ist beim Aufruf per direkter URL bereits gelöscht) → Nutzer landet auf der Übersicht, bleibt eingeloggt. **Out of scope for this story:** - Allgemeines Session-Timeout-Verhalten (das ist bereits korrekt, betrifft nur den Sonderfall "Liste existiert nicht mehr").
lena commented 2026-09-01 22:22:45 +02:00 (Migrated from git.butzei.de)

Claiming this - self-contained bug fix, no open questions blocking it. Plan: trace the routing logic that resolves /list/:id (likely Application.tsx/the list-selection effect) to find where a 404/403 on the selected list currently falls through to the login screen instead of redirecting to another list or the overview. Fix should distinguish "no valid session" (real login-required case) from "session valid but this list is gone" (redirect case).

Claiming this - self-contained bug fix, no open questions blocking it. Plan: trace the routing logic that resolves `/list/:id` (likely `Application.tsx`/the list-selection effect) to find where a 404/403 on the selected list currently falls through to the login screen instead of redirecting to another list or the overview. Fix should distinguish "no valid session" (real login-required case) from "session valid but this list is gone" (redirect case).
lena commented 2026-09-02 08:21:24 +02:00 (Migrated from git.butzei.de)

Closing - fixed across two commits from two sessions that ended up working this issue concurrently (same Gitea identity, discovered only when pushing):

b6a6d55807 - root cause and the main fix. Every Authorize*QueryHandler.cs (15 files) threw UnauthorizedAccessException (-> 403) for both "not logged in" and "logged in but no access to this resource", so callApi couldn't distinguish them and force-logged-out on any 403. Fixed by using the already-existing AuthenticationException (-> 401) for the "not logged in" branch specifically; resource-specific branches stay 403 (unchanged IDOR-safe behavior). Added a shared useRedirectIfListNotFound hook (wired into TodoList/ShoppingListPage/PantryPage/MasterPackingListPage/PriorityMatrixPage) that redirects to the overview once a URL's list id is confirmed absent from the store, gated on new xListsLoaded flags so it never fires before the initial fetch has settled.

7e582a9 - a follow-up from the second session's own code review: the shared hook redirected the instant an id looked missing, which raced MasterPackingListCheckoutDialog.tsx's post-checkout navigate and AcceptInvitePage.tsx's post-accept navigate - both jump straight to a just-created/just-joined id before its WebSocket broadcast lands, so a user with that list-type already loaded this session got bounced back to the overview right after checking out a packing list or accepting an invite. Fixed with a 2.5s grace period on top of the loaded gate, cancelled the instant the entity resolves.

Tests: 1128 frontend tests (124 files) green, including new regression coverage for both the original bug and the creation/invite-accept race. Backend: dotnet build clean (0 errors); the 9 updated Authorize*QueryHandlerTests.cs files need Testcontainers/Docker, unavailable in this sandbox, so verified by close reading instead - flagging that as the one thing worth a human's eye if Docker is available in real CI.

A related but out-of-scope hardening item (making the frontend's error handling immune to any future list-scoped call that skips the redirect guard, by design rather than by convention) was drafted as #154 and then closed again once b6a6d55 turned out to already cover its acceptance criteria.

Closing - fixed across two commits from two sessions that ended up working this issue concurrently (same Gitea identity, discovered only when pushing): **b6a6d558071847630ede1b3d40d919402027a6c0** - root cause and the main fix. Every `Authorize*QueryHandler.cs` (15 files) threw `UnauthorizedAccessException` (-> 403) for both "not logged in" and "logged in but no access to this resource", so `callApi` couldn't distinguish them and force-logged-out on any 403. Fixed by using the already-existing `AuthenticationException` (-> 401) for the "not logged in" branch specifically; resource-specific branches stay 403 (unchanged IDOR-safe behavior). Added a shared `useRedirectIfListNotFound` hook (wired into TodoList/ShoppingListPage/PantryPage/MasterPackingListPage/PriorityMatrixPage) that redirects to the overview once a URL's list id is confirmed absent from the store, gated on new `xListsLoaded` flags so it never fires before the initial fetch has settled. **7e582a9** - a follow-up from the second session's own code review: the shared hook redirected the instant an id looked missing, which raced `MasterPackingListCheckoutDialog.tsx`'s post-checkout navigate and `AcceptInvitePage.tsx`'s post-accept navigate - both jump straight to a just-created/just-joined id before its WebSocket broadcast lands, so a user with that list-type already loaded this session got bounced back to the overview right after checking out a packing list or accepting an invite. Fixed with a 2.5s grace period on top of the loaded gate, cancelled the instant the entity resolves. **Tests:** 1128 frontend tests (124 files) green, including new regression coverage for both the original bug and the creation/invite-accept race. Backend: `dotnet build` clean (0 errors); the 9 updated `Authorize*QueryHandlerTests.cs` files need Testcontainers/Docker, unavailable in this sandbox, so verified by close reading instead - flagging that as the one thing worth a human's eye if Docker is available in real CI. A related but out-of-scope hardening item (making the frontend's error handling immune to any *future* list-scoped call that skips the redirect guard, by design rather than by convention) was drafted as #154 and then closed again once b6a6d55 turned out to already cover its acceptance criteria.
lena commented 2026-09-02 08:34:22 +02:00 (Migrated from git.butzei.de)

Completed and merged to master (commit b6a6d55). Summary:

Root cause: every Authorize*QueryHandler.cs in Checkly/Features/Authorization/ (15 files) threw UnauthorizedAccessException (-> 403) for both "not authenticated at all" and "authenticated but no access to this specific resource" - a deliberate IDOR-safe collapse for the resource case, but it meant the frontend could not tell a genuinely dead session apart from a stale/deleted list, and treated every 403 as a forced logout.

Fix: swapped the "not logged in" branch in all 15 handler files to AuthenticationException (-> 401 via ExceptionHandler.cs), which already existed and was already used correctly elsewhere (LoginUserCommandHandler.cs) but was never wired up here. The resource-specific "not authorized" branches are unchanged - still UnauthorizedAccessException/403, same IDOR-safe behavior as before. callApi (ReactUi/src/api/api.tsx) now does a plain 401 check instead of a special-cased 403 recheck.

Added useRedirectIfListNotFound (new hook), wired into TodoList/ShoppingListPage/PantryPage/MasterPackingListPage/PriorityMatrixPage, which redirects to the overview once a list id from the URL is confirmed absent from the store - gated on a new per-list-type xListsLoaded flag in store.ts rather than a guessed timeout.

An earlier version of this fix added a session recheck inside callApi's own 403 handler instead of fixing the backend; a full 8-angle /code-review (5 of 8 angles independently) found that approach was compensating one layer too high (self-referential recursion risk, thundering-herd redundant rechecks, a silent-stuck-state on a transient recheck failure) - reverted in favor of the backend fix, recorded in ai/roles/memory/04_frontend_engineer_memory.md.

Tests: 9 of the 15 handlers' existing Authorization test files updated to assert AuthenticationException for the not-logged-in case. Docker unavailable in this sandbox so Checkly.Tests (Testcontainers) could not run locally - verified for real in CI instead, now green. Common.Tests (119/119) and Checkly.WebApi.Tests (50/50) green locally. Full frontend suite (npm run build + npm run coverage, 1125 tests, 124 files) green. Full CI (backend build, all 3 backend test suites, frontend, all 4 E2E legs, Docker image build) green on b6a6d55.

Completed and merged to master (commit b6a6d55). Summary: Root cause: every Authorize*QueryHandler.cs in Checkly/Features/Authorization/ (15 files) threw UnauthorizedAccessException (-> 403) for both "not authenticated at all" and "authenticated but no access to this specific resource" - a deliberate IDOR-safe collapse for the resource case, but it meant the frontend could not tell a genuinely dead session apart from a stale/deleted list, and treated every 403 as a forced logout. Fix: swapped the "not logged in" branch in all 15 handler files to AuthenticationException (-> 401 via ExceptionHandler.cs), which already existed and was already used correctly elsewhere (LoginUserCommandHandler.cs) but was never wired up here. The resource-specific "not authorized" branches are unchanged - still UnauthorizedAccessException/403, same IDOR-safe behavior as before. callApi (ReactUi/src/api/api.tsx) now does a plain 401 check instead of a special-cased 403 recheck. Added useRedirectIfListNotFound (new hook), wired into TodoList/ShoppingListPage/PantryPage/MasterPackingListPage/PriorityMatrixPage, which redirects to the overview once a list id from the URL is confirmed absent from the store - gated on a new per-list-type `xListsLoaded` flag in store.ts rather than a guessed timeout. An earlier version of this fix added a session recheck inside callApi's own 403 handler instead of fixing the backend; a full 8-angle /code-review (5 of 8 angles independently) found that approach was compensating one layer too high (self-referential recursion risk, thundering-herd redundant rechecks, a silent-stuck-state on a transient recheck failure) - reverted in favor of the backend fix, recorded in ai/roles/memory/04_frontend_engineer_memory.md. Tests: 9 of the 15 handlers' existing Authorization test files updated to assert AuthenticationException for the not-logged-in case. Docker unavailable in this sandbox so Checkly.Tests (Testcontainers) could not run locally - verified for real in CI instead, now green. Common.Tests (119/119) and Checkly.WebApi.Tests (50/50) green locally. Full frontend suite (npm run build + npm run coverage, 1125 tests, 124 files) green. Full CI (backend build, all 3 backend test suites, frontend, all 4 E2E legs, Docker image build) green on b6a6d55.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
robert/todo#151
No description provided.