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 => (
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}