290 lines
7.8 KiB
JavaScript
290 lines
7.8 KiB
JavaScript
import { useQuery, useMutation, useLazyQuery, gql } from '@apollo/client'
|
|
import Chip from '@mui/material/Chip'
|
|
import Switch from '@mui/material/Switch'
|
|
import { makeStyles } from '@mui/styles'
|
|
import { startAttestation } from '@simplewebauthn/browser'
|
|
import * as R from 'ramda'
|
|
import React, { useReducer, useState, useContext } from 'react'
|
|
import TitleSection from 'src/components/layout/TitleSection'
|
|
import DataTable from 'src/components/tables/DataTable'
|
|
import WhiteKeyIcon from 'src/styling/icons/button/key/white.svg?react'
|
|
import KeyIcon from 'src/styling/icons/button/key/zodiac.svg?react'
|
|
import WhiteLockIcon from 'src/styling/icons/button/lock/white.svg?react'
|
|
import LockIcon from 'src/styling/icons/button/lock/zodiac.svg?react'
|
|
import WhiteUserRoleIcon from 'src/styling/icons/button/user-role/white.svg?react'
|
|
import UserRoleIcon from 'src/styling/icons/button/user-role/zodiac.svg?react'
|
|
|
|
import AppContext from 'src/AppContext'
|
|
import { ActionButton, Link } from 'src/components/buttons'
|
|
import { IP_CHECK_REGEX } from 'src/utils/constants'
|
|
|
|
import styles from './UserManagement.styles'
|
|
import ChangeRoleModal from './modals/ChangeRoleModal'
|
|
import CreateUserModal from './modals/CreateUserModal'
|
|
import EnableUserModal from './modals/EnableUserModal'
|
|
import FIDOModal from './modals/FIDOModal'
|
|
import Reset2FAModal from './modals/Reset2FAModal'
|
|
import ResetPasswordModal from './modals/ResetPasswordModal'
|
|
|
|
const useStyles = makeStyles(styles)
|
|
|
|
const GET_USERS = gql`
|
|
query users {
|
|
users {
|
|
id
|
|
username
|
|
role
|
|
enabled
|
|
last_accessed
|
|
last_accessed_from
|
|
last_accessed_address
|
|
}
|
|
}
|
|
`
|
|
|
|
const GENERATE_ATTESTATION = gql`
|
|
query generateAttestationOptions($userID: ID!, $domain: String!) {
|
|
generateAttestationOptions(userID: $userID, domain: $domain)
|
|
}
|
|
`
|
|
|
|
const VALIDATE_ATTESTATION = gql`
|
|
mutation validateAttestation(
|
|
$userID: ID!
|
|
$attestationResponse: JSONObject!
|
|
$domain: String!
|
|
) {
|
|
validateAttestation(
|
|
userID: $userID
|
|
attestationResponse: $attestationResponse
|
|
domain: $domain
|
|
)
|
|
}
|
|
`
|
|
|
|
const initialState = {
|
|
showCreateUserModal: false,
|
|
showResetPasswordModal: false,
|
|
showReset2FAModal: false,
|
|
showRoleModal: false,
|
|
showEnableUserModal: false
|
|
}
|
|
|
|
const reducer = (_, action) => {
|
|
const { type, payload } = action
|
|
switch (type) {
|
|
case 'close':
|
|
return initialState
|
|
case 'open':
|
|
return { ...initialState, [payload]: true }
|
|
default:
|
|
return initialState
|
|
}
|
|
}
|
|
|
|
const roleMapper = {
|
|
user: 'Regular',
|
|
superuser: 'Superuser'
|
|
}
|
|
|
|
const Users = () => {
|
|
const classes = useStyles()
|
|
const { userData } = useContext(AppContext)
|
|
|
|
const { data: userResponse } = useQuery(GET_USERS)
|
|
const [state, dispatch] = useReducer(reducer, initialState)
|
|
|
|
const [userInfo, setUserInfo] = useState(null)
|
|
|
|
const [validateAttestation] = useMutation(VALIDATE_ATTESTATION, {
|
|
onCompleted: res => {
|
|
// TODO: show a brief popup to have UX feedback?
|
|
}
|
|
})
|
|
|
|
const [generateAttestationOptions] = useLazyQuery(GENERATE_ATTESTATION, {
|
|
onCompleted: ({ generateAttestationOptions: options }) => {
|
|
return startAttestation(options).then(res => {
|
|
validateAttestation({
|
|
variables: {
|
|
userID: userInfo.id,
|
|
attestationResponse: res,
|
|
domain: window.location.hostname
|
|
}
|
|
})
|
|
})
|
|
}
|
|
})
|
|
|
|
const elements = [
|
|
{
|
|
header: 'Login',
|
|
width: 307,
|
|
textAlign: 'left',
|
|
size: 'sm',
|
|
view: u => {
|
|
if (userData.id === u.id)
|
|
return (
|
|
<div className={classes.loginWrapper}>
|
|
<span className={classes.username}>{u.username}</span>
|
|
<Chip size="small" label="You" className={classes.chip} />
|
|
</div>
|
|
)
|
|
return <span className={classes.username}>{u.username}</span>
|
|
}
|
|
},
|
|
{
|
|
header: 'Role',
|
|
width: 160,
|
|
textAlign: 'left',
|
|
size: 'sm',
|
|
view: u => (
|
|
<div className={classes.loginWrapper}>
|
|
<span>{roleMapper[u.role]}</span>
|
|
<Switch
|
|
className={classes.roleSwitch}
|
|
disabled={userData.id === u.id}
|
|
checked={u.role === 'superuser'}
|
|
onClick={() => {
|
|
setUserInfo(u)
|
|
dispatch({
|
|
type: 'open',
|
|
payload: 'showRoleModal'
|
|
})
|
|
}}
|
|
value={u.role === 'superuser'}
|
|
/>
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
header: 'Actions',
|
|
width: 565,
|
|
textAlign: 'left',
|
|
size: 'sm',
|
|
view: u => {
|
|
return (
|
|
<div className={classes.actionButtonWrapper}>
|
|
<ActionButton
|
|
Icon={KeyIcon}
|
|
InverseIcon={WhiteKeyIcon}
|
|
color="primary"
|
|
onClick={() => {
|
|
setUserInfo(u)
|
|
dispatch({
|
|
type: 'open',
|
|
payload: 'showResetPasswordModal'
|
|
})
|
|
}}>
|
|
Reset password
|
|
</ActionButton>
|
|
<ActionButton
|
|
Icon={LockIcon}
|
|
InverseIcon={WhiteLockIcon}
|
|
color="primary"
|
|
onClick={() => {
|
|
setUserInfo(u)
|
|
dispatch({
|
|
type: 'open',
|
|
payload: 'showReset2FAModal'
|
|
})
|
|
}}>
|
|
Reset 2FA
|
|
</ActionButton>
|
|
<ActionButton
|
|
Icon={UserRoleIcon}
|
|
InverseIcon={WhiteUserRoleIcon}
|
|
color="primary"
|
|
onClick={() => {
|
|
if (IP_CHECK_REGEX.test(window.location.hostname)) {
|
|
dispatch({
|
|
type: 'open',
|
|
payload: 'showFIDOModal'
|
|
})
|
|
} else {
|
|
setUserInfo(u)
|
|
generateAttestationOptions({
|
|
variables: {
|
|
userID: u.id,
|
|
domain: window.location.hostname
|
|
}
|
|
})
|
|
}
|
|
}}>
|
|
Add FIDO
|
|
</ActionButton>
|
|
</div>
|
|
)
|
|
}
|
|
},
|
|
{
|
|
header: 'Enabled',
|
|
width: 100,
|
|
textAlign: 'center',
|
|
size: 'sm',
|
|
view: u => (
|
|
<Switch
|
|
disabled={userData.id === u.id}
|
|
checked={u.enabled}
|
|
onClick={() => {
|
|
setUserInfo(u)
|
|
dispatch({
|
|
type: 'open',
|
|
payload: 'showEnableUserModal'
|
|
})
|
|
}}
|
|
value={u.enabled}
|
|
/>
|
|
)
|
|
}
|
|
]
|
|
|
|
return (
|
|
<>
|
|
<TitleSection
|
|
title="User management"
|
|
appendixRight={
|
|
<Link
|
|
color="primary"
|
|
onClick={() => {
|
|
dispatch({
|
|
type: 'open',
|
|
payload: 'showCreateUserModal'
|
|
})
|
|
}}>
|
|
Add new user
|
|
</Link>
|
|
}
|
|
/>
|
|
<DataTable elements={elements} data={R.path(['users'])(userResponse)} />
|
|
<CreateUserModal state={state} dispatch={dispatch} />
|
|
<ResetPasswordModal
|
|
state={state}
|
|
dispatch={dispatch}
|
|
user={userInfo}
|
|
requiresConfirmation={userInfo?.role === 'superuser'}
|
|
/>
|
|
<Reset2FAModal
|
|
state={state}
|
|
dispatch={dispatch}
|
|
user={userInfo}
|
|
requiresConfirmation={userInfo?.role === 'superuser'}
|
|
/>
|
|
<ChangeRoleModal
|
|
state={state}
|
|
dispatch={dispatch}
|
|
user={userInfo}
|
|
requiresConfirmation={userInfo?.role === 'superuser'}
|
|
/>
|
|
<EnableUserModal
|
|
state={state}
|
|
dispatch={dispatch}
|
|
user={userInfo}
|
|
requiresConfirmation={userInfo?.role === 'superuser'}
|
|
/>
|
|
<FIDOModal state={state} dispatch={dispatch} />
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default Users
|