fix: resolve frontend audit findings
Frontend security / security (push) Successful in 10s
Frontend verify / verify (push) Successful in 4m57s
Conventional commits / commits (pull_request) Successful in 28s
Frontend security / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 5m10s

Repair investment recovery, draft preservation, date validation,
currency presentation, accessibility, and route error recovery.
Correct Docker and dependency checks and consume explicit income roles.
This commit is contained in:
2026-09-27 11:38:20 +03:00
parent b34c1c860a
commit f6db28defa
50 changed files with 2017 additions and 215 deletions
@@ -1,6 +1,7 @@
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { useParams, useSearchParams } from "react-router";
import { z } from "zod";
import {
type BrokerageOperation,
brokerageOperationsQueryOptions,
@@ -14,7 +15,7 @@ import { apiErrorCode } from "@/shared/api";
type AccountOperationsPageViewModel =
| { state: "loading" }
| { state: "error"; retry: () => void }
| { state: "error"; retry: () => void; onReset: () => void }
| {
state: "loaded";
sourceError?: boolean;
@@ -47,8 +48,11 @@ const useAccountOperationsPageVm = (
const accountId = scopeId ?? routeAccountId;
const queryClient = useQueryClient();
const [searchParams, setSearchParams] = useSearchParams();
const appliedFrom = searchParams.get("from");
const appliedTo = searchParams.get("to");
const urlFrom = searchParams.get("from");
const urlTo = searchParams.get("to");
const validUrlPeriod = isValidPeriod(urlFrom, urlTo);
const appliedFrom = validUrlPeriod ? urlFrom : null;
const appliedTo = validUrlPeriod ? urlTo : null;
const appliedCategories = operationCategories.filter((category) =>
(searchParams.get("categories") ?? "").split(",").includes(category),
);
@@ -58,8 +62,8 @@ const useAccountOperationsPageVm = (
useState<OperationCategory[]>(appliedCategories);
const scopeKey = JSON.stringify([
accountId,
appliedFrom,
appliedTo,
urlFrom,
urlTo,
appliedCategories,
]);
const [pagination, setPagination] = useState<{
@@ -69,14 +73,26 @@ const useAccountOperationsPageVm = (
const cursors = pagination.scopeKey === scopeKey ? pagination.cursors : [];
const cursor = cursors.at(-1);
useEffect(() => {
setDraftFrom(searchParams.get("from"));
setDraftTo(searchParams.get("to"));
setPagination((current) =>
current.scopeKey === scopeKey ? current : { scopeKey, cursors: [] },
);
}, [scopeKey]);
useEffect(() => {
if (validUrlPeriod) return;
const normalized = new URLSearchParams(searchParams);
normalized.delete("from");
normalized.delete("to");
setSearchParams(normalized, { replace: true });
}, [validUrlPeriod, searchParams, setSearchParams]);
useEffect(() => {
setDraftFrom(appliedFrom);
setDraftTo(appliedTo);
setDraftCategories(
operationCategories.filter((category) =>
(searchParams.get("categories") ?? "").split(",").includes(category),
),
);
}, [searchParams]);
}, [appliedFrom, appliedTo, searchParams]);
const firstPageQuery = {
from: dateStart(appliedFrom),
to: dateEnd(appliedTo),
@@ -104,7 +120,15 @@ const useAccountOperationsPageVm = (
exact: true,
});
resetCursors();
} else void sync.refresh();
} else void sync.retry();
};
const onReset = () => {
setDraftFrom(null);
setDraftTo(null);
setDraftCategories([]);
setSearchParams({});
resetCursors();
void queryClient.invalidateQueries({ queryKey: resetQueryKey });
};
if (operations.isLoading) return { state: "loading" };
@@ -113,7 +137,7 @@ const useAccountOperationsPageVm = (
(!operations.data ||
apiErrorCode(operations.error) === "INVALID_OPERATIONS_CURSOR")
)
return { state: "error", retry };
return { state: "error", retry, onReset };
if (!operations.data) return { state: "loading" };
const nextCursor = operations.data.nextCursor;
@@ -139,11 +163,12 @@ const useAccountOperationsPageVm = (
items: operations.data.items,
hasNext: Boolean(operations.data.hasNext && nextCursor),
hasPrevious: Boolean(cursors.length),
canApply: isDirty,
canApply: isDirty && isValidPeriod(draftFrom, draftTo),
onDraftFromChange: setDraftFrom,
onDraftToChange: setDraftTo,
onDraftCategoriesChange: setDraftCategories,
onApply: () => {
if (!isValidPeriod(draftFrom, draftTo)) return;
setSearchParams({
...(draftFrom ? { from: draftFrom } : {}),
...(draftTo ? { to: draftTo } : {}),
@@ -153,14 +178,7 @@ const useAccountOperationsPageVm = (
});
resetCursors();
},
onReset: () => {
setDraftFrom(null);
setDraftTo(null);
setDraftCategories([]);
setSearchParams({});
resetCursors();
void queryClient.invalidateQueries({ queryKey: resetQueryKey });
},
onReset,
onNext: () => {
if (nextCursor)
setPagination({ scopeKey, cursors: [...cursors, nextCursor] });
@@ -170,6 +188,16 @@ const useAccountOperationsPageVm = (
};
};
const calendarDate = z.string().date();
function isValidPeriod(from: string | null, to: string | null) {
return (
[from, to].every(
(date) => date === null || calendarDate.safeParse(date).success,
) &&
(!from || !to || from <= to)
);
}
function dateStart(value: string | null) {
return value ? `${value}T00:00:00.000Z` : undefined;
}