Skip to content

Commit 903111b

Browse files
committed
Adding duplicate user accounts (by name) detection and confirmation in create user form (for super-admins).
1 parent d32888a commit 903111b

File tree

5 files changed

+143
-50
lines changed

5 files changed

+143
-50
lines changed

src/components/forms/CreateUserForm/CreateUserForm.js

Lines changed: 95 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import { FormattedMessage } from 'react-intl';
4+
import { Table } from 'react-bootstrap';
45
import { reduxForm, Field, change } from 'redux-form';
56
import isEmail from 'validator/lib/isEmail.js';
67

78
import SubmitButton from '../SubmitButton';
89
import Callout from '../../widgets/Callout';
10+
import Explanation from '../../widgets/Explanation';
11+
import UsersName from '../../Users/UsersName';
12+
import { WarningIcon } from '../../icons';
913
import { validateRegistrationData } from '../../../redux/modules/users.js';
10-
import { TextField, PasswordField, PasswordStrength } from '../Fields';
14+
import { TextField, PasswordField, PasswordStrength, CheckboxField } from '../Fields';
1115

1216
const CreateUserForm = ({
17+
matchingUsers,
1318
submitting,
1419
handleSubmit,
1520
onSubmit,
@@ -19,6 +24,7 @@ const CreateUserForm = ({
1924
asyncValidating,
2025
invalid,
2126
reset,
27+
change,
2228
}) => (
2329
<div>
2430
<Field
@@ -69,6 +75,54 @@ const CreateUserForm = ({
6975
label={<FormattedMessage id="app.changePasswordForm.passwordCheck" defaultMessage="New Password (again):" />}
7076
/>
7177

78+
{matchingUsers && matchingUsers.length > 0 && (
79+
<>
80+
<hr />
81+
<h5>
82+
<WarningIcon className="text-warning" gapRight={2} />
83+
<FormattedMessage
84+
id="app.inviteUserForm.matchingUsers"
85+
defaultMessage="There are existing users of the same name"
86+
/>
87+
:
88+
</h5>
89+
90+
<Table bordered>
91+
<tbody>
92+
{matchingUsers.map(user => (
93+
<tr key={user.id}>
94+
<td>
95+
<UsersName {...user} showEmail="full" showExternalIdentifiers showRoleIcon />
96+
</td>
97+
</tr>
98+
))}
99+
</tbody>
100+
</Table>
101+
102+
<Field
103+
name="ignoreNameCollision"
104+
component={CheckboxField}
105+
onOff
106+
label={
107+
<span>
108+
<FormattedMessage
109+
id="app.inviteUserForm.ignoreNameCollision"
110+
defaultMessage="The user I am inviting does not match any of the existing users"
111+
/>
112+
<Explanation id="ignoreNameCollisionExplanation">
113+
<FormattedMessage
114+
id="app.inviteUserForm.ignoreNameCollisionExplanation"
115+
defaultMessage="Please, make sure the listed students are not the same person as the one you are inviting to prevent duplicate accounts in the system."
116+
/>
117+
</Explanation>
118+
</span>
119+
}
120+
/>
121+
</>
122+
)}
123+
124+
<hr />
125+
72126
{submitFailed && (
73127
<Callout variant="danger">
74128
<FormattedMessage id="generic.operationFailed" defaultMessage="Operation failed. Please try again later." />
@@ -78,7 +132,17 @@ const CreateUserForm = ({
78132
<div className="text-center">
79133
<SubmitButton
80134
id="createUser"
81-
handleSubmit={handleSubmit(data => onSubmit(data).then(reset))}
135+
handleSubmit={handleSubmit(data =>
136+
onSubmit(data).then(success => {
137+
if (success) {
138+
reset();
139+
} else {
140+
// a hack so the change takes place after the whole submit process is completed
141+
window.setTimeout(() => change('ignoreNameCollision', false), 0);
142+
}
143+
})
144+
)}
145+
resetTimeout={0}
82146
submitting={submitting}
83147
dirty={dirty}
84148
invalid={invalid}
@@ -88,14 +152,14 @@ const CreateUserForm = ({
88152
messages={{
89153
submit: <FormattedMessage id="generic.create" defaultMessage="Create" />,
90154
submitting: <FormattedMessage id="generic.creating" defaultMessage="Creating..." />,
91-
success: <FormattedMessage id="generic.created" defaultMessage="Created" />,
92155
}}
93156
/>
94157
</div>
95158
</div>
96159
);
97160

98161
CreateUserForm.propTypes = {
162+
matchingUsers: PropTypes.array,
99163
handleSubmit: PropTypes.func.isRequired,
100164
onSubmit: PropTypes.func.isRequired,
101165
asyncValidate: PropTypes.func.isRequired,
@@ -107,9 +171,13 @@ CreateUserForm.propTypes = {
107171
invalid: PropTypes.bool,
108172
pristine: PropTypes.bool,
109173
reset: PropTypes.func,
174+
change: PropTypes.func,
110175
};
111176

112-
const validate = ({ firstName, lastName, email, password, passwordConfirm }) => {
177+
const validate = (
178+
{ firstName, lastName, email, password, passwordConfirm, ignoreNameCollision },
179+
{ matchingUsers }
180+
) => {
113181
const errors = {};
114182

115183
if (!firstName) {
@@ -188,40 +256,40 @@ const validate = ({ firstName, lastName, email, password, passwordConfirm }) =>
188256
);
189257
}
190258

259+
if (matchingUsers && matchingUsers.length > 0 && !ignoreNameCollision) {
260+
errors.ignoreNameCollision = (
261+
<FormattedMessage
262+
id="app.inviteUserForm.validation.ignoreNameCollision"
263+
defaultMessage="Please check the list of existing users and confirm that the invited user is a new user."
264+
/>
265+
);
266+
}
191267
return errors;
192268
};
193269

194270
const asyncValidate = ({ email, password = '' }, dispatch) => {
195-
if (password === '') {
196-
dispatch(change('create-user', 'passwordStrength', null));
197-
return Promise.resolve();
271+
if (!password) {
272+
dispatch(change('create-user', 'passwordStrength', undefined));
198273
}
199274

200-
return new Promise((resolve, reject) =>
201-
dispatch(validateRegistrationData(email, password))
202-
.then(res => res.value)
203-
.then(({ usernameIsFree, passwordScore }) => {
204-
const errors = {};
205-
if (!usernameIsFree) {
206-
errors.email = (
275+
return dispatch(validateRegistrationData(email, password))
276+
.then(res => res.value)
277+
.then(({ usernameIsFree, passwordScore }) => {
278+
dispatch(change('create-user', 'passwordStrength', passwordScore));
279+
280+
if (!usernameIsFree) {
281+
const errors = {
282+
email: (
207283
<FormattedMessage
208284
id="app.createUserForm.validation.emailTaken"
209285
defaultMessage="This email address is already taken by someone else."
210286
/>
211-
);
212-
}
213-
214-
dispatch(change('create-user', 'passwordStrength', passwordScore));
215-
216-
if (Object.keys(errors).length > 0) {
217-
throw errors;
218-
}
219-
})
220-
.then(resolve())
221-
.catch(errors => reject(errors))
222-
);
287+
),
288+
};
289+
throw errors;
290+
}
291+
});
223292
};
224-
225293
export default reduxForm({
226294
form: 'create-user',
227295
validate,

src/locales/cs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2105,6 +2105,7 @@
21052105
"app.userName.userDeactivated": "Uživatelský účet byl deaktivován. Uživatel se nemůže přihlásit.",
21062106
"app.userSwitching.loginAs": "Přihlásit jako",
21072107
"app.users.createUser": "Vytvořit uživatele",
2108+
"app.users.createUser.userCreated": "Uživatelský účet byl vytvořen.",
21082109
"app.users.listTitle": "Uživatelé",
21092110
"app.users.takeOver": "Přihlásit jako",
21102111
"app.users.title": "Seznam všech uživatelů",

src/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2105,6 +2105,7 @@
21052105
"app.userName.userDeactivated": "The user account was deactivated. The user may not sign in.",
21062106
"app.userSwitching.loginAs": "Login as",
21072107
"app.users.createUser": "Create User",
2108+
"app.users.createUser.userCreated": "The user account was created.",
21082109
"app.users.listTitle": "Users",
21092110
"app.users.takeOver": "Login as",
21102111
"app.users.title": "List of All Users",

src/pages/Users/Users.js

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Row, Col, Modal } from 'react-bootstrap';
77
import { Link } from 'react-router-dom';
88
import { lruMemoize } from 'reselect';
99

10-
import { SettingsIcon, TransferIcon, BanIcon, UserIcon } from '../../components/icons';
10+
import { SettingsIcon, TransferIcon, BanIcon, SuccessIcon, UserIcon } from '../../components/icons';
1111
import Button, { TheButtonGroup } from '../../components/widgets/TheButton';
1212
import DeleteUserButtonContainer from '../../containers/DeleteUserButtonContainer';
1313
import AllowUserButtonContainer from '../../containers/AllowUserButtonContainer';
@@ -32,6 +32,7 @@ import { knownRoles, isSupervisorRole, isStudentRole, isSuperadminRole } from '.
3232
import withLinks from '../../helpers/withLinks.js';
3333
import { withRouterProps } from '../../helpers/withRouter.js';
3434
import { suspendAbortPendingRequestsOptimization } from '../../pages/routes.js';
35+
import { EMPTY_ARRAY } from '../../helpers/common.js';
3536

3637
const filterInitialValues = lruMemoize(({ search = '', roles = [] }) => {
3738
const initials = { search, roles: {} };
@@ -57,15 +58,16 @@ const createUserInitialValues = {
5758
email: '',
5859
password: '',
5960
passwordConfirm: '',
61+
ignoreNameCollision: undefined,
6062
};
6163

6264
const PAGINATION_CONTAINER_ID = 'users-all';
6365
const PAGINATION_CONTAINER_ENDPOINT = 'users';
6466

6567
class Users extends Component {
66-
state = { dialogOpen: false };
68+
state = { dialogOpen: false, userCreated: false, matchingUsers: EMPTY_ARRAY };
6769

68-
openDialog = () => this.setState({ dialogOpen: true });
70+
openDialog = () => this.setState({ dialogOpen: true, userCreated: false, matchingUsers: EMPTY_ARRAY });
6971

7072
closeDialog = () => this.setState({ dialogOpen: false });
7173

@@ -147,9 +149,15 @@ class Users extends Component {
147149
intl: { locale },
148150
} = this.props;
149151

150-
return createUser(data, instanceId).then(() => {
151-
this.closeDialog();
152-
return reloadPagination(locale);
152+
return createUser(data, instanceId).then(({ value: { user, usersWithSameName = null } }) => {
153+
if (user) {
154+
this.setState({ userCreated: true, matchingUsers: EMPTY_ARRAY });
155+
reloadPagination(locale);
156+
return true;
157+
} else {
158+
this.setState({ matchingUsers: usersWithSameName });
159+
return false;
160+
}
153161
});
154162
};
155163

@@ -195,10 +203,28 @@ class Users extends Component {
195203
</Modal.Title>
196204
</Modal.Header>
197205
<Modal.Body>
198-
<CreateUserForm
199-
onSubmit={this.createNewUserAccount}
200-
initialValues={createUserInitialValues}
201-
/>
206+
{this.state.userCreated ? (
207+
<Callout variant="success" className="mb-4">
208+
<p>
209+
<FormattedMessage
210+
id="app.users.createUser.userCreated"
211+
defaultMessage="The user account was created."
212+
/>
213+
</p>
214+
<div className="text-end">
215+
<Button onClick={this.closeDialog} variant="success">
216+
<SuccessIcon gapRight={2} />
217+
<FormattedMessage id="generic.close" defaultMessage="Close" />
218+
</Button>
219+
</div>
220+
</Callout>
221+
) : (
222+
<CreateUserForm
223+
onSubmit={this.createNewUserAccount}
224+
initialValues={createUserInitialValues}
225+
matchingUsers={this.state.matchingUsers}
226+
/>
227+
)}
202228
</Modal.Body>
203229
</Modal>
204230
</div>
@@ -263,8 +289,13 @@ export default withLinks(
263289
},
264290
dispatch => ({
265291
takeOver: userId => dispatch(takeOver(userId)),
266-
createUser: ({ firstName, lastName, email, password, passwordConfirm }, instanceId) =>
267-
dispatch(createAccount(firstName, lastName, email, password, passwordConfirm, instanceId, true)), // true = skip auth changes
292+
createUser: ({ firstName, lastName, email, password, passwordConfirm, ignoreNameCollision }, instanceId) =>
293+
dispatch(
294+
createAccount(
295+
{ firstName, lastName, email, password, passwordConfirm, ignoreNameCollision, instanceId },
296+
true // create by superadmin
297+
)
298+
), // true = skip auth changes
268299
reloadPagination: locale =>
269300
dispatch(fetchPaginated(PAGINATION_CONTAINER_ID, PAGINATION_CONTAINER_ENDPOINT)(locale, null, null, true)), // true = force invalidate
270301
})

src/redux/modules/registration.js

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,13 @@ export const statusTypes = {
2020
* Actions
2121
*/
2222

23-
export const createAccount = (
24-
firstName,
25-
lastName,
26-
email,
27-
password,
28-
passwordConfirm,
29-
instanceId,
30-
createdBySuperadmin = false
31-
) =>
23+
export const createAccount = (body, createdBySuperadmin = false) =>
3224
createApiAction({
3325
type: actionTypes.CREATE_ACCOUNT,
3426
method: 'POST',
3527
endpoint: '/users',
36-
body: { firstName, lastName, email, password, passwordConfirm, instanceId },
37-
meta: { instanceId, createdBySuperadmin },
28+
body,
29+
meta: { instanceId: body.instanceId, createdBySuperadmin },
3830
});
3931

4032
export const createExternalAccount = (instanceId, serviceId, credentials, authType = 'secondary') =>

0 commit comments

Comments
 (0)