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:
Sérgio Salgado 2020-10-27 10:05:06 +00:00 committed by Josh Harvey
parent 368781864e
commit fded22f39a
50 changed files with 9839 additions and 4501 deletions

File diff suppressed because it is too large Load diff

View file

@ -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",

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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

View 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

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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}>

View file

@ -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>
)
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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>
}
/>

View file

@ -0,0 +1,8 @@
export const isLoggedIn = userData => {
return userData
}
export const ROLES = {
USER: { key: 'user', value: '0' },
SUPERUSER: { key: 'superuser', value: '1' }
}

View file

@ -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>
}