From a841400f851684e294dc660e7f35a21dba22dda5 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Mon, 18 May 2020 17:16:49 +0200 Subject: [PATCH] Add register route Signed-off-by: Alexandre Bodin --- packages/strapi-admin/config/routes.json | 5 + .../controllers/authentication.js | 21 +++ .../services/__tests__/user.test.js | 159 ++++++++++++++++++ packages/strapi-admin/services/user.js | 39 +++++ .../strapi-admin/test/admin-auth.test.e2e.js | 101 +++++++++++ .../strapi-admin/validation/authentication.js | 41 +++++ packages/strapi-admin/validation/user.js | 39 +++-- 7 files changed, 387 insertions(+), 18 deletions(-) create mode 100644 packages/strapi-admin/validation/authentication.js diff --git a/packages/strapi-admin/config/routes.json b/packages/strapi-admin/config/routes.json index 2d214114d2..8873377d89 100644 --- a/packages/strapi-admin/config/routes.json +++ b/packages/strapi-admin/config/routes.json @@ -69,6 +69,11 @@ "path": "/registration-info", "handler": "authentication.registrationInfo" }, + { + "method": "POST", + "path": "/register", + "handler": "authentication.register" + }, { "method": "POST", "path": "/auth/local/register", diff --git a/packages/strapi-admin/controllers/authentication.js b/packages/strapi-admin/controllers/authentication.js index 15f3859cef..f65cca79d3 100644 --- a/packages/strapi-admin/controllers/authentication.js +++ b/packages/strapi-admin/controllers/authentication.js @@ -3,6 +3,8 @@ const passport = require('koa-passport'); const compose = require('koa-compose'); +const { validateRegistrationInput } = require('../validation/authentication'); + module.exports = { login: compose([ (ctx, next) => { @@ -68,4 +70,23 @@ module.exports = { ctx.body = { data: registrationInfo }; }, + + async register(ctx) { + const input = ctx.request.body; + + try { + await validateRegistrationInput(input); + } catch (err) { + return ctx.badRequest('ValidationError', err); + } + + const user = await strapi.admin.services.user.register(input); + + ctx.body = { + data: { + token: strapi.admin.services.token.createJwtToken(user), + user: strapi.admin.services.user.sanitizeUser(user), + }, + }; + }, }; diff --git a/packages/strapi-admin/services/__tests__/user.test.js b/packages/strapi-admin/services/__tests__/user.test.js index 03c7fa4980..50aaa48041 100644 --- a/packages/strapi-admin/services/__tests__/user.test.js +++ b/packages/strapi-admin/services/__tests__/user.test.js @@ -78,6 +78,27 @@ describe('User', () => { }); }); + describe('update', () => { + test('Forwards call to the query layer', async () => { + const user = { + email: 'test@strapi.io', + }; + const update = jest.fn(() => Promise.resolve(user)); + + global.strapi = { + query() { + return { update }; + }, + }; + const params = { id: 1 }; + const input = { email: 'test@strapi.io' }; + const result = await userService.update(params, input); + + expect(update).toHaveBeenCalledWith(params, input); + expect(result).toBe(user); + }); + }); + describe('exists', () => { test('Return true if the user already exists', async () => { const count = jest.fn(() => Promise.resolve(1)); @@ -148,4 +169,142 @@ describe('User', () => { }); }); }); + + describe('register', () => { + test('Fails if no matching user is found', async () => { + const findOne = jest.fn(() => Promise.resolve(undefined)); + + global.strapi = { + query() { + return { + findOne, + }; + }, + errors: { + badRequest(msg) { + throw new Error(msg); + }, + }, + }; + + const input = { + registrationToken: '123', + userInfo: { + firstname: 'test', + lastname: 'Strapi', + password: 'Test1234', + }, + }; + + expect(userService.register(input)).rejects.toThrowError('Invalid registration info'); + }); + + test('Create a password hash', async () => { + const findOne = jest.fn(() => Promise.resolve({ id: 1 })); + const update = jest.fn(user => Promise.resolve(user)); + const hashPassword = jest.fn(() => Promise.resolve('123456789')); + + global.strapi = { + query() { + return { + findOne, + }; + }, + admin: { + services: { + user: { update }, + auth: { hashPassword }, + }, + }, + }; + + const input = { + registrationToken: '123', + userInfo: { + firstname: 'test', + lastname: 'Strapi', + password: 'Test1234', + }, + }; + + await userService.register(input); + + expect(hashPassword).toHaveBeenCalledWith('Test1234'); + expect(update).toHaveBeenCalledWith( + { id: 1 }, + expect.objectContaining({ password: '123456789' }) + ); + }); + + test('Set user firstname and lastname', async () => { + const findOne = jest.fn(() => Promise.resolve({ id: 1 })); + const update = jest.fn(user => Promise.resolve(user)); + const hashPassword = jest.fn(() => Promise.resolve('123456789')); + + global.strapi = { + query() { + return { + findOne, + }; + }, + admin: { + services: { + user: { update }, + auth: { hashPassword }, + }, + }, + }; + + const input = { + registrationToken: '123', + userInfo: { + firstname: 'test', + lastname: 'Strapi', + password: 'Test1234', + }, + }; + + await userService.register(input); + + expect(hashPassword).toHaveBeenCalledWith('Test1234'); + expect(update).toHaveBeenCalledWith( + { id: 1 }, + expect.objectContaining({ firstname: 'test', lastname: 'Strapi' }) + ); + }); + + test('Set user to active', async () => { + const findOne = jest.fn(() => Promise.resolve({ id: 1 })); + const update = jest.fn(user => Promise.resolve(user)); + const hashPassword = jest.fn(() => Promise.resolve('123456789')); + + global.strapi = { + query() { + return { + findOne, + }; + }, + admin: { + services: { + user: { update }, + auth: { hashPassword }, + }, + }, + }; + + const input = { + registrationToken: '123', + userInfo: { + firstname: 'test', + lastname: 'Strapi', + password: 'Test1234', + }, + }; + + await userService.register(input); + + expect(hashPassword).toHaveBeenCalledWith('Test1234'); + expect(update).toHaveBeenCalledWith({ id: 1 }, expect.objectContaining({ isActive: true })); + }); + }); }); diff --git a/packages/strapi-admin/services/user.js b/packages/strapi-admin/services/user.js index 1e5e01918e..301fec8571 100644 --- a/packages/strapi-admin/services/user.js +++ b/packages/strapi-admin/services/user.js @@ -25,6 +25,16 @@ const create = async attributes => { return strapi.query('user', 'admin').create(user); }; +/** + * Update a user in database + * @param params query params to find the user to update + * @param attributes A partial user object + * @returns {Promise} + */ +const update = async (params, attributes) => { + return strapi.query('user', 'admin').update(params, attributes); +}; + /** * Check if a user with specific attributes exists in the database * @param attributes A partial user object @@ -49,9 +59,38 @@ const findRegistrationInfo = async registrationToken => { return _.pick(user, ['email', 'firstname', 'lastname']); }; +/** + * Registers a user based on a registrationToken and some informations to update + * @param {Object} params + * @param {Object} params.registrationInfo registration token + * @param {Object} params.userInfo user info + */ +const register = async ({ registrationToken, userInfo }) => { + const matchingUser = await strapi.query('user', 'admin').findOne({ registrationToken }); + + if (!matchingUser) { + throw strapi.errors.badRequest('Invalid registration info'); + } + + const hashedPassword = await strapi.admin.services.auth.hashPassword(userInfo.password); + + return strapi.admin.services.user.update( + { id: matchingUser.id }, + { + password: hashedPassword, + firstname: userInfo.firstname, + lastname: userInfo.lastname, + registrationToken: null, + isActive: true, + } + ); +}; + module.exports = { sanitizeUser, create, + update, exists, findRegistrationInfo, + register, }; diff --git a/packages/strapi-admin/test/admin-auth.test.e2e.js b/packages/strapi-admin/test/admin-auth.test.e2e.js index eb786cd9c3..aabdc375f8 100644 --- a/packages/strapi-admin/test/admin-auth.test.e2e.js +++ b/packages/strapi-admin/test/admin-auth.test.e2e.js @@ -234,4 +234,105 @@ describe('Admin Auth End to End', () => { }); }); }); + + describe('GET /register', () => { + test('Fails on missing payload', async () => { + const res = await rq({ + url: '/admin/register', + method: 'POST', + body: { + userInfo: {}, + }, + }); + + expect(res.statusCode).toBe(400); + expect(res.body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: 'ValidationError', + data: { + registrationToken: ['registrationToken is a required field'], + + 'userInfo.firstname': ['userInfo.firstname is a required field'], + 'userInfo.lastname': ['userInfo.lastname is a required field'], + 'userInfo.password': ['userInfo.password is a required field'], + }, + }); + }); + + test('Fails on invalid password', async () => { + const user = { + email: 'test1@strapi.io', // FIXME: Have to increment emails until we can delete the users after each test + firstname: 'test', + lastname: 'strapi', + }; + const createRes = await createUser(user); + + const registrationToken = createRes.body.data.registrationToken; + + const res = await rq({ + url: '/admin/register', + method: 'POST', + body: { + registrationToken, + userInfo: { + firstname: 'test', + lastname: 'Strapi', + password: '123', + }, + }, + }); + + console.log(res.body.data); + + expect(res.statusCode).toBe(400); + expect(res.body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: 'ValidationError', + data: { + 'userInfo.password': ['userInfo.password must contain at least one uppercase character'], + }, + }); + }); + + test('Registers user correctly', async () => { + const user = { + email: 'test2@strapi.io', // FIXME: Have to increment emails until we can delete the users after each test + firstname: 'test', + lastname: 'strapi', + }; + const createRes = await createUser(user); + + const registrationToken = createRes.body.data.registrationToken; + + const userInfo = { + firstname: 'test', + lastname: 'Strapi', + password: '1Test2azda3', + }; + + const res = await rq({ + url: '/admin/register', + method: 'POST', + body: { + registrationToken, + userInfo, + }, + }); + + expect(res.statusCode).toBe(200); + expect(res.body.data).toEqual({ + token: expect.any(String), + user: { + email: user.email, + firstname: expect.any(String), + lastname: expect.any(String), + password: expect.any(String), + }, + }); + + expect(res.body.data.user.password === userInfo.password).toBe(false); + }); + }); }); diff --git a/packages/strapi-admin/validation/authentication.js b/packages/strapi-admin/validation/authentication.js new file mode 100644 index 0000000000..a78795f4d3 --- /dev/null +++ b/packages/strapi-admin/validation/authentication.js @@ -0,0 +1,41 @@ +'use strict'; + +const { yup, formatYupErrors } = require('strapi-utils'); + +const registrationSchema = yup + .object() + .shape({ + registrationToken: yup.string().required(), + userInfo: yup + .object() + .shape({ + firstname: yup + .string() + .min(1) + .required(), + lastname: yup + .string() + .min(1) + .required(), + password: yup + .string() + .min(8) + .matches(/[a-z]/, '${path} must contain at least one lowercase character') + .matches(/[A-Z]/, '${path} must contain at least one uppercase character') + .matches(/\d/, '${path} must contain at least one number') + .required(), + }) + .required() + .noUnknown(), + }) + .noUnknown(); + +const validateRegistrationInput = data => { + return registrationSchema + .validate(data, { strict: true, abortEarly: false }) + .catch(error => Promise.reject(formatYupErrors(error))); +}; + +module.exports = { + validateRegistrationInput, +}; diff --git a/packages/strapi-admin/validation/user.js b/packages/strapi-admin/validation/user.js index 7370e1c4fa..162587c4b6 100644 --- a/packages/strapi-admin/validation/user.js +++ b/packages/strapi-admin/validation/user.js @@ -4,24 +4,27 @@ const { yup, formatYupErrors } = require('strapi-utils'); const handleReject = error => Promise.reject(formatYupErrors(error)); -const userCreationSchema = yup.object().shape({ - email: yup - .string() - .email() - .required(), - firstname: yup - .string() - .min(1) - .required(), - lastname: yup - .string() - .min(1) - .required(), - roles: yup - .array() - .min(1) - .required(), -}); +const userCreationSchema = yup + .object() + .shape({ + email: yup + .string() + .email() + .required(), + firstname: yup + .string() + .min(1) + .required(), + lastname: yup + .string() + .min(1) + .required(), + roles: yup + .array() + .min(1) + .required(), + }) + .noUnknown(); const validateUserCreationInput = data => { return userCreationSchema.validate(data, { strict: true, abortEarly: false }).catch(handleReject);