refactor: simplify auth and instrument pages
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user