Compare commits

...

1 Commits

Author SHA1 Message Date
9d372bee86
Upgrade deps 2022-01-05 16:28:49 +07:00
35 changed files with 532 additions and 265 deletions

View File

@ -14,8 +14,8 @@
"dependencies": { "dependencies": {
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^6.2.1",
"web-vitals": "^1.0.1" "web-vitals": "^2.1.2"
}, },
"scripts": { "scripts": {
"start": "craco start", "start": "craco start",
@ -49,11 +49,11 @@
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15", "@types/jest": "^26.0.15",
"@types/node": "^12.0.0", "@types/node": "^12.0.0",
"@types/react": "^17.0.0", "@types/react": "^17.0.2",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.2",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "^5.3.2",
"craco-less": "^2.0.0", "craco-less": "^2.0.0",
"prettier": "^2.3.1", "prettier": "^2.5.1",
"react-scripts": "5.0.0", "react-scripts": "5.0.0",
"typescript": "^4.5.4" "typescript": "^4.5.4"
} }

View File

@ -1,11 +0,0 @@
import AppProvider from '@/provider/AppProvider'
import RouterView from '../routes'
import './App.less'
const App = () => {
return (
<AppProvider><RouterView /></AppProvider>
)
}
export default App

18
src/app/index.tsx Normal file
View File

@ -0,0 +1,18 @@
import { BrowserRouter as Router } from 'react-router-dom'
import RouterView from '@/routes'
import { AuthProvider, LayoutProvider } from '@/context'
import './app.less'
const App = () => {
return (
<Router>
<AuthProvider>
<LayoutProvider>
<RouterView />
</LayoutProvider>
</AuthProvider>
</Router>
)
}
export default App

6
src/constants/index.ts Normal file
View File

@ -0,0 +1,6 @@
export const TOKEN_KEY = 'token'
export const ROUTE = {
ROOT: '/',
LOGIN: '/login',
}

View File

@ -1,131 +0,0 @@
import { AppConfig } from '@/config'
import { printError, printInfo } from '@/utils/log_util'
import { getStorage, setStorage } from '@/utils/ls_util'
import React, { useContext, useCallback, useReducer, useEffect } from 'react'
interface AuthContextState {
login: (args: { username: string; password: string }) => Promise<void>
state: {
user: {
username: string
userId: string
} | null
}
getToken: () => string | undefined | null
logout: () => void
isLogin: () => boolean
}
enum AuthActionType {
'LOGIN' = 'LOGIN',
'LOGOUT' = 'LOGOUT',
}
const AuthReducer: (
state: AuthContextState['state'],
action: {
type: AuthActionType
payload?: any
}
) => AuthContextState['state'] = (state, action) => {
switch (action.type) {
case AuthActionType.LOGIN:
return {
...state,
user: action.payload,
}
case AuthActionType.LOGOUT:
return {
...state,
user: null,
}
default:
return state
}
}
const AuthContext = React.createContext<AuthContextState>({
login: async () => {},
state: {
user: null,
},
getToken: () => {
return undefined
},
logout: () => {},
isLogin: () => { return false },
})
const AuthProvider: React.FC = (props) => {
const [state, dispatch] = useReducer(AuthReducer, {
user: null,
})
const login = async (args: { username: string; password: string }) => {
const res = {
data: {
token: 'DEMO_HAH',
},
}
setStorage(AppConfig.CLIENT_TOKEN_KEY, res.data.token)
await verify()
dispatch({
type: AuthActionType.LOGIN,
payload: args,
})
return
}
useEffect(() => {
const doVerify = async () => {
const token = getStorage(AppConfig.CLIENT_TOKEN_KEY)
const userAuthInfo = {}
printInfo("User details =>", token, userAuthInfo)
}
doVerify()
}, [])
const verify = async () => {
const token = getStorage(AppConfig.CLIENT_TOKEN_KEY)
printError('Verify not implemented with token: ', token)
const userAuthInfo = {}
return userAuthInfo
}
const logout = useCallback(() => {
setStorage(AppConfig.CLIENT_TOKEN_KEY, undefined)
dispatch({
type: AuthActionType.LOGOUT,
})
}, [])
const getToken = () => getStorage(AppConfig.CLIENT_TOKEN_KEY)
const isLogin = (): boolean => {
return getToken() != null
}
return (
<AuthContext.Provider
value={{
login,
state,
logout,
isLogin,
getToken,
}}
>
{props.children}
</AuthContext.Provider>
)
}
export const useAuthContext = () => {
return useContext(AuthContext)
}
export default AuthProvider

View File

@ -0,0 +1,149 @@
import React, { useContext, useCallback, useReducer, useEffect } from 'react'
import { ROUTE, TOKEN_KEY } from '@/constants'
import { useNavigate, useLocation } from 'react-router-dom'
import { handleError } from '@/helpers'
interface AuthContextState {
login: (args: {
username: string
password: string
}) => Promise<UserAuth | null>
state: {
user: UserAuth | null
}
getToken: () => string | undefined | null
logout: () => void
}
export interface UserAuth {
username: string
id: string
email: string
phone: string
getToken: () => string | null
roles: string[]
profile: any
authorities: string[]
}
enum AuthActionType {
'LOGIN' = 'LOGIN',
'LOGOUT' = 'LOGOUT',
}
const AuthReducer: (
state: AuthContextState['state'],
action: {
type: AuthActionType
payload?: any
}
) => AuthContextState['state'] = (state, action) => {
switch (action.type) {
case AuthActionType.LOGIN:
return {
...state,
user: action.payload,
}
case AuthActionType.LOGOUT:
return {
...state,
user: null,
}
default:
return state
}
}
const AuthContext = React.createContext<AuthContextState>({
login: async () => null,
state: {
user: null,
},
getToken: () => {
return undefined
},
logout: () => {},
})
const AuthProvider: React.FC = (props) => {
const [state, dispatch] = useReducer(AuthReducer, {
user: null,
})
const login = async (args: { username: string; password: string }) => {
let userAuthInfo: UserAuth | null = null
try {
localStorage.setItem(TOKEN_KEY, '')
userAuthInfo = await verify()
dispatch({
type: AuthActionType.LOGIN,
payload: userAuthInfo,
})
} catch (e) {
handleError(e)
}
return userAuthInfo
}
const navigate = useNavigate()
const location = useLocation()
useEffect(() => {
const doVerify = async () => {
const userAuthInfo = await verify()
if (userAuthInfo === null && location.pathname !== '/login') {
await navigate(ROUTE.LOGIN)
} else if (
userAuthInfo !== null &&
location.pathname === '/login'
) {
await navigate(ROUTE.ROOT)
}
}
doVerify()
}, [])
const verify = async () => {
const token = localStorage.getItem(TOKEN_KEY) as string
let userAuthInfo: UserAuth | null = null
try {
dispatch({
type: AuthActionType.LOGIN,
payload: userAuthInfo,
})
} catch (e) {
console.error(e)
}
return userAuthInfo
}
const logout = useCallback(async () => {
localStorage.setItem(TOKEN_KEY, '')
dispatch({
type: AuthActionType.LOGOUT,
})
navigate(ROUTE.LOGIN)
}, [state])
return (
<AuthContext.Provider
value={{
login,
state,
logout,
getToken: () => {
return localStorage.getItem(TOKEN_KEY)
},
}}
>
{props.children}
</AuthContext.Provider>
)
}
export const useAuthContext = () => {
return useContext(AuthContext)
}
export default AuthProvider

4
src/context/index.ts Normal file
View File

@ -0,0 +1,4 @@
import AuthProvider, { useAuthContext } from './auth.context'
import LayoutProvider, { useLayout } from './layout.context'
export { AuthProvider, LayoutProvider, useLayout, useAuthContext }

View File

@ -0,0 +1,38 @@
import React, { createContext, useContext, useState, useEffect } from 'react'
import { useLocation } from 'react-router-dom'
interface LayoutContextState {
selectedSideMenuKeys: string[]
setSelectedSideMenuKeys: React.Dispatch<React.SetStateAction<string[]>>
}
const LayoutContext = createContext<LayoutContextState>({
selectedSideMenuKeys: [],
setSelectedSideMenuKeys: () => {},
})
const LayoutProvider: React.FC = (props) => {
const [selectedSideMenuKeys, setSelectedSideMenuKeys] = useState<
LayoutContextState['selectedSideMenuKeys']
>([])
const location = useLocation()
useEffect(() => {
if (location.pathname === '/') {
setSelectedSideMenuKeys([])
}
}, [location.pathname])
return (
<LayoutContext.Provider
value={{ selectedSideMenuKeys, setSelectedSideMenuKeys }}
>
{props.children}
</LayoutContext.Provider>
)
}
export const useLayout = () => useContext(LayoutContext)
export default LayoutProvider

View File

@ -0,0 +1,6 @@
const handleError = (e: any) => {
console.error(e)
return <p>See console</p>
}
export default handleError

3
src/helpers/index.ts Normal file
View File

@ -0,0 +1,3 @@
import handleError from './error.helper'
export { handleError }

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import './index.less' import './index.less'
import App from '@/app/App' import App from '@/app'
import reportWebVitals from './reportWebVitals' import reportWebVitals from './reportWebVitals'
ReactDOM.render( ReactDOM.render(

View File

@ -0,0 +1,15 @@
import React from 'react'
const BaseLayout: React.FC<{
renderSideMenu: () => React.ReactNode
}> = (props) => {
const { renderSideMenu } = props
return (
<div>
{renderSideMenu()}
<div>{props.children}</div>
</div>
)
}
export default BaseLayout

3
src/layouts/index.ts Normal file
View File

@ -0,0 +1,3 @@
import BaseLayout from './base.layout'
export { BaseLayout }

7
src/pages/error/index.ts Normal file
View File

@ -0,0 +1,7 @@
import AccessDenied from "./403";
import NotFound from "./404";
export {
AccessDenied,
NotFound,
}

8
src/pages/index.ts Normal file
View File

@ -0,0 +1,8 @@
import About from './about'
import Home from './home'
import Info from './info'
import Login from './login'
import Profile from './profile'
import Reset from './reset'
export { Login, About, Home, Info, Profile, Reset }

View File

@ -1,7 +1,7 @@
import { RouteTypes } from '@/routes/types' import { RouteTypes } from '@/routes/types'
import { clearStorage } from '@/utils/ls_util' import { clearStorage } from '@/utils/ls_util'
import { useEffect } from 'react' import { useEffect } from 'react'
import { Redirect } from 'react-router' import { Navigate } from 'react-router'
const Reset = () => { const Reset = () => {
const clearAllCaches = () => { const clearAllCaches = () => {
@ -17,7 +17,7 @@ const Reset = () => {
clearAllCaches() clearAllCaches()
}, []) }, [])
return <Redirect to={RouteTypes.HOME} /> return <Navigate to={RouteTypes.HOME} />
} }
export default Reset export default Reset

View File

@ -1,5 +0,0 @@
const AppProvider = (props: any) => {
return <>{props.children}</>
}
export default AppProvider

View File

@ -1,21 +0,0 @@
import { useAuthContext } from '@/context/AuthContext'
import AccessDenied from '@/pages/Error/403'
import { Route } from 'react-router-dom'
const AuthRoute = (props: any) => {
const { component, ...rest } = props
const { isLogin } = useAuthContext()
if (!isLogin()) {
return (<AccessDenied />)
}
return (
<Route
{...rest}
render={component}
/>
)
}
export default AuthRoute

19
src/routes/auth.route.tsx Normal file
View File

@ -0,0 +1,19 @@
import { TOKEN_KEY } from '@/constants'
import React from 'react'
import { Route, useNavigate } from 'react-router-dom'
const AuthRoute: React.FC<{
component: () => React.ReactNode
}> = (props) => {
const { component, ...rest } = props
const navigate = useNavigate()
const token = localStorage.getItem(TOKEN_KEY) as string
if (!token) {
navigate('/login')
}
return <Route {...rest} element={component} />
}
export default AuthRoute

View File

@ -1,35 +1,186 @@
import AuthProvider from '@/context/AuthContext' import { useCallback } from 'react'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom' import routes, { Authority, sideMenuRouteObjs, SideSubMenuObj } from './routes'
import AuthRoute from './AuthRoute' import { Routes, Route, Link, useLocation } from 'react-router-dom'
import { routes } from './routes' import AuthRoute from './auth.route'
import { BaseLayout } from '@/layouts'
import { useLayout, useAuthContext } from '@/context'
import { Login } from '@/pages'
import { AccessDenied } from '@/pages/error'
const renderRoute = (item: any) => {
const routeProps = {
key: item.key,
path: item.path,
exact: item.exact || true,
component:
item?.disabled === true ? () => <AccessDenied /> : item.component,
headerLabel: item.headerLabel,
}
return <AuthRoute {...routeProps} />
}
const newSideMenuRouteObjs: any[] = sideMenuRouteObjs.reduce(
(acc, curr: any) => {
let routes: any[] = []
if (curr.subMenus) {
routes.push(...curr.subMenus)
} else {
routes.push(curr)
}
return [...acc, ...routes] as any
},
[]
)
const subMenusObjs: any[] = sideMenuRouteObjs.filter(
(item: any) => item.subMenus !== undefined
)
const RouterView = () => { const RouterView = () => {
return ( const { state } = useAuthContext()
<Router> const { selectedSideMenuKeys, setSelectedSideMenuKeys } = useLayout()
<Switch>
{routes.map((route) => { const authorities = (state && state.user && state.user.authorities) || []
const { withAuthority } = route const location = useLocation()
if (withAuthority) {
return ( const AuthChecker = (item: any): any | null => {
<AuthProvider key={route.key}> const authority = item?.authority as Authority
<AuthRoute {...route} /> if (!authority || !authority.privileges) return item
</AuthProvider> const privileges = authority.privileges
)
if (typeof privileges === 'string') {
// have permission
if (authorities.some((e) => e === privileges)) return item
} else if (Array.isArray(privileges)) {
// have permission
if (authority.strict === true) {
if (privileges.every((e) => authorities.some((a) => e === a)))
return item
} else { } else {
return ( if (privileges.some((e) => authorities.some((a) => e === a)))
<Route return item
key={route.key}
exact={route.exact}
component={route.component}
children={route.children}
location={route.location}
path={route.path}
/>
)
} }
}
if (authority.hideNoPrivilege === false) {
return {
...item,
disabled: true,
}
}
// not have permission
// ignore and return null for no permission
return null
}
const AuthFilterItems = (items: any[]) => {
const filters = items.map((item) => AuthChecker(item))
return filters.filter((s) => s !== null)
}
const renderMenuItem = useCallback((item: any) => {
const { key, path, label, disabled } = item
return (
<div
//disabled={disabled || false}
onClick={() => {
setSelectedSideMenuKeys([key])
}}
key={key}
// icon={item.icon}
>
<Link to={path}>{label}</Link>
</div>
)
}, [])
const renderSideMenuLinks = useCallback(() => {
const defaultSelectedKey = newSideMenuRouteObjs.find((item) => {
return '/' + item.key === location.pathname
})
const defaultOpenKeys: string[] = []
const openSubMenusObj = subMenusObjs.find((item: SideSubMenuObj) => {
return item.subMenus.some((s) => '/' + s.key === location.pathname)
})
if (openSubMenusObj) {
defaultOpenKeys.push(openSubMenusObj.key)
}
return (
<div
style={{
height: 'calc(100vh - 64px)',
overflow: 'hidden auto',
}}
>
<div
// theme="light"
// mode="inline"
// defaultSelectedKeys={
// defaultSelectedKey ? [defaultSelectedKey.key] : []
// }
// defaultOpenKeys={defaultOpenKeys}
>
{AuthFilterItems(sideMenuRouteObjs).map((item) => {
const { subMenus } = item as any
return subMenus ? (
<div
key={item.key}
// icon={item.icon}
title={item.label}
>
{AuthFilterItems(subMenus).map((s: any) =>
renderMenuItem(s)
)}
</div>
) : (
renderMenuItem(item)
)
})} })}
</Switch> </div>
</Router> </div>
)
}, [])
if (location.pathname === '/login') {
return <Login />
}
const AuthRenderRoutes = () => {
const allRoutes = [...routes, ...newSideMenuRouteObjs]
return AuthFilterItems(allRoutes)
}
return (
<BaseLayout
renderSideMenu={() => {
return renderSideMenuLinks()
}}
>
<Routes>
<Route
path={'/'}
element={() => {
return (
<>
{AuthRenderRoutes().map((item) =>
renderRoute(item)
)}
</>
)
}}
/>
</Routes>
</BaseLayout>
) )
} }

View File

@ -10,10 +10,12 @@ export interface AuthorityProps {
withAuthority?: boolean | undefined withAuthority?: boolean | undefined
authorities?: Authority | string[] | string | undefined authorities?: Authority | string[] | string | undefined
strictAuthority?: boolean | undefined strictAuthority?: boolean | undefined
hideNoPrivilege?: boolean | undefined
} }
export interface CustomRouteProps extends RouteProps, AuthorityProps { export interface CustomRouteProps extends RouteProps, AuthorityProps {
key: string key: string
location?: string | undefined
} }
export interface MenuProps { export interface MenuProps {

View File

@ -1,57 +1,65 @@
import About from '@/pages/About' import React from 'react'
import NotFound from '@/pages/Error/404' import { Reset, Home, Profile, Info } from '@/pages'
import Home from '@/pages/Home'
import Info from '@/pages/Info'
import Login from '@/pages/Login'
import Profile from '@/pages/Profile'
import Reset from '@/pages/Reset'
import { CustomRouteProps } from '@/routes/interfaces'
import { RouteTypes } from '@/routes/types'
const routes: CustomRouteProps[] = [ export interface Authority {
// Auth privileges?: string | Array<string>
// if true => AND | is false => OR (related to privileges)
// compare privileges of reponse and privileges above
strict?: boolean
// if true, hide or not render. Else show but disabled (element/children)
hideNoPrivilege?: boolean
}
export interface RouteObj {
path: string
component: () => React.ReactNode
exact?: boolean
key: string
headerLabel?: string
authority?: Authority
}
export interface SideMenuRouteObj extends RouteObj {
icon?: any
label: string
}
export interface SideSubMenuObj {
icon: any
label: string
key: string
subMenus: SideMenuRouteObj[]
}
const sideMenuRouteObjs: (SideMenuRouteObj | SideSubMenuObj)[] = [
{ {
key: 'login',
path: RouteTypes.LOGIN,
component: () => <Login />,
},
{
key: 'home',
exact: true,
path: RouteTypes.HOME,
component: () => <Home />, component: () => <Home />,
path: '/',
key: 'home',
headerLabel: '',
label: 'Home',
// authority: {
// privileges: ['ADMIN', 'USER'],
// hideNoPrivilege: false,
// strict: true,
// }
}, },
{ {
key: 'about',
path: RouteTypes.ABOUT,
component: () => <About />,
},
{
exact: true,
key: 'info',
path: RouteTypes.INFO,
component: () => <Info />,
},
{
exact: true,
key: 'reset',
path: RouteTypes.RESET,
component: () => <Reset />,
},
{
exact: true,
key: 'profile',
path: RouteTypes.PROFILE,
component: () => <Profile />, component: () => <Profile />,
withAuthority: true, path: '/profile',
}, key: 'profile',
headerLabel: '',
// Errors label: 'Profile',
{
key: 'notfound',
path: RouteTypes.ERROR_404,
component: () => <NotFound />,
}, },
] ]
export { routes } const routes: RouteObj[] = [
{
path: '/reset',
component: () => <Reset />,
key: 'reset',
headerLabel: 'Reset',
},
]
export { sideMenuRouteObjs, routes as default }

View File

@ -1,17 +1,15 @@
const RouteTypes = { const RouteTypes = {
HOME: "/", HOME: '/',
ABOUT: "/about", ABOUT: '/about',
PROFILE: "/profile", PROFILE: '/profile',
INFO: "/info", INFO: '/info',
RESET: "/reset", RESET: '/reset',
// Auth // Auth
LOGIN: '/login', LOGIN: '/login',
// Errors // Errors
ERROR_404: "**", ERROR_404: '*',
} }
export { export { RouteTypes }
RouteTypes,
}