From 4fefb808aa9bc201c27d15017236e6572cf7cd6a Mon Sep 17 00:00:00 2001
From: Serena Li <40174697+acrantel@users.noreply.github.com>
Date: Tue, 15 Aug 2023 19:06:12 -0400
Subject: [PATCH] Add state for current user (#665)
---
frontend2/src/App.tsx | 25 +-
.../src/components/CurrentUserProvider.tsx | 63 ++++
frontend2/src/components/PrivateRoute.tsx | 23 ++
frontend2/src/contexts/CurrentUserContext.ts | 33 ++
frontend2/src/utils/api.ts | 26 +-
frontend2/src/utils/apiTypes.ts | 284 ++++++++++++++++++
frontend2/src/utils/auth.ts | 8 +-
frontend2/src/utils/cookies.ts | 12 +
8 files changed, 464 insertions(+), 10 deletions(-)
create mode 100644 frontend2/src/components/CurrentUserProvider.tsx
create mode 100644 frontend2/src/components/PrivateRoute.tsx
create mode 100644 frontend2/src/contexts/CurrentUserContext.ts
create mode 100644 frontend2/src/utils/apiTypes.ts
create mode 100644 frontend2/src/utils/cookies.ts
diff --git a/frontend2/src/App.tsx b/frontend2/src/App.tsx
index cbdbf57f5..1417ce3f4 100644
--- a/frontend2/src/App.tsx
+++ b/frontend2/src/App.tsx
@@ -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 (
-
-
-
+
+
+
+
+
);
};
@@ -44,10 +48,19 @@ const router = createBrowserRouter([
{ path: "/:episodeId/quickstart", element: },
{ path: "/:episodeId/*", element: },
{ path: "/:episodeId/rankings", element: },
- // 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: ,
+ children: [
+ {
+ element: ,
+ children: [
+ // TODO: /:episodeId/team, /:episodeId/submissions, /:episodeId/scrimmaging
+ ],
+ },
{ path: "/account", element: },
- // etc
],
},
// Pages that should redirect
diff --git a/frontend2/src/components/CurrentUserProvider.tsx b/frontend2/src/components/CurrentUserProvider.tsx
new file mode 100644
index 000000000..59f2cc2fd
--- /dev/null
+++ b/frontend2/src/components/CurrentUserProvider.tsx
@@ -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({
+ user,
+ authState: AuthStateEnum.AUTHENTICATED,
+ });
+ };
+ const logout = (): void => {
+ setUserData({
+ authState: AuthStateEnum.NOT_AUTHENTICATED,
+ });
+ };
+
+ useEffect(() => {
+ const checkLoggedIn = async (): Promise => {
+ // 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 (
+
+ {children}
+
+ );
+};
diff --git a/frontend2/src/components/PrivateRoute.tsx b/frontend2/src/components/PrivateRoute.tsx
new file mode 100644
index 000000000..d8872dccb
--- /dev/null
+++ b/frontend2/src/components/PrivateRoute.tsx
@@ -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 ;
+ } else if (authState === AuthStateEnum.NOT_AUTHENTICATED) {
+ navigate("/login");
+ return null;
+ } else {
+ return (
+
+
+
+ );
+ }
+};
+
+export default PrivateRoute;
diff --git a/frontend2/src/contexts/CurrentUserContext.ts b/frontend2/src/contexts/CurrentUserContext.ts
new file mode 100644
index 000000000..7ab9d51c8
--- /dev/null
+++ b/frontend2/src/contexts/CurrentUserContext.ts
@@ -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(
+ null
+);
+
+export const useCurrentUser = (): CurrentUserContextType => {
+ const currentUserContext = useContext(CurrentUserContext);
+
+ if (currentUserContext === null) {
+ throw new Error(
+ "useCurrentUser has to be used within "
+ );
+ }
+
+ return currentUserContext;
+};
diff --git a/frontend2/src/utils/api.ts b/frontend2/src/utils/api.ts
index 8febbda1f..e932878ef 100644
--- a/frontend2/src/utils/api.ts
+++ b/frontend2/src/utils/api.ts
@@ -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";
@@ -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 => {
- 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;
};
/**
diff --git a/frontend2/src/utils/apiTypes.ts b/frontend2/src/utils/apiTypes.ts
new file mode 100644
index 000000000..6667bd6aa
--- /dev/null
+++ b/frontend2/src/utils/apiTypes.ts
@@ -0,0 +1,284 @@
+export enum GenderEnum {
+ FEMALE = "F",
+ MALE = "M",
+ NONBINARY = "N",
+ SELF_DESCRIBED = "*",
+ RATHER_NOT_SAY = "?",
+}
+
+export type Gender = `${GenderEnum}`;
+
+const countries = [
+ "AF",
+ "AX",
+ "AL",
+ "DZ",
+ "AS",
+ "AD",
+ "AO",
+ "AI",
+ "AQ",
+ "AG",
+ "AR",
+ "AM",
+ "AW",
+ "AU",
+ "AT",
+ "AZ",
+ "BS",
+ "BH",
+ "BD",
+ "BB",
+ "BY",
+ "BE",
+ "BZ",
+ "BJ",
+ "BM",
+ "BT",
+ "BO",
+ "BQ",
+ "BA",
+ "BW",
+ "BV",
+ "BR",
+ "IO",
+ "BN",
+ "BG",
+ "BF",
+ "BI",
+ "CV",
+ "KH",
+ "CM",
+ "CA",
+ "KY",
+ "CF",
+ "TD",
+ "CL",
+ "CN",
+ "CX",
+ "CC",
+ "CO",
+ "KM",
+ "CG",
+ "CD",
+ "CK",
+ "CR",
+ "CI",
+ "HR",
+ "CU",
+ "CW",
+ "CY",
+ "CZ",
+ "DK",
+ "DJ",
+ "DM",
+ "DO",
+ "EC",
+ "EG",
+ "SV",
+ "GQ",
+ "ER",
+ "EE",
+ "SZ",
+ "ET",
+ "FK",
+ "FO",
+ "FJ",
+ "FI",
+ "FR",
+ "GF",
+ "PF",
+ "TF",
+ "GA",
+ "GM",
+ "GE",
+ "DE",
+ "GH",
+ "GI",
+ "GR",
+ "GL",
+ "GD",
+ "GP",
+ "GU",
+ "GT",
+ "GG",
+ "GN",
+ "GW",
+ "GY",
+ "HT",
+ "HM",
+ "VA",
+ "HN",
+ "HK",
+ "HU",
+ "IS",
+ "IN",
+ "ID",
+ "IR",
+ "IQ",
+ "IE",
+ "IM",
+ "IL",
+ "IT",
+ "JM",
+ "JP",
+ "JE",
+ "JO",
+ "KZ",
+ "KE",
+ "KI",
+ "KW",
+ "KG",
+ "LA",
+ "LV",
+ "LB",
+ "LS",
+ "LR",
+ "LY",
+ "LI",
+ "LT",
+ "LU",
+ "MO",
+ "MG",
+ "MW",
+ "MY",
+ "MV",
+ "ML",
+ "MT",
+ "MH",
+ "MQ",
+ "MR",
+ "MU",
+ "YT",
+ "MX",
+ "FM",
+ "MD",
+ "MC",
+ "MN",
+ "ME",
+ "MS",
+ "MA",
+ "MZ",
+ "MM",
+ "NA",
+ "NR",
+ "NP",
+ "NL",
+ "NC",
+ "NZ",
+ "NI",
+ "NE",
+ "NG",
+ "NU",
+ "NF",
+ "KP",
+ "MK",
+ "MP",
+ "NO",
+ "OM",
+ "PK",
+ "PW",
+ "PS",
+ "PA",
+ "PG",
+ "PY",
+ "PE",
+ "PH",
+ "PN",
+ "PL",
+ "PT",
+ "PR",
+ "QA",
+ "RE",
+ "RO",
+ "RU",
+ "RW",
+ "BL",
+ "SH",
+ "KN",
+ "LC",
+ "MF",
+ "PM",
+ "VC",
+ "WS",
+ "SM",
+ "ST",
+ "SA",
+ "SN",
+ "RS",
+ "SC",
+ "SL",
+ "SG",
+ "SX",
+ "SK",
+ "SI",
+ "SB",
+ "SO",
+ "ZA",
+ "GS",
+ "KR",
+ "SS",
+ "ES",
+ "LK",
+ "SD",
+ "SR",
+ "SJ",
+ "SE",
+ "CH",
+ "SY",
+ "TW",
+ "TJ",
+ "TZ",
+ "TH",
+ "TL",
+ "TG",
+ "TK",
+ "TO",
+ "TT",
+ "TN",
+ "TR",
+ "TM",
+ "TC",
+ "TV",
+ "UG",
+ "UA",
+ "AE",
+ "GB",
+ "UM",
+ "US",
+ "UY",
+ "UZ",
+ "VU",
+ "VE",
+ "VN",
+ "VG",
+ "VI",
+ "WF",
+ "EH",
+ "YE",
+ "ZM",
+ "ZW",
+];
+
+export type Country = (typeof countries)[number];
+export interface CreateUserInput {
+ profile: {
+ gender: Gender;
+ genderDetails?: string;
+ school?: string;
+ country: Country;
+ };
+
+ /**
+ * Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.
+ */
+ username: string;
+
+ password: string;
+
+ email: string;
+
+ firstName: string;
+
+ lastName: string;
+}
diff --git a/frontend2/src/utils/auth.ts b/frontend2/src/utils/auth.ts
index 8f1ffc077..429128a53 100644
--- a/frontend2/src/utils/auth.ts
+++ b/frontend2/src/utils/auth.ts
@@ -2,6 +2,7 @@ import * as Api from "./api";
import Cookies from "js-cookie";
import type * as models from "./types/model/models";
import { ApiApi } from "./types/api/ApiApi";
+import type * as customModels from "./apiTypes";
/** This file contains all frontend authentication functions. Responsible for interacting with Cookies and expiring/setting JWT tokens. */
@@ -73,9 +74,12 @@ export const loginCheck = async (): Promise => {
* Register a new user.
* @param user The user to register.
*/
-export const register = async (user: models.UserCreate): Promise => {
- await Api.createUser(user);
+export const register = async (
+ user: customModels.CreateUserInput
+): Promise => {
+ const returnedUser = await Api.createUser(user);
await login(user.username, user.password);
+ return returnedUser;
};
/**
diff --git a/frontend2/src/utils/cookies.ts b/frontend2/src/utils/cookies.ts
new file mode 100644
index 000000000..ce72dd622
--- /dev/null
+++ b/frontend2/src/utils/cookies.ts
@@ -0,0 +1,12 @@
+import Cookies from "js-cookie";
+
+export const removeApiTokens = (): void => {
+ Cookies.remove("access");
+ Cookies.remove("refresh");
+};
+
+export const doApiTokensExist = (): boolean => {
+ return (
+ Cookies.get("access") !== undefined && Cookies.get("refresh") !== undefined
+ );
+};