Skip to content

Commit

Permalink
Factories Loaders and Token Validity (#746)
Browse files Browse the repository at this point in the history
Co-authored-by: Serena Li <[email protected]>
  • Loading branch information
lowtorola and acrantel authored Apr 26, 2024
1 parent 94066f2 commit e494b4c
Show file tree
Hide file tree
Showing 32 changed files with 1,445 additions and 525 deletions.
Binary file added .DS_Store
Binary file not shown.
93 changes: 73 additions & 20 deletions frontend2/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,18 @@ import Submissions from "./views/Submissions";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { toast, Toaster } from "react-hot-toast";
import { ResponseError } from "./api/_autogen/runtime";
import { userQueryKeys } from "./api/user/userKeys";
import { episodeQueryKeys } from "./api/episode/episodeKeys";
import { getEpisodeInfo } from "./api/episode/episodeApi";
import { loginCheck } from "./api/auth/authApi";
import { submissionsLoader } from "./api/loaders/submissionsLoader";
import { myTeamLoader } from "./api/loaders/myTeamLoader";
import { scrimmagingLoader } from "./api/loaders/scrimmagingLoader";
import { rankingsLoader } from "./api/loaders/rankingsLoader";
import { episodeInfoFactory } from "./api/episode/episodeFactories";
import { buildKey } from "./api/helpers";
import { queueLoader } from "./api/loaders/queueLoader";
import { tournamentsLoader } from "./api/loaders/tournamentsLoader";
import { tournamentLoader } from "./api/loaders/tournamentLoader";
import { homeLoader } from "./api/loaders/homeLoader";
import ErrorBoundary from "./views/ErrorBoundary";

const queryClient = new QueryClient({
queryCache: new QueryCache({
Expand All @@ -53,7 +62,10 @@ const queryClient = new QueryClient({
});

queryClient.setQueryDefaults(["team"], { retry: false });
queryClient.setQueryDefaults(userQueryKeys.meBase, { retry: false });
queryClient.setQueryDefaults(["user"], { retry: false });

// Run a check to see if the user has an invalid token
await loginCheck(queryClient);

const App: React.FC = () => {
return (
Expand All @@ -69,27 +81,40 @@ const App: React.FC = () => {
);
};

const episodeLoader: LoaderFunction = async ({ params }) => {
const episodeLoader: LoaderFunction = ({ params }) => {
// check if the episodeId is a valid one.
// if the episode is not found, throw an error.
const id = params.episodeId ?? "";
return await queryClient.fetchQuery({
queryKey: episodeQueryKeys.info({ id }),
queryFn: async () => await getEpisodeInfo({ id }),

// Prefetch the episode info.
void queryClient.ensureQueryData({
queryKey: buildKey(episodeInfoFactory.queryKey, { id }),
queryFn: async () => await episodeInfoFactory.queryFn({ id }),
staleTime: Infinity,
});

return null;
};

const router = createBrowserRouter([
// Pages that should render without a sidebar/navbar
{ path: "/login", element: <Login /> },
{ path: "/logout", element: <Logout /> },
{ path: "/register", element: <Register /> },
{ path: "/password_forgot", element: <PasswordForgot /> },
{ path: "/password_change", element: <PasswordChange /> },
{ path: "/login", element: <Login />, errorElement: <ErrorBoundary /> },
{ path: "/logout", element: <Logout />, errorElement: <ErrorBoundary /> },
{ path: "/register", element: <Register />, errorElement: <ErrorBoundary /> },
{
path: "/password_forgot",
element: <PasswordForgot />,
errorElement: <ErrorBoundary />,
},
{
path: "/password_change",
element: <PasswordChange />,
errorElement: <ErrorBoundary />,
},
// Account page doesn't have episode id in URL
{
element: <PrivateRoute />,
errorElement: <ErrorBoundary />,
children: [
{
element: <EpisodeLayout />,
Expand All @@ -100,9 +125,9 @@ const router = createBrowserRouter([
// Pages that will contain the episode sidebar and navbar (excl. account page)
{
element: <EpisodeLayout />,
errorElement: <ErrorBoundary />,
path: "/:episodeId",
loader: episodeLoader,
errorElement: <NotFound />,
children: [
{
// Pages that should only be visible when logged in
Expand All @@ -111,36 +136,64 @@ const router = createBrowserRouter([
{
path: "submissions",
element: <Submissions />,
loader: submissionsLoader(queryClient),
},
{
path: "team",
element: <MyTeam />,
loader: myTeamLoader(queryClient),
},
{
path: "scrimmaging",
element: <Scrimmaging />,
loader: scrimmagingLoader(queryClient),
},
],
},
// Pages that should always be visible
{ path: "", element: <Home /> },
{ path: "home", element: <Home /> },
{
path: "",
element: <Home />,
loader: homeLoader(queryClient),
},
{
path: "home",
element: <Home />,
loader: homeLoader(queryClient),
},
{ path: "resources", element: <Resources /> },
{ path: "quickstart", element: <QuickStart /> },
{ path: "rankings", element: <Rankings /> },
{ path: "queue", element: <Queue /> },
{ path: "tournaments", element: <Tournaments /> },
{
path: "rankings",
element: <Rankings />,
loader: rankingsLoader(queryClient),
},
{
path: "queue",
element: <Queue />,
loader: queueLoader(queryClient),
},
{
path: "tournaments",
element: <Tournaments />,
loader: tournamentsLoader(queryClient),
},
{
path: "tournament/:tournamentId",
element: <TournamentPage />,
loader: tournamentLoader(queryClient),
},
{
path: "*",
element: <NotFound />,
},
],
},
{ path: "/", element: <Navigate to={`/${DEFAULT_EPISODE}/home`} /> },
{
path: "/",
element: <Navigate to={`/${DEFAULT_EPISODE}/home`} />,
errorElement: <ErrorBoundary />,
},
]);

export default App;
53 changes: 53 additions & 0 deletions frontend2/src/api/apiTypes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,61 @@
import type { QueryClient, QueryKey } from "@tanstack/react-query";
import {
type CountryEnum,
GenderEnum as GeneratedGenderEnum,
} from "./_autogen";

export interface QueryKeyBuilder<T> {
key: (request: T) => QueryKey;
}

export interface QueryKeyHolder {
key: () => QueryKey;
}

export type QueryFuncBuilder<T, K = void> = (request: T) => Promise<K>;

export type PaginatedQueryFuncBuilder<T, K = void> = (
request: T,
queryClient: QueryClient,
prefetchNext: boolean,
) => Promise<K>;

/**
* Contains all of the information needed to represent a single useQuery hook.
* - `queryKey`: a container for the query key, which should be "built" using `helpers.buildKey`
* - `queryFn`: the function that will be called to fetch the data. This function should take a
* single argument, the `request` of type `K`, and return a `Promise<T>`.
*/
export interface QueryFactory<T, K> {
queryKey: QueryKeyHolder | QueryKeyBuilder<T>;
queryFn: QueryFuncBuilder<T, K>;
}

/**
* Contains all of the information needed to represent a single paginated useQuery hook.
*
* Note that this interface is similar to `QueryFactory`, but includes a PaginatedQueryFuncBuilder<T, K>
* which enables automatic prefetching of the next page of table data.
* - `queryKey`: a container for the query key, which should be "built" using `helpers.buildKey`
* - `queryFn`: the function that will be called to fetch the data. This function should take
* the `request` of type `T`, the `queryClient`, and a `prefetchNext` boolean, and return a `Promise<K>`.
*
* If `prefetchNext` is true, the query function will prefetch the next page of data.
*/
export interface PaginatedQueryFactory<T, K> {
queryKey: QueryKeyBuilder<T>;
queryFn: PaginatedQueryFuncBuilder<T, K>;
}

export interface PaginatedRequestMinimal {
page?: number;
}
export interface PaginatedResultMinimal {
count?: number;
next?: string | null;
previous?: string | null;
}

export enum GenderEnum {
FEMALE = GeneratedGenderEnum.F,
MALE = GeneratedGenderEnum.M,
Expand Down
34 changes: 23 additions & 11 deletions frontend2/src/api/auth/authApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export const login = async (
Cookies.set("access", res.access);
Cookies.set("refresh", res.refresh);
await queryClient.refetchQueries({
queryKey: userQueryKeys.meBase,
// OK to call KEY.key() here as we are refetching all user-me queries.
queryKey: userQueryKeys.meBase.key(),
});
};

Expand All @@ -39,19 +40,13 @@ export const login = async (
export const logout = async (queryClient: QueryClient): Promise<void> => {
Cookies.remove("access");
Cookies.remove("refresh");
await queryClient.refetchQueries({
queryKey: userQueryKeys.meBase,
await queryClient.resetQueries({
// OK to call KEY.key() here as we are resetting all user-me queries.
queryKey: userQueryKeys.meBase.key(),
});
};

/**
* Checks whether the currently held JWT access token is still valid (by posting it to the verify endpoint),
* hence whether or not the frontend still has logged-in access.
* @returns true or false
* Callers of this method should check this, before rendering their logged-in or un-logged-in versions.
* If not logged in, then api calls will give 403s, this function will return false, and the website will tell you to log in anyways.
*/
export const loginCheck = async (): Promise<boolean> => {
export const tokenVerify = async (): Promise<boolean> => {
const accessToken = Cookies.get("access");
if (accessToken === undefined) {
return false;
Expand All @@ -65,3 +60,20 @@ export const loginCheck = async (): Promise<boolean> => {
return false;
}
};

/**
* Checks whether the currently held JWT access token is still valid (by posting it to the verify endpoint),
* hence whether or not the frontend still has logged-in access.
* @returns true or false
* Callers of this method should check this, before rendering their logged-in or un-logged-in versions.
* If not logged in, then api calls will give 403s, this function will return false, and the website will tell you to log in anyways.
*/
export const loginCheck = async (
queryClient: QueryClient,
): Promise<boolean> => {
const verified = await tokenVerify();
if (!verified) {
await logout(queryClient);
}
return verified;
};
Loading

0 comments on commit e494b4c

Please sign in to comment.