fix: protect persisted dashboard data
This commit is contained in:
1 parent
e8ca3d3ad4
commit
3633b1f883
4 files changed
+16
-2
No files matched your search
@@ -42,6 +42,9 @@ describe("dashboard query persistence", () => {
|
||||
const supporting = createSuccessfulQuery([["app", "byIds"], { type: "query" }], [{ id: "app-1" }]);
|
||||
const ping = createSuccessfulQuery([["widget", "app", "ping"], { type: "query" }], { status: "online" });
|
||||
const beszel = createSuccessfulQuery([["widget", "beszel", "getSystemStats"], { type: "query" }], []);
|
||||
const customApi = createSuccessfulQuery([["widget", "customApi", "getData"], { type: "query" }], {
|
||||
apiKey: "secret",
|
||||
});
|
||||
const unrelated = createSuccessfulQuery([["board", "getBoardByName"], { type: "query" }], { name: "Home" });
|
||||
const pendingClient = new QueryClient();
|
||||
const pending = pendingClient.getQueryCache().build(pendingClient, {
|
||||
@@ -55,6 +58,7 @@ describe("dashboard query persistence", () => {
|
||||
expect(shouldPersistDashboardQuery(supporting.query)).toBe(true);
|
||||
expect(shouldPersistDashboardQuery(ping.query)).toBe(false);
|
||||
expect(shouldPersistDashboardQuery(beszel.query)).toBe(false);
|
||||
expect(shouldPersistDashboardQuery(customApi.query)).toBe(false);
|
||||
expect(shouldPersistDashboardQuery(unrelated.query)).toBe(false);
|
||||
expect(shouldPersistDashboardQuery(pending)).toBe(false);
|
||||
expect(shouldPersistDashboardQuery(failed.query)).toBe(true);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { removeOldestQuery } from "@tanstack/react-query-persist-client";
|
||||
import type { PersistQueryClientProviderProps } from "@tanstack/react-query-persist-client";
|
||||
import { parse, stringify } from "superjson";
|
||||
|
||||
import { isWidgetDataQueryKey, queryCacheDefaultGcTimeMs } from "@homarr/api/query-cache";
|
||||
import { isPersistableDashboardQueryKey, queryCacheDefaultGcTimeMs } from "@homarr/api/query-cache";
|
||||
|
||||
export const queryPersistenceBuster = "v5-dashboard-data";
|
||||
|
||||
@@ -19,7 +19,7 @@ interface PersistableQuery {
|
||||
}
|
||||
|
||||
export const shouldPersistDashboardQuery = (query: PersistableQuery) =>
|
||||
query.state.data !== undefined && isWidgetDataQueryKey(query.queryKey);
|
||||
query.state.data !== undefined && isPersistableDashboardQueryKey(query.queryKey);
|
||||
|
||||
const getSessionStorage = () => {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
@@ -114,6 +114,7 @@ const ScopedTRPCReactProvider = ({
|
||||
queryCache: new QueryCache({
|
||||
onError(error, query) {
|
||||
if (!isTrpcForbiddenError(error) || !isWidgetDataQueryKey(query.queryKey)) return;
|
||||
void queryPersistence.persister.removeClient();
|
||||
query.setState({ data: undefined, dataUpdatedAt: 0, isInvalidated: true });
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -19,6 +19,7 @@ export const queryCacheDefaultGcTimeMs = 1000 * 60 * 5;
|
||||
export const queryCacheMetadataStaleTimeMs = 1000 * 60 * 5;
|
||||
|
||||
const widgetDataQueryPaths = new Set(["app.byId", "app.byIds", "docker.getContainers", "integration.byIds"]);
|
||||
const persistenceExcludedWidgetPaths = new Set(["widget.customApi.getData", "widget.customApi.queryRequest"]);
|
||||
// Beszel system stats are large time-series payloads and were deliberately kept out of
|
||||
// the persisted cache in #6289; live stats come from an SSE subscription instead.
|
||||
const excludedWidgetPaths = new Set(["widget.app.ping", "widget.beszel.getSystemStats"]);
|
||||
@@ -77,3 +78,11 @@ export const isWidgetDataQueryKey = (queryKey: QueryKey) => {
|
||||
|
||||
return isWidgetDataTrpcPath(path.join("."));
|
||||
};
|
||||
|
||||
export const isPersistableDashboardQueryKey = (queryKey: QueryKey) => {
|
||||
const path = getTrpcPathFromQueryKey(queryKey);
|
||||
if (!path) return false;
|
||||
|
||||
const joinedPath = path.join(".");
|
||||
return isWidgetDataTrpcPath(joinedPath) && !persistenceExcludedWidgetPaths.has(joinedPath);
|
||||
};
|
||||
Reference in new issue
Block a user