Skip to content

Commit

Permalink
♻️: API and backend refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
Gamer92000 committed Jun 30, 2023
1 parent 603c361 commit 91afde3
Show file tree
Hide file tree
Showing 54 changed files with 323 additions and 3,609 deletions.
72 changes: 40 additions & 32 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import "@styles/App.css";
import React, { useEffect } from "react";
import { Route, Routes, useLocation } from "react-router-dom";
import { useAppSelector } from "@redux/hooks";
import Helper from "@util/helper";
import { ConfigProvider } from "antd";

import Home from "@pages/home";
import Login from "@pages/login";
import Register from "@pages/register";
import AutoLogin from "@pages/login/autoLogin";
import Settings from "@pages/settings";
import EditPlan from "@pages/manage/editPlan";
import ManagePlans from "@pages/manage/plans";
import Exercises from "@pages/exercises";
import Leaderboard from "@pages/leaderboard";
import Train from "@pages/train";
import Error404 from "@pages/error/404";
import Error418 from "@pages/error/418";
Expand All @@ -29,22 +24,25 @@ import "moment/locale/en-gb";

// import available languages from ant locales
import useLanguageUpdater from "@hooks/languageUpdater";
import { useAppDispatch, useAppSelector } from "@redux/hooks";
import Login from "@pages/login";
import useApi from "@hooks/api";
import ApiRoutes from "@util/routes";
import { login } from "@redux/profile/profileSlice";

/**
* Wraps the normal {@link App} with a {@link ConfigProvider} to set the locale of the app.
* @returns {JSX.Element} The app.
*/
const LocalizedApp: React.FC = (): JSX.Element => {
const token = useAppSelector((state) => state.token.token);
const languageUpdater = useLanguageUpdater();

const loggedIn = useAppSelector((state) => state.profile.loggedIn);

useEffect(() => {
if (!token) {
return;
}
languageUpdater.updateLanguage();
if (loggedIn) languageUpdater.updateLanguage();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
}, [loggedIn]);

return (
<ConfigProvider>
Expand All @@ -58,12 +56,31 @@ const LocalizedApp: React.FC = (): JSX.Element => {
* @returns {JSX.Element} The app.
*/
const App: React.FC = (): JSX.Element => {
const token = useAppSelector((state) => state.token.token);
const refreshToken = useAppSelector((state) => state.token.refreshToken);

const useQuery = new URLSearchParams(useLocation().search);
const new_user_token = useQuery.get("new_user_token");
const reset_token = useQuery.get("reset_token");
const username = useQuery.get("username");
const loggedIn = useAppSelector((state) => state.profile.loggedIn);
const role = useAppSelector((state) => state.profile.role);
const api = useApi();
const dispatch = useAppDispatch();

useEffect(() => {
// test for login when loading the app
api.execute(ApiRoutes.checkLogin()).then((response) => {
console.log(response);
if (!response || !response.success) {
return;
}
dispatch(
login({
username: response.data["username"],
role: response.data["role"],
})
);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

if (new_user_token && reset_token) {
return <Error418 />;
Expand All @@ -73,31 +90,23 @@ const App: React.FC = (): JSX.Element => {
return <Register registerToken={new_user_token} />;
}

if (reset_token) {
return <ResetForm resetToken={reset_token} />;
if ((reset_token || username) && !reset_token && !username) {
return <Error418 />;
}

if (
!Helper.isRefreshTokenValid(refreshToken) &&
!Helper.isSessionTokenValid(token)
) {
return <Login />;
if (reset_token && username) {
return <ResetForm resetToken={reset_token} username={username} />;
}

if (!Helper.isSessionTokenValid(token)) {
return <AutoLogin />;
if (!loggedIn) {
return <Login />;
}

const isUser = token && Helper.getAccountType(token) === "user";
const isTrainer = token && Helper.getAccountType(token) === "trainer";
const isAdmin = token && Helper.getAccountType(token) === "admin";

if (isUser) {
if (role === "player") {
return (
<Routes>
<Route path="/" element={<Exercises />} />
<Route path="/train/:exercisePlanId" element={<Train />} />
<Route path="/leaderboard" element={<Leaderboard />} />
<Route path="/profile" element={<UserProfile />} />
<Route path="/settings" element={<Settings />} />
<Route path="/settings/change_password" element={<ChangePassword />} />
Expand All @@ -106,11 +115,10 @@ const App: React.FC = (): JSX.Element => {
);
}

if (isTrainer) {
if (role === "trainer") {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/leaderboard" element={<Leaderboard />} />
<Route path="/profile" element={<TrainerProfile />} />
<Route path="/settings" element={<Settings />} />
<Route path="/settings/change_password" element={<ChangePassword />} />
Expand All @@ -124,7 +132,7 @@ const App: React.FC = (): JSX.Element => {
);
}

if (isAdmin) {
if (role === "admin") {
return (
<Routes>
<Route path="/" element={<Home />} />
Expand Down
134 changes: 38 additions & 96 deletions src/hooks/api.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import { useAppDispatch, useAppSelector } from "@redux/hooks";
import { Route } from "@util/routes";
import { unsetRefreshToken, unsetToken } from "@redux/token/tokenSlice";
import { message } from "antd";
import { useTranslation } from "react-i18next";
import Translations from "@localization/translations";
import { useAppDispatch } from "@redux/hooks";

/**
* This api handles all requests to the backend.
* @returns {Api} The api.
*/
const useApi = () => {
const token = useAppSelector((state) => state.token.token) ?? "";
const dispatch = useAppDispatch();
const { t } = useTranslation();

/**
* Executes a requests to the backend with the given {@link Route}.
Expand All @@ -28,7 +22,7 @@ const useApi = () => {
let response;
switch (route.method) {
case "GET":
response = executeGet(route);
response = get(route.route);
break;
case "POST":
response = executePost(route);
Expand All @@ -38,37 +32,21 @@ const useApi = () => {
if (response.success) {
return;
}
if (response.description === "Token is not valid") {
message.error(t(Translations.errors.loggedOut));
dispatch(unsetToken());
dispatch(unsetRefreshToken());
if (response.status === 401) {
dispatch({ type: "USER_LOGOUT" });
}
});
return response.catch((error) => {
console.error(error);
return {
status: 500,
success: false,
data: {},
description: "Unable to connect to server.",
};
});
};

/**
* Fetches a {@link Route} with the GET method.
* It forwards the call to {@link getWithAuth} or {@link get} depending on whether the route requires
* authentication or not.
* @param {Route} route the given {@link Route}
* @returns {Promise<Response>} The response of the request.
*/
const executeGet = (route: Route): Promise<ApiResponse> => {
if (route.needsAuth) {
return getWithAuth(route.route);
} else {
return get(route.route);
}
};

/**
* Fetches a {@link Route} with the GET method without authentication.
* @param {Route} route the given {@link Route}
Expand All @@ -77,20 +55,15 @@ const useApi = () => {
const get = (route: string): Promise<ApiResponse> => {
return fetch(parseRoute(route), {
method: "GET",
}).then((r) => r.json());
};

/**
* Fetches a {@link Route} with the GET method with authentication.
* The user's session token will be sent inside the request header.
* @param {Route} route the given {@link Route}
* @returns {Promise<Response>} The response of the request.
*/
const getWithAuth = (route: string): Promise<ApiResponse> => {
return fetch(parseRoute(route), {
method: "GET",
headers: { "Session-Token": token },
}).then((r) => r.json());
credentials: "include",
}).then((r) => {
if (r.status !== 200) {
return { status: r.status, success: false };
}
return r.json().then((data) => {
return { ...data, status: r.status };
});
});
};

/**
Expand All @@ -103,18 +76,10 @@ const useApi = () => {
* @returns {Promise<Response>} The response of the request.
*/
const executePost = (route: Route): Promise<ApiResponse> => {
if (route.needsAuth) {
if (route.body == null) {
return postWithAuth(route.route);
} else {
return postWithAuthAndBody(route.route, route.body);
}
if (route.body == null) {
return post(route.route);
} else {
if (route.body == null) {
return post(route.route);
} else {
return postWithBody(route.route, route.body);
}
return postWithBody(route.route, route.body);
}
};

Expand All @@ -126,7 +91,15 @@ const useApi = () => {
const post = (route: string): Promise<ApiResponse> => {
return fetch(parseRoute(route), {
method: "POST",
}).then((r) => r.json());
credentials: "include",
}).then((r) => {
if (r.status !== 200) {
return { status: r.status, success: false };
}
return r.json().then((data) => {
return { ...data, status: r.status };
});
});
};

/**
Expand All @@ -141,41 +114,17 @@ const useApi = () => {
): Promise<ApiResponse> => {
return fetch(parseRoute(route), {
method: "POST",
body: JSON.stringify(body),
credentials: "include",
headers: { "Content-Type": "application/json" },
}).then((r) => r.json());
};

/**
* Requests a {@link Route} with the POST method with authentication and without any data to send.
* @param {Route} route the given {@link Route}
* @returns {Promise<Response>} The response of the request.
*/
const postWithAuth = (route: string): Promise<ApiResponse> => {
return fetch(parseRoute(route), {
method: "POST",
headers: { "Session-Token": token },
}).then((r) => r.json());
};

/**
* Requests a {@link Route} with the POST method with authentication and with data to send.
* @param {Route} route the given {@link Route}
* @param {Record<string, unkown>} body object containing the data to send
* @returns {Promise<Response>} The response of the request.
*/
const postWithAuthAndBody = (
route: string,
body: Record<string, unknown>
): Promise<ApiResponse> => {
return fetch(parseRoute(route), {
method: "POST",
body: JSON.stringify(body),
headers: {
"Session-Token": token,
"Content-Type": "application/json",
},
}).then((r) => r.json());
}).then((r) => {
if (r.status !== 200) {
return { status: r.status, success: false };
}
return r.json().then((data) => {
return { ...data, status: r.status };
});
});
};

/**
Expand All @@ -200,7 +149,7 @@ const useApi = () => {
* @returns {ApiSocketConnection} the created {@link ApiSocketConnection}
*/
const openSocket = (): ApiSocketConnection => {
return new ApiSocketConnection(token, window._env_.WEBSOCKET_URL);
return new ApiSocketConnection(window._env_.WEBSOCKET_URL);
};

return { execute, openSocket };
Expand All @@ -210,20 +159,12 @@ const useApi = () => {
* Utility class to handle the websocket connection to the backend.
*/
export class ApiSocketConnection {
readonly token: string;
private ws: WebSocket;

constructor(token: string, url: string) {
this.token = token;
constructor(url: string) {
this.ws = new WebSocket(url + "/ws/socket");

this.ws.onopen = (event) => {
this.ws.send(
JSON.stringify({
message_type: "authenticate",
data: { session_token: this.token },
})
);
if (this.onopen) this.onopen(event);
};

Expand Down Expand Up @@ -288,6 +229,7 @@ export class ApiSocketConnection {
* Wrapper for any response from the backend.
*/
interface ApiResponse {
status: number;
success: boolean;
description: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
Loading

0 comments on commit 91afde3

Please sign in to comment.