Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add state for current user #665

Merged
merged 2 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions frontend2/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ import {
import { DEFAULT_EPISODE } from "./utils/constants";
import NotFound from "./views/NotFound";
import Rankings from "./views/Rankings";
import { CurrentUserProvider } from "./components/CurrentUserProvider";
import PrivateRoute from "./components/PrivateRoute";

const App: React.FC = () => {
const [episodeId, setEpisodeId] = useState(DEFAULT_EPISODE);
return (
<EpisodeContext.Provider value={{ episodeId, setEpisodeId }}>
<RouterProvider router={router} />
</EpisodeContext.Provider>
<CurrentUserProvider>
<EpisodeContext.Provider value={{ episodeId, setEpisodeId }}>
<RouterProvider router={router} />
</EpisodeContext.Provider>
</CurrentUserProvider>
);
};

Expand All @@ -44,10 +48,19 @@ const router = createBrowserRouter([
{ path: "/:episodeId/quickstart", element: <QuickStart /> },
{ path: "/:episodeId/*", element: <NotFound /> },
{ path: "/:episodeId/rankings", element: <Rankings /> },
// Pages that should only be visible when logged in
// TODO: /:episodeId/team, /:episodeId/submissions, /:episodeId/scrimmaging
],
},
// Pages that should only be visible when logged in
{
element: <PrivateRoute />,
children: [
{
element: <EpisodeLayout />,
children: [
// TODO: /:episodeId/team, /:episodeId/submissions, /:episodeId/scrimmaging
],
},
{ path: "/account", element: <Account /> },
// etc
],
},
// Pages that should redirect
Expand Down
63 changes: 63 additions & 0 deletions frontend2/src/components/CurrentUserProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { useState, useEffect } from "react";
import { type UserPrivate } from "../utils/types";
import {
AuthStateEnum,
type AuthState,
CurrentUserContext,
} from "../contexts/CurrentUserContext";
import * as Api from "../utils/api";
import { removeApiTokens, doApiTokensExist } from "../utils/cookies";

export const CurrentUserProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [userData, setUserData] = useState<{
user?: UserPrivate;
authState: AuthState;
}>({
authState: AuthStateEnum.LOADING,
});

const login = (user: UserPrivate): void => {
setUserData({
lowtorola marked this conversation as resolved.
Show resolved Hide resolved
user,
authState: AuthStateEnum.AUTHENTICATED,
});
};
const logout = (): void => {
setUserData({
authState: AuthStateEnum.NOT_AUTHENTICATED,
});
};

useEffect(() => {
const checkLoggedIn = async (): Promise<void> => {
// check if cookies exist before attempting to load user
if (!doApiTokensExist()) {
logout();
return;
}
try {
const user = await Api.getUserUserProfile();
login(user);
} catch (error) {
logout();
removeApiTokens();
}
};
void checkLoggedIn();
}, []);

return (
<CurrentUserContext.Provider
value={{
authState: userData.authState,
user: userData.user,
login,
logout,
}}
>
{children}
</CurrentUserContext.Provider>
);
};
23 changes: 23 additions & 0 deletions frontend2/src/components/PrivateRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";
import { AuthStateEnum, useCurrentUser } from "../contexts/CurrentUserContext";
import { Outlet, useNavigate } from "react-router-dom";
import Spinner from "./Spinner";

const PrivateRoute: React.FC = () => {
const { authState } = useCurrentUser();
const navigate = useNavigate();
if (authState === AuthStateEnum.AUTHENTICATED) {
return <Outlet />;
} else if (authState === AuthStateEnum.NOT_AUTHENTICATED) {
navigate("/login");
return null;
} else {
return (
<div className="h-screen flex items-center justify-center">
<Spinner />
</div>
);
}
};

export default PrivateRoute;
33 changes: 33 additions & 0 deletions frontend2/src/contexts/CurrentUserContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createContext, useContext } from "react";
import { type UserPrivate } from "../utils/types/model/models";

export enum AuthStateEnum {
LOADING = "loading",
AUTHENTICATED = "authenticated",
NOT_AUTHENTICATED = "not_authenticated",
}

export type AuthState = `${AuthStateEnum}`;

interface CurrentUserContextType {
authState: AuthState;
user?: UserPrivate;
login: (user: UserPrivate) => void;
logout: () => void;
}

export const CurrentUserContext = createContext<CurrentUserContextType | null>(
null
);

export const useCurrentUser = (): CurrentUserContextType => {
const currentUserContext = useContext(CurrentUserContext);

if (currentUserContext === null) {
throw new Error(
"useCurrentUser has to be used within <CurrentUserContext.Provider>"
);
}

return currentUserContext;
};
26 changes: 24 additions & 2 deletions frontend2/src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ApiApi } from "./types/api/ApiApi";
import Cookies from "js-cookie";
import * as $ from "jquery";
import * as models from "./types/model/models";
import type * as customModels from "./apiTypes";

// hacky, fall back to localhost for now
const baseUrl = process.env.REACT_APP_BACKEND_URL ?? "http://localhost:8000";
Expand Down Expand Up @@ -297,9 +298,30 @@ export const getAllUserTournamentSubmissions = async (
* @param user The user's info.
*/
export const createUser = async (
user: models.UserCreate
user: customModels.CreateUserInput
): Promise<models.UserCreate> => {
return (await API.apiUserUCreate(user)).body;
const defaultUser = {
id: -1,
isStaff: false,
profile: {
avatarUrl: "",
hasAvatar: false,
hasResume: false,
},
};
// convert our input into models.UserCreate.
return (
await API.apiUserUCreate({
...defaultUser,
...user,
profile: {
...defaultUser.profile,
...user.profile,
gender: user.profile.gender as unknown as models.GenderEnum,
country: user.profile.country as unknown as models.CountryEnum,
},
})
).body;
};

/**
Expand Down
Loading