refactor: simplify auth and instrument pages
Frontend verify / commits (pull_request) Successful in 24s
Frontend verify / security (pull_request) Failing after 9s
Frontend verify / verify (pull_request) Skipped

This commit is contained in:
2026-09-29 05:45:08 +03:00
parent 32a77860c8
commit b404c1802a
38 changed files with 644 additions and 790 deletions
-2
View File
@@ -3,11 +3,9 @@ import { createContext, use } from "react";
import type { paths } from "@/shared/api";
type AuthContextValue = {
accessToken: string | undefined;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
ready: boolean;
refresh: () => Promise<boolean>;
register: (email: string, password: string) => Promise<void>;
user: undefined | User;
};
+34 -8
View File
@@ -27,6 +27,32 @@ function createAuthWrapper(
);
}
async function expireSession() {
server.use(http.get("*/api/v1/brokerage-accounts", () =>
HttpResponse.json({ code: "UNAUTHORIZED" }, { status: 401 }),
{ once: true }));
try {
await apiClient.GET("/api/v1/brokerage-accounts");
}
catch (error) {
// A refresh that changes identity cancels the original user's request.
if (!(error instanceof DOMException && error.name === "AbortError")) throw error;
}
}
async function requestAuthorization() {
let authorization: null | string = null;
server.use(http.get("*/api/v1/brokerage-accounts", ({ request }) => {
authorization = request.headers.get("Authorization");
return HttpResponse.json(resource([]));
}, { once: true }));
await apiClient.GET("/api/v1/brokerage-accounts");
return authorization;
}
describe("AuthProvider", () => {
it("authorizes requests after restoring the initial session", async () => {
let authorization: null | string
@@ -77,7 +103,7 @@ describe("AuthProvider", () => {
await waitFor(() => expect(result.current.ready).toBe(true));
expect(result.current.user).toBeUndefined();
expect(result.current.accessToken).toBeUndefined();
expect(await requestAuthorization()).toBeNull();
expect(queryClient.getQueryData(["cached-query"])).toBeUndefined();
expect(queryClient.getQueryData(bankCardsKey)).toBeUndefined();
});
@@ -106,13 +132,13 @@ describe("AuthProvider", () => {
const { result } = renderHook(() => useAuth(), { wrapper });
await waitFor(() =>
expect(result.current.accessToken).toBe("restored-token"),
expect(result.current.ready).toBe(true),
);
const response = await apiClient.GET("/api/v1/brokerage-accounts");
expect(response.response.status).toBe(401);
await waitFor(() => expect(result.current.user).toBeUndefined());
expect(result.current.accessToken).toBeUndefined();
expect(await requestAuthorization()).toBeNull();
expect(queryClient.getQueryData(["cached-query"])).toBeUndefined();
expect(queryClient.getQueryData(bankCardsKey)).toBeUndefined();
});
@@ -147,7 +173,7 @@ describe("AuthProvider", () => {
queryClient.setQueryData(bankCardsKey, [{ id: "bank-card-1" }]);
const wrapper: FC<PropsWithChildren> = createAuthWrapper(queryClient);
const { result } = renderHook(() => useAuth(), { wrapper });
await waitFor(() => expect(result.current.accessToken).toBe("test-token"));
await waitFor(() => expect(result.current.ready).toBe(true));
expect(queryClient.getQueryData(bankCardsKey)).toEqual([
{ id: "bank-card-1" },
@@ -155,7 +181,7 @@ describe("AuthProvider", () => {
await act(() => result.current.logout());
expect(result.current.user).toBeUndefined();
expect(result.current.accessToken).toBeUndefined();
expect(await requestAuthorization()).toBeNull();
expect(queryClient.getQueryData(statusKey)).toBeUndefined();
expect(queryClient.getQueryData(portfolioKey)).toBeUndefined();
expect(queryClient.getQueryData(bankCardsKey)).toBeUndefined();
@@ -265,7 +291,7 @@ describe("AuthProvider", () => {
const { result } = renderHook(() => useAuth(), { wrapper });
await waitFor(() => expect(result.current.ready).toBe(true));
queryClient.setQueryData(bankCardsKey, [{ id: "a-card" }]);
await act(() => result.current.refresh());
await act(() => expireSession());
expect(queryClient.getQueryData(bankCardsKey)).toEqual([{ id: "a-card" }]);
let release!: (value: string) => void;
@@ -283,7 +309,7 @@ describe("AuthProvider", () => {
.catch(() => {});
identity = "b";
await act(() => result.current.refresh());
await act(() => expireSession());
release("a-data");
await pending;
expect(result.current.user?.id).toBe("b");
@@ -322,7 +348,7 @@ describe("AuthProvider", () => {
expect(result.current.cards.data?.[0].id).toBe("a-card"),
);
identity = "b";
await act(() => result.current.auth.refresh());
await act(() => expireSession());
await waitFor(() =>
expect(result.current.cards.data?.[0].id).toBe("b-card"),
);
-5
View File
@@ -29,7 +29,6 @@ async function request(
function useAuthProviderVm() {
const queryClient = useQueryClient();
const [user, setUser] = useState<undefined | User>(undefined);
const [accessToken, setAccessToken] = useState<string | undefined>(undefined);
const accessTokenRef = useRef<string | undefined>(undefined);
const [ready, setReady] = useState(false);
@@ -69,7 +68,6 @@ function useAuthProviderVm() {
userRef.current = authentication.user;
setUser(authentication.user);
setAccessToken(authentication.accessToken);
},
[queryClient],
);
@@ -78,7 +76,6 @@ function useAuthProviderVm() {
generationRef.current += 1;
userRef.current = undefined;
setUser(undefined);
setAccessToken(undefined);
accessTokenRef.current = undefined;
queryClient.clear();
}, [queryClient]);
@@ -156,12 +153,10 @@ function useAuthProviderVm() {
useEffect(() => () => configureAuthClient(), []);
const authContextValue = {
accessToken,
login: (email: string, password: string) =>
authenticate("/api/v1/auth/login", email, password),
logout,
ready,
refresh,
register: (email: string, password: string) =>
authenticate("/api/v1/auth/register", email, password),
user,
+3 -3
View File
@@ -1,12 +1,12 @@
import type { FC } from "react";
import { useAuth } from "../../model/auth-context";
import { AuthForm } from "../AuthForm/AuthForm";
import { useLoginFormVm } from "./LoginForm.vm";
const LoginForm: FC = () => {
const { onSubmit } = useLoginFormVm();
const { login } = useAuth();
return <AuthForm onSubmit={onSubmit} />;
return <AuthForm onSubmit={login} />;
};
export { LoginForm };
@@ -1,28 +0,0 @@
import type { ComponentProps } from "react";
import { useLocation, useNavigate } from "react-router";
import { useAuth } from "../../model/auth-context";
import { AuthForm } from "../AuthForm/AuthForm";
function useLoginFormVm() {
const { login } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const returnTo
= (location.state as null
| { returnTo?: string })?.returnTo ?? "/accounts";
const onSubmit: ComponentProps<typeof AuthForm>["onSubmit"] = async (
email,
password,
) => {
await login(email, password);
await navigate(returnTo, { replace: true });
};
return { onSubmit };
}
export { useLoginFormVm };
@@ -1,12 +1,12 @@
import type { FC } from "react";
import { useAuth } from "../../model/auth-context";
import { AuthForm } from "../AuthForm/AuthForm";
import { useRegisterFormVm } from "./RegisterForm.vm";
const RegisterForm: FC = () => {
const { onSubmit } = useRegisterFormVm();
const { register } = useAuth();
return <AuthForm onSubmit={onSubmit} register />;
return <AuthForm onSubmit={register} register />;
};
export { RegisterForm };
@@ -1,28 +0,0 @@
import type { ComponentProps } from "react";
import { useLocation, useNavigate } from "react-router";
import { useAuth } from "../../model/auth-context";
import { AuthForm } from "../AuthForm/AuthForm";
function useRegisterFormVm() {
const { register } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const returnTo
= (location.state as null
| { returnTo?: string })?.returnTo ?? "/accounts";
const onSubmit: ComponentProps<typeof AuthForm>["onSubmit"] = async (
email,
password,
) => {
await register(email, password);
await navigate(returnTo, { replace: true });
};
return { onSubmit, register };
}
export { useRegisterFormVm };