chore: use monorepo organization
This commit is contained in:
parent
deaf7d6ecc
commit
a687827f7e
1099 changed files with 8184 additions and 11535 deletions
287
packages/admin-ui/src/pages/UserManagement/UserManagement.jsx
Normal file
287
packages/admin-ui/src/pages/UserManagement/UserManagement.jsx
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
import { useQuery, useMutation, useLazyQuery, gql } from '@apollo/client'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Switch from '@mui/material/Switch'
|
||||
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 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'
|
||||
|
||||
import classes from './UserManagement.module.css'
|
||||
|
||||
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 { 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
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: auto 0 24px 0;
|
||||
}
|
||||
|
||||
.modalTitle {
|
||||
margin-top: -5px;
|
||||
color: var(--zodiac);
|
||||
font-family: var(--mont);
|
||||
}
|
||||
|
||||
.modalLabel1 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.modalLabel2 {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.inputLabel {
|
||||
color: var(--zodiac);
|
||||
font-family: var(--mont);
|
||||
font-size: 24px;
|
||||
margin-left: 8px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.tableWidth {
|
||||
width: 1132px;
|
||||
}
|
||||
|
||||
.radioGroup {
|
||||
flex-direction: row;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.radioLabel {
|
||||
width: 150px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.copyToClipboard {
|
||||
margin-left: auto;
|
||||
padding-top: 7px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
background-color: var(--zircon);
|
||||
font-family: var(--mont);
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-family: var(--museo);
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.addressWrapper {
|
||||
background-color: var(--zircon);
|
||||
margin-top: 8px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.address {
|
||||
margin: 0px 16px 0px 16px;
|
||||
padding-right: -15px;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
font-family: var(--museo);
|
||||
color: var(--tomato);
|
||||
}
|
||||
|
||||
.codeContainer {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin: auto 0 0 auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--tomato);
|
||||
}
|
||||
|
||||
.link {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 0;
|
||||
bottom: -20px;
|
||||
right: -20px;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
width: 92.5%;
|
||||
}
|
||||
|
||||
.linkWrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loginWrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.username {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roleSwitch {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.actionButtonWrapper {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { useMutation, gql } from '@apollo/client'
|
||||
import React, { useState } from 'react'
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Info2, P } from 'src/components/typography'
|
||||
|
||||
import { Button } from 'src/components/buttons'
|
||||
|
||||
import Input2FAModal from './Input2FAModal'
|
||||
import classes from '../UserManagement.module.css'
|
||||
|
||||
const CHANGE_USER_ROLE = gql`
|
||||
mutation changeUserRole(
|
||||
$confirmationCode: String
|
||||
$id: ID!
|
||||
$newRole: String!
|
||||
) {
|
||||
changeUserRole(
|
||||
confirmationCode: $confirmationCode
|
||||
id: $id
|
||||
newRole: $newRole
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const ChangeRoleModal = ({ state, dispatch, user, requiresConfirmation }) => {
|
||||
const [changeUserRole, { error }] = useMutation(CHANGE_USER_ROLE, {
|
||||
onCompleted: () => handleClose(),
|
||||
refetchQueries: () => ['users']
|
||||
})
|
||||
|
||||
const [confirmation, setConfirmation] = useState(null)
|
||||
|
||||
const submit = () => {
|
||||
changeUserRole({
|
||||
variables: {
|
||||
confirmationCode: confirmation,
|
||||
id: user.id,
|
||||
newRole: user.role === 'superuser' ? 'user' : 'superuser'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setConfirmation(null)
|
||||
dispatch({
|
||||
type: 'close',
|
||||
payload: 'showRoleModal'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
(state.showRoleModal && requiresConfirmation && !confirmation && (
|
||||
<Input2FAModal
|
||||
showModal={state.showRoleModal}
|
||||
handleClose={handleClose}
|
||||
setConfirmation={setConfirmation}
|
||||
/>
|
||||
)) ||
|
||||
(state.showRoleModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={450}
|
||||
height={250}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<Info2 className={classes.modalTitle}>
|
||||
Change {user.username}'s role?
|
||||
</Info2>
|
||||
<P className={classes.info}>
|
||||
You are about to alter {user.username}'s role. This will change this
|
||||
user's permission to access certain resources.
|
||||
</P>
|
||||
<P className={classes.info}>Do you wish to proceed?</P>
|
||||
<div className={classes.footer}>
|
||||
{error && <ErrorMessage>{error}</ErrorMessage>}
|
||||
<Button className={classes.submit} onClick={() => submit()}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangeRoleModal
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
import { useMutation, gql } from '@apollo/client'
|
||||
import classnames from 'classnames'
|
||||
import { Field, Form, Formik } from 'formik'
|
||||
import React, { useState } from 'react'
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { H1, H3, Info2, P, Mono } from 'src/components/typography'
|
||||
import CopyToClipboard from 'src/components/CopyToClipboard.jsx'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { TextInput, RadioGroup } from 'src/components/inputs/formik'
|
||||
import { urlResolver } from 'src/utils/urlResolver'
|
||||
|
||||
import classes from '../UserManagement.module.css'
|
||||
|
||||
const CREATE_USER = gql`
|
||||
mutation createRegisterToken($username: String!, $role: String!) {
|
||||
createRegisterToken(username: $username, role: $role) {
|
||||
token
|
||||
expire
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
username: Yup.string()
|
||||
.email('Username field should be in an email format!')
|
||||
.required('Username field is required!'),
|
||||
role: Yup.string().required('Role field is required!')
|
||||
})
|
||||
|
||||
const initialValues = {
|
||||
username: '',
|
||||
role: ''
|
||||
}
|
||||
|
||||
const radioOptions = [
|
||||
{
|
||||
code: 'user',
|
||||
display: 'Regular user'
|
||||
},
|
||||
{
|
||||
code: 'superuser',
|
||||
display: 'Superuser'
|
||||
}
|
||||
]
|
||||
|
||||
const getErrorMsg = (formikErrors, formikTouched, mutationError) => {
|
||||
if (!formikErrors || !formikTouched) return null
|
||||
if (mutationError) return 'Internal server error'
|
||||
if (formikErrors.username && formikTouched.username)
|
||||
return formikErrors.username
|
||||
return null
|
||||
}
|
||||
|
||||
const CreateUserModal = ({ state, dispatch }) => {
|
||||
const [usernameField, setUsernameField] = useState('')
|
||||
const [createUserURL, setCreateUserURL] = useState(null)
|
||||
|
||||
const handleClose = () => {
|
||||
setCreateUserURL(null)
|
||||
dispatch({
|
||||
type: 'close',
|
||||
payload: 'showCreateUserModal'
|
||||
})
|
||||
}
|
||||
|
||||
const [createUser, { error }] = useMutation(CREATE_USER, {
|
||||
onCompleted: ({ createRegisterToken: token }) => {
|
||||
setCreateUserURL(urlResolver(`/register?t=${token.token}`))
|
||||
}
|
||||
})
|
||||
|
||||
const roleClass = (formikErrors, formikTouched) => ({
|
||||
[classes.error]: formikErrors.role && formikTouched.role
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{state.showCreateUserModal && !createUserURL && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={600}
|
||||
height={400}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<Formik
|
||||
validationSchema={validationSchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={values => {
|
||||
setUsernameField(values.username)
|
||||
createUser({
|
||||
variables: { username: values.username, role: values.role }
|
||||
})
|
||||
}}>
|
||||
{({ errors, touched }) => (
|
||||
<Form id="register-user-form" className={classes.form}>
|
||||
<H1 className={classes.modalTitle}>Create new user</H1>
|
||||
<Field
|
||||
component={TextInput}
|
||||
name="username"
|
||||
width={338}
|
||||
autoFocus
|
||||
label="User login"
|
||||
/>
|
||||
<H3
|
||||
className={classnames(
|
||||
roleClass(errors, touched),
|
||||
classes.modalLabel2
|
||||
)}>
|
||||
Role
|
||||
</H3>
|
||||
<Field
|
||||
component={RadioGroup}
|
||||
name="role"
|
||||
labelClassName={classes.radioLabel}
|
||||
className={classes.radioGroup}
|
||||
options={radioOptions}
|
||||
/>
|
||||
<div className={classes.footer}>
|
||||
{getErrorMsg(errors, touched, error) && (
|
||||
<ErrorMessage>
|
||||
{getErrorMsg(errors, touched, error)}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
form="register-user-form"
|
||||
className={classes.submit}>
|
||||
Finish
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
)}
|
||||
{state.showCreateUserModal && createUserURL && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={500}
|
||||
height={200}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<Info2 className={classes.modalTitle}>
|
||||
Creating {usernameField}...
|
||||
</Info2>
|
||||
<P className={classes.info}>
|
||||
Safely share this link with {usernameField} to finish the
|
||||
registration process.
|
||||
</P>
|
||||
<div className={classes.addressWrapper}>
|
||||
<Mono className={classes.address}>
|
||||
<strong>
|
||||
<CopyToClipboard
|
||||
className={classes.link}
|
||||
buttonClassname={classes.copyToClipboard}
|
||||
wrapperClassname={classes.linkWrapper}>
|
||||
{createUserURL}
|
||||
</CopyToClipboard>
|
||||
</strong>
|
||||
</Mono>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateUserModal
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import { useMutation, gql } from '@apollo/client'
|
||||
import React, { useState } from 'react'
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Info2, P } from 'src/components/typography'
|
||||
|
||||
import { Button } from 'src/components/buttons'
|
||||
|
||||
import Input2FAModal from './Input2FAModal'
|
||||
import classes from '../UserManagement.module.css'
|
||||
|
||||
const ENABLE_USER = gql`
|
||||
mutation enableUser($confirmationCode: String, $id: ID!) {
|
||||
enableUser(confirmationCode: $confirmationCode, id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const DISABLE_USER = gql`
|
||||
mutation disableUser($confirmationCode: String, $id: ID!) {
|
||||
disableUser(confirmationCode: $confirmationCode, id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const EnableUserModal = ({ state, dispatch, user, requiresConfirmation }) => {
|
||||
const [enableUser, { error: enableError }] = useMutation(ENABLE_USER, {
|
||||
onCompleted: () => handleClose(),
|
||||
refetchQueries: () => ['users']
|
||||
})
|
||||
|
||||
const [disableUser, { error: disableError }] = useMutation(DISABLE_USER, {
|
||||
onCompleted: () => handleClose(),
|
||||
refetchQueries: () => ['users']
|
||||
})
|
||||
|
||||
const [confirmation, setConfirmation] = useState(null)
|
||||
|
||||
const disable = () => {
|
||||
disableUser({
|
||||
variables: {
|
||||
confirmationCode: confirmation,
|
||||
id: user.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const enable = () => {
|
||||
enableUser({
|
||||
variables: {
|
||||
confirmationCode: confirmation,
|
||||
id: user.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
user?.enabled ? disable() : enable()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setConfirmation(null)
|
||||
dispatch({
|
||||
type: 'close',
|
||||
payload: 'showEnableUserModal'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
(state.showEnableUserModal && requiresConfirmation && !confirmation && (
|
||||
<Input2FAModal
|
||||
showModal={state.showEnableUserModal}
|
||||
handleClose={handleClose}
|
||||
setConfirmation={setConfirmation}
|
||||
/>
|
||||
)) ||
|
||||
(state.showEnableUserModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={450}
|
||||
height={275}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
{!user.enabled && (
|
||||
<>
|
||||
<Info2 className={classes.modalTitle}>
|
||||
Enable {user.username}?
|
||||
</Info2>
|
||||
<P className={classes.info}>
|
||||
You are about to enable {user.username} into the system,
|
||||
activating previous eligible sessions and grant permissions to
|
||||
access the system.
|
||||
</P>
|
||||
<P className={classes.info}>Do you wish to proceed?</P>
|
||||
</>
|
||||
)}
|
||||
{user.enabled && (
|
||||
<>
|
||||
<Info2 className={classes.modalTitle}>
|
||||
Disable {user.username}?
|
||||
</Info2>
|
||||
<P className={classes.info}>
|
||||
You are about to disable {user.username} from the system,
|
||||
deactivating previous eligible sessions and removing permissions
|
||||
to access the system.
|
||||
</P>
|
||||
<P className={classes.info}>Do you wish to proceed?</P>
|
||||
</>
|
||||
)}
|
||||
<div className={classes.footer}>
|
||||
{disableError && <ErrorMessage>{disableError}</ErrorMessage>}
|
||||
{enableError && <ErrorMessage>{enableError}</ErrorMessage>}
|
||||
<Button className={classes.submit} onClick={() => submit()}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
export default EnableUserModal
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import React from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Info2, P } from 'src/components/typography'
|
||||
import { Button } from 'src/components/buttons'
|
||||
|
||||
import classes from '../UserManagement.module.css'
|
||||
|
||||
const ChangeRoleModal = ({ state, dispatch }) => {
|
||||
const handleClose = () => {
|
||||
dispatch({
|
||||
type: 'close',
|
||||
payload: 'showFIDOModal'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={450}
|
||||
height={275}
|
||||
handleClose={handleClose}
|
||||
open={state.showFIDOModal}>
|
||||
<Info2 className={classes.modalTitle}>About FIDO authentication</Info2>
|
||||
<P className={classes.info}>
|
||||
This feature is only available for websites with configured domains, and
|
||||
we detected that a domain is not configured at the moment.
|
||||
</P>
|
||||
<P>
|
||||
Make sure that a domain is configured for this website and try again
|
||||
later.
|
||||
</P>
|
||||
<div className={classes.footer}>
|
||||
<Button className={classes.submit} onClick={() => handleClose()}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangeRoleModal
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import { useLazyQuery, gql } from '@apollo/client'
|
||||
import { Form, Formik } from 'formik'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Info2, P } from 'src/components/typography'
|
||||
import { Button } from 'src/components/buttons'
|
||||
import { CodeInput } from 'src/components/inputs/base'
|
||||
|
||||
import classes from '../UserManagement.module.css'
|
||||
|
||||
const CONFIRM_2FA = gql`
|
||||
query confirm2FA($code: String!) {
|
||||
confirm2FA(code: $code)
|
||||
}
|
||||
`
|
||||
|
||||
const Input2FAModal = ({ showModal, handleClose, setConfirmation }) => {
|
||||
const [twoFACode, setTwoFACode] = useState('')
|
||||
const [invalidCode, setInvalidCode] = useState(false)
|
||||
|
||||
const handleCodeChange = value => {
|
||||
setTwoFACode(value)
|
||||
setInvalidCode(false)
|
||||
}
|
||||
|
||||
const onContinue = () => {
|
||||
setConfirmation(twoFACode)
|
||||
setTwoFACode('')
|
||||
setInvalidCode(false)
|
||||
}
|
||||
|
||||
const [confirm2FA, { error: queryError }] = useLazyQuery(CONFIRM_2FA, {
|
||||
onCompleted: ({ confirm2FA: success }) =>
|
||||
!success ? setInvalidCode(true) : onContinue()
|
||||
})
|
||||
|
||||
const getErrorMsg = () => {
|
||||
if (queryError) return 'Internal server error'
|
||||
if (twoFACode.length !== 6 && invalidCode)
|
||||
return 'The code should have 6 characters!'
|
||||
if (invalidCode) return 'Code is invalid. Please try again.'
|
||||
return null
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (twoFACode.length !== 6) {
|
||||
setInvalidCode(true)
|
||||
return
|
||||
}
|
||||
confirm2FA({ variables: { code: twoFACode } })
|
||||
}
|
||||
|
||||
return (
|
||||
showModal && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={500}
|
||||
height={350}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<Info2 className={classes.modalTitle}>Confirm action</Info2>
|
||||
<P className={classes.info}>
|
||||
To make changes on this user, please confirm this action by entering
|
||||
your two-factor authentication code below.
|
||||
</P>
|
||||
{/* TODO: refactor the 2FA CodeInput to properly use Formik */}
|
||||
<Formik onSubmit={() => {}} initialValues={{}}>
|
||||
<Form>
|
||||
<CodeInput
|
||||
name="2fa"
|
||||
value={twoFACode}
|
||||
onChange={handleCodeChange}
|
||||
numInputs={6}
|
||||
error={invalidCode}
|
||||
containerStyle={classes.codeContainer}
|
||||
/>
|
||||
{getErrorMsg() && (
|
||||
<P className={classes.errorMessage}>{getErrorMsg()}</P>
|
||||
)}
|
||||
<div className={classes.footer}>
|
||||
<Button className={classes.submit} onClick={handleSubmit}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Formik>
|
||||
</Modal>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export default Input2FAModal
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import { useMutation, gql } from '@apollo/client'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Info2, P, Mono } from 'src/components/typography'
|
||||
import CopyToClipboard from 'src/components/CopyToClipboard.jsx'
|
||||
import { urlResolver } from 'src/utils/urlResolver'
|
||||
|
||||
import Input2FAModal from './Input2FAModal'
|
||||
import classes from '../UserManagement.module.css'
|
||||
|
||||
const CREATE_RESET_2FA_TOKEN = gql`
|
||||
mutation createReset2FAToken($confirmationCode: String, $userID: ID!) {
|
||||
createReset2FAToken(confirmationCode: $confirmationCode, userID: $userID) {
|
||||
token
|
||||
user_id
|
||||
expire
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Reset2FAModal = ({ state, dispatch, user, requiresConfirmation }) => {
|
||||
const [reset2FAUrl, setReset2FAUrl] = useState('')
|
||||
|
||||
const [createReset2FAToken, { loading, error }] = useMutation(
|
||||
CREATE_RESET_2FA_TOKEN,
|
||||
{
|
||||
onCompleted: ({ createReset2FAToken: token }) => {
|
||||
setReset2FAUrl(urlResolver(`/reset2fa?t=${token.token}`))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const [confirmation, setConfirmation] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
state.showReset2FAModal &&
|
||||
(confirmation || !requiresConfirmation) &&
|
||||
createReset2FAToken({
|
||||
variables: {
|
||||
confirmationCode: confirmation,
|
||||
userID: user?.id
|
||||
}
|
||||
})
|
||||
}, [
|
||||
confirmation,
|
||||
createReset2FAToken,
|
||||
requiresConfirmation,
|
||||
state.showReset2FAModal,
|
||||
user?.id
|
||||
])
|
||||
|
||||
const handleClose = () => {
|
||||
setConfirmation(null)
|
||||
dispatch({
|
||||
type: 'close',
|
||||
payload: 'showReset2FAModal'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
(state.showReset2FAModal && requiresConfirmation && !confirmation && (
|
||||
<Input2FAModal
|
||||
showModal={state.showReset2FAModal}
|
||||
handleClose={handleClose}
|
||||
setConfirmation={setConfirmation}
|
||||
/>
|
||||
)) ||
|
||||
(state.showReset2FAModal &&
|
||||
(confirmation || !requiresConfirmation) &&
|
||||
!loading && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={500}
|
||||
height={200}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<Info2 className={classes.modalTitle}>
|
||||
Reset 2FA for {user.username}
|
||||
</Info2>
|
||||
<P className={classes.info}>
|
||||
Safely share this link with {user.username} for a two-factor
|
||||
authentication reset.
|
||||
</P>
|
||||
{!error && (
|
||||
<div className={classes.addressWrapper}>
|
||||
<Mono className={classes.address}>
|
||||
<strong>
|
||||
<CopyToClipboard
|
||||
className={classes.link}
|
||||
buttonClassname={classes.copyToClipboard}
|
||||
wrapperClassname={classes.linkWrapper}>
|
||||
{reset2FAUrl}
|
||||
</CopyToClipboard>
|
||||
</strong>
|
||||
</Mono>
|
||||
</div>
|
||||
)}
|
||||
{error && <ErrorMessage>{error}</ErrorMessage>}
|
||||
</Modal>
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
export default Reset2FAModal
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
import { useMutation, gql } from '@apollo/client'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { Info2, P, Mono } from 'src/components/typography'
|
||||
import CopyToClipboard from 'src/components/CopyToClipboard.jsx'
|
||||
|
||||
import { urlResolver } from 'src/utils/urlResolver'
|
||||
|
||||
import Input2FAModal from './Input2FAModal'
|
||||
import classes from '../UserManagement.module.css'
|
||||
|
||||
const CREATE_RESET_PASSWORD_TOKEN = gql`
|
||||
mutation createResetPasswordToken($confirmationCode: String, $userID: ID!) {
|
||||
createResetPasswordToken(
|
||||
confirmationCode: $confirmationCode
|
||||
userID: $userID
|
||||
) {
|
||||
token
|
||||
user_id
|
||||
expire
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const ResetPasswordModal = ({
|
||||
state,
|
||||
dispatch,
|
||||
user,
|
||||
requiresConfirmation
|
||||
}) => {
|
||||
const [resetPasswordUrl, setResetPasswordUrl] = useState('')
|
||||
|
||||
const [createResetPasswordToken, { loading, error }] = useMutation(
|
||||
CREATE_RESET_PASSWORD_TOKEN,
|
||||
{
|
||||
onCompleted: ({ createResetPasswordToken: token }) => {
|
||||
setResetPasswordUrl(urlResolver(`/resetpassword?t=${token.token}`))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const [confirmation, setConfirmation] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
state.showResetPasswordModal &&
|
||||
(confirmation || !requiresConfirmation) &&
|
||||
createResetPasswordToken({
|
||||
variables: {
|
||||
confirmationCode: confirmation,
|
||||
userID: user?.id
|
||||
}
|
||||
})
|
||||
}, [
|
||||
confirmation,
|
||||
createResetPasswordToken,
|
||||
requiresConfirmation,
|
||||
state.showResetPasswordModal,
|
||||
user?.id
|
||||
])
|
||||
|
||||
const handleClose = () => {
|
||||
setConfirmation(null)
|
||||
dispatch({
|
||||
type: 'close',
|
||||
payload: 'showResetPasswordModal'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
(state.showResetPasswordModal && requiresConfirmation && !confirmation && (
|
||||
<Input2FAModal
|
||||
showModal={state.showResetPasswordModal}
|
||||
handleClose={handleClose}
|
||||
setConfirmation={setConfirmation}
|
||||
/>
|
||||
)) ||
|
||||
(state.showResetPasswordModal &&
|
||||
(confirmation || !requiresConfirmation) &&
|
||||
!loading && (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
width={500}
|
||||
height={180}
|
||||
handleClose={handleClose}
|
||||
open={true}>
|
||||
<Info2 className={classes.modalTitle}>
|
||||
Reset password for {user.username}
|
||||
</Info2>
|
||||
<P className={classes.info}>
|
||||
Safely share this link with {user.username} for a password reset.
|
||||
</P>
|
||||
{!error && (
|
||||
<div className={classes.addressWrapper}>
|
||||
<Mono className={classes.address}>
|
||||
<strong>
|
||||
<CopyToClipboard
|
||||
className={classes.link}
|
||||
buttonClassname={classes.copyToClipboard}
|
||||
wrapperClassname={classes.linkWrapper}>
|
||||
{resetPasswordUrl}
|
||||
</CopyToClipboard>
|
||||
</strong>
|
||||
</Mono>
|
||||
</div>
|
||||
)}
|
||||
{error && <ErrorMessage>{error}</ErrorMessage>}
|
||||
</Modal>
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
export default ResetPasswordModal
|
||||
Loading…
Add table
Add a link
Reference in a new issue