diff --git a/lib/new-admin/graphql/modules/authentication.js b/lib/new-admin/graphql/modules/authentication.js index 16aec232..5520b328 100644 --- a/lib/new-admin/graphql/modules/authentication.js +++ b/lib/new-admin/graphql/modules/authentication.js @@ -220,17 +220,14 @@ const resetPassword = (token, userID, newPassword, context) => { .then(() => true) } -const reset2FA = (token, userID, code, secret, context) => { - const isCodeValid = otplib.authenticator.verify({ token: code, secret }) - if (!isCodeValid) throw new authErrors.InvalidTwoFactorError() - +const reset2FA = (token, userID, code, context) => { return users.getUserById(userID) .then(user => { + const isCodeValid = otplib.authenticator.verify({ token: code, secret: user.temp_twofa_code }) + if (!isCodeValid) throw new authErrors.InvalidTwoFactorError() + destroySessionIfSameUser(context, user) - if (user.temp_twofa_code !== secret) { - throw new authErrors.InvalidTwoFactorError() - } - return users.reset2FASecret(token, user.id, secret) + return users.reset2FASecret(token, user.id, user.temp_twofa_code) }) .then(() => true) } diff --git a/lib/new-admin/graphql/resolvers/users.resolver.js b/lib/new-admin/graphql/resolvers/users.resolver.js index d26ca689..149a8252 100644 --- a/lib/new-admin/graphql/resolvers/users.resolver.js +++ b/lib/new-admin/graphql/resolvers/users.resolver.js @@ -28,7 +28,7 @@ const resolver = { 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), - reset2FA: (...[, { token, userID, code, secret }, context]) => authentication.reset2FA(token, userID, code, secret, context) + reset2FA: (...[, { token, userID, code }, context]) => authentication.reset2FA(token, userID, code, context) } } diff --git a/lib/new-admin/graphql/types/users.type.js b/lib/new-admin/graphql/types/users.type.js index a010ac61..9362d747 100644 --- a/lib/new-admin/graphql/types/users.type.js +++ b/lib/new-admin/graphql/types/users.type.js @@ -71,7 +71,7 @@ const typeDef = ` 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 - reset2FA(token: String!, userID: ID!, secret: String!, code: String!): Boolean + reset2FA(token: String!, userID: ID!, code: String!): Boolean } ` diff --git a/new-lamassu-admin/src/lamassu/App.js b/new-lamassu-admin/src/lamassu/App.js index a2b74e3f..48373584 100644 --- a/new-lamassu-admin/src/lamassu/App.js +++ b/new-lamassu-admin/src/lamassu/App.js @@ -1,4 +1,4 @@ -import { useLazyQuery } from '@apollo/react-hooks' +import { useQuery } from '@apollo/react-hooks' import CssBaseline from '@material-ui/core/CssBaseline' import Grid from '@material-ui/core/Grid' import Slide from '@material-ui/core/Slide' @@ -11,7 +11,7 @@ import { import gql from 'graphql-tag' import { create } from 'jss' import extendJss from 'jss-plugin-extend' -import React, { useContext, useEffect, useState } from 'react' +import React, { useContext, useState } from 'react' import { useLocation, useHistory, @@ -91,17 +91,13 @@ const Main = () => { const history = useHistory() const { wizardTested, userData, setUserData } = useContext(AppContext) - const [getUserData, { loading }] = useLazyQuery(GET_USER_DATA, { + const { loading } = useQuery(GET_USER_DATA, { onCompleted: userResponse => { if (!userData && userResponse?.userData) setUserData(userResponse.userData) } }) - useEffect(() => { - getUserData() - }, [getUserData]) - const route = location.pathname const sidebar = hasSidebar(route) diff --git a/new-lamassu-admin/src/pages/Authentication/Input2FAState.js b/new-lamassu-admin/src/pages/Authentication/Input2FAState.js index 70fe8691..2cb6cce1 100644 --- a/new-lamassu-admin/src/pages/Authentication/Input2FAState.js +++ b/new-lamassu-admin/src/pages/Authentication/Input2FAState.js @@ -47,6 +47,19 @@ const Input2FAState = ({ state, dispatch }) => { const [invalidToken, setInvalidToken] = useState(false) + const [getUserData, { error: queryError }] = useLazyQuery(GET_USER_DATA, { + onCompleted: ({ userData }) => { + setUserData(userData) + history.push('/') + } + }) + + const [input2FA, { error: mutationError }] = useMutation(INPUT_2FA, { + onCompleted: ({ input2FA: success }) => { + success ? getUserData() : setInvalidToken(true) + } + }) + const handle2FAChange = value => { dispatch({ type: STATES.INPUT_2FA, @@ -73,19 +86,6 @@ const Input2FAState = ({ state, dispatch }) => { }) } - const [input2FA, { error: mutationError }] = useMutation(INPUT_2FA, { - onCompleted: ({ input2FA: success }) => { - success ? getUserData() : setInvalidToken(true) - } - }) - - const [getUserData, { error: queryError }] = useLazyQuery(GET_USER_DATA, { - onCompleted: ({ userData }) => { - setUserData(userData) - history.push('/') - } - }) - const getErrorMsg = () => { if (queryError) return 'Internal server error' if (state.twoFAField.length !== 6 && invalidToken) diff --git a/new-lamassu-admin/src/pages/Authentication/Register.js b/new-lamassu-admin/src/pages/Authentication/Register.js index f529d93c..1572429f 100644 --- a/new-lamassu-admin/src/pages/Authentication/Register.js +++ b/new-lamassu-admin/src/pages/Authentication/Register.js @@ -69,12 +69,12 @@ const Register = () => { const initialState = { username: null, role: null, - wasSuccessful: false + result: '' } const reducer = (state, action) => { const { type, payload } = action - return { ...state, [type]: payload } + return { ...state, ...payload, result: type } } const [state, dispatch] = useReducer(reducer, initialState) @@ -83,15 +83,23 @@ const Register = () => { variables: { token: token }, onCompleted: ({ validateRegisterLink: info }) => { if (!info) { - dispatch({ type: 'wasSuccessful', payload: false }) + dispatch({ + type: 'failure' + }) } else { - dispatch({ type: 'wasSuccessful', payload: true }) - dispatch({ type: 'username', payload: info.username }) - dispatch({ type: 'role', payload: info.role }) + dispatch({ + type: 'success', + payload: { + username: info.username, + role: info.role + } + }) } }, onError: () => { - dispatch({ type: 'wasSuccessful', payload: false }) + dispatch({ + type: 'failure' + }) } }) @@ -127,7 +135,7 @@ const Register = () => {

Lamassu Admin

- {!loading && state.wasSuccessful && ( + {!loading && state.result === 'success' && ( { )} )} - {!loading && !state.wasSuccessful && ( + {!loading && state.result === 'failure' && ( <> Link has expired diff --git a/new-lamassu-admin/src/pages/Authentication/Reset2FA.js b/new-lamassu-admin/src/pages/Authentication/Reset2FA.js index f54b8b6d..b58ffc8d 100644 --- a/new-lamassu-admin/src/pages/Authentication/Reset2FA.js +++ b/new-lamassu-admin/src/pages/Authentication/Reset2FA.js @@ -3,7 +3,7 @@ import { makeStyles, Grid } from '@material-ui/core' import Paper from '@material-ui/core/Paper' import gql from 'graphql-tag' import QRCode from 'qrcode.react' -import React, { useState } from 'react' +import React, { useReducer, useState } from 'react' import { useLocation, useHistory } from 'react-router-dom' import { ActionButton, Button } from 'src/components/buttons' @@ -28,13 +28,8 @@ const VALIDATE_RESET_2FA_LINK = gql` ` const RESET_2FA = gql` - mutation reset2FA( - $token: String! - $userID: ID! - $secret: String! - $code: String! - ) { - reset2FA(token: $token, userID: $userID, secret: $secret, code: $code) + mutation reset2FA($token: String!, $userID: ID!, $code: String!) { + reset2FA(token: $token, userID: $userID, code: $code) } ` @@ -42,37 +37,52 @@ const Reset2FA = () => { const classes = useStyles() const history = useHistory() const token = QueryParams().get('t') - 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 initialState = { + userID: null, + secret: null, + otpauth: null, + result: null + } + + const reducer = (state, action) => { + const { type, payload } = action + return { ...state, ...payload, result: type } + } + + const [state, dispatch] = useReducer(reducer, initialState) + const handle2FAChange = value => { setTwoFAConfirmation(value) setInvalidToken(false) } - const { error: queryError } = useQuery(VALIDATE_RESET_2FA_LINK, { + const { error: queryError, loading } = useQuery(VALIDATE_RESET_2FA_LINK, { variables: { token: token }, onCompleted: ({ validateReset2FALink: info }) => { - setLoading(false) if (!info) { - setSuccess(false) + dispatch({ + type: 'failure' + }) } else { - setUserID(info.user_id) - setSecret(info.secret) - setOtpauth(info.otpauth) - setSuccess(true) + dispatch({ + type: 'success', + payload: { + userID: info.user_id, + secret: info.secret, + otpauth: info.otpauth + } + }) } }, onError: () => { - setLoading(false) - setSuccess(false) + dispatch({ + type: 'failure' + }) } }) @@ -107,7 +117,7 @@ const Reset2FA = () => {

Lamassu Admin

- {!isLoading && wasSuccessful && ( + {!loading && state.result === 'success' && ( <>
@@ -117,7 +127,11 @@ const Reset2FA = () => {
- +
@@ -127,7 +141,7 @@ const Reset2FA = () => { className={ isShowing ? classes.secret : classes.hiddenSecret }> - {secret} + {state.secret} { reset2FA({ variables: { token: token, - userID: userID, - secret: secret, + userID: state.userID, code: twoFAConfirmation } }) @@ -172,7 +185,7 @@ const Reset2FA = () => {
)} - {!isLoading && !wasSuccessful && ( + {!loading && state.result === 'failure' && ( <> Link has expired diff --git a/new-lamassu-admin/src/pages/UserManagement/UserManagement.js b/new-lamassu-admin/src/pages/UserManagement/UserManagement.js index 12688a90..5d7ec85c 100644 --- a/new-lamassu-admin/src/pages/UserManagement/UserManagement.js +++ b/new-lamassu-admin/src/pages/UserManagement/UserManagement.js @@ -2,7 +2,7 @@ import { useQuery } from '@apollo/react-hooks' import { makeStyles, Box, Chip } from '@material-ui/core' import gql from 'graphql-tag' import * as R from 'ramda' -import React, { useState, useContext } from 'react' +import React, { useReducer, useState, useContext } from 'react' import AppContext from 'src/AppContext' import { Link } from 'src/components/buttons' @@ -39,23 +39,27 @@ const Users = () => { const { data: userResponse } = useQuery(GET_USERS) - const [showCreateUserModal, setShowCreateUserModal] = useState(false) - const toggleCreateUserModal = () => - setShowCreateUserModal(!showCreateUserModal) + const initialState = { + showCreateUserModal: false, + showResetPasswordModal: false, + showReset2FAModal: false, + showRoleModal: false, + showEnableUserModal: false + } - const [showResetPasswordModal, setShowResetPasswordModal] = useState(false) - const toggleResetPasswordModal = () => - setShowResetPasswordModal(!showResetPasswordModal) + const reducer = (_, action) => { + const { type, payload } = action + switch (type) { + case 'close': + return initialState + case 'open': + return { ...initialState, [payload]: true } + default: + return initialState + } + } - const [showReset2FAModal, setShowReset2FAModal] = useState(false) - const toggleReset2FAModal = () => setShowReset2FAModal(!showReset2FAModal) - - const [showRoleModal, setShowRoleModal] = useState(false) - const toggleRoleModal = () => setShowRoleModal(!showRoleModal) - - const [showEnableUserModal, setShowEnableUserModal] = useState(false) - const toggleEnableUserModal = () => - setShowEnableUserModal(!showEnableUserModal) + const [state, dispatch] = useReducer(reducer, initialState) const [userInfo, setUserInfo] = useState(null) @@ -103,7 +107,10 @@ const Users = () => { checked={u.role === 'superuser'} onClick={() => { setUserInfo(u) - toggleRoleModal() + dispatch({ + type: 'open', + payload: 'showRoleModal' + }) }} value={u.role === 'superuser'} /> @@ -130,7 +137,10 @@ const Users = () => { className={classes.actionChip} onClick={() => { setUserInfo(u) - toggleResetPasswordModal() + dispatch({ + type: 'open', + payload: 'showResetPasswordModal' + }) }} /> { className={classes.actionChip} onClick={() => { setUserInfo(u) - toggleReset2FAModal() + dispatch({ + type: 'open', + payload: 'showReset2FAModal' + }) }} /> @@ -157,7 +170,10 @@ const Users = () => { checked={u.enabled} onClick={() => { setUserInfo(u) - toggleEnableUserModal() + dispatch({ + type: 'open', + payload: 'showEnableUserModal' + }) }} value={u.enabled} /> @@ -174,36 +190,40 @@ const Users = () => { className={classes.tableWidth} display="flex" justifyContent="flex-end"> - + { + dispatch({ + type: 'open', + payload: 'showCreateUserModal' + }) + }}> Add new user - + diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/ChangeRoleModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/ChangeRoleModal.js index 265f02f6..3704a6bb 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/ChangeRoleModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/ChangeRoleModal.js @@ -29,12 +29,7 @@ const CHANGE_USER_ROLE = gql` const useStyles = makeStyles(styles) -const ChangeRoleModal = ({ - showModal, - toggleModal, - user, - requiresConfirmation -}) => { +const ChangeRoleModal = ({ state, dispatch, user, requiresConfirmation }) => { const classes = useStyles() const [changeUserRole] = useMutation(CHANGE_USER_ROLE, { @@ -56,18 +51,21 @@ const ChangeRoleModal = ({ const handleClose = () => { setConfirmation(null) - toggleModal() + dispatch({ + type: 'close', + payload: 'showRoleModal' + }) } return ( - (showModal && requiresConfirmation && !confirmation && ( + (state.showRoleModal && requiresConfirmation && !confirmation && ( )) || - (showModal && ( + (state.showRoleModal && ( { +const CreateUserModal = ({ state, dispatch }) => { const classes = useStyles() const [usernameField, setUsernameField] = useState('') @@ -58,12 +58,15 @@ const CreateUserModal = ({ showModal, toggleModal }) => { const handleClose = () => { setCreateUserURL(null) - toggleModal() + dispatch({ + type: 'close', + payload: 'showCreateUserModal' + }) } const [createUser, { error }] = useMutation(CREATE_USER, { onCompleted: ({ createRegisterToken: token }) => { - setCreateUserURL(`${URI}/register?t=${token.token}`) + setCreateUserURL(urlResolver(`/register?t=${token.token}`)) } }) @@ -81,7 +84,7 @@ const CreateUserModal = ({ showModal, toggleModal }) => { return ( <> - {showModal && !createUserURL && ( + {state.showCreateUserModal && !createUserURL && ( { )} - {showModal && createUserURL && ( + {state.showCreateUserModal && createUserURL && ( { - const classes = useStyles() - - const handleClose = () => { - toggleModal() - } - - return ( - showModal && ( - - Delete {user.username}? -

- You are about to delete {user.username}. This will remove existent - sessions and revoke this user's permissions to access the system. -

-

- This is a PERMANENT operation. Do you wish to proceed? -

-
- -
-
- ) - ) -} - -export default DeleteUserModal diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/EnableUserModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/EnableUserModal.js index 8bf06d37..5757f6ea 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/EnableUserModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/EnableUserModal.js @@ -29,12 +29,7 @@ const DISABLE_USER = gql` const useStyles = makeStyles(styles) -const EnableUserModal = ({ - showModal, - toggleModal, - user, - requiresConfirmation -}) => { +const EnableUserModal = ({ state, dispatch, user, requiresConfirmation }) => { const classes = useStyles() const [enableUser] = useMutation(ENABLE_USER, { @@ -47,37 +42,46 @@ const EnableUserModal = ({ 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 - ? disableUser({ - variables: { - confirmationCode: confirmation, - id: user.id - } - }) - : enableUser({ - variables: { - confirmationCode: confirmation, - id: user.id - } - }) + user?.enabled ? disable() : enable() handleClose() } const handleClose = () => { setConfirmation(null) - toggleModal() + dispatch({ + type: 'close', + payload: 'showEnableUserModal' + }) } return ( - (showModal && requiresConfirmation && !confirmation && ( + (state.showEnableUserModal && requiresConfirmation && !confirmation && ( )) || - (showModal && ( + (state.showEnableUserModal && ( { +const Reset2FAModal = ({ state, dispatch, user, requiresConfirmation }) => { const classes = useStyles() const [reset2FAUrl, setReset2FAUrl] = useState('') @@ -37,7 +32,7 @@ const Reset2FAModal = ({ CREATE_RESET_2FA_TOKEN, { onCompleted: ({ createReset2FAToken: token }) => { - setReset2FAUrl(`${URI}/reset2fa?t=${token.token}`) + setReset2FAUrl(urlResolver(`/reset2fa?t=${token.token}`)) } } ) @@ -45,7 +40,7 @@ const Reset2FAModal = ({ const [confirmation, setConfirmation] = useState(null) useEffect(() => { - showModal && + state.showReset2FAModal && (confirmation || !requiresConfirmation) && createReset2FAToken({ variables: { @@ -53,49 +48,60 @@ const Reset2FAModal = ({ userID: user?.id } }) - }, [confirmation, createReset2FAToken, requiresConfirmation, showModal, user]) + }, [ + confirmation, + createReset2FAToken, + requiresConfirmation, + state.showReset2FAModal, + user?.id + ]) const handleClose = () => { setConfirmation(null) - toggleModal() + dispatch({ + type: 'close', + payload: 'showReset2FAModal' + }) } return ( - (showModal && requiresConfirmation && !confirmation && ( + (state.showReset2FAModal && requiresConfirmation && !confirmation && ( )) || - (showModal && (confirmation || !requiresConfirmation) && !loading && ( - - - Reset 2FA for {user.username} - -

- Safely share this link with {user.username} for a two-factor - authentication reset. -

-
- - - - {reset2FAUrl} - - - -
-
- )) + (state.showReset2FAModal && + (confirmation || !requiresConfirmation) && + !loading && ( + + + Reset 2FA for {user.username} + +

+ Safely share this link with {user.username} for a two-factor + authentication reset. +

+
+ + + + {reset2FAUrl} + + + +
+
+ )) ) } diff --git a/new-lamassu-admin/src/pages/UserManagement/modals/ResetPasswordModal.js b/new-lamassu-admin/src/pages/UserManagement/modals/ResetPasswordModal.js index d7590bce..c0875e5b 100644 --- a/new-lamassu-admin/src/pages/UserManagement/modals/ResetPasswordModal.js +++ b/new-lamassu-admin/src/pages/UserManagement/modals/ResetPasswordModal.js @@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react' import Modal from 'src/components/Modal' import { Info2, P, Mono } from 'src/components/typography' import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard' -import { URI } from 'src/utils/apollo' +import { urlResolver } from 'src/utils/urlResolver' import styles from '../UserManagement.styles' @@ -28,8 +28,8 @@ const CREATE_RESET_PASSWORD_TOKEN = gql` const useStyles = makeStyles(styles) const ResetPasswordModal = ({ - showModal, - toggleModal, + state, + dispatch, user, requiresConfirmation }) => { @@ -40,7 +40,7 @@ const ResetPasswordModal = ({ CREATE_RESET_PASSWORD_TOKEN, { onCompleted: ({ createResetPasswordToken: token }) => { - setResetPasswordUrl(`${URI}/resetpassword?t=${token.token}`) + setResetPasswordUrl(urlResolver(`/resetpassword?t=${token.token}`)) } } ) @@ -48,7 +48,7 @@ const ResetPasswordModal = ({ const [confirmation, setConfirmation] = useState(null) useEffect(() => { - showModal && + state.showResetPasswordModal && (confirmation || !requiresConfirmation) && createResetPasswordToken({ variables: { @@ -59,51 +59,56 @@ const ResetPasswordModal = ({ }, [ confirmation, createResetPasswordToken, - showModal, - user, - requiresConfirmation + requiresConfirmation, + state.showResetPasswordModal, + user?.id ]) const handleClose = () => { setConfirmation(null) - toggleModal() + dispatch({ + type: 'close', + payload: 'showResetPasswordModal' + }) } return ( - (showModal && requiresConfirmation && !confirmation && ( + (state.showResetPasswordModal && requiresConfirmation && !confirmation && ( )) || - (showModal && (confirmation || !requiresConfirmation) && !loading && ( - - - Reset password for {user.username} - -

- Safely share this link with {user.username} for a password reset. -

-
- - - - {resetPasswordUrl} - - - -
-
- )) + (state.showResetPasswordModal && + (confirmation || !requiresConfirmation) && + !loading && ( + + + Reset password for {user.username} + +

+ Safely share this link with {user.username} for a password reset. +

+
+ + + + {resetPasswordUrl} + + + +
+
+ )) ) } diff --git a/new-lamassu-admin/src/utils/urlResolver.js b/new-lamassu-admin/src/utils/urlResolver.js new file mode 100644 index 00000000..5e0785bb --- /dev/null +++ b/new-lamassu-admin/src/utils/urlResolver.js @@ -0,0 +1,6 @@ +const url = + process.env.NODE_ENV === 'development' ? 'https://localhost:3001' : '' + +const urlResolver = content => `${url}${content}` + +export { urlResolver }