mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 07:55:25 +02:00
added dashboard buttons; added use user hook
This commit is contained in:
parent
0a0b9b55f8
commit
e37aa919cc
15
src/App.tsx
15
src/App.tsx
@ -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 />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
20
src/hooks/use-user.ts
Normal 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
|
||||||
|
}
|
66
src/route-widgets/AuthenticateRoute/NavigationButton.tsx
Normal file
66
src/route-widgets/AuthenticateRoute/NavigationButton.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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"
|
85
src/routes/AuthenticatedRoute.tsx
Normal file
85
src/routes/AuthenticatedRoute.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user