refactor: full refactor of user management

This commit is contained in:
Sérgio Salgado 2021-04-16 19:33:56 +01:00 committed by Josh Harvey
parent bbc37c0202
commit 9d028897bd
10 changed files with 328 additions and 225 deletions

View file

@ -29,7 +29,7 @@ function authenticateUser(username, password) {
const getUserData = context => {
const lidCookie = context.req.cookies && context.req.cookies.lid
if (!lidCookie) throw new AuthenticationError()
if (!lidCookie) return
const user = context.req.session.user
return user
@ -145,26 +145,78 @@ const setup2FA = (username, password, rememberMe, secret, codeConfirmation, cont
context.req.session.user = finalUser
if (rememberMe) context.req.session.cookie.maxAge = REMEMBER_ME_AGE
return true
return users.save2FASecret(user.id, secret)
})
.then(() => true)
}
const createResetPasswordToken = userID => {
const changeUserRole = (code, id, newRole, context) => {
const action = (id, newRole) => users.changeUserRole(id, newRole)
if (!code) {
return action(id, newRole)
}
return confirm2FA(code, context)
.then(() => action(id, newRole))
}
const enableUser = (code, id, context) => {
const action = id => users.enableUser(id)
if (!code) {
return action(id)
}
return confirm2FA(code, context)
.then(() => action(id))
}
const disableUser = (code, id, context) => {
const action = id => users.disableUser(id)
if (!code) {
return action(id)
}
return confirm2FA(code, context)
.then(() => action(id))
}
const createResetPasswordToken = (code, userID, context) => {
const action = userID => {
return users.getUserById(userID)
.then(user => {
if (!user) throw new authErrors.InvalidCredentialsError()
return users.createAuthToken(user.id, 'reset_password')
})
.catch(err => console.error(err))
}
if (!code) {
return action(userID)
}
return confirm2FA(code, context)
.then(() => action(userID))
}
const createReset2FAToken = userID => {
const createReset2FAToken = (code, userID, context) => {
const action = userID => {
return users.getUserById(userID)
.then(user => {
if (!user) throw new authErrors.InvalidCredentialsError()
return users.createAuthToken(user.id, 'reset_twofa')
})
.catch(err => console.error(err))
}
if (!code) {
return action(userID)
}
return confirm2FA(code, context)
.then(() => action(userID))
}
const createRegisterToken = (username, role) => {
@ -220,6 +272,9 @@ module.exports = {
login,
input2FA,
setup2FA,
changeUserRole,
enableUser,
disableUser,
createResetPasswordToken,
createReset2FAToken,
createRegisterToken,

View file

@ -15,16 +15,16 @@ const resolver = {
validateReset2FALink: (...[, { token }]) => authentication.validateReset2FALink(token)
},
Mutation: {
enableUser: (...[, { id }]) => users.enableUser(id),
disableUser: (...[, { id }]) => users.disableUser(id),
enableUser: (...[, { confirmationCode, id }, context]) => authentication.enableUser(confirmationCode, id, context),
disableUser: (...[, { confirmationCode, id }, context]) => authentication.disableUser(confirmationCode, id, context),
deleteSession: (...[, { sid }, context]) => authentication.deleteSession(sid, context),
deleteUserSessions: (...[, { username }]) => sessionManager.deleteSessionsByUsername(username),
changeUserRole: (...[, { id, newRole }]) => users.changeUserRole(id, newRole),
changeUserRole: (...[, { confirmationCode, id, newRole }, context]) => authentication.changeUserRole(confirmationCode, id, newRole, context),
login: (...[, { username, password }]) => authentication.login(username, password),
input2FA: (...[, { username, password, rememberMe, code }, context]) => authentication.input2FA(username, password, rememberMe, code, context),
setup2FA: (...[, { username, password, rememberMe, secret, codeConfirmation }, context]) => authentication.setup2FA(username, password, rememberMe, secret, codeConfirmation, context),
createResetPasswordToken: (...[, { userID }]) => authentication.createResetPasswordToken(userID),
createReset2FAToken: (...[, { userID }]) => authentication.createReset2FAToken(userID),
createResetPasswordToken: (...[, { confirmationCode, userID }, context]) => authentication.createResetPasswordToken(confirmationCode, userID, context),
createReset2FAToken: (...[, { confirmationCode, userID }, context]) => authentication.createReset2FAToken(confirmationCode, userID, context),
createRegisterToken: (...[, { username, role }]) => authentication.createRegisterToken(username, role),
register: (...[, { token, username, password, role }]) => authentication.register(token, username, password, role),
resetPassword: (...[, { token, userID, newPassword }, context]) => authentication.resetPassword(token, userID, newPassword, context),

View file

@ -57,17 +57,17 @@ const typeDef = `
}
type Mutation {
enableUser(id: ID!): User @auth(requires: [SUPERUSER])
disableUser(id: ID!): User @auth(requires: [SUPERUSER])
enableUser(confirmationCode: String, id: ID!): User @auth(requires: [SUPERUSER])
disableUser(confirmationCode: String, id: ID!): User @auth(requires: [SUPERUSER])
deleteSession(sid: String!): UserSession @auth(requires: [SUPERUSER])
deleteUserSessions(username: String!): [UserSession] @auth(requires: [SUPERUSER])
changeUserRole(id: ID!, newRole: String!): User @auth(requires: [SUPERUSER])
changeUserRole(confirmationCode: String, id: ID!, newRole: String!): User @auth(requires: [SUPERUSER])
toggleUserEnable(id: ID!): User @auth(requires: [SUPERUSER])
login(username: String!, password: String!): String
input2FA(username: String!, password: String!, code: String!, rememberMe: Boolean!): Boolean
setup2FA(username: String!, password: String!, rememberMe: Boolean!, secret: String!, codeConfirmation: String!): Boolean
createResetPasswordToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER])
createReset2FAToken(userID: ID!): ResetToken @auth(requires: [SUPERUSER])
createResetPasswordToken(confirmationCode: String, userID: ID!): ResetToken @auth(requires: [SUPERUSER])
createReset2FAToken(confirmationCode: String, userID: ID!): ResetToken @auth(requires: [SUPERUSER])
createRegisterToken(username: String!, role: String!): RegistrationToken @auth(requires: [SUPERUSER])
register(token: String!, username: String!, password: String!, role: String!): Boolean
resetPassword(token: String!, userID: ID!, newPassword: String!): Boolean

View file

@ -10,6 +10,7 @@ import { CodeInput } from 'src/components/inputs/base'
import { H2, P } from 'src/components/typography'
import styles from './Login.styles'
import { STATES } from './states'
const useStyles = makeStyles(styles)
@ -47,7 +48,12 @@ const Input2FAState = ({ state, dispatch }) => {
const [invalidToken, setInvalidToken] = useState(false)
const handle2FAChange = value => {
dispatch({ type: 'twoFAField', payload: value })
dispatch({
type: STATES.INPUT_2FA,
payload: {
twoFAField: value
}
})
setInvalidToken(false)
}

View file

@ -1,4 +1,4 @@
import { useQuery, useMutation } from '@apollo/react-hooks'
import { useQuery } from '@apollo/react-hooks'
import { makeStyles, Box, Chip } from '@material-ui/core'
import gql from 'graphql-tag'
import * as R from 'ramda'
@ -14,7 +14,6 @@ import styles from './UserManagement.styles'
import ChangeRoleModal from './modals/ChangeRoleModal'
import CreateUserModal from './modals/CreateUserModal'
import EnableUserModal from './modals/EnableUserModal'
import Input2FAModal from './modals/Input2FAModal'
import Reset2FAModal from './modals/Reset2FAModal'
import ResetPasswordModal from './modals/ResetPasswordModal'
@ -34,98 +33,21 @@ const GET_USERS = gql`
}
`
const CHANGE_USER_ROLE = gql`
mutation changeUserRole($id: ID!, $newRole: String!) {
changeUserRole(id: $id, newRole: $newRole) {
id
}
}
`
const ENABLE_USER = gql`
mutation enableUser($id: ID!) {
enableUser(id: $id) {
id
}
}
`
const DISABLE_USER = gql`
mutation disableUser($id: ID!) {
disableUser(id: $id) {
id
}
}
`
const CREATE_RESET_PASSWORD_TOKEN = gql`
mutation createResetPasswordToken($userID: ID!) {
createResetPasswordToken(userID: $userID) {
token
user_id
expire
}
}
`
const CREATE_RESET_2FA_TOKEN = gql`
mutation createReset2FAToken($userID: ID!) {
createReset2FAToken(userID: $userID) {
token
user_id
expire
}
}
`
const Users = () => {
const classes = useStyles()
const { userData } = useContext(AppContext)
const { data: userResponse } = useQuery(GET_USERS)
const [changeUserRole] = useMutation(CHANGE_USER_ROLE, {
refetchQueries: () => ['users']
})
const [enableUser] = useMutation(ENABLE_USER, {
refetchQueries: () => ['users']
})
const [disableUser] = useMutation(DISABLE_USER, {
refetchQueries: () => ['users']
})
const [createResetPasswordToken] = useMutation(CREATE_RESET_PASSWORD_TOKEN, {
onCompleted: ({ createResetPasswordToken: token }) => {
setResetPasswordUrl(
`https://localhost:3001/resetpassword?t=${token.token}`
)
toggleResetPasswordModal()
}
})
const [createReset2FAToken] = useMutation(CREATE_RESET_2FA_TOKEN, {
onCompleted: ({ createReset2FAToken: token }) => {
setReset2FAUrl(`https://localhost:3001/reset2fa?t=${token.token}`)
toggleReset2FAModal()
}
})
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)
@ -135,11 +57,7 @@ const Users = () => {
const toggleEnableUserModal = () =>
setShowEnableUserModal(!showEnableUserModal)
const [showInputConfirmModal, setShowInputConfirmModal] = useState(false)
const toggleInputConfirmModal = () =>
setShowInputConfirmModal(!showInputConfirmModal)
const [action, setAction] = useState(null)
const [userInfo, setUserInfo] = useState(null)
const elements = [
{
@ -212,22 +130,7 @@ const Users = () => {
className={classes.actionChip}
onClick={() => {
setUserInfo(u)
if (u.role === 'superuser') {
setAction(() =>
createResetPasswordToken.bind(null, {
variables: {
userID: u.id
}
})
)
toggleInputConfirmModal()
} else {
createResetPasswordToken({
variables: {
userID: u.id
}
})
}
toggleResetPasswordModal()
}}
/>
<Chip
@ -236,22 +139,7 @@ const Users = () => {
className={classes.actionChip}
onClick={() => {
setUserInfo(u)
if (u.role === 'superuser') {
setAction(() => () =>
createReset2FAToken({
variables: {
userID: u.id
}
})
)
toggleInputConfirmModal()
} else {
createReset2FAToken({
variables: {
userID: u.id
}
})
}
toggleReset2FAModal()
}}
/>
</>
@ -298,35 +186,26 @@ const Users = () => {
<ResetPasswordModal
showModal={showResetPasswordModal}
toggleModal={toggleResetPasswordModal}
resetPasswordURL={resetPasswordUrl}
user={userInfo}
requiresConfirmation={userInfo?.role === 'superuser'}
/>
<Reset2FAModal
showModal={showReset2FAModal}
toggleModal={toggleReset2FAModal}
reset2FAURL={reset2FAUrl}
user={userInfo}
requiresConfirmation={userInfo?.role === 'superuser'}
/>
<ChangeRoleModal
showModal={showRoleModal}
toggleModal={toggleRoleModal}
user={userInfo}
confirm={changeUserRole}
inputConfirmToggle={toggleInputConfirmModal}
setAction={setAction}
requiresConfirmation={userInfo?.role === 'superuser'}
/>
<EnableUserModal
showModal={showEnableUserModal}
toggleModal={toggleEnableUserModal}
user={userInfo}
confirm={userInfo?.enabled ? disableUser : enableUser}
inputConfirmToggle={toggleInputConfirmModal}
setAction={setAction}
/>
<Input2FAModal
showModal={showInputConfirmModal}
toggleModal={toggleInputConfirmModal}
action={action}
requiresConfirmation={userInfo?.role === 'superuser'}
/>
</>
)

View file

@ -1,5 +1,7 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import React from 'react'
import gql from 'graphql-tag'
import React, { useState } from 'react'
import Modal from 'src/components/Modal'
import { Button } from 'src/components/buttons'
@ -7,24 +9,65 @@ import { Info2, P } from 'src/components/typography'
import styles from '../UserManagement.styles'
import Input2FAModal from './Input2FAModal'
const CHANGE_USER_ROLE = gql`
mutation changeUserRole(
$confirmationCode: String
$id: ID!
$newRole: String!
) {
changeUserRole(
confirmationCode: $confirmationCode
id: $id
newRole: $newRole
) {
id
}
}
`
const useStyles = makeStyles(styles)
const ChangeRoleModal = ({
showModal,
toggleModal,
user,
confirm,
inputConfirmToggle,
setAction
requiresConfirmation
}) => {
const classes = useStyles()
const [changeUserRole] = useMutation(CHANGE_USER_ROLE, {
refetchQueries: () => ['users']
})
const [confirmation, setConfirmation] = useState(null)
const submit = () => {
changeUserRole({
variables: {
confirmationCode: confirmation,
id: user.id,
newRole: user.role === 'superuser' ? 'user' : 'superuser'
}
})
handleClose()
}
const handleClose = () => {
setConfirmation(null)
toggleModal()
}
return (
showModal && (
(showModal && requiresConfirmation && !confirmation && (
<Input2FAModal
showModal={showModal}
handleClose={handleClose}
setConfirmation={setConfirmation}
/>
)) ||
(showModal && (
<Modal
closeOnBackdropClick={true}
width={450}
@ -40,25 +83,12 @@ const ChangeRoleModal = ({
</P>
<P className={classes.info}>Do you wish to proceed?</P>
<div className={classes.footer}>
<Button
className={classes.submit}
onClick={() => {
setAction(() =>
confirm.bind(null, {
variables: {
id: user.id,
newRole: user.role === 'superuser' ? 'user' : 'superuser'
}
})
)
inputConfirmToggle()
handleClose()
}}>
<Button className={classes.submit} onClick={() => submit()}>
Confirm
</Button>
</div>
</Modal>
)
))
)
}

View file

@ -1,5 +1,7 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import React from 'react'
import gql from 'graphql-tag'
import React, { useState } from 'react'
import Modal from 'src/components/Modal'
import { Button } from 'src/components/buttons'
@ -7,24 +9,75 @@ import { Info2, P } from 'src/components/typography'
import styles from '../UserManagement.styles'
import Input2FAModal from './Input2FAModal'
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 useStyles = makeStyles(styles)
const EnableUserModal = ({
showModal,
toggleModal,
user,
confirm,
inputConfirmToggle,
setAction
requiresConfirmation
}) => {
const classes = useStyles()
const [enableUser] = useMutation(ENABLE_USER, {
refetchQueries: () => ['users']
})
const [disableUser] = useMutation(DISABLE_USER, {
refetchQueries: () => ['users']
})
const [confirmation, setConfirmation] = useState(null)
const submit = () => {
user?.enabled
? disableUser({
variables: {
confirmationCode: confirmation,
id: user.id
}
})
: enableUser({
variables: {
confirmationCode: confirmation,
id: user.id
}
})
handleClose()
}
const handleClose = () => {
setConfirmation(null)
toggleModal()
}
return (
showModal && (
(showModal && requiresConfirmation && !confirmation && (
<Input2FAModal
showModal={showModal}
handleClose={handleClose}
setConfirmation={setConfirmation}
/>
)) ||
(showModal && (
<Modal
closeOnBackdropClick={true}
width={450}
@ -58,32 +111,12 @@ const EnableUserModal = ({
</>
)}
<div className={classes.footer}>
<Button
className={classes.submit}
onClick={() => {
if (user.role === 'superuser') {
setAction(() =>
confirm.bind(null, {
variables: {
id: user.id
}
})
)
inputConfirmToggle()
} else {
confirm({
variables: {
id: user.id
}
})
}
handleClose()
}}>
<Button className={classes.submit} onClick={() => submit()}>
Confirm
</Button>
</div>
</Modal>
)
))
)
}

View file

@ -18,7 +18,7 @@ const CONFIRM_2FA = gql`
}
`
const Input2FAModal = ({ showModal, toggleModal, action, vars }) => {
const Input2FAModal = ({ showModal, handleClose, setConfirmation }) => {
const classes = useStyles()
const [twoFACode, setTwoFACode] = useState('')
@ -29,21 +29,15 @@ const Input2FAModal = ({ showModal, toggleModal, action, vars }) => {
setInvalidCode(false)
}
const handleClose = () => {
const onContinue = () => {
setConfirmation(twoFACode)
setTwoFACode('')
setInvalidCode(false)
toggleModal()
}
const [confirm2FA, { error: queryError }] = useLazyQuery(CONFIRM_2FA, {
onCompleted: ({ confirm2FA: success }) => {
if (!success) {
setInvalidCode(true)
} else {
action()
handleClose()
}
}
onCompleted: ({ confirm2FA: success }) =>
!success ? setInvalidCode(true) : onContinue()
})
const getErrorMsg = () => {

View file

@ -1,5 +1,7 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import React from 'react'
import gql from 'graphql-tag'
import React, { useEffect, useState } from 'react'
import Modal from 'src/components/Modal'
import { Info2, P, Mono } from 'src/components/typography'
@ -7,17 +9,65 @@ import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
import styles from '../UserManagement.styles'
import Input2FAModal from './Input2FAModal'
const CREATE_RESET_2FA_TOKEN = gql`
mutation createReset2FAToken($confirmationCode: String, $userID: ID!) {
createReset2FAToken(confirmationCode: $confirmationCode, userID: $userID) {
token
user_id
expire
}
}
`
const useStyles = makeStyles(styles)
const Reset2FAModal = ({ showModal, toggleModal, reset2FAURL, user }) => {
const Reset2FAModal = ({
showModal,
toggleModal,
user,
requiresConfirmation
}) => {
const classes = useStyles()
const [reset2FAUrl, setReset2FAUrl] = useState('')
const [createReset2FAToken, { loading }] = useMutation(
CREATE_RESET_2FA_TOKEN,
{
onCompleted: ({ createReset2FAToken: token }) => {
setReset2FAUrl(`https://localhost:3001/reset2fa?t=${token.token}`)
}
}
)
const [confirmation, setConfirmation] = useState(null)
useEffect(() => {
showModal &&
(confirmation || !requiresConfirmation) &&
createReset2FAToken({
variables: {
confirmationCode: confirmation,
userID: user?.id
}
})
}, [confirmation, createReset2FAToken, requiresConfirmation, showModal, user])
const handleClose = () => {
setConfirmation(null)
toggleModal()
}
return (
showModal && (
(showModal && requiresConfirmation && !confirmation && (
<Input2FAModal
showModal={showModal}
handleClose={handleClose}
setConfirmation={setConfirmation}
/>
)) ||
(showModal && (confirmation || !requiresConfirmation) && !loading && (
<Modal
closeOnBackdropClick={true}
width={500}
@ -38,13 +88,13 @@ const Reset2FAModal = ({ showModal, toggleModal, reset2FAURL, user }) => {
className={classes.link}
buttonClassname={classes.copyToClipboard}
wrapperClassname={classes.linkWrapper}>
{reset2FAURL}
{reset2FAUrl}
</CopyToClipboard>
</strong>
</Mono>
</div>
</Modal>
)
))
)
}

View file

@ -1,5 +1,7 @@
import { useMutation } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core/styles'
import React from 'react'
import gql from 'graphql-tag'
import React, { useEffect, useState } from 'react'
import Modal from 'src/components/Modal'
import { Info2, P, Mono } from 'src/components/typography'
@ -7,22 +9,76 @@ import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
import styles from '../UserManagement.styles'
import Input2FAModal from './Input2FAModal'
const CREATE_RESET_PASSWORD_TOKEN = gql`
mutation createResetPasswordToken($confirmationCode: String, $userID: ID!) {
createResetPasswordToken(
confirmationCode: $confirmationCode
userID: $userID
) {
token
user_id
expire
}
}
`
const useStyles = makeStyles(styles)
const ResetPasswordModal = ({
showModal,
toggleModal,
resetPasswordURL,
user
user,
requiresConfirmation
}) => {
const classes = useStyles()
const [resetPasswordUrl, setResetPasswordUrl] = useState('')
const [createResetPasswordToken, { loading }] = useMutation(
CREATE_RESET_PASSWORD_TOKEN,
{
onCompleted: ({ createResetPasswordToken: token }) => {
setResetPasswordUrl(
`https://localhost:3001/resetpassword?t=${token.token}`
)
}
}
)
const [confirmation, setConfirmation] = useState(null)
useEffect(() => {
showModal &&
(confirmation || !requiresConfirmation) &&
createResetPasswordToken({
variables: {
confirmationCode: confirmation,
userID: user?.id
}
})
}, [
confirmation,
createResetPasswordToken,
showModal,
user,
requiresConfirmation
])
const handleClose = () => {
setConfirmation(null)
toggleModal()
}
return (
showModal && (
(showModal && requiresConfirmation && !confirmation && (
<Input2FAModal
showModal={showModal}
handleClose={handleClose}
setConfirmation={setConfirmation}
/>
)) ||
(showModal && (confirmation || !requiresConfirmation) && !loading && (
<Modal
closeOnBackdropClick={true}
width={500}
@ -42,13 +98,13 @@ const ResetPasswordModal = ({
className={classes.link}
buttonClassname={classes.copyToClipboard}
wrapperClassname={classes.linkWrapper}>
{resetPasswordURL}
{resetPasswordUrl}
</CopyToClipboard>
</strong>
</Mono>
</div>
</Modal>
)
))
)
}