Add /users/me routes

Signed-off-by: Alexandre Bodin <bodin.alex@gmail.com>
This commit is contained in:
Alexandre Bodin 2020-05-18 19:54:43 +02:00
parent cf4c70b01e
commit 851ba201ac
9 changed files with 169 additions and 103 deletions

View File

@ -148,18 +148,12 @@
{
"method": "GET",
"path": "/users/me",
"handler": "User.getAuthenticatedUser",
"config": {
"policies": []
}
"handler": "authenticated-user.getMe"
},
{
"method": "PUT",
"path": "/users/me",
"handler": "User.updateAuthenticatedUser",
"config": {
"policies": []
}
"handler": "authenticated-user.updateMe"
},
{
"method": "POST",

View File

@ -0,0 +1,37 @@
'use strict';
const { validateProfileUpdateInput } = require('../validation/user');
module.exports = {
async getMe(ctx) {
if (!ctx.state.user || !ctx.state.isAuthenticatedAdmin) {
return ctx.forbidden();
}
const userInfo = strapi.admin.services.user.sanitizeUser(ctx.state.user);
ctx.body = {
data: userInfo,
};
},
async updateMe(ctx) {
const input = ctx.request.body;
if (!ctx.state.user || !ctx.state.isAuthenticatedAdmin) {
return ctx.forbidden();
}
try {
await validateProfileUpdateInput(input);
} catch (err) {
return ctx.badRequest('ValidationError', err);
}
const updatedUser = strapi.admin.services.user.update({ id: ctx.state.user.id }, input);
ctx.body = {
data: strapi.admin.services.user.sanitizeUser(updatedUser),
};
},
};

View File

@ -43,28 +43,4 @@ module.exports = {
},
};
},
async getAuthenticatedUser(ctx) {
ctx.body = {
data: {
email: '',
firstname: '',
lastname: '',
username: '',
roles: [],
},
};
},
async updateAuthenticatedUser(ctx) {
ctx.body = {
data: {
email: '',
firstname: '',
lastname: '',
username: '',
roles: [],
},
};
},
};

View File

@ -44,6 +44,7 @@ module.exports = strapi => ({
ctx.state.admin = admin;
ctx.state.user = admin;
ctx.state.isAuthenticatedAdmin = true;
return next();
}
}

View File

@ -79,6 +79,36 @@ describe('User', () => {
});
describe('update', () => {
test('Hash password', async () => {
const hash = 'aoizdnoaizndoainzodiaz';
const params = { id: 1 };
const input = { email: 'test@strapi.io', password: '123' };
const update = jest.fn((_, user) => Promise.resolve(user));
const hashPassword = jest.fn(() => Promise.resolve(hash));
global.strapi = {
query() {
return { update };
},
admin: {
services: {
auth: { hashPassword },
},
},
};
const result = await userService.update(params, input);
expect(hashPassword).toHaveBeenCalledWith(input.password);
expect(update).toHaveBeenCalledWith(params, { email: input.email, password: hash });
expect(result).toEqual({
email: 'test@strapi.io',
password: 'aoizdnoaizndoainzodiaz',
});
});
test('Forwards call to the query layer', async () => {
const user = {
email: 'test@strapi.io',
@ -259,10 +289,9 @@ describe('User', () => {
expect(userService.register(input)).rejects.toThrowError('Invalid registration info');
});
test('Create a password hash', async () => {
test('Calls udpate service', 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() {
@ -273,7 +302,6 @@ describe('User', () => {
admin: {
services: {
user: { update },
auth: { hashPassword },
},
},
};
@ -289,54 +317,15 @@ describe('User', () => {
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' })
expect.objectContaining({ firstname: 'test', lastname: 'Strapi', password: 'Test1234' })
);
});
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() {
@ -347,7 +336,6 @@ describe('User', () => {
admin: {
services: {
user: { update },
auth: { hashPassword },
},
},
};
@ -363,7 +351,41 @@ describe('User', () => {
await userService.register(input);
expect(hashPassword).toHaveBeenCalledWith('Test1234');
expect(update).toHaveBeenCalledWith({ id: 1 }, expect.objectContaining({ isActive: true }));
});
test('Reset registrationToken', async () => {
const findOne = jest.fn(() => Promise.resolve({ id: 1 }));
const update = jest.fn(user => Promise.resolve(user));
global.strapi = {
query() {
return {
findOne,
};
},
admin: {
services: {
user: { update },
},
},
};
const input = {
registrationToken: '123',
userInfo: {
firstname: 'test',
lastname: 'Strapi',
password: 'Test1234',
},
};
await userService.register(input);
expect(update).toHaveBeenCalledWith(
{ id: 1 },
expect.objectContaining({ registrationToken: null })
);
});
});
});

View File

@ -32,6 +32,16 @@ const create = async attributes => {
* @returns {Promise<user>}
*/
const update = async (params, attributes) => {
// hash password if a new one is sent
if (_.has(attributes, 'password')) {
const hashedPassword = await strapi.admin.services.auth.hashPassword(attributes.password);
return strapi.query('user', 'admin').update(params, {
...attributes,
password: hashedPassword,
});
}
return strapi.query('user', 'admin').update(params, attributes);
};
@ -72,12 +82,10 @@ const register = async ({ registrationToken, userInfo }) => {
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,
password: userInfo.password,
firstname: userInfo.firstname,
lastname: userInfo.lastname,
registrationToken: null,

View File

@ -1,6 +1,7 @@
'use strict';
const { yup, formatYupErrors } = require('strapi-utils');
const validators = require('./common-validators');
const registrationSchema = yup
.object()
@ -9,21 +10,9 @@ const registrationSchema = yup
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(),
firstname: validators.firstname,
lastname: validators.lastname,
password: validators.password,
})
.required()
.noUnknown(),

View File

@ -0,0 +1,23 @@
'use strict';
const { yup } = require('strapi-utils');
const validators = {
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(),
};
module.exports = validators;

View File

@ -1,6 +1,7 @@
'use strict';
const { yup, formatYupErrors } = require('strapi-utils');
const validators = require('./common-validators');
const handleReject = error => Promise.reject(formatYupErrors(error));
@ -11,14 +12,8 @@ const userCreationSchema = yup
.string()
.email()
.required(),
firstname: yup
.string()
.min(1)
.required(),
lastname: yup
.string()
.min(1)
.required(),
firstname: validators.firstname,
lastname: validators.lastname,
roles: yup.array(), // FIXME: set min to 1 once the create role API is created,
})
.noUnknown();
@ -27,6 +22,27 @@ const validateUserCreationInput = data => {
return userCreationSchema.validate(data, { strict: true, abortEarly: false }).catch(handleReject);
};
const profileUpdateSchema = yup
.object()
.shape({
email: yup
.string()
.email()
.required(),
firstname: validators.firstname,
lastname: validators.lastname,
username: yup.string().min(1),
password: validators.password,
})
.noUnknown();
const validateProfileUpdateInput = data => {
return profileUpdateSchema
.validate(data, { strict: true, abortEarly: false })
.catch(handleReject);
};
module.exports = {
validateUserCreationInput,
validateProfileUpdateInput,
};