From 9d028897bd4e6dab06adf7c3c0f74df5b6e4328e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Salgado?= Date: Fri, 16 Apr 2021 19:33:56 +0100 Subject: [PATCH] refactor: full refactor of user management --- .../graphql/modules/authentication.js | 87 +++++++++-- .../graphql/resolvers/users.resolver.js | 10 +- lib/new-admin/graphql/types/users.type.js | 10 +- .../src/pages/Authentication/Input2FAState.js | 8 +- .../pages/UserManagement/UserManagement.js | 137 +----------------- .../UserManagement/modals/ChangeRoleModal.js | 70 ++++++--- .../UserManagement/modals/EnableUserModal.js | 87 +++++++---- .../UserManagement/modals/Input2FAModal.js | 16 +- .../UserManagement/modals/Reset2FAModal.js | 60 +++++++- .../modals/ResetPasswordModal.js | 68 ++++++++- 10 files changed, 328 insertions(+), 225 deletions(-) diff --git a/lib/new-admin/graphql/modules/authentication.js b/lib/new-admin/graphql/modules/authentication.js index cc9c55d5..9abe8f4f 100644 --- a/lib/new-admin/graphql/modules/authentication.js +++ b/lib/new-admin/graphql/modules/authentication.js @@ -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 => { - return users.getUserById(userID) - .then(user => { - if (!user) throw new authErrors.InvalidCredentialsError() - return users.createAuthToken(user.id, 'reset_password') - }) - .catch(err => console.error(err)) +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 createReset2FAToken = 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)) +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 = (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, diff --git a/lib/new-admin/graphql/resolvers/users.resolver.js b/lib/new-admin/graphql/resolvers/users.resolver.js index 2f344d58..97af898c 100644 --- a/lib/new-admin/graphql/resolvers/users.resolver.js +++ b/lib/new-admin/graphql/resolvers/users.resolver.js @@ -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), diff --git a/lib/new-admin/graphql/types/users.type.js b/lib/new-admin/graphql/types/users.type.js index fc92683f..0816ff4d 100644 --- a/lib/new-admin/graphql/types/users.type.js +++ b/lib/new-admin/graphql/types/users.type.js @@ -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 diff --git a/new-lamassu-admin/src/pages/Authentication/Input2FAState.js b/new-lamassu-admin/src/pages/Authentication/Input2FAState.js index 4712ac11..2c779de7 100644 --- a/new-lamassu-admin/src/pages/Authentication/Input2FAState.js +++ b/new-lamassu-admin/src/pages/Authentication/Input2FAState.js @@ -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) } diff --git a/new-lamassu-admin/src/pages/UserManagement/UserManagement.js b/new-lamassu-admin/src/pages/UserManagement/UserManagement.js index 0eba2487..12688a90 100644 --- a/new-lamassu-admin/src/pages/UserManagement/UserManagement.js +++ b/new-lamassu-admin/src/pages/UserManagement/UserManagement.js @@ -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() }} /> { 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 = () => { - ) diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/ChangeRoleModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/ChangeRoleModal.js index 329a02c0..265f02f6 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/ChangeRoleModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/ChangeRoleModal.js @@ -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 && ( + + )) || + (showModal && (

Do you wish to proceed?

-
- ) + )) ) } diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/EnableUserModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/EnableUserModal.js index 1d9a3aa7..8bf06d37 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/EnableUserModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/EnableUserModal.js @@ -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 && ( + + )) || + (showModal && ( )}
-
- ) + )) ) } diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/Input2FAModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/Input2FAModal.js index 017721a6..d3773fbb 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/Input2FAModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/Input2FAModal.js @@ -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 = () => { diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/Reset2FAModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/Reset2FAModal.js index 45f37f8c..722783e7 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/Reset2FAModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/Reset2FAModal.js @@ -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 && ( + + )) || + (showModal && (confirmation || !requiresConfirmation) && !loading && ( { className={classes.link} buttonClassname={classes.copyToClipboard} wrapperClassname={classes.linkWrapper}> - {reset2FAURL} + {reset2FAUrl} - ) + )) ) } diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/ResetPasswordModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/ResetPasswordModal.js index 19462e48..e02a14b4 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/ResetPasswordModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/ResetPasswordModal.js @@ -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 && ( + + )) || + (showModal && (confirmation || !requiresConfirmation) && !loading && ( - {resetPasswordURL} + {resetPasswordUrl} - ) + )) ) }