Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
9d372bee86
|
|||
|
9d14fe60f0
|
|||
|
3d4e0e7664
|
|||
| 025ff76a20 | |||
|
a50437a27b
|
7
.dockerignore
Normal file
7
.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.vscode/
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
test/
|
||||||
|
yarn-error.log
|
||||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true,
|
||||||
|
"[dockerfile]": {
|
||||||
|
"editor.defaultFormatter": "ms-azuretools.vscode-docker"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Build application
|
||||||
|
FROM cubetiq/calpine-node AS builder
|
||||||
|
|
||||||
|
RUN apk update && \
|
||||||
|
# For build commit hash in "process.env.COMMIT_ID"
|
||||||
|
apk add git && \
|
||||||
|
apk add tzdata && \
|
||||||
|
cp /usr/share/zoneinfo/Asia/Phnom_Penh /etc/localtime && \
|
||||||
|
echo "Asia/Phnom_Penh" > /etc/timezone
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json ./
|
||||||
|
# Set custom registry for npm registry (from cubetiq local server)
|
||||||
|
RUN yarn config set registry https://r.ctdn.net
|
||||||
|
RUN yarn
|
||||||
|
COPY . .
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
# Clean up unused packages
|
||||||
|
RUN apk del tzdata && \
|
||||||
|
apk del git
|
||||||
|
|
||||||
|
# Build production image
|
||||||
|
FROM nginx:alpine
|
||||||
|
LABEL maintainer="sombochea@cubetiqs.com"
|
||||||
|
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
COPY --from=builder /app/build/ /usr/share/nginx/html
|
||||||
|
COPY --from=builder /app/docker/nginx.conf /etc/nginx/conf.d
|
||||||
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
@@ -1,16 +1,50 @@
|
|||||||
const path = require('path');
|
const path = require('path')
|
||||||
|
const webpack = require('webpack')
|
||||||
|
const npmPackage = require('./package.json')
|
||||||
|
const { v4: uuidv4 } = require('uuid')
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
|
||||||
|
// Get the last commit id/log
|
||||||
|
const gitFetchCommitIdCommand = 'git rev-parse HEAD'
|
||||||
|
|
||||||
|
// Execute the command
|
||||||
|
const fetchGitCommitId = () => {
|
||||||
|
try {
|
||||||
|
return execSync(gitFetchCommitIdCommand).toString().trim()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
return '-1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get current date (build date time)
|
||||||
|
const today = new Date()
|
||||||
|
|
||||||
|
// get date as version suffix: 10112021
|
||||||
|
const dateVersion = `${today.getDay()}${today.getMonth()}${today.getFullYear()}`
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
webpack: {
|
webpack: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, 'src/')
|
'@': path.resolve(__dirname, 'src/'),
|
||||||
}
|
},
|
||||||
},
|
plugins: {
|
||||||
jest: {
|
add: [
|
||||||
configure: {
|
new webpack.DefinePlugin({
|
||||||
moduleNameMapper: {
|
'process.env.PACKAGE_NAME': `"${npmPackage.name}"`,
|
||||||
'^@(.*)$': '<rootDir>/src$1'
|
'process.env.PACKAGE_VERSION': `"${dateVersion}"`,
|
||||||
}
|
'process.env.BUILD_NUMBER': `"${uuidv4()}"`,
|
||||||
}
|
'process.env.BUILD_DATE': `"${today.toLocaleString()}"`,
|
||||||
}
|
'process.env.COMMIT_ID': `"${fetchGitCommitId()}"`,
|
||||||
};
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
jest: {
|
||||||
|
configure: {
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^@(.*)$': '<rootDir>/src$1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
21
docker/nginx.conf
Normal file
21
docker/nginx.conf
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
# serve static files
|
||||||
|
location ~ ^/(images|javascript|js|css|flash|media|static)/ {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
expires 30d;
|
||||||
|
}
|
||||||
|
|
||||||
|
# serve default location
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
package.json
20
package.json
@@ -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",
|
||||||
@@ -43,18 +43,18 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@craco/craco": "^6.1.2",
|
"@craco/craco": "^6.4.3",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
"@testing-library/react": "^11.1.0",
|
"@testing-library/react": "^11.1.0",
|
||||||
"@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": "^1.17.1",
|
"craco-less": "^2.0.0",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.5.1",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "5.0.0",
|
||||||
"typescript": "^4.1.2"
|
"typescript": "^4.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
run-in-docker.sh
Executable file
9
run-in-docker.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh -e
|
||||||
|
|
||||||
|
echo "Building docker image..."
|
||||||
|
docker build . -t myreactapp:latest
|
||||||
|
|
||||||
|
echo "Running docker image..."
|
||||||
|
docker run --cpus=".1" --memory="5m" --rm -it -p 3003:3000 myreactapp:latest
|
||||||
|
|
||||||
|
# Now you can access the app at http://localhost:3003
|
||||||
5
src/app.config.ts
Normal file
5
src/app.config.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const APP_NAME = process.env.PACKAGE_NAME
|
||||||
|
export const APP_VERSION = process.env.PACKAGE_VERSION
|
||||||
|
export const APP_BUILD_NUMBER = process.env.BUILD_NUMBER
|
||||||
|
export const APP_BUILD_DATE = process.env.BUILD_DATE
|
||||||
|
export const APP_COMMIT_ID = process.env.COMMIT_ID
|
||||||
@@ -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
18
src/app/index.tsx
Normal 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
6
src/constants/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const TOKEN_KEY = 'token'
|
||||||
|
|
||||||
|
export const ROUTE = {
|
||||||
|
ROOT: '/',
|
||||||
|
LOGIN: '/login',
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
149
src/context/auth.context.tsx
Normal file
149
src/context/auth.context.tsx
Normal 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
4
src/context/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import AuthProvider, { useAuthContext } from './auth.context'
|
||||||
|
import LayoutProvider, { useLayout } from './layout.context'
|
||||||
|
|
||||||
|
export { AuthProvider, LayoutProvider, useLayout, useAuthContext }
|
||||||
38
src/context/layout.context.tsx
Normal file
38
src/context/layout.context.tsx
Normal 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
|
||||||
6
src/helpers/error.helper.tsx
Normal file
6
src/helpers/error.helper.tsx
Normal 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
3
src/helpers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import handleError from './error.helper'
|
||||||
|
|
||||||
|
export { handleError }
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
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(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
)
|
)
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
|||||||
15
src/layouts/base.layout.tsx
Normal file
15
src/layouts/base.layout.tsx
Normal 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
3
src/layouts/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import BaseLayout from './base.layout'
|
||||||
|
|
||||||
|
export { BaseLayout }
|
||||||
7
src/pages/error/index.ts
Normal file
7
src/pages/error/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import AccessDenied from "./403";
|
||||||
|
import NotFound from "./404";
|
||||||
|
|
||||||
|
export {
|
||||||
|
AccessDenied,
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
8
src/pages/index.ts
Normal file
8
src/pages/index.ts
Normal 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 }
|
||||||
0
src/pages/info/index.less
Normal file
0
src/pages/info/index.less
Normal file
25
src/pages/info/index.tsx
Normal file
25
src/pages/info/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
APP_BUILD_DATE,
|
||||||
|
APP_BUILD_NUMBER,
|
||||||
|
APP_COMMIT_ID,
|
||||||
|
APP_NAME,
|
||||||
|
APP_VERSION,
|
||||||
|
} from '@/app.config'
|
||||||
|
import { RouteTypes } from '@/routes/types'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
export default function Info() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>App Info</h1>
|
||||||
|
<p>App name: {APP_NAME}</p>
|
||||||
|
<p>App version: {APP_VERSION}</p>
|
||||||
|
<p>App build number: {APP_BUILD_NUMBER}</p>
|
||||||
|
<p>App build date: {APP_BUILD_DATE}</p>
|
||||||
|
<p> App commit id: {APP_COMMIT_ID}</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<a href={RouteTypes.RESET}>Reset</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
23
src/pages/reset/index.tsx
Normal file
23
src/pages/reset/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { RouteTypes } from '@/routes/types'
|
||||||
|
import { clearStorage } from '@/utils/ls_util'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { Navigate } from 'react-router'
|
||||||
|
|
||||||
|
const Reset = () => {
|
||||||
|
const clearAllCaches = () => {
|
||||||
|
caches.keys().then((cacheNames) => {
|
||||||
|
cacheNames.forEach((cacheName) => {
|
||||||
|
caches.delete(cacheName).then((r) => console.log('Caches cleared', r))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
clearStorage()
|
||||||
|
sessionStorage.clear()
|
||||||
|
clearAllCaches()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <Navigate to={RouteTypes.HOME} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Reset
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
const AppProvider = (props: any) => {
|
|
||||||
return <>{props.children}</>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AppProvider
|
|
||||||
@@ -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
19
src/routes/auth.route.tsx
Normal 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
|
||||||
@@ -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 = () => {
|
||||||
|
const { state } = useAuthContext()
|
||||||
|
const { selectedSideMenuKeys, setSelectedSideMenuKeys } = useLayout()
|
||||||
|
|
||||||
|
const authorities = (state && state.user && state.user.authorities) || []
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
const AuthChecker = (item: any): any | null => {
|
||||||
|
const authority = item?.authority as Authority
|
||||||
|
if (!authority || !authority.privileges) return item
|
||||||
|
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 {
|
||||||
|
if (privileges.some((e) => authorities.some((a) => e === a)))
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (location.pathname === '/login') {
|
||||||
|
return <Login />
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthRenderRoutes = () => {
|
||||||
|
const allRoutes = [...routes, ...newSideMenuRouteObjs]
|
||||||
|
|
||||||
|
return AuthFilterItems(allRoutes)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<BaseLayout
|
||||||
<Switch>
|
renderSideMenu={() => {
|
||||||
{routes.map((route) => {
|
return renderSideMenuLinks()
|
||||||
const { withAuthority } = route
|
}}
|
||||||
if (withAuthority) {
|
>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path={'/'}
|
||||||
|
element={() => {
|
||||||
return (
|
return (
|
||||||
<AuthProvider key={route.key}>
|
<>
|
||||||
<AuthRoute {...route} />
|
{AuthRenderRoutes().map((item) =>
|
||||||
</AuthProvider>
|
renderRoute(item)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
} else {
|
}}
|
||||||
return (
|
/>
|
||||||
<Route
|
</Routes>
|
||||||
key={route.key}
|
</BaseLayout>
|
||||||
exact={route.exact}
|
|
||||||
component={route.component}
|
|
||||||
children={route.children}
|
|
||||||
location={route.location}
|
|
||||||
path={route.path}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</Switch>
|
|
||||||
</Router>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,43 +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 Login from '@/pages/Login'
|
|
||||||
import Profile from '@/pages/Profile'
|
|
||||||
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: '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 }
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
const RouteTypes = {
|
const RouteTypes = {
|
||||||
HOME: "/",
|
HOME: '/',
|
||||||
ABOUT: "/about",
|
ABOUT: '/about',
|
||||||
PROFILE: "/profile",
|
PROFILE: '/profile',
|
||||||
|
INFO: '/info',
|
||||||
|
RESET: '/reset',
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
LOGIN: '/login',
|
LOGIN: '/login',
|
||||||
|
|
||||||
// Errors
|
// Errors
|
||||||
ERROR_404: "**",
|
ERROR_404: '*',
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export { RouteTypes }
|
||||||
RouteTypes,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,4 +4,8 @@ export const setStorage = (key: string, value?: any) => {
|
|||||||
|
|
||||||
export const getStorage = (key: string, defaultValue?: string): (string | undefined | null) => {
|
export const getStorage = (key: string, defaultValue?: string): (string | undefined | null) => {
|
||||||
return localStorage.getItem(key) ?? defaultValue
|
return localStorage.getItem(key) ?? defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
export const clearStorage = () => {
|
||||||
|
return localStorage.clear()
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user