added dashboard buttons; added use user hook

This commit is contained in:
Myzel394 2022-10-16 11:46:44 +02:00
parent 0a0b9b55f8
commit e37aa919cc
8 changed files with 222 additions and 12 deletions

View File

@ -8,9 +8,10 @@ import {queryClient} from "~/constants/react-query"
import {lightTheme} from "~/constants/themes" import {lightTheme} from "~/constants/themes"
import {getServerSettings} from "~/apis" import {getServerSettings} from "~/apis"
import AuthContextProvider from "~/AuthContext/AuthContextProvider" import AuthContextProvider from "~/AuthContext/AuthContextProvider"
import AuthenticateRoute from "~/routes/AuthenticateRoute"
import AuthenticatedRoute from "~/routes/AuthenticatedRoute"
import RootRoute from "~/routes/Root" import RootRoute from "~/routes/Root"
import SignupRoute from "~/routes/SignupRoute" import SignupRoute from "~/routes/SignupRoute"
import SingleElementRoute from "~/routes/SingleElementRoute"
import VerifyEmailRoute from "~/routes/VerifyEmailRoute" import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
const router = createBrowserRouter([ const router = createBrowserRouter([
@ -20,21 +21,25 @@ const router = createBrowserRouter([
errorElement: <div></div>, errorElement: <div></div>,
children: [ children: [
{ {
path: "/", path: "/auth",
element: <SingleElementRoute />, element: <AuthenticateRoute />,
children: [ children: [
{ {
loader: getServerSettings, loader: getServerSettings,
path: "/verify-email", path: "/auth/verify-email",
element: <VerifyEmailRoute />, element: <VerifyEmailRoute />,
}, },
{ {
loader: getServerSettings, loader: getServerSettings,
path: "/signup", path: "/auth/signup",
element: <SignupRoute />, element: <SignupRoute />,
}, },
], ],
}, },
{
path: "/",
element: <AuthenticatedRoute />,
},
], ],
}, },
]) ])

View File

@ -1,11 +1,16 @@
import {ReactElement, ReactNode, useCallback, useMemo} from "react" import {ReactElement, ReactNode, useCallback, useEffect, useMemo} from "react"
import {AxiosError} from "axios"
import {useLocalStorage} from "react-use" import {useLocalStorage} from "react-use"
import axios, {AxiosError} from "axios"
import {useMutation} from "@tanstack/react-query" import {useMutation} from "@tanstack/react-query"
import {User} from "~/server-types" 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" import AuthContext, {AuthContextType} from "./AuthContext"
@ -51,5 +56,30 @@ export default function AuthContextProvider({
[refresh, login, logout], [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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider> return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
} }

View File

@ -7,10 +7,12 @@ export interface RefreshTokenResult {
detail: string detail: string
} }
export const REFRESH_TOKEN_URL = `${
import.meta.env.VITE_SERVER_BASE_URL
}/api/refresh-token`
export default async function refreshToken(): Promise<RefreshTokenResult> { export default async function refreshToken(): Promise<RefreshTokenResult> {
const {data} = await axios.post( const {data} = await axios.post(REFRESH_TOKEN_URL)
`${import.meta.env.VITE_SERVER_BASE_URL}/api/refresh-token`,
)
return data return data
} }

View File

@ -2,3 +2,5 @@ export * from "./use-interval-update"
export {default as useIntervalUpdate} from "./use-interval-update" export {default as useIntervalUpdate} from "./use-interval-update"
export * from "./use-query-params" export * from "./use-query-params"
export {default as useQueryParams} from "./use-query-params" export {default as useQueryParams} from "./use-query-params"
export * from "./use-user"
export {default as useUser} from "./use-user"

20
src/hooks/use-user.ts Normal file
View File

@ -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
}

View File

@ -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, ReactElement> = {
[NavigationSection.Overview]: <BiStats />,
[NavigationSection.Aliases]: <MdMail />,
[NavigationSection.Reports]: <IoMdDocument />,
[NavigationSection.Settings]: <MdSettings />,
}
const SECTION_TEXT_MAP: Record<NavigationSection, string> = {
[NavigationSection.Overview]: "Overview",
[NavigationSection.Aliases]: "Aliases",
[NavigationSection.Reports]: "Reports",
[NavigationSection.Settings]: "Settings",
}
const PATH_SECTION_MAP: Record<string, NavigationSection> = {
"/": 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 (
<Button
fullWidth
color="inherit"
variant={section === currentSection ? "outlined" : "text"}
startIcon={Icon}
component={RouterLink}
to={
Object.keys(PATH_SECTION_MAP).find(
path => PATH_SECTION_MAP[path] === section,
) ?? "/"
}
>
{text}
</Button>
)
}

View File

@ -4,7 +4,7 @@ import {MdAdd, MdLogin} from "react-icons/md"
import {Box, Button, Grid} from "@mui/material" import {Box, Button, Grid} from "@mui/material"
export default function SingleElementRoute(): ReactElement { export default function AuthenticateRoute(): ReactElement {
return ( return (
<Box <Box
display="flex" display="flex"

View File

@ -0,0 +1,85 @@
import {ReactElement} from "react"
import {Outlet, useLocation} from "react-router-dom"
import {Box, Container, List, ListItem, Paper, useTheme} from "@mui/material"
import {useUser} from "~/hooks"
import NavigationButton, {
NavigationSection,
} from "~/route-widgets/AuthenticateRoute/NavigationButton"
enum Section {
Overview,
Aliases,
Reports,
Settings,
}
export default function AuthenticatedRoute(): ReactElement {
const theme = useTheme()
const route = useLocation()
const section = (() => {
switch (route.pathname) {
case "/":
return Section.Overview
case "/aliases":
return Section.Aliases
case "/reports":
return Section.Reports
case "/settings":
return Section.Settings
}
})()
useUser()
return (
<Box
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
height="100vh"
>
<Box width="90vw" justifyContent="center" alignItems="center">
<Container
maxWidth="md"
style={{
backgroundColor: "transparent",
}}
>
<Box
display="flex"
flexDirection="row"
justifyContent="center"
>
<Box
bgcolor={theme.palette.background.paper}
component="nav"
>
<List>
{(
Object.keys(NavigationSection) as Array<
keyof typeof NavigationSection
>
).map(key => (
<ListItem key={key}>
<NavigationButton
section={NavigationSection[key]}
/>
</ListItem>
))}
</List>
</Box>
<Paper>
<Box padding={4}>
<Outlet />
</Box>
</Paper>
</Box>
</Container>
</Box>
</Box>
)
}