feat: add user management screen
feat: login screen fix: login routing and layout feat: add users migration feat: passport login strategy fix: users migration feat: simple authentication fix: request body feat: JWT authorization feat: 2fa step on login feat: 2fa flow feat: add rememberme to req body fix: hide 2fa secret from jwt fix: block login access to logged in user fix: rerouting to wizard refactor: login screen feat: setup 2fa state on login feat: 2fa secret qr code fix: remove jwt from 2fa secret fix: wizard redirect after login fix: 2fa setup flow fix: user id to uuid feat: user roles feat: user sessions and db persistence feat: session saving on DB and cookie refactor: unused code feat: cookie auto renew on request feat: get user data endpoint fix: repeated requests feat: react routing fix: private routes refactor: auth feat: sessions aware of ua and ip feat: sessions on gql feat: session management screen feat: replace user_tokens usage for users feat: user deletion also deletes active sessions feat: remember me alters session cookie accordingly feat: last session by all users fix: login feedback fix: page loading UX feat: routes based on user role feat: header aware of roles feat: reset password fix: reset password endpoint feat: handle password change feat: reset 2FA feat: user role on management screen feat: change user role fix: user last session query fix: context fix: destroy own session feat: reset password now resets sessions feat: reset 2fa now resets sessions refactor: user data refactor: user management screen feat: user enable feat: schema directives fix: remove schema directive temp feat: create new users feat: register endpoint feat: modals for reset links fix: directive Date errors feat: superuser directive feat: create user url modal fix: user management layout feat: confirmation modals fix: info text feat: 2fa input component feat: code input on 2fa state feat: add button styling feat: confirmation modal on superuser action feat: rework 2fa setup screen feat: rework reset 2fa screen fix: session management screen fix: user management screen fix: blacklist roles chore: migrate old customer values to new columns fix: value migration fix: value migration refactor: remove old code
This commit is contained in:
parent
368781864e
commit
fded22f39a
50 changed files with 9839 additions and 4501 deletions
10549
new-lamassu-admin/package-lock.json
generated
10549
new-lamassu-admin/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -32,6 +32,7 @@
|
|||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-dom": "^16.10.2",
|
||||
"react-number-format": "^4.4.1",
|
||||
"react-otp-input": "^2.3.0",
|
||||
"react-router-dom": "5.1.2",
|
||||
"react-use": "15.3.2",
|
||||
"react-virtualized": "^9.21.2",
|
||||
|
|
|
|||
BIN
new-lamassu-admin/public/fonts/Rubik/Rubik-Black.otf
Normal file
BIN
new-lamassu-admin/public/fonts/Rubik/Rubik-Black.otf
Normal file
Binary file not shown.
BIN
new-lamassu-admin/public/fonts/Rubik/Rubik-Bold.otf
Normal file
BIN
new-lamassu-admin/public/fonts/Rubik/Rubik-Bold.otf
Normal file
Binary file not shown.
BIN
new-lamassu-admin/public/fonts/Rubik/Rubik-Medium.otf
Normal file
BIN
new-lamassu-admin/public/fonts/Rubik/Rubik-Medium.otf
Normal file
Binary file not shown.
|
|
@ -6,15 +6,19 @@ import styles from './Button.styles'
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ActionButton = memo(({ size = 'lg', children, className, ...props }) => {
|
||||
const classes = useStyles({ size })
|
||||
return (
|
||||
<div className={classnames(className, classes.wrapper)}>
|
||||
<button className={classes.button} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const ActionButton = memo(
|
||||
({ size = 'lg', children, className, buttonClassName, ...props }) => {
|
||||
const classes = useStyles({ size })
|
||||
return (
|
||||
<div className={classnames(className, classes.wrapper)}>
|
||||
<button
|
||||
className={classnames(buttonClassName, classes.button)}
|
||||
{...props}>
|
||||
{children}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default ActionButton
|
||||
|
|
|
|||
40
new-lamassu-admin/src/components/inputs/base/CodeInput.js
Normal file
40
new-lamassu-admin/src/components/inputs/base/CodeInput.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
import OtpInput from 'react-otp-input'
|
||||
|
||||
import styles from './CodeInput.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const CodeInput = ({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
numInputs,
|
||||
error,
|
||||
inputStyle,
|
||||
containerStyle,
|
||||
...props
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<OtpInput
|
||||
id={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
numInputs={numInputs}
|
||||
separator={<span> </span>}
|
||||
containerStyle={classnames(containerStyle, classes.container)}
|
||||
inputStyle={classnames(inputStyle, classes.input)}
|
||||
focusStyle={classes.focus}
|
||||
errorStyle={classes.error}
|
||||
hasErrored={error}
|
||||
isInputNum={true}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default CodeInput
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import {
|
||||
fontPrimary,
|
||||
primaryColor,
|
||||
zircon,
|
||||
errorColor
|
||||
} from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
input: {
|
||||
width: '3.5rem !important',
|
||||
height: '5rem',
|
||||
fontFamily: fontPrimary,
|
||||
fontSize: 35,
|
||||
color: primaryColor,
|
||||
border: '2px solid',
|
||||
borderColor: zircon,
|
||||
borderRadius: '4px'
|
||||
},
|
||||
focus: {
|
||||
color: primaryColor,
|
||||
border: '2px solid',
|
||||
borderColor: primaryColor,
|
||||
borderRadius: '4px'
|
||||
},
|
||||
error: {
|
||||
borderColor: errorColor
|
||||
},
|
||||
container: {
|
||||
justifyContent: 'space-evenly'
|
||||
}
|
||||
}
|
||||
|
||||
export default styles
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import Autocomplete from './Autocomplete'
|
||||
import Checkbox from './Checkbox'
|
||||
import CodeInput from './CodeInput'
|
||||
import NumberInput from './NumberInput'
|
||||
import RadioGroup from './RadioGroup'
|
||||
import SecretInput from './SecretInput'
|
||||
|
|
@ -8,6 +9,7 @@ import TextInput from './TextInput'
|
|||
|
||||
export {
|
||||
Checkbox,
|
||||
CodeInput,
|
||||
TextInput,
|
||||
NumberInput,
|
||||
Switch,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Autocomplete from './base/Autocomplete'
|
||||
import Checkbox from './base/Checkbox'
|
||||
import CodeInput from './base/CodeInput'
|
||||
import RadioGroup from './base/RadioGroup'
|
||||
import Select from './base/Select'
|
||||
import Switch from './base/Switch'
|
||||
|
|
@ -10,6 +11,7 @@ export {
|
|||
Autocomplete,
|
||||
TextInput,
|
||||
Checkbox,
|
||||
CodeInput,
|
||||
Switch,
|
||||
Select,
|
||||
RadioGroup,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ const Subheader = ({ item, classes }) => {
|
|||
|
||||
const notNil = R.compose(R.not, R.isNil)
|
||||
|
||||
const Header = memo(({ tree }) => {
|
||||
const Header = memo(({ tree, user }) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [notifButtonCoords, setNotifButtonCoords] = useState({ x: 0, y: 0 })
|
||||
|
|
@ -119,24 +119,35 @@ const Header = memo(({ tree }) => {
|
|||
</div>
|
||||
<nav className={classes.nav}>
|
||||
<ul className={classes.ul}>
|
||||
{tree.map((it, idx) => (
|
||||
<NavLink
|
||||
key={idx}
|
||||
to={it.route || it.children[0].route}
|
||||
isActive={match => {
|
||||
if (!match) return false
|
||||
setActive(it)
|
||||
return true
|
||||
}}
|
||||
className={classnames(classes.link, classes.whiteLink)}
|
||||
activeClassName={classes.activeLink}>
|
||||
<li className={classes.li}>
|
||||
<span className={classes.forceSize} forcesize={it.label}>
|
||||
{it.label}
|
||||
</span>
|
||||
</li>
|
||||
</NavLink>
|
||||
))}
|
||||
{tree.map((it, idx) => {
|
||||
if (
|
||||
!R.includes(
|
||||
user.role,
|
||||
it.allowedRoles.map(v => {
|
||||
return v.key
|
||||
})
|
||||
)
|
||||
)
|
||||
return <></>
|
||||
return (
|
||||
<NavLink
|
||||
key={idx}
|
||||
to={it.route || it.children[0].route}
|
||||
isActive={match => {
|
||||
if (!match) return false
|
||||
setActive(it)
|
||||
return true
|
||||
}}
|
||||
className={classnames(classes.link, classes.whiteLink)}
|
||||
activeClassName={classes.activeLink}>
|
||||
<li className={classes.li}>
|
||||
<span className={classes.forceSize} forcesize={it.label}>
|
||||
{it.label}
|
||||
</span>
|
||||
</li>
|
||||
</NavLink>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className={classes.actionButtonsContainer}>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import Slide from '@material-ui/core/Slide'
|
||||
import {
|
||||
StylesProvider,
|
||||
jssPreset,
|
||||
MuiThemeProvider,
|
||||
makeStyles
|
||||
} from '@material-ui/core/styles'
|
||||
import { axios } from '@use-hooks/axios'
|
||||
import { create } from 'jss'
|
||||
import extendJss from 'jss-plugin-extend'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react'
|
||||
import {
|
||||
useLocation,
|
||||
useHistory,
|
||||
|
|
@ -17,15 +17,14 @@ import {
|
|||
} from 'react-router-dom'
|
||||
|
||||
import AppContext from 'src/AppContext'
|
||||
import Header from 'src/components/layout/Header'
|
||||
import Sidebar from 'src/components/layout/Sidebar'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import ApolloProvider from 'src/utils/apollo'
|
||||
|
||||
import Header from '../components/layout/Header'
|
||||
import { tree, hasSidebar, Routes, getParent } from '../routing/routes'
|
||||
import global from '../styling/global'
|
||||
import theme from '../styling/theme'
|
||||
import { backgroundColor, mainWidth } from '../styling/variables'
|
||||
import ApolloProvider from 'src/pazuz/apollo/Provider'
|
||||
import { tree, hasSidebar, Routes, getParent } from 'src/pazuz/routing/routes'
|
||||
import global from 'src/styling/global'
|
||||
import theme from 'src/styling/theme'
|
||||
import { backgroundColor, mainWidth } from 'src/styling/variables'
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
||||
|
|
@ -74,7 +73,7 @@ const Main = () => {
|
|||
const classes = useStyles()
|
||||
const location = useLocation()
|
||||
const history = useHistory()
|
||||
const { wizardTested } = useContext(AppContext)
|
||||
const { wizardTested, userData } = useContext(AppContext)
|
||||
|
||||
const route = location.pathname
|
||||
|
||||
|
|
@ -93,20 +92,12 @@ const Main = () => {
|
|||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{!is404 && wizardTested && <Header tree={tree} />}
|
||||
{!is404 && wizardTested && userData && (
|
||||
<Header tree={tree} user={userData} />
|
||||
)}
|
||||
<main className={classes.wrapper}>
|
||||
{sidebar && !is404 && wizardTested && (
|
||||
<Slide
|
||||
direction="left"
|
||||
in={true}
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
children={
|
||||
<div>
|
||||
<TitleSection title={parent.title}></TitleSection>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<TitleSection title={parent.title}></TitleSection>
|
||||
)}
|
||||
|
||||
<Grid container className={classes.grid}>
|
||||
|
|
@ -129,19 +120,47 @@ const Main = () => {
|
|||
|
||||
const App = () => {
|
||||
const [wizardTested, setWizardTested] = useState(false)
|
||||
const [userData, setUserData] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
useEffect(() => {
|
||||
getUserData()
|
||||
}, [])
|
||||
|
||||
const getUserData = () => {
|
||||
axios({
|
||||
method: 'GET',
|
||||
url: `${url}/user-data`,
|
||||
withCredentials: true
|
||||
})
|
||||
.then(res => {
|
||||
setLoading(false)
|
||||
if (res.status === 200) setUserData(res.data.user)
|
||||
})
|
||||
.catch(err => {
|
||||
setLoading(false)
|
||||
if (err.status === 403) setUserData(null)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{ wizardTested, setWizardTested }}>
|
||||
<Router>
|
||||
<ApolloProvider>
|
||||
<StylesProvider jss={jss}>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Main />
|
||||
</MuiThemeProvider>
|
||||
</StylesProvider>
|
||||
</ApolloProvider>
|
||||
</Router>
|
||||
<AppContext.Provider
|
||||
value={{ wizardTested, setWizardTested, userData, setUserData }}>
|
||||
{!loading && (
|
||||
<Router>
|
||||
<ApolloProvider>
|
||||
<StylesProvider jss={jss}>
|
||||
<MuiThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Main />
|
||||
</MuiThemeProvider>
|
||||
</StylesProvider>
|
||||
</ApolloProvider>
|
||||
</Router>
|
||||
)}
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
115
new-lamassu-admin/src/pages/Authentication/Input2FAState.js
Normal file
115
new-lamassu-admin/src/pages/Authentication/Input2FAState.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import axios from 'axios'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
import { AppContext } from 'src/App'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { CodeInput } from 'src/components/inputs/base'
|
||||
import { H2, P } from 'src/components/typography'
|
||||
|
||||
import styles from './Login.styles'
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Input2FAState = ({
|
||||
twoFAField,
|
||||
onTwoFAChange,
|
||||
clientField,
|
||||
passwordField,
|
||||
rememberMeField
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
const history = useHistory()
|
||||
const { setUserData } = useContext(AppContext)
|
||||
|
||||
const [invalidToken, setInvalidToken] = useState(false)
|
||||
|
||||
const handle2FAChange = value => {
|
||||
onTwoFAChange(value)
|
||||
setInvalidToken(false)
|
||||
}
|
||||
|
||||
const handle2FA = () => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/login/2fa`,
|
||||
data: {
|
||||
username: clientField,
|
||||
password: passwordField,
|
||||
rememberMe: rememberMeField,
|
||||
twoFACode: twoFAField
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
const status = res.status
|
||||
if (status === 200) {
|
||||
getUserData()
|
||||
history.push('/')
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.response && err.response.data) {
|
||||
if (err.response.status === 403) {
|
||||
onTwoFAChange('')
|
||||
setInvalidToken(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getUserData = () => {
|
||||
axios({
|
||||
method: 'GET',
|
||||
url: `${url}/user-data`,
|
||||
withCredentials: true
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status === 200) setUserData(res.data.user)
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.status === 403) setUserData(null)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<H2 className={classes.info}>
|
||||
Enter your two-factor authentication code
|
||||
</H2>
|
||||
<CodeInput
|
||||
name="2fa"
|
||||
value={twoFAField}
|
||||
onChange={handle2FAChange}
|
||||
numInputs={6}
|
||||
error={invalidToken}
|
||||
/>
|
||||
<div className={classes.twofaFooter}>
|
||||
{invalidToken && (
|
||||
<P className={classes.errorMessage}>
|
||||
Code is invalid. Please try again.
|
||||
</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
handle2FA()
|
||||
}}
|
||||
buttonClassName={classes.loginButton}>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input2FAState
|
||||
30
new-lamassu-admin/src/pages/Authentication/Login.js
Normal file
30
new-lamassu-admin/src/pages/Authentication/Login.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { makeStyles, Grid } from '@material-ui/core'
|
||||
import React from 'react'
|
||||
|
||||
import styles from './Login.styles'
|
||||
import LoginCard from './LoginCard'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Login = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{ minHeight: '100vh' }}
|
||||
className={classes.welcomeBackground}>
|
||||
<Grid>
|
||||
<LoginCard />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login
|
||||
121
new-lamassu-admin/src/pages/Authentication/Login.styles.js
Normal file
121
new-lamassu-admin/src/pages/Authentication/Login.styles.js
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import {
|
||||
fontPrimary,
|
||||
fontSecondary,
|
||||
primaryColor,
|
||||
backgroundColor,
|
||||
errorColor
|
||||
} from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
title: {
|
||||
color: primaryColor,
|
||||
fontFamily: fontPrimary,
|
||||
fontSize: '18px',
|
||||
fontWeight: 'normal',
|
||||
paddingTop: 8
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: '16px',
|
||||
fontWeight: 550
|
||||
},
|
||||
input: {
|
||||
marginBottom: 25,
|
||||
marginTop: -15
|
||||
},
|
||||
wrapper: {
|
||||
padding: '2.5em 4em',
|
||||
width: 575,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
titleWrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 30
|
||||
},
|
||||
rememberMeWrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
icon: {
|
||||
transform: 'scale(1.5)',
|
||||
marginRight: 25
|
||||
},
|
||||
checkbox: {
|
||||
transform: 'scale(2)',
|
||||
marginRight: 5,
|
||||
marginLeft: -5
|
||||
},
|
||||
footer: {
|
||||
marginTop: '10vh'
|
||||
},
|
||||
twofaFooter: {
|
||||
marginTop: '6vh'
|
||||
},
|
||||
loginButton: {
|
||||
display: 'block',
|
||||
width: '100%'
|
||||
},
|
||||
welcomeBackground: {
|
||||
background: 'url(/wizard-background.svg) no-repeat center center fixed',
|
||||
backgroundColor: backgroundColor,
|
||||
backgroundSize: 'cover',
|
||||
// filter: 'blur(4px)',
|
||||
// pointerEvents: 'none',
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
position: 'relative',
|
||||
left: '50%',
|
||||
right: '50%',
|
||||
marginLeft: '-50vw',
|
||||
marginRight: '-50vw'
|
||||
},
|
||||
info: {
|
||||
fontFamily: fontSecondary,
|
||||
marginBottom: '5vh'
|
||||
},
|
||||
info2: {
|
||||
fontFamily: fontSecondary,
|
||||
fontSize: '14px',
|
||||
textAlign: 'justify'
|
||||
},
|
||||
infoWrapper: {
|
||||
marginBottom: '3vh'
|
||||
},
|
||||
errorMessage: {
|
||||
fontFamily: fontSecondary,
|
||||
color: errorColor
|
||||
},
|
||||
qrCodeWrapper: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
marginBottom: '3vh'
|
||||
},
|
||||
secretWrapper: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
secretLabel: {
|
||||
fontSize: '16px',
|
||||
fontWeight: 550,
|
||||
marginRight: 15
|
||||
},
|
||||
secret: {
|
||||
fontSize: '16px',
|
||||
fontWeight: 550,
|
||||
marginRight: 35
|
||||
},
|
||||
hiddenSecret: {
|
||||
fontSize: '16px',
|
||||
fontWeight: 550,
|
||||
marginRight: 35,
|
||||
filter: 'blur(8px)'
|
||||
},
|
||||
confirm2FAInput: {
|
||||
marginTop: 25
|
||||
}
|
||||
}
|
||||
|
||||
export default styles
|
||||
104
new-lamassu-admin/src/pages/Authentication/LoginCard.js
Normal file
104
new-lamassu-admin/src/pages/Authentication/LoginCard.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import Paper from '@material-ui/core/Paper'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { H2 } from 'src/components/typography'
|
||||
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||
|
||||
import Input2FAState from './Input2FAState'
|
||||
import styles from './Login.styles'
|
||||
import LoginState from './LoginState'
|
||||
import Setup2FAState from './Setup2FAState'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const STATES = {
|
||||
LOGIN: 'Login',
|
||||
SETUP_2FA: 'Setup 2FA',
|
||||
INPUT_2FA: 'Input 2FA'
|
||||
}
|
||||
|
||||
const LoginCard = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [twoFAField, setTwoFAField] = useState('')
|
||||
const [clientField, setClientField] = useState('')
|
||||
const [passwordField, setPasswordField] = useState('')
|
||||
const [rememberMeField, setRememberMeField] = useState(false)
|
||||
const [loginState, setLoginState] = useState(STATES.LOGIN)
|
||||
|
||||
const onClientChange = newValue => {
|
||||
setClientField(newValue)
|
||||
}
|
||||
|
||||
const onPasswordChange = newValue => {
|
||||
setPasswordField(newValue)
|
||||
}
|
||||
|
||||
const onRememberMeChange = newValue => {
|
||||
setRememberMeField(newValue)
|
||||
}
|
||||
|
||||
const onTwoFAChange = newValue => {
|
||||
setTwoFAField(newValue)
|
||||
}
|
||||
|
||||
const handleLoginState = newState => {
|
||||
setLoginState(newState)
|
||||
}
|
||||
|
||||
const renderState = () => {
|
||||
switch (loginState) {
|
||||
case STATES.LOGIN:
|
||||
return (
|
||||
<LoginState
|
||||
clientField={clientField}
|
||||
onClientChange={onClientChange}
|
||||
passwordField={passwordField}
|
||||
onPasswordChange={onPasswordChange}
|
||||
rememberMeField={rememberMeField}
|
||||
onRememberMeChange={onRememberMeChange}
|
||||
STATES={STATES}
|
||||
handleLoginState={handleLoginState}
|
||||
/>
|
||||
)
|
||||
case STATES.INPUT_2FA:
|
||||
return (
|
||||
<Input2FAState
|
||||
twoFAField={twoFAField}
|
||||
onTwoFAChange={onTwoFAChange}
|
||||
clientField={clientField}
|
||||
passwordField={passwordField}
|
||||
rememberMeField={rememberMeField}
|
||||
/>
|
||||
)
|
||||
case STATES.SETUP_2FA:
|
||||
return (
|
||||
<Setup2FAState
|
||||
clientField={clientField}
|
||||
passwordField={passwordField}
|
||||
STATES={STATES}
|
||||
handleLoginState={handleLoginState}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Paper elevation={1}>
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classes.titleWrapper}>
|
||||
<Logo className={classes.icon} />
|
||||
<H2 className={classes.title}>Lamassu Admin</H2>
|
||||
</div>
|
||||
{renderState()}
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginCard
|
||||
130
new-lamassu-admin/src/pages/Authentication/LoginState.js
Normal file
130
new-lamassu-admin/src/pages/Authentication/LoginState.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import axios from 'axios'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { Checkbox, TextInput } from 'src/components/inputs/base'
|
||||
import { Label2, P } from 'src/components/typography'
|
||||
|
||||
import styles from './Login.styles'
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const LoginState = ({
|
||||
clientField,
|
||||
onClientChange,
|
||||
passwordField,
|
||||
onPasswordChange,
|
||||
rememberMeField,
|
||||
onRememberMeChange,
|
||||
STATES,
|
||||
handleLoginState
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [invalidLogin, setInvalidLogin] = useState(false)
|
||||
|
||||
const handleClientChange = event => {
|
||||
onClientChange(event.target.value)
|
||||
setInvalidLogin(false)
|
||||
}
|
||||
|
||||
const handlePasswordChange = event => {
|
||||
onPasswordChange(event.target.value)
|
||||
setInvalidLogin(false)
|
||||
}
|
||||
|
||||
const handleRememberMeChange = () => {
|
||||
onRememberMeChange(!rememberMeField)
|
||||
}
|
||||
|
||||
const handleLogin = () => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/login`,
|
||||
data: {
|
||||
username: clientField,
|
||||
password: passwordField,
|
||||
rememberMe: rememberMeField
|
||||
},
|
||||
options: {
|
||||
withCredentials: true
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
const status = res.status
|
||||
const message = res.data.message
|
||||
if (status === 200 && message === 'INPUT2FA')
|
||||
handleLoginState(STATES.INPUT_2FA)
|
||||
if (status === 200 && message === 'SETUP2FA')
|
||||
handleLoginState(STATES.SETUP_2FA)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.response && err.response.data) {
|
||||
if (err.response.status === 403) setInvalidLogin(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Label2 className={classes.inputLabel}>Client</Label2>
|
||||
<TextInput
|
||||
className={classes.input}
|
||||
error={invalidLogin}
|
||||
name="client-name"
|
||||
autoFocus
|
||||
id="client-name"
|
||||
type="text"
|
||||
size="lg"
|
||||
onChange={handleClientChange}
|
||||
value={clientField}
|
||||
/>
|
||||
<Label2 className={classes.inputLabel}>Password</Label2>
|
||||
<TextInput
|
||||
className={classes.input}
|
||||
error={invalidLogin}
|
||||
name="password"
|
||||
id="password"
|
||||
type="password"
|
||||
size="lg"
|
||||
onChange={handlePasswordChange}
|
||||
value={passwordField}
|
||||
/>
|
||||
<div className={classes.rememberMeWrapper}>
|
||||
<Checkbox
|
||||
className={classes.checkbox}
|
||||
id="remember-me"
|
||||
onChange={handleRememberMeChange}
|
||||
value={rememberMeField}
|
||||
/>
|
||||
<Label2 className={classes.inputLabel}>Keep me logged in</Label2>
|
||||
</div>
|
||||
<div className={classes.footer}>
|
||||
{invalidLogin && (
|
||||
<P className={classes.errorMessage}>
|
||||
Invalid login/password combination.
|
||||
</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleLogin()
|
||||
}}
|
||||
buttonClassName={classes.loginButton}>
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginState
|
||||
180
new-lamassu-admin/src/pages/Authentication/Register.js
Normal file
180
new-lamassu-admin/src/pages/Authentication/Register.js
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { makeStyles, Grid } from '@material-ui/core'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import axios from 'axios'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useLocation, useHistory } from 'react-router-dom'
|
||||
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs/base'
|
||||
import { H2, Label2, P } from 'src/components/typography'
|
||||
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||
|
||||
import styles from './Login.styles'
|
||||
|
||||
const useQuery = () => new URLSearchParams(useLocation().search)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const Register = () => {
|
||||
const classes = useStyles()
|
||||
const history = useHistory()
|
||||
const query = useQuery()
|
||||
const [passwordField, setPasswordField] = useState('')
|
||||
const [confirmPasswordField, setConfirmPasswordField] = useState('')
|
||||
const [invalidPassword, setInvalidPassword] = useState(false)
|
||||
const [username, setUsername] = useState(null)
|
||||
const [role, setRole] = useState(null)
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [wasSuccessful, setSuccess] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
validateQuery()
|
||||
}, [])
|
||||
|
||||
const validateQuery = () => {
|
||||
axios({
|
||||
url: `${url}/api/register?t=${query.get('t')}`,
|
||||
method: 'GET',
|
||||
options: {
|
||||
withCredentials: true
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res && res.status === 200) {
|
||||
setLoading(false)
|
||||
if (res.data === 'The link has expired') setSuccess(false)
|
||||
else {
|
||||
setSuccess(true)
|
||||
setUsername(res.data.username)
|
||||
setRole(res.data.role)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
history.push('/')
|
||||
})
|
||||
}
|
||||
|
||||
const handleRegister = () => {
|
||||
if (!isValidPassword()) return setInvalidPassword(true)
|
||||
axios({
|
||||
url: `${url}/api/register`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
username: username,
|
||||
password: passwordField,
|
||||
role: role
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res && res.status === 200) {
|
||||
history.push('/wizard', { fromAuthRegister: true })
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
history.push('/')
|
||||
})
|
||||
}
|
||||
|
||||
const isValidPassword = () => {
|
||||
return passwordField === confirmPasswordField
|
||||
}
|
||||
|
||||
const handlePasswordChange = event => {
|
||||
setInvalidPassword(false)
|
||||
setPasswordField(event.target.value)
|
||||
}
|
||||
|
||||
const handleConfirmPasswordChange = event => {
|
||||
setInvalidPassword(false)
|
||||
setConfirmPasswordField(event.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{ minHeight: '100vh' }}
|
||||
className={classes.welcomeBackground}>
|
||||
<Grid>
|
||||
<div>
|
||||
<Paper elevation={1}>
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classes.titleWrapper}>
|
||||
<Logo className={classes.icon} />
|
||||
<H2 className={classes.title}>Lamassu Admin</H2>
|
||||
</div>
|
||||
{!isLoading && wasSuccessful && (
|
||||
<>
|
||||
<Label2 className={classes.inputLabel}>
|
||||
Insert a password
|
||||
</Label2>
|
||||
<TextInput
|
||||
className={classes.input}
|
||||
error={invalidPassword}
|
||||
name="new-password"
|
||||
autoFocus
|
||||
id="new-password"
|
||||
type="password"
|
||||
size="lg"
|
||||
onChange={handlePasswordChange}
|
||||
value={passwordField}
|
||||
/>
|
||||
<Label2 className={classes.inputLabel}>
|
||||
Confirm password
|
||||
</Label2>
|
||||
<TextInput
|
||||
className={classes.input}
|
||||
error={invalidPassword}
|
||||
name="confirm-password"
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
size="lg"
|
||||
onChange={handleConfirmPasswordChange}
|
||||
value={confirmPasswordField}
|
||||
/>
|
||||
<div className={classes.footer}>
|
||||
{invalidPassword && (
|
||||
<P className={classes.errorMessage}>
|
||||
Passwords do not match!
|
||||
</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleRegister()
|
||||
}}
|
||||
buttonClassName={classes.loginButton}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isLoading && !wasSuccessful && (
|
||||
<>
|
||||
<Label2 className={classes.inputLabel}>
|
||||
Link has expired
|
||||
</Label2>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default Register
|
||||
186
new-lamassu-admin/src/pages/Authentication/Reset2FA.js
Normal file
186
new-lamassu-admin/src/pages/Authentication/Reset2FA.js
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import { makeStyles, Grid } from '@material-ui/core'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import axios from 'axios'
|
||||
import QRCode from 'qrcode.react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useLocation, useHistory } from 'react-router-dom'
|
||||
|
||||
import { ActionButton, Button } from 'src/components/buttons'
|
||||
import { CodeInput } from 'src/components/inputs/base'
|
||||
import { H2, Label2, P } from 'src/components/typography'
|
||||
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||
import { primaryColor } from 'src/styling/variables'
|
||||
|
||||
import styles from './Login.styles'
|
||||
|
||||
const useQuery = () => new URLSearchParams(useLocation().search)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const Reset2FA = () => {
|
||||
const classes = useStyles()
|
||||
const history = useHistory()
|
||||
const query = useQuery()
|
||||
const [userID, setUserID] = useState(null)
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [wasSuccessful, setSuccess] = useState(false)
|
||||
const [secret, setSecret] = useState(null)
|
||||
const [otpauth, setOtpauth] = useState(null)
|
||||
|
||||
const [isShowing, setShowing] = useState(false)
|
||||
const [invalidToken, setInvalidToken] = useState(false)
|
||||
const [twoFAConfirmation, setTwoFAConfirmation] = useState('')
|
||||
|
||||
const handle2FAChange = value => {
|
||||
setTwoFAConfirmation(value)
|
||||
setInvalidToken(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
validateQuery()
|
||||
}, [])
|
||||
|
||||
const validateQuery = () => {
|
||||
axios({
|
||||
url: `${url}/api/reset2fa?t=${query.get('t')}`,
|
||||
method: 'GET',
|
||||
options: {
|
||||
withCredentials: true
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res && res.status === 200) {
|
||||
setLoading(false)
|
||||
if (res.data === 'The link has expired') setSuccess(false)
|
||||
else {
|
||||
setUserID(res.data.userID)
|
||||
setSecret(res.data.secret)
|
||||
setOtpauth(res.data.otpauth)
|
||||
setSuccess(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
history.push('/')
|
||||
})
|
||||
}
|
||||
|
||||
const handle2FAReset = () => {
|
||||
axios({
|
||||
url: `${url}/api/update2fa`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
userID: userID,
|
||||
secret: secret,
|
||||
code: twoFAConfirmation
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res && res.status === 200) {
|
||||
history.push('/')
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
setInvalidToken(true)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{ minHeight: '100vh' }}
|
||||
className={classes.welcomeBackground}>
|
||||
<Grid>
|
||||
<div>
|
||||
<Paper elevation={1}>
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classes.titleWrapper}>
|
||||
<Logo className={classes.icon} />
|
||||
<H2 className={classes.title}>Lamassu Admin</H2>
|
||||
</div>
|
||||
{!isLoading && wasSuccessful && (
|
||||
<>
|
||||
<div className={classes.infoWrapper}>
|
||||
<Label2 className={classes.info2}>
|
||||
To finish this process, please scan the following QR code
|
||||
or insert the secret further below on an authentication
|
||||
app of your choice, preferably Google Authenticator or
|
||||
Authy.
|
||||
</Label2>
|
||||
</div>
|
||||
<div className={classes.qrCodeWrapper}>
|
||||
<QRCode size={240} fgColor={primaryColor} value={otpauth} />
|
||||
</div>
|
||||
<div className={classes.secretWrapper}>
|
||||
<Label2 className={classes.secretLabel}>
|
||||
Your secret:
|
||||
</Label2>
|
||||
<Label2
|
||||
className={
|
||||
isShowing ? classes.secret : classes.hiddenSecret
|
||||
}>
|
||||
{secret}
|
||||
</Label2>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setShowing(!isShowing)
|
||||
}}>
|
||||
{isShowing ? 'Hide' : 'Show'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className={classes.confirm2FAInput}>
|
||||
<CodeInput
|
||||
name="2fa"
|
||||
value={twoFAConfirmation}
|
||||
onChange={handle2FAChange}
|
||||
numInputs={6}
|
||||
error={invalidToken}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.twofaFooter}>
|
||||
{invalidToken && (
|
||||
<P className={classes.errorMessage}>
|
||||
Code is invalid. Please try again.
|
||||
</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
handle2FAReset()
|
||||
}}
|
||||
buttonClassName={classes.loginButton}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isLoading && !wasSuccessful && (
|
||||
<>
|
||||
<Label2 className={classes.inputLabel}>
|
||||
Link has expired
|
||||
</Label2>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default Reset2FA
|
||||
177
new-lamassu-admin/src/pages/Authentication/ResetPassword.js
Normal file
177
new-lamassu-admin/src/pages/Authentication/ResetPassword.js
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { makeStyles, Grid } from '@material-ui/core'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import axios from 'axios'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useLocation, useHistory } from 'react-router-dom'
|
||||
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs/base'
|
||||
import { H2, Label2, P } from 'src/components/typography'
|
||||
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||
|
||||
import styles from './Login.styles'
|
||||
|
||||
const useQuery = () => new URLSearchParams(useLocation().search)
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const ResetPassword = () => {
|
||||
const classes = useStyles()
|
||||
const history = useHistory()
|
||||
const query = useQuery()
|
||||
const [newPasswordField, setNewPasswordField] = useState('')
|
||||
const [confirmPasswordField, setConfirmPasswordField] = useState('')
|
||||
const [invalidPassword, setInvalidPassword] = useState(false)
|
||||
const [userID, setUserID] = useState(null)
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [wasSuccessful, setSuccess] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
validateQuery()
|
||||
}, [])
|
||||
|
||||
const validateQuery = () => {
|
||||
axios({
|
||||
url: `${url}/api/resetpassword?t=${query.get('t')}`,
|
||||
method: 'GET',
|
||||
options: {
|
||||
withCredentials: true
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res && res.status === 200) {
|
||||
setLoading(false)
|
||||
if (res.data === 'The link has expired') setSuccess(false)
|
||||
else {
|
||||
setSuccess(true)
|
||||
setUserID(res.data.userID)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
history.push('/')
|
||||
})
|
||||
}
|
||||
|
||||
const handlePasswordReset = () => {
|
||||
if (!isValidPasswordChange()) return setInvalidPassword(true)
|
||||
axios({
|
||||
url: `${url}/api/updatepassword`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
userID: userID,
|
||||
newPassword: newPasswordField
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res && res.status === 200) {
|
||||
history.push('/')
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
history.push('/')
|
||||
})
|
||||
}
|
||||
|
||||
const isValidPasswordChange = () => {
|
||||
return newPasswordField === confirmPasswordField
|
||||
}
|
||||
|
||||
const handleNewPasswordChange = event => {
|
||||
setInvalidPassword(false)
|
||||
setNewPasswordField(event.target.value)
|
||||
}
|
||||
|
||||
const handleConfirmPasswordChange = event => {
|
||||
setInvalidPassword(false)
|
||||
setConfirmPasswordField(event.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
spacing={0}
|
||||
direction="column"
|
||||
alignItems="center"
|
||||
justify="center"
|
||||
style={{ minHeight: '100vh' }}
|
||||
className={classes.welcomeBackground}>
|
||||
<Grid>
|
||||
<div>
|
||||
<Paper elevation={1}>
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classes.titleWrapper}>
|
||||
<Logo className={classes.icon} />
|
||||
<H2 className={classes.title}>Lamassu Admin</H2>
|
||||
</div>
|
||||
{!isLoading && wasSuccessful && (
|
||||
<>
|
||||
<Label2 className={classes.inputLabel}>
|
||||
Insert new password
|
||||
</Label2>
|
||||
<TextInput
|
||||
className={classes.input}
|
||||
error={invalidPassword}
|
||||
name="new-password"
|
||||
autoFocus
|
||||
id="new-password"
|
||||
type="password"
|
||||
size="lg"
|
||||
onChange={handleNewPasswordChange}
|
||||
value={newPasswordField}
|
||||
/>
|
||||
<Label2 className={classes.inputLabel}>
|
||||
Confirm new password
|
||||
</Label2>
|
||||
<TextInput
|
||||
className={classes.input}
|
||||
error={invalidPassword}
|
||||
name="confirm-password"
|
||||
id="confirm-password"
|
||||
type="password"
|
||||
size="lg"
|
||||
onChange={handleConfirmPasswordChange}
|
||||
value={confirmPasswordField}
|
||||
/>
|
||||
<div className={classes.footer}>
|
||||
{invalidPassword && (
|
||||
<P className={classes.errorMessage}>
|
||||
Passwords do not match!
|
||||
</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePasswordReset()
|
||||
}}
|
||||
buttonClassName={classes.loginButton}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!isLoading && !wasSuccessful && (
|
||||
<>
|
||||
<Label2 className={classes.inputLabel}>
|
||||
Link has expired
|
||||
</Label2>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResetPassword
|
||||
180
new-lamassu-admin/src/pages/Authentication/Setup2FAState.js
Normal file
180
new-lamassu-admin/src/pages/Authentication/Setup2FAState.js
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import axios from 'axios'
|
||||
import QRCode from 'qrcode.react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
import { ActionButton, Button } from 'src/components/buttons'
|
||||
import { CodeInput } from 'src/components/inputs/base'
|
||||
import { Label2, P } from 'src/components/typography'
|
||||
import { primaryColor } from 'src/styling/variables'
|
||||
|
||||
import styles from './Login.styles'
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Setup2FAState = ({
|
||||
clientField,
|
||||
passwordField,
|
||||
STATES,
|
||||
handleLoginState
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [secret, setSecret] = useState(null)
|
||||
const [otpauth, setOtpauth] = useState(null)
|
||||
const [isShowing, setShowing] = useState(false)
|
||||
|
||||
const [invalidToken, setInvalidToken] = useState(false)
|
||||
const [twoFAConfirmation, setTwoFAConfirmation] = useState('')
|
||||
|
||||
const handle2FAChange = value => {
|
||||
setTwoFAConfirmation(value)
|
||||
setInvalidToken(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
get2FASecret()
|
||||
}, [])
|
||||
|
||||
const get2FASecret = () => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/login/2fa/setup`,
|
||||
data: {
|
||||
username: clientField,
|
||||
password: passwordField
|
||||
},
|
||||
options: {
|
||||
withCredentials: true
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
setSecret(res.data.secret)
|
||||
setOtpauth(res.data.otpauth)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.response && err.response.data) {
|
||||
if (err.response.status === 403) {
|
||||
handleLoginState(STATES.LOGIN)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const save2FASecret = () => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/login/2fa/save`,
|
||||
data: {
|
||||
username: clientField,
|
||||
password: passwordField,
|
||||
secret: secret,
|
||||
code: twoFAConfirmation
|
||||
},
|
||||
options: {
|
||||
withCredentials: true
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) console.log(err)
|
||||
if (res) {
|
||||
const status = res.status
|
||||
if (status === 200) handleLoginState(STATES.LOGIN)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.response && err.response.data) {
|
||||
if (err.response.status === 403) {
|
||||
setInvalidToken(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{secret && otpauth ? (
|
||||
<>
|
||||
<div className={classes.infoWrapper}>
|
||||
<Label2 className={classes.info2}>
|
||||
We detected that this account does not have its two-factor
|
||||
authentication enabled. In order to protect the resources in the
|
||||
system, a two-factor authentication is enforced.
|
||||
</Label2>
|
||||
<Label2 className={classes.info2}>
|
||||
To finish this process, please scan the following QR code or
|
||||
insert the secret further below on an authentication app of your
|
||||
choice, preferably Google Authenticator or Authy.
|
||||
</Label2>
|
||||
</div>
|
||||
<div className={classes.qrCodeWrapper}>
|
||||
<QRCode size={240} fgColor={primaryColor} value={otpauth} />
|
||||
</div>
|
||||
<div className={classes.secretWrapper}>
|
||||
<Label2 className={classes.secretLabel}>Your secret:</Label2>
|
||||
<Label2
|
||||
className={isShowing ? classes.secret : classes.hiddenSecret}>
|
||||
{secret}
|
||||
</Label2>
|
||||
<ActionButton
|
||||
disabled={!secret && !otpauth}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setShowing(!isShowing)
|
||||
}}>
|
||||
{isShowing ? 'Hide' : 'Show'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className={classes.confirm2FAInput}>
|
||||
<CodeInput
|
||||
name="2fa"
|
||||
value={twoFAConfirmation}
|
||||
onChange={handle2FAChange}
|
||||
numInputs={6}
|
||||
error={invalidToken}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.twofaFooter}>
|
||||
{invalidToken && (
|
||||
<P className={classes.errorMessage}>
|
||||
Code is invalid. Please try again.
|
||||
</P>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
save2FASecret()
|
||||
}}
|
||||
buttonClassName={classes.loginButton}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
// TODO: should maybe show a spinner here?
|
||||
<div className={classes.twofaFooter}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
console.log('response should be arriving soon')
|
||||
}}
|
||||
buttonClassName={classes.loginButton}>
|
||||
Generate Two Factor Authentication Secret
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Setup2FAState
|
||||
|
|
@ -34,7 +34,12 @@ const GET_MACHINES = gql`
|
|||
const NUM_LOG_RESULTS = 500
|
||||
|
||||
const GET_MACHINE_LOGS_CSV = gql`
|
||||
query MachineLogs($deviceId: ID!, $limit: Int, $from: Date, $until: Date) {
|
||||
query MachineLogs(
|
||||
$deviceId: ID!
|
||||
$limit: Int
|
||||
$from: DateTime
|
||||
$until: DateTime
|
||||
) {
|
||||
machineLogsCsv(
|
||||
deviceId: $deviceId
|
||||
limit: $limit
|
||||
|
|
@ -45,7 +50,12 @@ const GET_MACHINE_LOGS_CSV = gql`
|
|||
`
|
||||
|
||||
const GET_MACHINE_LOGS = gql`
|
||||
query MachineLogs($deviceId: ID!, $limit: Int, $from: Date, $until: Date) {
|
||||
query MachineLogs(
|
||||
$deviceId: ID!
|
||||
$limit: Int
|
||||
$from: DateTime
|
||||
$until: DateTime
|
||||
) {
|
||||
machineLogs(
|
||||
deviceId: $deviceId
|
||||
limit: $limit
|
||||
|
|
|
|||
|
|
@ -61,13 +61,13 @@ const formatDate = date => {
|
|||
const NUM_LOG_RESULTS = 500
|
||||
|
||||
const GET_CSV = gql`
|
||||
query ServerData($limit: Int, $from: Date, $until: Date) {
|
||||
query ServerData($limit: Int, $from: DateTime, $until: DateTime) {
|
||||
serverLogsCsv(limit: $limit, from: $from, until: $until)
|
||||
}
|
||||
`
|
||||
|
||||
const GET_DATA = gql`
|
||||
query ServerData($limit: Int, $from: Date, $until: Date) {
|
||||
query ServerData($limit: Int, $from: DateTime, $until: DateTime) {
|
||||
serverVersion
|
||||
uptime {
|
||||
name
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import gql from 'graphql-tag'
|
||||
import moment from 'moment'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import parser from 'ua-parser-js'
|
||||
|
||||
import { IconButton } from 'src/components/buttons'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||
|
||||
const GET_SESSIONS = gql`
|
||||
query sessions {
|
||||
sessions {
|
||||
sid
|
||||
sess
|
||||
expire
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const DELETE_SESSION = gql`
|
||||
mutation deleteSession($sid: String!) {
|
||||
deleteSession(sid: $sid) {
|
||||
sid
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const isLocalhost = ip => {
|
||||
return ip === 'localhost' || ip === '::1' || ip === '127.0.0.1'
|
||||
}
|
||||
|
||||
const SessionManagement = () => {
|
||||
const { data: tknResponse } = useQuery(GET_SESSIONS)
|
||||
|
||||
const [deleteSession] = useMutation(DELETE_SESSION, {
|
||||
refetchQueries: () => ['sessions']
|
||||
})
|
||||
|
||||
const elements = [
|
||||
{
|
||||
header: 'Login',
|
||||
width: 207,
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
view: s => s.sess.user.username
|
||||
},
|
||||
{
|
||||
header: 'Last known use',
|
||||
width: 305,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: s => {
|
||||
const ua = parser(s.sess.ua)
|
||||
return s.sess.ua
|
||||
? `${ua.browser.name} ${ua.browser.version} on ${ua.os.name} ${ua.os.version}`
|
||||
: `No Record`
|
||||
}
|
||||
},
|
||||
{
|
||||
header: 'Last known location',
|
||||
width: 250,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: s => {
|
||||
return isLocalhost(s.sess.ipAddress) ? 'This device' : s.sess.ipAddress
|
||||
}
|
||||
},
|
||||
{
|
||||
header: 'Expiration date (UTC)',
|
||||
width: 290,
|
||||
textAlign: 'right',
|
||||
size: 'sm',
|
||||
view: s =>
|
||||
`${moment.utc(s.expire).format('YYYY-MM-DD')} ${moment
|
||||
.utc(s.expire)
|
||||
.format('HH:mm:ss')}`
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
width: 80,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: s => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
deleteSession({ variables: { sid: s.sid } })
|
||||
}}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleSection title="Session Management" />
|
||||
<DataTable elements={elements} data={R.path(['sessions'])(tknResponse)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SessionManagement
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import gql from 'graphql-tag'
|
||||
import moment from 'moment'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
|
||||
import { IconButton } from 'src/components/buttons'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||
|
||||
const GET_USER_TOKENS = gql`
|
||||
query userTokens {
|
||||
userTokens {
|
||||
token
|
||||
name
|
||||
created
|
||||
user_agent
|
||||
ip_address
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const REVOKE_USER_TOKEN = gql`
|
||||
mutation revokeToken($token: String!) {
|
||||
revokeToken(token: $token) {
|
||||
token
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Tokens = () => {
|
||||
const { data: tknResponse } = useQuery(GET_USER_TOKENS)
|
||||
|
||||
const [revokeToken] = useMutation(REVOKE_USER_TOKEN, {
|
||||
refetchQueries: () => ['userTokens']
|
||||
})
|
||||
|
||||
const elements = [
|
||||
{
|
||||
header: 'Name',
|
||||
width: 257,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: t => t.name
|
||||
},
|
||||
{
|
||||
header: 'Token',
|
||||
width: 505,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: t => t.token
|
||||
},
|
||||
{
|
||||
header: 'Date (UTC)',
|
||||
width: 145,
|
||||
textAlign: 'right',
|
||||
size: 'sm',
|
||||
view: t => moment.utc(t.created).format('YYYY-MM-DD')
|
||||
},
|
||||
{
|
||||
header: 'Time (UTC)',
|
||||
width: 145,
|
||||
textAlign: 'right',
|
||||
size: 'sm',
|
||||
view: t => moment.utc(t.created).format('HH:mm:ss')
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
width: 80,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: t => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
revokeToken({ variables: { token: t.token } })
|
||||
}}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleSection title="Token Management" />
|
||||
<DataTable
|
||||
elements={elements}
|
||||
data={R.path(['userTokens'])(tknResponse)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tokens
|
||||
|
|
@ -24,13 +24,13 @@ const useStyles = makeStyles(mainStyles)
|
|||
const NUM_LOG_RESULTS = 1000
|
||||
|
||||
const GET_TRANSACTIONS_CSV = gql`
|
||||
query transactions($limit: Int, $from: Date, $until: Date) {
|
||||
query transactions($limit: Int, $from: DateTime, $until: DateTime) {
|
||||
transactionsCsv(limit: $limit, from: $from, until: $until)
|
||||
}
|
||||
`
|
||||
|
||||
const GET_TRANSACTIONS = gql`
|
||||
query transactions($limit: Int, $from: Date, $until: Date) {
|
||||
query transactions($limit: Int, $from: DateTime, $until: DateTime) {
|
||||
transactions(limit: $limit, from: $from, until: $until) {
|
||||
id
|
||||
txClass
|
||||
|
|
|
|||
386
new-lamassu-admin/src/pages/UserManagement/UserManagement.js
Normal file
386
new-lamassu-admin/src/pages/UserManagement/UserManagement.js
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
/* eslint-disable prettier/prettier */
|
||||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles, Box, Chip } from '@material-ui/core'
|
||||
import axios from 'axios'
|
||||
import gql from 'graphql-tag'
|
||||
// import moment from 'moment'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useContext } from 'react'
|
||||
// import parser from 'ua-parser-js'
|
||||
|
||||
import { AppContext } from 'src/App'
|
||||
import { Link /*, IconButton */ } from 'src/components/buttons'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
// import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||
|
||||
import styles from './UserManagement.styles'
|
||||
import ChangeRoleModal from './modals/ChangeRoleModal'
|
||||
import CreateUserModal from './modals/CreateUserModal'
|
||||
// import DeleteUserModal from './modals/DeleteUserModal'
|
||||
import EnableUserModal from './modals/EnableUserModal'
|
||||
import Input2FAModal from './modals/Input2FAModal'
|
||||
import Reset2FAModal from './modals/Reset2FAModal'
|
||||
import ResetPasswordModal from './modals/ResetPasswordModal'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const GET_USERS = gql`
|
||||
query users {
|
||||
users {
|
||||
id
|
||||
username
|
||||
role
|
||||
enabled
|
||||
last_accessed
|
||||
last_accessed_from
|
||||
last_accessed_address
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
/* const DELETE_USERS = gql`
|
||||
mutation deleteUser($id: ID!) {
|
||||
deleteUser(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
` */
|
||||
|
||||
const CHANGE_USER_ROLE = gql`
|
||||
mutation changeUserRole($id: ID!, $newRole: String!) {
|
||||
changeUserRole(id: $id, newRole: $newRole) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TOGGLE_USER_ENABLE = gql`
|
||||
mutation toggleUserEnable($id: ID!) {
|
||||
toggleUserEnable(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Users = () => {
|
||||
const classes = useStyles()
|
||||
|
||||
const { userData } = useContext(AppContext)
|
||||
|
||||
const { data: userResponse } = useQuery(GET_USERS)
|
||||
|
||||
/* const [deleteUser] = useMutation(DELETE_USERS, {
|
||||
refetchQueries: () => ['users']
|
||||
}) */
|
||||
|
||||
const [changeUserRole] = useMutation(CHANGE_USER_ROLE, {
|
||||
refetchQueries: () => ['users']
|
||||
})
|
||||
|
||||
const [toggleUserEnable] = useMutation(TOGGLE_USER_ENABLE, {
|
||||
refetchQueries: () => ['users']
|
||||
})
|
||||
|
||||
const [userInfo, setUserInfo] = useState(null)
|
||||
|
||||
const [showCreateUserModal, setShowCreateUserModal] = useState(false)
|
||||
const toggleCreateUserModal = () =>
|
||||
setShowCreateUserModal(!showCreateUserModal)
|
||||
|
||||
const [showResetPasswordModal, setShowResetPasswordModal] = useState(false)
|
||||
const [resetPasswordUrl, setResetPasswordUrl] = useState('')
|
||||
const toggleResetPasswordModal = () =>
|
||||
setShowResetPasswordModal(!showResetPasswordModal)
|
||||
|
||||
const [showReset2FAModal, setShowReset2FAModal] = useState(false)
|
||||
const [reset2FAUrl, setReset2FAUrl] = useState('')
|
||||
const toggleReset2FAModal = () => setShowReset2FAModal(!showReset2FAModal)
|
||||
|
||||
const [showRoleModal, setShowRoleModal] = useState(false)
|
||||
const toggleRoleModal = () =>
|
||||
setShowRoleModal(!showRoleModal)
|
||||
|
||||
const [showEnableUserModal, setShowEnableUserModal] = useState(false)
|
||||
const toggleEnableUserModal = () =>
|
||||
setShowEnableUserModal(!showEnableUserModal)
|
||||
|
||||
/* const [showDeleteUserModal, setShowDeleteUserModal] = useState(false)
|
||||
const toggleDeleteUserModal = () =>
|
||||
setShowDeleteUserModal(!showDeleteUserModal) */
|
||||
|
||||
const [showInputConfirmModal, setShowInputConfirmModal] = useState(false)
|
||||
const toggleInputConfirmModal = () =>
|
||||
setShowInputConfirmModal(!showInputConfirmModal)
|
||||
|
||||
const [action, setAction] = useState(null)
|
||||
|
||||
const requestNewPassword = userID => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/resetpassword`,
|
||||
data: {
|
||||
userID: userID
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
const status = res.status
|
||||
if (status === 200) {
|
||||
const token = res.data.token
|
||||
setResetPasswordUrl(
|
||||
`https://localhost:3001/resetpassword?t=${token.token}`
|
||||
)
|
||||
toggleResetPasswordModal()
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err) console.log('error')
|
||||
})
|
||||
}
|
||||
|
||||
const requestNew2FA = userID => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/reset2fa`,
|
||||
data: {
|
||||
userID: userID
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
const status = res.status
|
||||
if (status === 200) {
|
||||
const token = res.data.token
|
||||
setReset2FAUrl(`https://localhost:3001/reset2fa?t=${token.token}`)
|
||||
toggleReset2FAModal()
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err) console.log('error')
|
||||
})
|
||||
}
|
||||
|
||||
const elements = [
|
||||
{
|
||||
header: 'Login',
|
||||
width: 257,
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
view: u => {
|
||||
if (userData.id === u.id)
|
||||
return (
|
||||
<>
|
||||
{u.username}
|
||||
<Chip size="small" label="You" className={classes.chip} />
|
||||
</>
|
||||
)
|
||||
return u.username
|
||||
}
|
||||
},
|
||||
{
|
||||
header: 'Role',
|
||||
width: 105,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: u => {
|
||||
switch (u.role) {
|
||||
case 'user':
|
||||
return 'Regular'
|
||||
case 'superuser':
|
||||
return 'Superuser'
|
||||
default:
|
||||
return u.role
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
width: 80,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: u => (
|
||||
<Switch
|
||||
disabled={userData.id === u.id}
|
||||
checked={u.role === 'superuser'}
|
||||
onClick={() => {
|
||||
setUserInfo(u)
|
||||
toggleRoleModal()
|
||||
}}
|
||||
value={u.role === 'superuser'}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
width: 25,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: u => {}
|
||||
},
|
||||
{
|
||||
header: 'Actions',
|
||||
width: 565,
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
view: u => {
|
||||
return (
|
||||
<>
|
||||
<Chip
|
||||
size="small"
|
||||
label="Reset password"
|
||||
className={classes.actionChip}
|
||||
onClick={() => {
|
||||
setUserInfo(u)
|
||||
if(u.role === 'superuser') {
|
||||
setAction(() => requestNewPassword.bind(null, u.id))
|
||||
toggleInputConfirmModal()
|
||||
} else {
|
||||
requestNewPassword(u.id)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Chip
|
||||
size="small"
|
||||
label="Reset 2FA"
|
||||
className={classes.actionChip}
|
||||
onClick={() => {
|
||||
setUserInfo(u)
|
||||
if(u.role === 'superuser') {
|
||||
setAction(() => requestNew2FA.bind(null, u.id))
|
||||
toggleInputConfirmModal()
|
||||
} else {
|
||||
requestNew2FA(u.id)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
},
|
||||
/* {
|
||||
header: 'Actions',
|
||||
width: 535,
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
view: u => {
|
||||
const ua = parser(u.last_accessed_from)
|
||||
return u.last_accessed_from
|
||||
? `${ua.browser.name} ${ua.browser.version} on ${ua.os.name} ${ua.os.version}`
|
||||
: `No Record`
|
||||
}
|
||||
}, */
|
||||
{
|
||||
header: 'Enabled',
|
||||
width: 100,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: u => (
|
||||
<Switch
|
||||
disabled={userData.id === u.id}
|
||||
checked={u.enabled}
|
||||
onClick={() => {
|
||||
setUserInfo(u)
|
||||
toggleEnableUserModal()
|
||||
}}
|
||||
value={u.enabled}
|
||||
/>
|
||||
)
|
||||
}/* ,
|
||||
{
|
||||
header: 'Delete',
|
||||
width: 100,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: u => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
setUserInfo(u)
|
||||
toggleDeleteUserModal()
|
||||
}}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
)
|
||||
} */
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleSection title="User Management" />
|
||||
<Box
|
||||
marginBottom={3}
|
||||
marginTop={-5}
|
||||
className={classes.tableWidth}
|
||||
display="flex"
|
||||
justifyContent="flex-end">
|
||||
<Link color="primary" onClick={toggleCreateUserModal}>
|
||||
Add new user
|
||||
</Link>
|
||||
</Box>
|
||||
<DataTable elements={elements} data={R.path(['users'])(userResponse)} />
|
||||
<CreateUserModal
|
||||
showModal={showCreateUserModal}
|
||||
toggleModal={toggleCreateUserModal}
|
||||
/>
|
||||
<ResetPasswordModal
|
||||
showModal={showResetPasswordModal}
|
||||
toggleModal={toggleResetPasswordModal}
|
||||
resetPasswordURL={resetPasswordUrl}
|
||||
user={userInfo}
|
||||
/>
|
||||
<Reset2FAModal
|
||||
showModal={showReset2FAModal}
|
||||
toggleModal={toggleReset2FAModal}
|
||||
reset2FAURL={reset2FAUrl}
|
||||
user={userInfo}
|
||||
/>
|
||||
<ChangeRoleModal
|
||||
showModal={showRoleModal}
|
||||
toggleModal={toggleRoleModal}
|
||||
user={userInfo}
|
||||
confirm={changeUserRole}
|
||||
inputConfirmToggle={toggleInputConfirmModal}
|
||||
setAction={setAction}
|
||||
/>
|
||||
<EnableUserModal
|
||||
showModal={showEnableUserModal}
|
||||
toggleModal={toggleEnableUserModal}
|
||||
user={userInfo}
|
||||
confirm={toggleUserEnable}
|
||||
inputConfirmToggle={toggleInputConfirmModal}
|
||||
setAction={setAction}
|
||||
/>
|
||||
{/* <DeleteUserModal
|
||||
showModal={showDeleteUserModal}
|
||||
toggleModal={toggleDeleteUserModal}
|
||||
user={userInfo}
|
||||
confirm={deleteUser}
|
||||
inputConfirmToggle={toggleInputConfirmModal}
|
||||
setAction={setAction}
|
||||
/> */}
|
||||
<Input2FAModal
|
||||
showModal={showInputConfirmModal}
|
||||
toggleModal={toggleInputConfirmModal}
|
||||
action={action}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Users
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import {
|
||||
spacer,
|
||||
fontPrimary,
|
||||
fontSecondary,
|
||||
primaryColor,
|
||||
subheaderColor,
|
||||
errorColor
|
||||
} from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
footer: {
|
||||
margin: [['auto', 0, spacer * 3, 'auto']]
|
||||
},
|
||||
modalTitle: {
|
||||
marginTop: -5,
|
||||
color: primaryColor,
|
||||
fontFamily: fontPrimary
|
||||
},
|
||||
modalLabel1: {
|
||||
marginTop: 20
|
||||
},
|
||||
modalLabel2: {
|
||||
marginTop: 40
|
||||
},
|
||||
inputLabel: {
|
||||
color: primaryColor,
|
||||
fontFamily: fontPrimary,
|
||||
fontSize: 24,
|
||||
marginLeft: 8,
|
||||
marginTop: 15
|
||||
},
|
||||
tableWidth: {
|
||||
width: 1132
|
||||
},
|
||||
radioGroup: {
|
||||
flexDirection: 'row',
|
||||
width: 500
|
||||
},
|
||||
radioLabel: {
|
||||
width: 150,
|
||||
height: 48
|
||||
},
|
||||
copyToClipboard: {
|
||||
marginLeft: 'auto',
|
||||
paddingTop: 6,
|
||||
paddingLeft: 15,
|
||||
marginRight: -11
|
||||
},
|
||||
chip: {
|
||||
backgroundColor: subheaderColor,
|
||||
fontFamily: fontPrimary,
|
||||
marginLeft: 15,
|
||||
marginTop: -5
|
||||
},
|
||||
actionChip: {
|
||||
backgroundColor: subheaderColor,
|
||||
marginRight: 15,
|
||||
marginTop: -5
|
||||
},
|
||||
info: {
|
||||
fontFamily: fontSecondary,
|
||||
textAlign: 'justify'
|
||||
},
|
||||
addressWrapper: {
|
||||
backgroundColor: subheaderColor,
|
||||
marginTop: 8
|
||||
},
|
||||
address: {
|
||||
margin: `${spacer * 1.5}px ${spacer * 3}px`
|
||||
},
|
||||
errorMessage: {
|
||||
fontFamily: fontSecondary,
|
||||
color: errorColor
|
||||
},
|
||||
codeContainer: {
|
||||
marginTop: 15,
|
||||
marginBottom: 15
|
||||
}
|
||||
}
|
||||
|
||||
export default styles
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { H2, Info3 } from 'src/components/typography'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ChangeRoleModal = ({
|
||||
showModal,
|
||||
toggleModal,
|
||||
user,
|
||||
confirm,
|
||||
inputConfirmToggle,
|
||||
setAction
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const handleClose = () => {
|
||||
toggleModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={275}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<H2 className={classes.modalTitle}>Change {user.username}'s role?</H2>
|
||||
<Info3 className={classes.info}>
|
||||
You are about to alter {user.username}'s role. This will change this
|
||||
user's permission to access certain resources.
|
||||
</Info3>
|
||||
<Info3 className={classes.info}>Do you wish to proceed?</Info3>
|
||||
<div className={classes.footer}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setAction(() =>
|
||||
confirm.bind(null, {
|
||||
variables: {
|
||||
id: user.id,
|
||||
newRole: user.role === 'superuser' ? 'user' : 'superuser'
|
||||
}
|
||||
})
|
||||
)
|
||||
inputConfirmToggle()
|
||||
handleClose()
|
||||
}}>
|
||||
Finish
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangeRoleModal
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import axios from 'axios'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { RadioGroup } from 'src/components/inputs'
|
||||
import { TextInput } from 'src/components/inputs/base'
|
||||
import { H1, H2, H3, Info3, Mono } from 'src/components/typography'
|
||||
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const CreateUserModal = ({ showModal, toggleModal }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [usernameField, setUsernameField] = useState('')
|
||||
const [roleField, setRoleField] = useState('')
|
||||
const [createUserURL, setCreateUserURL] = useState(null)
|
||||
const [invalidUser, setInvalidUser] = useState(false)
|
||||
|
||||
const radioOptions = [
|
||||
{
|
||||
code: 'user',
|
||||
display: 'Regular user'
|
||||
},
|
||||
{
|
||||
code: 'superuser',
|
||||
display: 'Superuser'
|
||||
}
|
||||
]
|
||||
|
||||
const handleUsernameChange = event => {
|
||||
if (event.target.value === '') {
|
||||
setInvalidUser(false)
|
||||
}
|
||||
setUsernameField(event.target.value)
|
||||
}
|
||||
|
||||
const handleRoleChange = event => {
|
||||
setRoleField(event.target.value)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setUsernameField('')
|
||||
setRoleField('')
|
||||
setInvalidUser(false)
|
||||
setCreateUserURL(null)
|
||||
toggleModal()
|
||||
}
|
||||
|
||||
const handleCreateUser = () => {
|
||||
const username = usernameField.trim()
|
||||
|
||||
if (username === '') {
|
||||
setInvalidUser(true)
|
||||
return
|
||||
}
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/createuser`,
|
||||
data: {
|
||||
username: username,
|
||||
role: roleField
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
const status = res.status
|
||||
const message = res.data.message
|
||||
if (status === 200 && message) setInvalidUser(true)
|
||||
if (status === 200 && !message) {
|
||||
const token = res.data.token
|
||||
setCreateUserURL(`https://localhost:3001/register?t=${token.token}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err) console.log('error')
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && !createUserURL && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={400}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<H1 className={classes.modalTitle}>Create new user</H1>
|
||||
<H3 className={classes.modalLabel1}>User login</H3>
|
||||
<TextInput
|
||||
error={invalidUser}
|
||||
name="username"
|
||||
autoFocus
|
||||
id="username"
|
||||
type="text"
|
||||
size="lg"
|
||||
width={338}
|
||||
onChange={handleUsernameChange}
|
||||
value={usernameField}
|
||||
/>
|
||||
<H3 className={classes.modalLabel2}>Role</H3>
|
||||
<RadioGroup
|
||||
name="userrole"
|
||||
value={roleField}
|
||||
options={radioOptions}
|
||||
onChange={handleRoleChange}
|
||||
className={classes.radioGroup}
|
||||
labelClassName={classes.radioLabel}
|
||||
/>
|
||||
<div className={classes.footer}>
|
||||
<Button onClick={handleCreateUser}>Finish</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
{showModal && createUserURL && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={215}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<H2 className={classes.modalTitle}>Creating {usernameField}...</H2>
|
||||
<Info3 className={classes.info}>
|
||||
Safely share this link with {usernameField} to finish the
|
||||
registration process.
|
||||
</Info3>
|
||||
<div className={classes.addressWrapper}>
|
||||
<Mono className={classes.address}>
|
||||
<strong>
|
||||
<CopyToClipboard buttonClassname={classes.copyToClipboard}>
|
||||
{createUserURL}
|
||||
</CopyToClipboard>
|
||||
</strong>
|
||||
</Mono>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateUserModal
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { H2, Info3 } from 'src/components/typography'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const DeleteUserModal = ({
|
||||
showModal,
|
||||
toggleModal,
|
||||
user,
|
||||
confirm,
|
||||
inputConfirmToggle,
|
||||
setAction
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const handleClose = () => {
|
||||
toggleModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={275}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<H2 className={classes.modalTitle}>Delete {user.username}?</H2>
|
||||
<Info3 className={classes.info}>
|
||||
You are about to delete {user.username}. This will remove existent
|
||||
sessions and revoke this user's permissions to access the system.
|
||||
</Info3>
|
||||
<Info3 className={classes.info}>
|
||||
This is a <b>PERMANENT</b> operation. Do you wish to proceed?
|
||||
</Info3>
|
||||
<div className={classes.footer}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (user.role === 'superuser') {
|
||||
setAction(() =>
|
||||
confirm.bind(null, {
|
||||
variables: {
|
||||
id: user.id
|
||||
}
|
||||
})
|
||||
)
|
||||
inputConfirmToggle()
|
||||
} else {
|
||||
confirm({
|
||||
variables: {
|
||||
id: user.id
|
||||
}
|
||||
})
|
||||
}
|
||||
handleClose()
|
||||
}}>
|
||||
Finish
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeleteUserModal
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { H2, Info3 } from 'src/components/typography'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const EnableUserModal = ({
|
||||
showModal,
|
||||
toggleModal,
|
||||
user,
|
||||
confirm,
|
||||
inputConfirmToggle,
|
||||
setAction
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const handleClose = () => {
|
||||
toggleModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={275}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
{!user.enabled && (
|
||||
<>
|
||||
<H2 className={classes.modalTitle}>Enable {user.username}?</H2>
|
||||
<Info3 className={classes.info}>
|
||||
You are about to enable {user.username} into the system,
|
||||
activating previous eligible sessions and grant permissions to
|
||||
access the system.
|
||||
</Info3>
|
||||
<Info3 className={classes.info}>Do you wish to proceed?</Info3>
|
||||
</>
|
||||
)}
|
||||
{user.enabled && (
|
||||
<>
|
||||
<H2 className={classes.modalTitle}>Disable {user.username}?</H2>
|
||||
<Info3 className={classes.info}>
|
||||
You are about to disable {user.username} from the system,
|
||||
deactivating previous eligible sessions and removing permissions
|
||||
to access the system.
|
||||
</Info3>
|
||||
<Info3 className={classes.info}>Do you wish to proceed?</Info3>
|
||||
</>
|
||||
)}
|
||||
<div className={classes.footer}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (user.role === 'superuser') {
|
||||
setAction(() =>
|
||||
confirm.bind(null, {
|
||||
variables: {
|
||||
id: user.id
|
||||
}
|
||||
})
|
||||
)
|
||||
inputConfirmToggle()
|
||||
} else {
|
||||
confirm({
|
||||
variables: {
|
||||
id: user.id
|
||||
}
|
||||
})
|
||||
}
|
||||
handleClose()
|
||||
}}>
|
||||
Finish
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default EnableUserModal
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import axios from 'axios'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { CodeInput } from 'src/components/inputs/base'
|
||||
import { H2, Info3, P } from 'src/components/typography'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
||||
const url =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Input2FAModal = ({ showModal, toggleModal, action, vars }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const [twoFACode, setTwoFACode] = useState('')
|
||||
const [invalidCode, setInvalidCode] = useState(false)
|
||||
|
||||
const handleCodeChange = value => {
|
||||
setTwoFACode(value)
|
||||
setInvalidCode(false)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setTwoFACode('')
|
||||
setInvalidCode(false)
|
||||
toggleModal()
|
||||
}
|
||||
|
||||
const handleActionConfirm = () => {
|
||||
axios({
|
||||
method: 'POST',
|
||||
url: `${url}/api/confirm2fa`,
|
||||
data: {
|
||||
code: twoFACode
|
||||
},
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((res, err) => {
|
||||
if (err) return
|
||||
if (res) {
|
||||
const status = res.status
|
||||
if (status === 200) {
|
||||
action()
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
const errStatus = err.response.status
|
||||
if (errStatus === 401) setInvalidCode(true)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={400}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<H2 className={classes.modalTitle}>Confirm action</H2>
|
||||
<Info3 className={classes.info}>
|
||||
Please confirm this action by placing your two-factor authentication
|
||||
code below.
|
||||
</Info3>
|
||||
<CodeInput
|
||||
name="2fa"
|
||||
value={twoFACode}
|
||||
onChange={handleCodeChange}
|
||||
numInputs={6}
|
||||
error={invalidCode}
|
||||
containerStyle={classes.codeContainer}
|
||||
shouldAutoFocus
|
||||
/>
|
||||
{invalidCode && (
|
||||
<P className={classes.errorMessage}>
|
||||
Code is invalid. Please try again.
|
||||
</P>
|
||||
)}
|
||||
<div className={classes.footer}>
|
||||
<Button onClick={handleActionConfirm}>Finish</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Input2FAModal
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { H2, Info3, Mono } from 'src/components/typography'
|
||||
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const Reset2FAModal = ({ showModal, toggleModal, reset2FAURL, user }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const handleClose = () => {
|
||||
toggleModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={215}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<H2 className={classes.modalTitle}>Reset 2FA for {user.username}</H2>
|
||||
<Info3 className={classes.info}>
|
||||
Safely share this link with {user.username} for a two-factor
|
||||
authentication reset.
|
||||
</Info3>
|
||||
<div className={classes.addressWrapper}>
|
||||
<Mono className={classes.address}>
|
||||
<strong>
|
||||
<CopyToClipboard buttonClassname={classes.copyToClipboard}>
|
||||
{reset2FAURL}
|
||||
</CopyToClipboard>
|
||||
</strong>
|
||||
</Mono>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Reset2FAModal
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { H2, Info3, Mono } from 'src/components/typography'
|
||||
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
||||
|
||||
import styles from '../UserManagement.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const ResetPasswordModal = ({
|
||||
showModal,
|
||||
toggleModal,
|
||||
resetPasswordURL,
|
||||
user
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const handleClose = () => {
|
||||
toggleModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={215}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<H2 className={classes.modalTitle}>
|
||||
Reset password for {user.username}
|
||||
</H2>
|
||||
<Info3 className={classes.info}>
|
||||
Safely share this link with {user.username} for a password reset.
|
||||
</Info3>
|
||||
<div className={classes.addressWrapper}>
|
||||
<Mono className={classes.address}>
|
||||
<strong>
|
||||
<CopyToClipboard buttonClassname={classes.copyToClipboard}>
|
||||
{resetPasswordURL}
|
||||
</CopyToClipboard>
|
||||
</strong>
|
||||
</Mono>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResetPasswordModal
|
||||
|
|
@ -1,27 +1,14 @@
|
|||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { Route, Redirect } from 'react-router-dom'
|
||||
|
||||
const isAuthenticated = () => {
|
||||
return localStorage.getItem('loggedIn')
|
||||
}
|
||||
import { AppContext } from 'src/App'
|
||||
|
||||
const PrivateRoute = ({ children, ...rest }) => {
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
render={({ location }) =>
|
||||
isAuthenticated() ? (
|
||||
children
|
||||
) : (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/login'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
import { isLoggedIn } from './utils'
|
||||
|
||||
const PrivateRoute = ({ ...rest }) => {
|
||||
const { userData } = useContext(AppContext)
|
||||
|
||||
return isLoggedIn(userData) ? <Route {...rest} /> : <Redirect to="/login" />
|
||||
}
|
||||
|
||||
export default PrivateRoute
|
||||
|
|
|
|||
25
new-lamassu-admin/src/routing/PublicRoute.js
Normal file
25
new-lamassu-admin/src/routing/PublicRoute.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import React, { useContext } from 'react'
|
||||
import { Route, Redirect } from 'react-router-dom'
|
||||
|
||||
import { AppContext } from 'src/App'
|
||||
|
||||
import { isLoggedIn } from './utils'
|
||||
|
||||
const PublicRoute = ({ component: Component, restricted, ...rest }) => {
|
||||
const { userData } = useContext(AppContext)
|
||||
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
render={props =>
|
||||
isLoggedIn(userData) && restricted ? (
|
||||
<Redirect to="/" />
|
||||
) : (
|
||||
<Component {...props} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default PublicRoute
|
||||
|
|
@ -13,7 +13,11 @@ import {
|
|||
} from 'react-router-dom'
|
||||
|
||||
import AppContext from 'src/AppContext'
|
||||
import AuthRegister from 'src/pages/AuthRegister'
|
||||
// import AuthRegister from 'src/pages/AuthRegister'
|
||||
import Login from 'src/pages/Authentication/Login'
|
||||
import Register from 'src/pages/Authentication/Register'
|
||||
import Reset2FA from 'src/pages/Authentication/Reset2FA'
|
||||
import ResetPassword from 'src/pages/Authentication/ResetPassword'
|
||||
import Blacklist from 'src/pages/Blacklist'
|
||||
import Cashout from 'src/pages/Cashout'
|
||||
import Commissions from 'src/pages/Commissions'
|
||||
|
|
@ -34,13 +38,18 @@ import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
|||
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
||||
import ServerLogs from 'src/pages/ServerLogs'
|
||||
import Services from 'src/pages/Services/Services'
|
||||
// import TokenManagement from 'src/pages/TokenManagement/TokenManagement'
|
||||
import SessionManagement from 'src/pages/SessionManagement/SessionManagement'
|
||||
import Transactions from 'src/pages/Transactions/Transactions'
|
||||
import Triggers from 'src/pages/Triggers'
|
||||
import UserManagement from 'src/pages/UserManagement/UserManagement'
|
||||
import WalletSettings from 'src/pages/Wallet/Wallet'
|
||||
import Wizard from 'src/pages/Wizard'
|
||||
import { namespaces } from 'src/utils/config'
|
||||
|
||||
import PrivateRoute from './PrivateRoute'
|
||||
import PublicRoute from './PublicRoute'
|
||||
import { ROLES } from './utils'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
|
|
@ -55,12 +64,14 @@ const tree = [
|
|||
key: 'transactions',
|
||||
label: 'Transactions',
|
||||
route: '/transactions',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: Transactions
|
||||
},
|
||||
{
|
||||
key: 'maintenance',
|
||||
label: 'Maintenance',
|
||||
route: '/maintenance',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
get component() {
|
||||
return () => <Redirect to={this.children[0].route} />
|
||||
},
|
||||
|
|
@ -69,30 +80,35 @@ const tree = [
|
|||
key: 'cash_cassettes',
|
||||
label: 'Cash Cassettes',
|
||||
route: '/maintenance/cash-cassettes',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: CashCassettes
|
||||
},
|
||||
{
|
||||
key: 'funding',
|
||||
label: 'Funding',
|
||||
route: '/maintenance/funding',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: Funding
|
||||
},
|
||||
{
|
||||
key: 'logs',
|
||||
label: 'Machine Logs',
|
||||
route: '/maintenance/logs',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: MachineLogs
|
||||
},
|
||||
{
|
||||
key: 'machine-status',
|
||||
label: 'Machine Status',
|
||||
route: '/maintenance/machine-status',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: MachineStatus
|
||||
},
|
||||
{
|
||||
key: 'server-logs',
|
||||
label: 'Server',
|
||||
route: '/maintenance/server-logs',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: ServerLogs
|
||||
}
|
||||
]
|
||||
|
|
@ -101,6 +117,7 @@ const tree = [
|
|||
key: 'settings',
|
||||
label: 'Settings',
|
||||
route: '/settings',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
get component() {
|
||||
return () => <Redirect to={this.children[0].route} />
|
||||
},
|
||||
|
|
@ -109,36 +126,42 @@ const tree = [
|
|||
key: namespaces.COMMISSIONS,
|
||||
label: 'Commissions',
|
||||
route: '/settings/commissions',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: Commissions
|
||||
},
|
||||
{
|
||||
key: namespaces.LOCALE,
|
||||
label: 'Locales',
|
||||
route: '/settings/locale',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: Locales
|
||||
},
|
||||
{
|
||||
key: namespaces.CASH_OUT,
|
||||
label: 'Cash-out',
|
||||
route: '/settings/cash-out',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: Cashout
|
||||
},
|
||||
{
|
||||
key: namespaces.NOTIFICATIONS,
|
||||
label: 'Notifications',
|
||||
route: '/settings/notifications',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: Notifications
|
||||
},
|
||||
{
|
||||
key: 'services',
|
||||
label: '3rd party services',
|
||||
route: '/settings/3rd-party-services',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: Services
|
||||
},
|
||||
{
|
||||
key: namespaces.WALLETS,
|
||||
label: 'Wallet',
|
||||
route: '/settings/wallet-settings',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: WalletSettings
|
||||
},
|
||||
{
|
||||
|
|
@ -146,6 +169,7 @@ const tree = [
|
|||
label: 'Operator Info',
|
||||
route: '/settings/operator-info',
|
||||
title: 'Operator Information',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
get component() {
|
||||
return () => (
|
||||
<Redirect
|
||||
|
|
@ -161,24 +185,28 @@ const tree = [
|
|||
key: 'contact-info',
|
||||
label: 'Contact information',
|
||||
route: '/settings/operator-info/contact-info',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: ContactInfo
|
||||
},
|
||||
{
|
||||
key: 'receipt-printing',
|
||||
label: 'Receipt',
|
||||
route: '/settings/operator-info/receipt-printing',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: ReceiptPrinting
|
||||
},
|
||||
{
|
||||
key: 'coin-atm-radar',
|
||||
label: 'Coin ATM Radar',
|
||||
route: '/settings/operator-info/coin-atm-radar',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: CoinAtmRadar
|
||||
},
|
||||
{
|
||||
key: 'terms-conditions',
|
||||
label: 'Terms & Conditions',
|
||||
route: '/settings/operator-info/terms-conditions',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: TermsConditions
|
||||
}
|
||||
]
|
||||
|
|
@ -189,6 +217,7 @@ const tree = [
|
|||
key: 'compliance',
|
||||
label: 'Compliance',
|
||||
route: '/compliance',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
get component() {
|
||||
return () => <Redirect to={this.children[0].route} />
|
||||
},
|
||||
|
|
@ -197,18 +226,21 @@ const tree = [
|
|||
key: 'triggers',
|
||||
label: 'Triggers',
|
||||
route: '/compliance/triggers',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: Triggers
|
||||
},
|
||||
{
|
||||
key: 'customers',
|
||||
label: 'Customers',
|
||||
route: '/compliance/customers',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: Customers
|
||||
},
|
||||
{
|
||||
key: 'blacklist',
|
||||
label: 'Blacklist',
|
||||
route: '/compliance/blacklist',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: Blacklist
|
||||
},
|
||||
{
|
||||
|
|
@ -220,9 +252,35 @@ const tree = [
|
|||
{
|
||||
key: 'customer',
|
||||
route: '/compliance/customer/:id',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
component: CustomerProfile
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
label: 'System',
|
||||
route: '/system',
|
||||
allowedRoles: [ROLES.SUPERUSER],
|
||||
get component() {
|
||||
return () => <Redirect to={this.children[0].route} />
|
||||
},
|
||||
children: [
|
||||
{
|
||||
key: 'user-management',
|
||||
label: 'User Management',
|
||||
route: '/system/user-management',
|
||||
allowedRoles: [ROLES.SUPERUSER],
|
||||
component: UserManagement
|
||||
},
|
||||
{
|
||||
key: 'session-management',
|
||||
label: 'Session Management',
|
||||
route: '/system/session-management',
|
||||
allowedRoles: [ROLES.SUPERUSER],
|
||||
component: SessionManagement
|
||||
}
|
||||
]
|
||||
}
|
||||
// {
|
||||
// key: 'system',
|
||||
|
|
@ -276,13 +334,32 @@ const Routes = () => {
|
|||
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
const { wizardTested, userData } = useContext(AppContext)
|
||||
|
||||
const { wizardTested } = useContext(AppContext)
|
||||
|
||||
const dontTriggerPages = ['/404', '/register', '/wizard']
|
||||
const dontTriggerPages = [
|
||||
'/404',
|
||||
'/register',
|
||||
'/wizard',
|
||||
'/login',
|
||||
'/register',
|
||||
'/resetpassword',
|
||||
'/reset2fa'
|
||||
]
|
||||
|
||||
if (!wizardTested && !R.contains(location.pathname)(dontTriggerPages)) {
|
||||
history.push('/wizard')
|
||||
return null
|
||||
}
|
||||
|
||||
const getFilteredRoutes = () => {
|
||||
if (!userData) return []
|
||||
|
||||
return flattened.filter(value => {
|
||||
const keys = value.allowedRoles.map(v => {
|
||||
return v.key
|
||||
})
|
||||
return R.includes(userData.role, keys)
|
||||
})
|
||||
}
|
||||
|
||||
const Transition = location.state ? Slide : Fade
|
||||
|
|
@ -300,10 +377,10 @@ const Routes = () => {
|
|||
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Redirect to={{ pathname: '/dashboard' }} />
|
||||
</Route>
|
||||
<Route path={'/dashboard'}>
|
||||
<PrivateRoute exact path="/">
|
||||
<Redirect to={{ pathname: '/transactions' }} />
|
||||
</PrivateRoute>
|
||||
<PrivateRoute path={'/dashboard'}>
|
||||
<Transition
|
||||
className={classes.wrapper}
|
||||
{...transitionProps}
|
||||
|
|
@ -316,12 +393,15 @@ const Routes = () => {
|
|||
</div>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/machines" component={Machines} />
|
||||
<Route path="/wizard" component={Wizard} />
|
||||
<Route path="/register" component={AuthRegister} />
|
||||
</PrivateRoute>
|
||||
<PrivateRoute path="/machines" component={Machines} />
|
||||
<PrivateRoute path="/wizard" component={Wizard} />
|
||||
<Route path="/register" component={Register} />
|
||||
<PublicRoute path="/login" restricted component={Login} />
|
||||
<Route path="/resetpassword" component={ResetPassword} />
|
||||
<Route path="/reset2fa" component={Reset2FA} />
|
||||
{/* <Route path="/configmigration" component={ConfigMigration} /> */}
|
||||
{flattened.map(({ route, component: Page, key }) => (
|
||||
{getFilteredRoutes().map(({ route, component: Page, key }) => (
|
||||
<Route path={route} key={key}>
|
||||
<Transition
|
||||
className={classes.wrapper}
|
||||
|
|
@ -331,7 +411,9 @@ const Routes = () => {
|
|||
unmountOnExit
|
||||
children={
|
||||
<div className={classes.wrapper}>
|
||||
<Page name={key} />
|
||||
<PrivateRoute path={route} key={key}>
|
||||
<Page name={key} />
|
||||
</PrivateRoute>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
8
new-lamassu-admin/src/routing/utils.js
Normal file
8
new-lamassu-admin/src/routing/utils.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export const isLoggedIn = userData => {
|
||||
return userData
|
||||
}
|
||||
|
||||
export const ROLES = {
|
||||
USER: { key: 'user', value: '0' },
|
||||
SUPERUSER: { key: 'superuser', value: '1' }
|
||||
}
|
||||
|
|
@ -4,20 +4,23 @@ import { ApolloClient } from 'apollo-client'
|
|||
import { ApolloLink } from 'apollo-link'
|
||||
import { onError } from 'apollo-link-error'
|
||||
import { HttpLink } from 'apollo-link-http'
|
||||
import React from 'react'
|
||||
import React, { useContext } from 'react'
|
||||
import { useHistory, useLocation } from 'react-router-dom'
|
||||
|
||||
import { AppContext } from 'src/App'
|
||||
|
||||
const URI =
|
||||
process.env.NODE_ENV === 'development' ? 'https://localhost:8070' : ''
|
||||
|
||||
const getClient = (history, location) =>
|
||||
const getClient = (history, location, setUserData) =>
|
||||
new ApolloClient({
|
||||
link: ApolloLink.from([
|
||||
onError(({ graphQLErrors, networkError }) => {
|
||||
if (graphQLErrors)
|
||||
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
|
||||
if (extensions?.code === 'UNAUTHENTICATED') {
|
||||
if (location.pathname !== '/404') history.push('/404')
|
||||
setUserData(null)
|
||||
if (location.pathname !== '/login') history.push('/login')
|
||||
}
|
||||
console.log(
|
||||
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
||||
|
|
@ -49,7 +52,9 @@ const getClient = (history, location) =>
|
|||
const Provider = ({ children }) => {
|
||||
const history = useHistory()
|
||||
const location = useLocation()
|
||||
const client = getClient(history, location)
|
||||
const { setUserData } = useContext(AppContext)
|
||||
const client = getClient(history, location, setUserData)
|
||||
|
||||
return <ApolloProvider client={client}>{children}</ApolloProvider>
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue