Fix auth session latch: prevent redirects during token refresh

Root cause: AuthSessionStore.isAuthenticated is a computed signal that
returns false during token refresh ('loading' status). Since all routes
use canMatch guards that read isAuthenticated, a token refresh causes
ALL routes to fail guard evaluation simultaneously, redirecting the user
to random pages.

Fix: Add wasEverAuthenticated latch that stays true once set. During
transient 'loading' states, isAuthenticated returns true if the user
was previously authenticated — the session is being refreshed, not lost.

This eliminates the "phantom redirect" bug that made every page in the
app unstable (pages would load then silently navigate away after 1-5
seconds). Verified stable on /setup/identity-access and /evidence/audit-log
with 12-second wait after navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-16 22:15:17 +02:00
parent 092779f0f4
commit 78681e71d0

View File

@@ -38,9 +38,27 @@ export class AuthSessionStore {
() => this.sessionSignal()?.tokens.expiresAtEpochMs ?? null
);
readonly isAuthenticated = computed(
() => this.sessionSignal() !== null && this.statusSignal() !== 'loading'
);
/**
* True when the user has an active session and is not in a transient
* 'loading' state. During token refresh the status briefly becomes
* 'loading', which would make this computed return false. To prevent
* route guards from redirecting away during that transient window, we
* latch a `wasEverAuthenticated` flag that stays true for the entire
* browser session once the user has logged in at least once. Guards
* reading `isAuthenticated` will see true throughout the refresh cycle.
*/
private wasEverAuthenticated = false;
readonly isAuthenticated = computed(() => {
const hasSession = this.sessionSignal() !== null;
const notLoading = this.statusSignal() !== 'loading';
const authenticated = hasSession && notLoading;
if (authenticated) {
this.wasEverAuthenticated = true;
}
// During transient 'loading' states, return true if the user was
// previously authenticated — the session is being refreshed, not lost.
return authenticated || (this.wasEverAuthenticated && this.statusSignal() === 'loading');
});
readonly tenantId = computed(
() =>