fix(review): share logout/expiry cleanup, never save a PIN credential without its hash

This commit is contained in:
Gauvain
2026-07-15 21:14:13 +02:00
parent 34d9b43dcb
commit 25ee7dad85

View File

@@ -186,20 +186,30 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
// session on the first 401 so the app drops cleanly to the login screen. // session on the first 401 so the app drops cleanly to the login screen.
const sessionExpiredRef = useRef(false); const sessionExpiredRef = useRef(false);
// Shared teardown for manual logout AND forced session expiry — keeping it
// in one place prevents the two paths from drifting (a 401 expiry must wipe
// plugin settings / Jellyseerr state too, or the next login on the same
// device inherits the previous user's data).
// Saved credentials are kept so the user can quick-login again.
const clearSessionState = useCallback(async () => {
storage.remove("token");
storage.remove("user");
clearTVDiscoverySafely();
setUser(null);
setApi(null);
setPluginSettings(undefined);
await clearAllJellyseerData();
queryClient.clear();
storage.remove("REACT_QUERY_OFFLINE_CACHE");
}, [setUser, setApi, setPluginSettings, clearAllJellyseerData, queryClient]);
const handleSessionExpired = useCallback(() => { const handleSessionExpired = useCallback(() => {
if (sessionExpiredRef.current) return; // run once per session if (sessionExpiredRef.current) return; // run once per session
sessionExpiredRef.current = true; sessionExpiredRef.current = true;
storage.remove("token"); clearSessionState().catch((e) =>
storage.remove("user"); writeErrorLog(`Session-expiry cleanup failed: ${e?.message ?? e}`),
setUser(null); );
setApi(null); }, [clearSessionState]);
queryClient.clear();
storage.remove("REACT_QUERY_OFFLINE_CACHE");
// Same TV-discovery cleanup as a manual logout, so a forced expiry
// doesn't leave stale discovery state behind.
clearTVDiscoverySafely();
// Saved credentials are kept so the user can quick-login again.
}, [setUser, setApi, queryClient]);
useEffect(() => { useEffect(() => {
// Only guard an authenticated session. A pre-auth api (login screen) keeps // Only guard an authenticated session. A pre-auth api (login screen) keeps
@@ -375,7 +385,10 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
if (!api?.basePath || !user?.Id || !user.Name || !token) return; if (!api?.basePath || !user?.Id || !user.Name || !token) return;
const securityType = options?.securityType || "none"; const securityType = options?.securityType || "none";
let pinHash: string | undefined; let pinHash: string | undefined;
if (securityType === "pin" && options?.pinCode) { if (securityType === "pin") {
// Never persist a "pin" credential without its hash — it would be
// impossible to unlock.
if (!options?.pinCode) throw new Error("PIN code is required");
pinHash = await hashPIN(options.pinCode); pinHash = await hashPIN(options.pinCode);
} }
await saveAccountCredential({ await saveAccountCredential({
@@ -424,18 +437,24 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
if (securityType === "pin" && options.pinCode) { if (securityType === "pin" && options.pinCode) {
pinHash = await hashPIN(options.pinCode); pinHash = await hashPIN(options.pinCode);
} }
if (securityType === "pin" && !pinHash) {
await saveAccountCredential({ // Never persist a "pin" credential without its hash — it would be
serverUrl: api.basePath, // impossible to unlock. Skip the save rather than failing a login
serverName: serverName || "", // that already succeeded.
token: auth.data.AccessToken, writeErrorLog("Account save skipped: PIN required but missing");
userId: auth.data.User.Id || "", } else {
username, await saveAccountCredential({
savedAt: Date.now(), serverUrl: api.basePath,
securityType, serverName: serverName || "",
pinHash, token: auth.data.AccessToken,
primaryImageTag: auth.data.User.PrimaryImageTag ?? undefined, userId: auth.data.User.Id || "",
}); username,
savedAt: Date.now(),
securityType,
pinHash,
primaryImageTag: auth.data.User.PrimaryImageTag ?? undefined,
});
}
} }
const recentPluginSettings = await refreshStreamyfinPluginSettings(); const recentPluginSettings = await refreshStreamyfinPluginSettings();
@@ -495,19 +514,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
writeErrorLog("Failed to delete expo push token for device"), writeErrorLog("Failed to delete expo push token for device"),
); );
storage.remove("token"); await clearSessionState();
storage.remove("user");
clearTVDiscoverySafely();
setUser(null);
setApi(null);
setPluginSettings(undefined);
await clearAllJellyseerData();
// Clear React Query cache to prevent data from previous account lingering
queryClient.clear();
storage.remove("REACT_QUERY_OFFLINE_CACHE");
// Note: We keep saved credentials for quick switching back
}, },
onError: (error) => { onError: (error) => {
console.error("Logout failed:", error); console.error("Logout failed:", error);