mirror of
https://github.com/sasjs/server.git
synced 2025-12-11 03:34:35 +00:00
Merge pull request #136 from sasjs/issue-78
feat: add user name and logout functionality
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { Route, HashRouter, Switch } from 'react-router-dom'
|
import { Route, HashRouter, Switch } from 'react-router-dom'
|
||||||
import { ThemeProvider } from '@mui/material/styles'
|
import { ThemeProvider } from '@mui/material/styles'
|
||||||
import { theme } from './theme'
|
import { theme } from './theme'
|
||||||
@@ -9,12 +9,12 @@ import Home from './components/home'
|
|||||||
import Drive from './containers/Drive'
|
import Drive from './containers/Drive'
|
||||||
import Studio from './containers/Studio'
|
import Studio from './containers/Studio'
|
||||||
|
|
||||||
import useTokens from './components/useTokens'
|
import { AppContext } from './context/appContext'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { tokens, setTokens } = useTokens()
|
const appContext = useContext(AppContext)
|
||||||
|
|
||||||
if (!tokens) {
|
if (!appContext.tokens) {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
@@ -24,7 +24,7 @@ function App() {
|
|||||||
<Login getCodeOnly />
|
<Login getCodeOnly />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Login setTokens={setTokens} />
|
<Login />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState, useContext } from 'react'
|
||||||
import { Link, useHistory, useLocation } from 'react-router-dom'
|
import { Link, useHistory, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import AppBar from '@mui/material/AppBar'
|
import {
|
||||||
import Toolbar from '@mui/material/Toolbar'
|
AppBar,
|
||||||
import Tabs from '@mui/material/Tabs'
|
Toolbar,
|
||||||
import Tab from '@mui/material/Tab'
|
Tabs,
|
||||||
import Button from '@mui/material/Button'
|
Tab,
|
||||||
|
Button,
|
||||||
|
Menu,
|
||||||
|
MenuItem
|
||||||
|
} from '@mui/material'
|
||||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
|
|
||||||
|
import UserName from './userName'
|
||||||
|
import { AppContext } from '../context/appContext'
|
||||||
|
|
||||||
const NODE_ENV = process.env.NODE_ENV
|
const NODE_ENV = process.env.NODE_ENV
|
||||||
const PORT_API = process.env.PORT_API
|
const PORT_API = process.env.PORT_API
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
@@ -16,11 +23,29 @@ const baseUrl =
|
|||||||
const Header = (props: any) => {
|
const Header = (props: any) => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
|
const appContext = useContext(AppContext)
|
||||||
const [tabValue, setTabValue] = useState(pathname)
|
const [tabValue, setTabValue] = useState(pathname)
|
||||||
|
const [anchorEl, setAnchorEl] = useState<
|
||||||
|
(EventTarget & HTMLButtonElement) | null
|
||||||
|
>(null)
|
||||||
|
|
||||||
|
const handleMenu = (
|
||||||
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
setAnchorEl(event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
|
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
|
||||||
setTabValue(value)
|
setTabValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
if (appContext.logout) appContext.logout()
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
position="fixed"
|
position="fixed"
|
||||||
@@ -81,6 +106,39 @@ const Header = (props: any) => {
|
|||||||
>
|
>
|
||||||
App Stream
|
App Stream
|
||||||
</Button>
|
</Button>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
justifyContent: 'flex-end'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UserName
|
||||||
|
userName={appContext.userName}
|
||||||
|
onClickHandler={handleMenu}
|
||||||
|
/>
|
||||||
|
<Menu
|
||||||
|
id="menu-appbar"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
keepMounted
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
open={!!anchorEl}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleLogout} sx={{ justifyContent: 'center' }}>
|
||||||
|
<Button variant="contained" color="primary">
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState, useContext } from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import { CssBaseline, Box, TextField, Button, Typography } from '@mui/material'
|
import { CssBaseline, Box, TextField, Button, Typography } from '@mui/material'
|
||||||
|
import { AppContext } from '../context/appContext'
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
@@ -33,8 +34,9 @@ const getTokens = async (payload: any) => {
|
|||||||
}).then((data) => data.json())
|
}).then((data) => data.json())
|
||||||
}
|
}
|
||||||
|
|
||||||
const Login = ({ setTokens, getCodeOnly }: any) => {
|
const Login = ({ getCodeOnly }: any) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const appContext = useContext(AppContext)
|
||||||
const [username, setUserName] = useState('')
|
const [username, setUserName] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [errorMessage, setErrorMessage] = useState('')
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
@@ -71,7 +73,8 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
|
|||||||
code
|
code
|
||||||
})
|
})
|
||||||
|
|
||||||
setTokens(accessToken, refreshToken)
|
if (appContext.setTokens) appContext.setTokens(accessToken, refreshToken)
|
||||||
|
if (appContext.setUserName) appContext.setUserName(username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +129,7 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
{errorMessage && <span>{errorMessage}</span>}
|
{errorMessage && <span>{errorMessage}</span>}
|
||||||
<Button type="submit" variant="outlined">
|
<Button type="submit" variant="outlined" disabled={!appContext.setTokens}>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -134,7 +137,6 @@ const Login = ({ setTokens, getCodeOnly }: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Login.propTypes = {
|
Login.propTypes = {
|
||||||
setTokens: PropTypes.func,
|
|
||||||
getCodeOnly: PropTypes.bool
|
getCodeOnly: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
web/src/components/userName.tsx
Normal file
30
web/src/components/userName.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Typography, IconButton } from '@mui/material'
|
||||||
|
import AccountCircle from '@mui/icons-material/AccountCircle'
|
||||||
|
|
||||||
|
const UserName = (props: any) => {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
aria-label="account of current user"
|
||||||
|
aria-controls="menu-appbar"
|
||||||
|
aria-haspopup="true"
|
||||||
|
onClick={props.onClickHandler}
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
{props.avatarContent ? (
|
||||||
|
<img
|
||||||
|
src={props.avatarContent}
|
||||||
|
alt="user-avatar"
|
||||||
|
style={{ width: '25px' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AccountCircle></AccountCircle>
|
||||||
|
)}
|
||||||
|
<Typography variant="h6" sx={{ color: 'white', padding: '0 8px' }}>
|
||||||
|
{props.userName}
|
||||||
|
</Typography>
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserName
|
||||||
139
web/src/context/appContext.tsx
Normal file
139
web/src/context/appContext.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
ReactNode
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const NODE_ENV = process.env.NODE_ENV
|
||||||
|
const PORT_API = process.env.PORT_API
|
||||||
|
const baseUrl =
|
||||||
|
NODE_ENV === 'development' ? `http://localhost:${PORT_API ?? 5000}` : ''
|
||||||
|
|
||||||
|
const isAbsoluteURLRegex = /^(?:\w+:)\/\//
|
||||||
|
|
||||||
|
const setAxiosRequestHeader = (accessToken: string) => {
|
||||||
|
axios.interceptors.request.use(function (config) {
|
||||||
|
if (baseUrl && !isAbsoluteURLRegex.test(config.url as string)) {
|
||||||
|
config.url = baseUrl + config.url
|
||||||
|
}
|
||||||
|
console.log('axios.interceptors.request.use', accessToken)
|
||||||
|
config.headers!['Authorization'] = `Bearer ${accessToken}`
|
||||||
|
config.withCredentials = true
|
||||||
|
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAxiosResponse = (setTokens: Function) => {
|
||||||
|
// Add a response interceptor
|
||||||
|
axios.interceptors.response.use(
|
||||||
|
function (response) {
|
||||||
|
// Any status code that lie within the range of 2xx cause this function to trigger
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
async function (error) {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
// refresh token
|
||||||
|
// const { accessToken, refreshToken: newRefresh } = await refreshMyToken(
|
||||||
|
// refreshToken
|
||||||
|
// )
|
||||||
|
|
||||||
|
// if (accessToken && newRefresh) {
|
||||||
|
// setTokens(accessToken, newRefresh)
|
||||||
|
// error.config.headers['Authorization'] = 'Bearer ' + accessToken
|
||||||
|
// error.config.baseURL = undefined
|
||||||
|
|
||||||
|
// return axios.request(error.config)
|
||||||
|
// }
|
||||||
|
console.log(53)
|
||||||
|
setTokens(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTokens = () => {
|
||||||
|
const accessToken = localStorage.getItem('accessToken')
|
||||||
|
const refreshToken = localStorage.getItem('refreshToken')
|
||||||
|
|
||||||
|
if (accessToken && refreshToken) {
|
||||||
|
setAxiosRequestHeader(accessToken)
|
||||||
|
return { accessToken, refreshToken }
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppContextProps {
|
||||||
|
userName: string
|
||||||
|
setUserName: Dispatch<SetStateAction<string>> | null
|
||||||
|
tokens?: { accessToken: string; refreshToken: string }
|
||||||
|
setTokens: ((accessToken: string, refreshToken: string) => void) | null
|
||||||
|
logout: (() => void) | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppContext = createContext<AppContextProps>({
|
||||||
|
userName: '',
|
||||||
|
tokens: getTokens(),
|
||||||
|
setUserName: null,
|
||||||
|
setTokens: null,
|
||||||
|
logout: null
|
||||||
|
})
|
||||||
|
|
||||||
|
const AppContextProvider = (props: { children: ReactNode }) => {
|
||||||
|
const { children } = props
|
||||||
|
const [userName, setUserName] = useState('')
|
||||||
|
const [tokens, setTokens] = useState(getTokens())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAxiosResponse(setTokens)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(97)
|
||||||
|
if (tokens === undefined) {
|
||||||
|
console.log(99)
|
||||||
|
localStorage.removeItem('accessToken')
|
||||||
|
localStorage.removeItem('refreshToken')
|
||||||
|
}
|
||||||
|
}, [tokens])
|
||||||
|
|
||||||
|
const saveTokens = useCallback(
|
||||||
|
(accessToken: string, refreshToken: string) => {
|
||||||
|
localStorage.setItem('accessToken', accessToken)
|
||||||
|
localStorage.setItem('refreshToken', refreshToken)
|
||||||
|
console.log(accessToken)
|
||||||
|
setAxiosRequestHeader(accessToken)
|
||||||
|
setTokens({ accessToken, refreshToken })
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const logout = useCallback(() => {
|
||||||
|
setUserName('')
|
||||||
|
setTokens(undefined)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppContext.Provider
|
||||||
|
value={{
|
||||||
|
userName,
|
||||||
|
setUserName,
|
||||||
|
tokens,
|
||||||
|
setTokens: saveTokens,
|
||||||
|
logout
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AppContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppContextProvider
|
||||||
@@ -2,10 +2,13 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import AppContextProvider from './context/appContext'
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<AppContextProvider>
|
||||||
|
<App />
|
||||||
|
</AppContextProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user