Add register route

Signed-off-by: Alexandre Bodin <bodin.alex@gmail.com>
This commit is contained in:
Alexandre Bodin 2020-05-18 17:16:49 +02:00
parent 01854f431f
commit a841400f85
7 changed files with 387 additions and 18 deletions

View File

@ -69,6 +69,11 @@
"path": "/registration-info",
"handler": "authentication.registrationInfo"
},
{
"method": "POST",
"path": "/register",
"handler": "authentication.register"
},
{
"method": "POST",
"path": "/auth/local/register",

View File

@ -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),
},
};
},
};

View File

@ -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 }));
});
});
});

View File

@ -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<user>}
*/
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,
};

View File

@ -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);
});
});
});

View File

@ -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,
};

View File

@ -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);