@strapi/plugin-users-permissions 4.3.0-beta.1 → 4.3.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strapi/plugin-users-permissions",
3
- "version": "4.3.0-beta.1",
3
+ "version": "4.3.0-beta.2",
4
4
  "description": "Protect your API with a full-authentication process based on JWT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,8 +27,8 @@
27
27
  "test:front:watch:ce": "cross-env IS_EE=false jest --config ./jest.config.front.js --watchAll"
28
28
  },
29
29
  "dependencies": {
30
- "@strapi/helper-plugin": "4.3.0-beta.1",
31
- "@strapi/utils": "4.3.0-beta.1",
30
+ "@strapi/helper-plugin": "4.3.0-beta.2",
31
+ "@strapi/utils": "4.3.0-beta.2",
32
32
  "bcryptjs": "2.4.3",
33
33
  "grant-koa": "5.4.8",
34
34
  "jsonwebtoken": "^8.1.0",
@@ -38,7 +38,7 @@
38
38
  "react": "^17.0.2",
39
39
  "react-dom": "^17.0.2",
40
40
  "react-intl": "5.20.2",
41
- "react-redux": "7.2.3",
41
+ "react-redux": "7.2.8",
42
42
  "react-router": "^5.2.0",
43
43
  "react-router-dom": "5.2.0",
44
44
  "redux-saga": "^0.16.0",
@@ -59,5 +59,5 @@
59
59
  "required": true,
60
60
  "kind": "plugin"
61
61
  },
62
- "gitHead": "9d6555398960c39159d66bb4eea3bcb0362e37e3"
62
+ "gitHead": "42aba356ad1b0751584d3b375e83baa4a2c18f65"
63
63
  }
@@ -15,13 +15,14 @@ const {
15
15
  validateCallbackBody,
16
16
  validateRegisterBody,
17
17
  validateSendEmailConfirmationBody,
18
+ validateForgotPasswordBody,
19
+ validateResetPasswordBody,
20
+ validateEmailConfirmationBody,
18
21
  } = require('./validation/auth');
19
22
 
20
23
  const { getAbsoluteAdminUrl, getAbsoluteServerUrl, sanitize } = utils;
21
24
  const { ApplicationError, ValidationError } = utils.errors;
22
25
 
23
- const emailRegExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
24
-
25
26
  const sanitizeUser = (user, ctx) => {
26
27
  const { auth } = ctx.state;
27
28
  const userSchema = strapi.getModel('plugin::users-permissions.user');
@@ -35,49 +36,33 @@ module.exports = {
35
36
  const params = ctx.request.body;
36
37
 
37
38
  const store = strapi.store({ type: 'plugin', name: 'users-permissions' });
39
+ const grantSettings = await store.get({ key: 'grant' });
38
40
 
39
- if (provider === 'local') {
40
- if (!_.get(await store.get({ key: 'grant' }), 'email.enabled')) {
41
- throw new ApplicationError('This provider is disabled');
42
- }
43
-
44
- await validateCallbackBody(params);
41
+ const grantProvider = provider === 'local' ? 'email' : provider;
45
42
 
46
- const query = { provider };
43
+ if (!_.get(grantSettings, [grantProvider, 'enabled'])) {
44
+ throw new ApplicationError('This provider is disabled');
45
+ }
47
46
 
48
- // Check if the provided identifier is an email or not.
49
- const isEmail = emailRegExp.test(params.identifier);
47
+ if (provider === 'local') {
48
+ await validateCallbackBody(params);
50
49
 
51
- // Set the identifier to the appropriate query field.
52
- if (isEmail) {
53
- query.email = params.identifier.toLowerCase();
54
- } else {
55
- query.username = params.identifier;
56
- }
50
+ const { identifier } = params;
57
51
 
58
52
  // Check if the user exists.
59
- const user = await strapi.query('plugin::users-permissions.user').findOne({ where: query });
53
+ const user = await strapi.query('plugin::users-permissions.user').findOne({
54
+ where: {
55
+ provider,
56
+ $or: [{ email: identifier.toLowerCase() }, { username: identifier }],
57
+ },
58
+ });
60
59
 
61
60
  if (!user) {
62
61
  throw new ValidationError('Invalid identifier or password');
63
62
  }
64
63
 
65
- if (
66
- _.get(await store.get({ key: 'advanced' }), 'email_confirmation') &&
67
- user.confirmed !== true
68
- ) {
69
- throw new ApplicationError('Your account email is not confirmed');
70
- }
71
-
72
- if (user.blocked === true) {
73
- throw new ApplicationError('Your account has been blocked by an administrator');
74
- }
75
-
76
- // The user never authenticated with the `local` provider.
77
64
  if (!user.password) {
78
- throw new ApplicationError(
79
- 'This user never set a local password, please login with the provider used during account creation'
80
- );
65
+ throw new ValidationError('Invalid identifier or password');
81
66
  }
82
67
 
83
68
  const validPassword = await getService('user').validatePassword(
@@ -87,67 +72,65 @@ module.exports = {
87
72
 
88
73
  if (!validPassword) {
89
74
  throw new ValidationError('Invalid identifier or password');
90
- } else {
91
- ctx.send({
92
- jwt: getService('jwt').issue({
93
- id: user.id,
94
- }),
95
- user: await sanitizeUser(user, ctx),
96
- });
97
- }
98
- } else {
99
- if (!_.get(await store.get({ key: 'grant' }), [provider, 'enabled'])) {
100
- throw new ApplicationError('This provider is disabled');
101
75
  }
102
76
 
103
- // Connect the user with the third-party provider.
104
- try {
105
- const user = await getService('providers').connect(provider, ctx.query);
106
- ctx.send({
107
- jwt: getService('jwt').issue({ id: user.id }),
108
- user: await sanitizeUser(user, ctx),
109
- });
110
- } catch (error) {
111
- throw new ApplicationError(error.message);
112
- }
113
- }
114
- },
77
+ const advancedSettings = await store.get({ key: 'advanced' });
78
+ const requiresConfirmation = _.get(advancedSettings, 'email_confirmation');
115
79
 
116
- async resetPassword(ctx) {
117
- const params = _.assign({}, ctx.request.body, ctx.params);
118
-
119
- if (
120
- params.password &&
121
- params.passwordConfirmation &&
122
- params.password === params.passwordConfirmation &&
123
- params.code
124
- ) {
125
- const user = await strapi
126
- .query('plugin::users-permissions.user')
127
- .findOne({ where: { resetPasswordToken: `${params.code}` } });
80
+ if (requiresConfirmation && user.confirmed !== true) {
81
+ throw new ApplicationError('Your account email is not confirmed');
82
+ }
128
83
 
129
- if (!user) {
130
- throw new ValidationError('Incorrect code provided');
84
+ if (user.blocked === true) {
85
+ throw new ApplicationError('Your account has been blocked by an administrator');
131
86
  }
132
87
 
133
- await getService('user').edit(user.id, {
134
- resetPasswordToken: null,
135
- password: params.password,
88
+ return ctx.send({
89
+ jwt: getService('jwt').issue({ id: user.id }),
90
+ user: await sanitizeUser(user, ctx),
136
91
  });
137
- // Update the user.
138
- ctx.send({
92
+ }
93
+
94
+ // Connect the user with the third-party provider.
95
+ try {
96
+ const user = await getService('providers').connect(provider, ctx.query);
97
+
98
+ return ctx.send({
139
99
  jwt: getService('jwt').issue({ id: user.id }),
140
100
  user: await sanitizeUser(user, ctx),
141
101
  });
142
- } else if (
143
- params.password &&
144
- params.passwordConfirmation &&
145
- params.password !== params.passwordConfirmation
146
- ) {
102
+ } catch (error) {
103
+ throw new ApplicationError(error.message);
104
+ }
105
+ },
106
+
107
+ async resetPassword(ctx) {
108
+ const { password, passwordConfirmation, code } = await validateResetPasswordBody(
109
+ ctx.request.body
110
+ );
111
+
112
+ if (password !== passwordConfirmation) {
147
113
  throw new ValidationError('Passwords do not match');
148
- } else {
149
- throw new ValidationError('Incorrect params provided');
150
114
  }
115
+
116
+ const user = await strapi
117
+ .query('plugin::users-permissions.user')
118
+ .findOne({ where: { resetPasswordToken: code } });
119
+
120
+ if (!user) {
121
+ throw new ValidationError('Incorrect code provided');
122
+ }
123
+
124
+ await getService('user').edit(user.id, {
125
+ resetPasswordToken: null,
126
+ password,
127
+ });
128
+
129
+ // Update the user.
130
+ ctx.send({
131
+ jwt: getService('jwt').issue({ id: user.id }),
132
+ user: await sanitizeUser(user, ctx),
133
+ });
151
134
  },
152
135
 
153
136
  async connect(ctx, next) {
@@ -189,87 +172,66 @@ module.exports = {
189
172
  },
190
173
 
191
174
  async forgotPassword(ctx) {
192
- let { email } = ctx.request.body;
193
-
194
- // Check if the provided email is valid or not.
195
- const isEmail = emailRegExp.test(email);
196
-
197
- if (isEmail) {
198
- email = email.toLowerCase();
199
- } else {
200
- throw new ValidationError('Please provide a valid email address');
201
- }
175
+ const { email } = await validateForgotPasswordBody(ctx.request.body);
202
176
 
203
177
  const pluginStore = await strapi.store({ type: 'plugin', name: 'users-permissions' });
204
178
 
179
+ const emailSettings = await pluginStore.get({ key: 'email' });
180
+ const advancedSettings = await pluginStore.get({ key: 'advanced' });
181
+
205
182
  // Find the user by email.
206
183
  const user = await strapi
207
184
  .query('plugin::users-permissions.user')
208
185
  .findOne({ where: { email: email.toLowerCase() } });
209
186
 
210
- // User not found.
211
- if (!user) {
212
- throw new ApplicationError('This email does not exist');
213
- }
214
-
215
- // User blocked
216
- if (user.blocked) {
217
- throw new ApplicationError('This user is disabled');
187
+ if (!user || user.blocked) {
188
+ return ctx.send({ ok: true });
218
189
  }
219
190
 
220
191
  // Generate random token.
192
+ const userInfo = await sanitizeUser(user, ctx);
193
+
221
194
  const resetPasswordToken = crypto.randomBytes(64).toString('hex');
222
195
 
223
- const settings = await pluginStore.get({ key: 'email' }).then(storeEmail => {
224
- try {
225
- return storeEmail['reset_password'].options;
226
- } catch (error) {
227
- return {};
196
+ const resetPasswordSettings = _.get(emailSettings, 'reset_password.options', {});
197
+ const emailBody = await getService('users-permissions').template(
198
+ resetPasswordSettings.message,
199
+ {
200
+ URL: advancedSettings.email_reset_password,
201
+ SERVER_URL: getAbsoluteServerUrl(strapi.config),
202
+ ADMIN_URL: getAbsoluteAdminUrl(strapi.config),
203
+ USER: userInfo,
204
+ TOKEN: resetPasswordToken,
228
205
  }
229
- });
230
-
231
- const advanced = await pluginStore.get({
232
- key: 'advanced',
233
- });
234
-
235
- const userInfo = await sanitizeUser(user, ctx);
236
-
237
- settings.message = await getService('users-permissions').template(settings.message, {
238
- URL: advanced.email_reset_password,
239
- SERVER_URL: getAbsoluteServerUrl(strapi.config),
240
- ADMIN_URL: getAbsoluteAdminUrl(strapi.config),
241
- USER: userInfo,
242
- TOKEN: resetPasswordToken,
243
- });
206
+ );
244
207
 
245
- settings.object = await getService('users-permissions').template(settings.object, {
246
- USER: userInfo,
247
- });
208
+ const emailObject = await getService('users-permissions').template(
209
+ resetPasswordSettings.object,
210
+ {
211
+ USER: userInfo,
212
+ }
213
+ );
214
+
215
+ const emailToSend = {
216
+ to: user.email,
217
+ from:
218
+ resetPasswordSettings.from.email || resetPasswordSettings.from.name
219
+ ? `${resetPasswordSettings.from.name} <${resetPasswordSettings.from.email}>`
220
+ : undefined,
221
+ replyTo: resetPasswordSettings.response_email,
222
+ subject: emailObject,
223
+ text: emailBody,
224
+ html: emailBody,
225
+ };
248
226
 
249
- try {
250
- // Send an email to the user.
251
- await strapi
252
- .plugin('email')
253
- .service('email')
254
- .send({
255
- to: user.email,
256
- from:
257
- settings.from.email || settings.from.name
258
- ? `${settings.from.name} <${settings.from.email}>`
259
- : undefined,
260
- replyTo: settings.response_email,
261
- subject: settings.object,
262
- text: settings.message,
263
- html: settings.message,
264
- });
265
- } catch (err) {
266
- throw new ApplicationError(err.message);
267
- }
227
+ // NOTE: Update the user before sending the email so an Admin can generate the link if the email fails
228
+ await getService('user').edit(user.id, { resetPasswordToken });
268
229
 
269
- // Update the user.
230
+ // Send an email to the user.
270
231
  await strapi
271
- .query('plugin::users-permissions.user')
272
- .update({ where: { id: user.id }, data: { resetPasswordToken } });
232
+ .plugin('email')
233
+ .service('email')
234
+ .send(emailToSend);
273
235
 
274
236
  ctx.send({ ok: true });
275
237
  },
@@ -277,29 +239,25 @@ module.exports = {
277
239
  async register(ctx) {
278
240
  const pluginStore = await strapi.store({ type: 'plugin', name: 'users-permissions' });
279
241
 
280
- const settings = await pluginStore.get({
281
- key: 'advanced',
282
- });
242
+ const settings = await pluginStore.get({ key: 'advanced' });
283
243
 
284
244
  if (!settings.allow_register) {
285
245
  throw new ApplicationError('Register action is currently disabled');
286
246
  }
287
247
 
288
248
  const params = {
289
- ..._.omit(ctx.request.body, ['confirmed', 'confirmationToken', 'resetPasswordToken']),
249
+ ..._.omit(ctx.request.body, [
250
+ 'confirmed',
251
+ 'blocked',
252
+ 'confirmationToken',
253
+ 'resetPasswordToken',
254
+ 'provider',
255
+ ]),
290
256
  provider: 'local',
291
257
  };
292
258
 
293
259
  await validateRegisterBody(params);
294
260
 
295
- // Throw an error if the password selected by the user
296
- // contains more than three times the symbol '$'.
297
- if (getService('user').isHashed(params.password)) {
298
- throw new ValidationError(
299
- 'Your password cannot contain more than three times the symbol `$`'
300
- );
301
- }
302
-
303
261
  const role = await strapi
304
262
  .query('plugin::users-permissions.role')
305
263
  .findOne({ where: { type: settings.default_role } });
@@ -308,80 +266,75 @@ module.exports = {
308
266
  throw new ApplicationError('Impossible to find the default role');
309
267
  }
310
268
 
311
- // Check if the provided email is valid or not.
312
- const isEmail = emailRegExp.test(params.email);
313
-
314
- if (isEmail) {
315
- params.email = params.email.toLowerCase();
316
- } else {
317
- throw new ValidationError('Please provide a valid email address');
318
- }
269
+ const { email, username, provider } = params;
319
270
 
320
- params.role = role.id;
271
+ const identifierFilter = {
272
+ $or: [
273
+ { email: email.toLowerCase() },
274
+ { username: email.toLowerCase() },
275
+ { username },
276
+ { email: username },
277
+ ],
278
+ };
321
279
 
322
- const user = await strapi.query('plugin::users-permissions.user').findOne({
323
- where: { email: params.email },
280
+ const conflictingUserCount = await strapi.query('plugin::users-permissions.user').count({
281
+ where: { ...identifierFilter, provider },
324
282
  });
325
283
 
326
- if (user && user.provider === params.provider) {
327
- throw new ApplicationError('Email is already taken');
284
+ if (conflictingUserCount > 0) {
285
+ throw new ApplicationError('Email or Username are already taken');
328
286
  }
329
287
 
330
- if (user && user.provider !== params.provider && settings.unique_email) {
331
- throw new ApplicationError('Email is already taken');
332
- }
288
+ if (settings.unique_email) {
289
+ const conflictingUserCount = await strapi.query('plugin::users-permissions.user').count({
290
+ where: { ...identifierFilter },
291
+ });
333
292
 
334
- try {
335
- if (!settings.email_confirmation) {
336
- params.confirmed = true;
293
+ if (conflictingUserCount > 0) {
294
+ throw new ApplicationError('Email or Username are already taken');
337
295
  }
296
+ }
338
297
 
339
- const user = await getService('user').add(params);
298
+ let newUser = {
299
+ ...params,
300
+ role: role.id,
301
+ email: email.toLowerCase(),
302
+ username,
303
+ confirmed: !settings.email_confirmation,
304
+ };
340
305
 
341
- const sanitizedUser = await sanitizeUser(user, ctx);
306
+ const user = await getService('user').add(newUser);
342
307
 
343
- if (settings.email_confirmation) {
344
- try {
345
- await getService('user').sendConfirmationEmail(sanitizedUser);
346
- } catch (err) {
347
- throw new ApplicationError(err.message);
348
- }
308
+ const sanitizedUser = await sanitizeUser(user, ctx);
349
309
 
350
- return ctx.send({ user: sanitizedUser });
310
+ if (settings.email_confirmation) {
311
+ try {
312
+ await getService('user').sendConfirmationEmail(sanitizedUser);
313
+ } catch (err) {
314
+ throw new ApplicationError(err.message);
351
315
  }
352
316
 
353
- const jwt = getService('jwt').issue(_.pick(user, ['id']));
354
-
355
- return ctx.send({
356
- jwt,
357
- user: sanitizedUser,
358
- });
359
- } catch (err) {
360
- if (_.includes(err.message, 'username')) {
361
- throw new ApplicationError('Username already taken');
362
- } else if (_.includes(err.message, 'email')) {
363
- throw new ApplicationError('Email already taken');
364
- } else {
365
- strapi.log.error(err);
366
- throw new ApplicationError('An error occurred during account creation');
367
- }
317
+ return ctx.send({ user: sanitizedUser });
368
318
  }
319
+
320
+ const jwt = getService('jwt').issue(_.pick(user, ['id']));
321
+
322
+ return ctx.send({
323
+ jwt,
324
+ user: sanitizedUser,
325
+ });
369
326
  },
370
327
 
371
328
  async emailConfirmation(ctx, next, returnUser) {
372
- const { confirmation: confirmationToken } = ctx.query;
329
+ const { confirmation: confirmationToken } = await validateEmailConfirmationBody(ctx.query);
373
330
 
374
331
  const userService = getService('user');
375
332
  const jwtService = getService('jwt');
376
333
 
377
- if (_.isEmpty(confirmationToken)) {
378
- throw new ValidationError('token.invalid');
379
- }
380
-
381
334
  const [user] = await userService.fetchAll({ filters: { confirmationToken } });
382
335
 
383
336
  if (!user) {
384
- throw new ValidationError('token.invalid');
337
+ throw new ValidationError('Invalid token');
385
338
  }
386
339
 
387
340
  await userService.edit(user.id, { confirmed: true, confirmationToken: null });
@@ -401,45 +354,29 @@ module.exports = {
401
354
  },
402
355
 
403
356
  async sendEmailConfirmation(ctx) {
404
- const params = _.assign(ctx.request.body);
405
-
406
- await validateSendEmailConfirmationBody(params);
407
-
408
- const isEmail = emailRegExp.test(params.email);
409
-
410
- if (isEmail) {
411
- params.email = params.email.toLowerCase();
412
- } else {
413
- throw new ValidationError('wrong.email');
414
- }
357
+ const { email } = await validateSendEmailConfirmationBody(ctx.request.body);
415
358
 
416
359
  const user = await strapi.query('plugin::users-permissions.user').findOne({
417
- where: { email: params.email },
360
+ where: { email: email.toLowerCase() },
418
361
  });
419
362
 
420
363
  if (!user) {
421
- return ctx.send({
422
- email: params.email,
423
- sent: true,
424
- });
364
+ return ctx.send({ email, sent: true });
425
365
  }
426
366
 
427
367
  if (user.confirmed) {
428
- throw new ApplicationError('already.confirmed');
368
+ throw new ApplicationError('Already confirmed');
429
369
  }
430
370
 
431
371
  if (user.blocked) {
432
- throw new ApplicationError('blocked.user');
372
+ throw new ApplicationError('User blocked');
433
373
  }
434
374
 
435
- try {
436
- await getService('user').sendConfirmationEmail(user);
437
- ctx.send({
438
- email: user.email,
439
- sent: true,
440
- });
441
- } catch (err) {
442
- throw new ApplicationError(err.message);
443
- }
375
+ await getService('user').sendConfirmationEmail(user);
376
+
377
+ ctx.send({
378
+ email: user.email,
379
+ sent: true,
380
+ });
444
381
  },
445
382
  };
@@ -55,11 +55,10 @@ module.exports = {
55
55
 
56
56
  const user = {
57
57
  ...ctx.request.body,
58
+ email: email.toLowerCase(),
58
59
  provider: 'local',
59
60
  };
60
61
 
61
- user.email = _.toLower(user.email);
62
-
63
62
  if (!role) {
64
63
  const defaultRole = await strapi
65
64
  .query('plugin::users-permissions.role')
@@ -185,12 +184,15 @@ module.exports = {
185
184
  * @return {Object|Array}
186
185
  */
187
186
  async me(ctx) {
188
- const user = ctx.state.user;
187
+ const authUser = ctx.state.user;
188
+ const { query } = ctx;
189
189
 
190
- if (!user) {
190
+ if (!authUser) {
191
191
  return ctx.unauthorized();
192
192
  }
193
193
 
194
+ const user = await getService('user').fetch(authUser.id, query);
195
+
194
196
  ctx.body = await sanitizeOutput(user, ctx);
195
197
  },
196
198
  };
@@ -2,28 +2,53 @@
2
2
 
3
3
  const { yup, validateYupSchema } = require('@strapi/utils');
4
4
 
5
- const callbackBodySchema = yup.object().shape({
5
+ const callbackSchema = yup.object({
6
6
  identifier: yup.string().required(),
7
7
  password: yup.string().required(),
8
8
  });
9
9
 
10
- const registerBodySchema = yup.object().shape({
10
+ const registerSchema = yup.object({
11
11
  email: yup
12
12
  .string()
13
13
  .email()
14
14
  .required(),
15
+ username: yup.string().required(),
15
16
  password: yup.string().required(),
16
17
  });
17
18
 
18
- const sendEmailConfirmationBodySchema = yup.object().shape({
19
+ const sendEmailConfirmationSchema = yup.object({
19
20
  email: yup
20
21
  .string()
21
22
  .email()
22
23
  .required(),
23
24
  });
24
25
 
26
+ const validateEmailConfirmationSchema = yup.object({
27
+ confirmation: yup.string().required(),
28
+ });
29
+
30
+ const forgotPasswordSchema = yup
31
+ .object({
32
+ email: yup
33
+ .string()
34
+ .email()
35
+ .required(),
36
+ })
37
+ .noUnknown();
38
+
39
+ const resetPasswordSchema = yup
40
+ .object({
41
+ password: yup.string().required(),
42
+ passwordConfirmation: yup.string().required(),
43
+ code: yup.string().required(),
44
+ })
45
+ .noUnknown();
46
+
25
47
  module.exports = {
26
- validateCallbackBody: validateYupSchema(callbackBodySchema),
27
- validateRegisterBody: validateYupSchema(registerBodySchema),
28
- validateSendEmailConfirmationBody: validateYupSchema(sendEmailConfirmationBodySchema),
48
+ validateCallbackBody: validateYupSchema(callbackSchema),
49
+ validateRegisterBody: validateYupSchema(registerSchema),
50
+ validateSendEmailConfirmationBody: validateYupSchema(sendEmailConfirmationSchema),
51
+ validateEmailConfirmationBody: validateYupSchema(validateEmailConfirmationSchema),
52
+ validateForgotPasswordBody: validateYupSchema(forgotPasswordSchema),
53
+ validateResetPasswordBody: validateYupSchema(resetPasswordSchema),
29
54
  };