fix(web): code review round 2 -- critical session/connection races
- RequireAuth: a page reload lost the session even with a valid token sitting in sessionStorage -- myUserId is never persisted (by design) but nothing ever re-derived it from the reusable login token. Added connection.ts's restoreSession(), which also fixes the race the first attempt at this ran into live: React 19 StrictMode double-invokes the restore effect in dev, and Tinode.loginToken() isn't idempotent (second call on an already-authenticated connection gets 409'd), so both the connect() and the login itself needed module-level in-flight dedup, not just a per-effect cancelled flag - topics.ts: same dedup pattern for openConversation/closeConversation -- StrictMode's mount->cleanup->remount always overlapped an open with itself, and a leave() landing before its own subscribe() resolved was silently no-op'ing instead of detaching - LOGGING_ENABLED was unconditionally on, including prod builds -- Tinode's verbose logging prints the raw login secret and full message content to the console - imPost/imPatch/imPut/imDelete could silently drop the Idempotency-Key header for a caller that passes its own headers (object-spread order bug, dormant today since no call site does this yet) - DetailColumn's getFriends()/getBlocks() only ever read page 1 despite fetchAllPages existing specifically to prevent this bug class - availableActions() now defers to the server's message_policy. supported_actions when present instead of only a client-side time window, and exposes recallRemainingSeconds for the context menu's countdown label Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
1bd8d7f804
commit
d3a0eb0dbc
@@ -1,13 +1,54 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
|
||||
import { restoreSession } from '@/lib/tinode/connection';
|
||||
import { subscribeMeTopic } from '@/lib/tinode/topics';
|
||||
import { useSessionStore } from '@/stores/session';
|
||||
|
||||
/** Gate for everything inside AppShell: needs a resolved tenant and an authenticated user. */
|
||||
/**
|
||||
* Gate for everything inside AppShell: needs a resolved tenant and an authenticated user.
|
||||
*
|
||||
* `tenant` survives a same-tab reload (persisted, see stores/session.ts) but `myUserId` doesn't --
|
||||
* it used to always come back `null` on reload, bouncing straight to /login even though every
|
||||
* login/register already writes a perfectly good reusable token to `sessionStorage['im-session-
|
||||
* token']` for exactly this case. This re-derives `myUserId` from that token via connection.ts's
|
||||
* restoreSession() (deduped against React 19 StrictMode's double-invoke in dev -- see its own
|
||||
* comment) before giving up and redirecting, instead of ignoring the token entirely.
|
||||
*/
|
||||
export function RequireAuth() {
|
||||
const tenant = useSessionStore((s) => s.tenant);
|
||||
const myUserId = useSessionStore((s) => s.myUserId);
|
||||
const setMyUserId = useSessionStore((s) => s.setMyUserId);
|
||||
const [hasToken] = useState(() => !!sessionStorage.getItem('im-session-token'));
|
||||
const [restoreFailed, setRestoreFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (myUserId || !tenant || !hasToken) return undefined;
|
||||
const token = sessionStorage.getItem('im-session-token');
|
||||
if (!token) return undefined;
|
||||
let cancelled = false;
|
||||
restoreSession(token)
|
||||
.then((uid) => {
|
||||
if (cancelled) return;
|
||||
setMyUserId(uid);
|
||||
subscribeMeTopic().catch(() => {});
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
sessionStorage.removeItem('im-session-token');
|
||||
setRestoreFailed(true);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [tenant, myUserId, hasToken, setMyUserId]);
|
||||
|
||||
if (!tenant) return <Navigate to="/tenant" replace />;
|
||||
if (!myUserId) return <Navigate to="/login" replace />;
|
||||
return <Outlet />;
|
||||
if (myUserId) return <Outlet />;
|
||||
if (hasToken && !restoreFailed) {
|
||||
return <div className="grid h-dvh place-items-center bg-surface-page text-sm text-text-tertiary-3">正在恢复登录状态…</div>;
|
||||
}
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user