From e37aa919ccff14000ce460c057be8ea217eb260c Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 16 Oct 2022 11:46:44 +0200 Subject: [PATCH] added dashboard buttons; added use user hook --- src/App.tsx | 15 ++-- src/AuthContext/AuthContextProvider.tsx | 36 +++++++- src/apis/refresh-token.ts | 8 +- src/hooks/index.ts | 2 + src/hooks/use-user.ts | 20 +++++ .../AuthenticateRoute/NavigationButton.tsx | 66 ++++++++++++++ ...ElementRoute.tsx => AuthenticateRoute.tsx} | 2 +- src/routes/AuthenticatedRoute.tsx | 85 +++++++++++++++++++ 8 files changed, 222 insertions(+), 12 deletions(-) create mode 100644 src/hooks/use-user.ts create mode 100644 src/route-widgets/AuthenticateRoute/NavigationButton.tsx rename src/routes/{SingleElementRoute.tsx => AuthenticateRoute.tsx} (93%) create mode 100644 src/routes/AuthenticatedRoute.tsx diff --git a/src/App.tsx b/src/App.tsx index ddde9a8..0bba9e4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,9 +8,10 @@ import {queryClient} from "~/constants/react-query" import {lightTheme} from "~/constants/themes" import {getServerSettings} from "~/apis" import AuthContextProvider from "~/AuthContext/AuthContextProvider" +import AuthenticateRoute from "~/routes/AuthenticateRoute" +import AuthenticatedRoute from "~/routes/AuthenticatedRoute" import RootRoute from "~/routes/Root" import SignupRoute from "~/routes/SignupRoute" -import SingleElementRoute from "~/routes/SingleElementRoute" import VerifyEmailRoute from "~/routes/VerifyEmailRoute" const router = createBrowserRouter([ @@ -20,21 +21,25 @@ const router = createBrowserRouter([ errorElement:
, children: [ { - path: "/", - element: , + path: "/auth", + element: , children: [ { loader: getServerSettings, - path: "/verify-email", + path: "/auth/verify-email", element: , }, { loader: getServerSettings, - path: "/signup", + path: "/auth/signup", element: , }, ], }, + { + path: "/", + element: , + }, ], }, ]) diff --git a/src/AuthContext/AuthContextProvider.tsx b/src/AuthContext/AuthContextProvider.tsx index d9b6e19..d3b1c10 100644 --- a/src/AuthContext/AuthContextProvider.tsx +++ b/src/AuthContext/AuthContextProvider.tsx @@ -1,11 +1,16 @@ -import {ReactElement, ReactNode, useCallback, useMemo} from "react" -import {AxiosError} from "axios" +import {ReactElement, ReactNode, useCallback, useEffect, useMemo} from "react" import {useLocalStorage} from "react-use" +import axios, {AxiosError} from "axios" import {useMutation} from "@tanstack/react-query" import {User} from "~/server-types" -import {RefreshTokenResult, logout as logoutUser, refreshToken} from "~/apis" +import { + REFRESH_TOKEN_URL, + RefreshTokenResult, + logout as logoutUser, + refreshToken, +} from "~/apis" import AuthContext, {AuthContextType} from "./AuthContext" @@ -51,5 +56,30 @@ export default function AuthContextProvider({ [refresh, login, logout], ) + useEffect(() => { + const interceptor = axios.interceptors.response.use( + response => response, + async (error: AxiosError) => { + if (error.isAxiosError) { + if (error.response?.status === 401) { + // Check if error comes from refreshing the token. + // If yes, the user has been logged out completely. + const request: XMLHttpRequest = error.request + + if (request.responseURL === REFRESH_TOKEN_URL) { + await logout(false) + } else { + await refresh() + } + } + } + + throw error + }, + ) + + return () => axios.interceptors.response.eject(interceptor) + }, [logout, refresh]) + return {children} } diff --git a/src/apis/refresh-token.ts b/src/apis/refresh-token.ts index 9f80f06..7c460d3 100644 --- a/src/apis/refresh-token.ts +++ b/src/apis/refresh-token.ts @@ -7,10 +7,12 @@ export interface RefreshTokenResult { detail: string } +export const REFRESH_TOKEN_URL = `${ + import.meta.env.VITE_SERVER_BASE_URL +}/api/refresh-token` + export default async function refreshToken(): Promise { - const {data} = await axios.post( - `${import.meta.env.VITE_SERVER_BASE_URL}/api/refresh-token`, - ) + const {data} = await axios.post(REFRESH_TOKEN_URL) return data } diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 5da00ae..e117140 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,3 +2,5 @@ export * from "./use-interval-update" export {default as useIntervalUpdate} from "./use-interval-update" export * from "./use-query-params" export {default as useQueryParams} from "./use-query-params" +export * from "./use-user" +export {default as useUser} from "./use-user" diff --git a/src/hooks/use-user.ts b/src/hooks/use-user.ts new file mode 100644 index 0000000..fe3dfb6 --- /dev/null +++ b/src/hooks/use-user.ts @@ -0,0 +1,20 @@ +import {useNavigate} from "react-router-dom" +import {useContext, useLayoutEffect} from "react" + +import {User} from "~/server-types" +import AuthContext from "~/AuthContext/AuthContext" + +/// Returns the currently authenticated user. +// If the user is not authenticated, it will automatically redirect to the login page. +export default function useUser(): User { + const navigate = useNavigate() + const {user, isAuthenticated} = useContext(AuthContext) + + useLayoutEffect(() => { + if (!isAuthenticated) { + navigate("/login") + } + }, [isAuthenticated, navigate]) + + return user as User +} diff --git a/src/route-widgets/AuthenticateRoute/NavigationButton.tsx b/src/route-widgets/AuthenticateRoute/NavigationButton.tsx new file mode 100644 index 0000000..8dae933 --- /dev/null +++ b/src/route-widgets/AuthenticateRoute/NavigationButton.tsx @@ -0,0 +1,66 @@ +import {ReactElement} from "react" +import {BiStats} from "react-icons/bi" +import {MdMail, MdSettings} from "react-icons/md" +import {IoMdDocument} from "react-icons/io" +import {Link as RouterLink, useLocation} from "react-router-dom" + +import {Button} from "@mui/material" + +export enum NavigationSection { + Overview, + Aliases, + Reports, + Settings, +} + +export interface NavigationButtonProps { + section: NavigationSection +} + +const SECTION_ICON_MAP: Record = { + [NavigationSection.Overview]: , + [NavigationSection.Aliases]: , + [NavigationSection.Reports]: , + [NavigationSection.Settings]: , +} + +const SECTION_TEXT_MAP: Record = { + [NavigationSection.Overview]: "Overview", + [NavigationSection.Aliases]: "Aliases", + [NavigationSection.Reports]: "Reports", + [NavigationSection.Settings]: "Settings", +} + +const PATH_SECTION_MAP: Record = { + "/": NavigationSection.Overview, + "/aliases": NavigationSection.Aliases, + "/reports": NavigationSection.Reports, + "/settings": NavigationSection.Settings, +} + +export default function NavigationButton({ + section, +}: NavigationButtonProps): ReactElement { + const location = useLocation() + + const currentSection = PATH_SECTION_MAP[location.pathname] + const Icon = SECTION_ICON_MAP[section] + const text = SECTION_TEXT_MAP[section] + + return ( + + ) +} diff --git a/src/routes/SingleElementRoute.tsx b/src/routes/AuthenticateRoute.tsx similarity index 93% rename from src/routes/SingleElementRoute.tsx rename to src/routes/AuthenticateRoute.tsx index bbf5d69..0570e76 100644 --- a/src/routes/SingleElementRoute.tsx +++ b/src/routes/AuthenticateRoute.tsx @@ -4,7 +4,7 @@ import {MdAdd, MdLogin} from "react-icons/md" import {Box, Button, Grid} from "@mui/material" -export default function SingleElementRoute(): ReactElement { +export default function AuthenticateRoute(): ReactElement { return ( { + switch (route.pathname) { + case "/": + return Section.Overview + case "/aliases": + return Section.Aliases + case "/reports": + return Section.Reports + case "/settings": + return Section.Settings + } + })() + + useUser() + + return ( + + + + + + + {( + Object.keys(NavigationSection) as Array< + keyof typeof NavigationSection + > + ).map(key => ( + + + + ))} + + + + + + + + + + + + ) +}