mirror of
https://github.com/strapi/strapi.git
synced 2025-12-28 15:44:59 +00:00
Merge pull request #12890 from WalkingPizza/feature/user-permissions-change-password
Allow users to change own password
This commit is contained in:
commit
6c68176efc
@ -185,6 +185,41 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/auth/change-password:
|
||||
post:
|
||||
tags:
|
||||
- Users-Permissions - Auth
|
||||
summary: Update user's own password
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
password:
|
||||
required: true
|
||||
type: string
|
||||
currentPassword:
|
||||
required: true
|
||||
type: string
|
||||
passwordConfirmation:
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Returns a jwt token and user info
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Users-Permissions-UserRegistration'
|
||||
default:
|
||||
description: Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/auth/email-confirmation:
|
||||
get:
|
||||
tags:
|
||||
|
||||
@ -18,6 +18,7 @@ const {
|
||||
validateForgotPasswordBody,
|
||||
validateResetPasswordBody,
|
||||
validateEmailConfirmationBody,
|
||||
validateChangePasswordBody,
|
||||
} = require('./validation/auth');
|
||||
|
||||
const { getAbsoluteAdminUrl, getAbsoluteServerUrl, sanitize } = utils;
|
||||
@ -90,18 +91,36 @@ module.exports = {
|
||||
user: await sanitizeUser(user, ctx),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Connect the user with the third-party provider.
|
||||
try {
|
||||
const user = await getService('providers').connect(provider, ctx.query);
|
||||
|
||||
return ctx.send({
|
||||
jwt: getService('jwt').issue({ id: user.id }),
|
||||
user: await sanitizeUser(user, ctx),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new ApplicationError(error.message);
|
||||
async changePassword(ctx) {
|
||||
if (!ctx.state.user) {
|
||||
throw new ApplicationError('You must be authenticated to reset your password');
|
||||
}
|
||||
|
||||
const { currentPassword, password } = await validateChangePasswordBody(ctx.request.body);
|
||||
|
||||
const user = await strapi.entityService.findOne(
|
||||
'plugin::users-permissions.user',
|
||||
ctx.state.user.id
|
||||
);
|
||||
|
||||
const validPassword = await getService('user').validatePassword(currentPassword, user.password);
|
||||
|
||||
if (!validPassword) {
|
||||
throw new ValidationError('The provided current password is invalid');
|
||||
}
|
||||
|
||||
if (currentPassword === password) {
|
||||
throw new ValidationError('Your new password must be different than your current password');
|
||||
}
|
||||
|
||||
await getService('user').edit(user.id, { password });
|
||||
|
||||
ctx.send({
|
||||
jwt: getService('jwt').issue({ id: user.id }),
|
||||
user: await sanitizeUser(user, ctx),
|
||||
});
|
||||
},
|
||||
|
||||
async resetPassword(ctx) {
|
||||
|
||||
@ -44,6 +44,17 @@ const resetPasswordSchema = yup
|
||||
})
|
||||
.noUnknown();
|
||||
|
||||
const changePasswordSchema = yup
|
||||
.object({
|
||||
password: yup.string().required(),
|
||||
passwordConfirmation: yup
|
||||
.string()
|
||||
.required()
|
||||
.oneOf([yup.ref('password')], 'Passwords do not match'),
|
||||
currentPassword: yup.string().required(),
|
||||
})
|
||||
.noUnknown();
|
||||
|
||||
module.exports = {
|
||||
validateCallbackBody: validateYupSchema(callbackSchema),
|
||||
validateRegisterBody: validateYupSchema(registerSchema),
|
||||
@ -51,4 +62,5 @@ module.exports = {
|
||||
validateEmailConfirmationBody: validateYupSchema(validateEmailConfirmationSchema),
|
||||
validateForgotPasswordBody: validateYupSchema(forgotPasswordSchema),
|
||||
validateResetPasswordBody: validateYupSchema(resetPasswordSchema),
|
||||
validateChangePasswordBody: validateYupSchema(changePasswordSchema),
|
||||
};
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const { toPlainObject } = require('lodash/fp');
|
||||
|
||||
const { checkBadRequest } = require('../../utils');
|
||||
|
||||
module.exports = ({ nexus, strapi }) => {
|
||||
const { nonNull } = nexus;
|
||||
|
||||
return {
|
||||
type: 'UsersPermissionsLoginPayload',
|
||||
|
||||
args: {
|
||||
currentPassword: nonNull('String'),
|
||||
password: nonNull('String'),
|
||||
passwordConfirmation: nonNull('String'),
|
||||
},
|
||||
|
||||
description: 'Change user password. Confirm with the current password.',
|
||||
|
||||
async resolve(parent, args, context) {
|
||||
const { koaContext } = context;
|
||||
|
||||
koaContext.request.body = toPlainObject(args);
|
||||
|
||||
await strapi
|
||||
.plugin('users-permissions')
|
||||
.controller('auth')
|
||||
.changePassword(koaContext);
|
||||
|
||||
const output = koaContext.body;
|
||||
|
||||
checkBadRequest(output);
|
||||
|
||||
return {
|
||||
user: output.user || output,
|
||||
jwt: output.jwt,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -25,6 +25,7 @@ module.exports = context => {
|
||||
register: require('./auth/register'),
|
||||
forgotPassword: require('./auth/forgot-password'),
|
||||
resetPassword: require('./auth/reset-password'),
|
||||
changePassword: require('./auth/change-password'),
|
||||
emailConfirmation: require('./auth/email-confirmation'),
|
||||
};
|
||||
|
||||
|
||||
@ -23,6 +23,11 @@ module.exports = ({ strapi }) => {
|
||||
'Mutation.forgotPassword': { auth: false },
|
||||
'Mutation.resetPassword': { auth: false },
|
||||
'Mutation.emailConfirmation': { auth: false },
|
||||
'Mutation.changePassword': {
|
||||
auth: {
|
||||
scope: 'plugin::users-permissions.auth.changePassword',
|
||||
},
|
||||
},
|
||||
|
||||
// Scoped auth for replaced CRUD operations
|
||||
// Role
|
||||
|
||||
@ -70,4 +70,13 @@ module.exports = [
|
||||
prefix: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/auth/change-password',
|
||||
handler: 'auth.changePassword',
|
||||
config: {
|
||||
middlewares: ['plugin::users-permissions.rateLimit'],
|
||||
prefix: '',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -87,13 +87,6 @@ module.exports = ({ strapi }) => ({
|
||||
async remove(params) {
|
||||
return strapi.query('plugin::users-permissions.user').delete({ where: params });
|
||||
},
|
||||
isHashed(password) {
|
||||
if (typeof password !== 'string' || !password) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return password.split('$').length === 4;
|
||||
},
|
||||
|
||||
validatePassword(password, hash) {
|
||||
return bcrypt.compare(password, hash);
|
||||
|
||||
@ -15,6 +15,7 @@ const DEFAULT_PERMISSIONS = [
|
||||
{ action: 'plugin::users-permissions.auth.emailConfirmation', roleType: 'public' },
|
||||
{ action: 'plugin::users-permissions.auth.sendEmailConfirmation', roleType: 'public' },
|
||||
{ action: 'plugin::users-permissions.user.me', roleType: 'authenticated' },
|
||||
{ action: 'plugin::users-permissions.auth.changePassword', roleType: 'authenticated' },
|
||||
];
|
||||
|
||||
const transformRoutePrefixFor = pluginName => route => {
|
||||
|
||||
@ -0,0 +1,140 @@
|
||||
'use strict';
|
||||
|
||||
const { createStrapiInstance } = require('../../../../../test/helpers/strapi');
|
||||
const { createRequest } = require('../../../../../test/helpers/request');
|
||||
const { createAuthenticatedUser } = require('../utils');
|
||||
|
||||
let strapi;
|
||||
let rq;
|
||||
|
||||
const internals = {
|
||||
user: {
|
||||
username: 'test',
|
||||
email: 'test@strapi.io',
|
||||
password: 'Test1234',
|
||||
confirmed: true,
|
||||
provider: 'local',
|
||||
},
|
||||
newPassword: 'Test12345',
|
||||
};
|
||||
|
||||
const data = {};
|
||||
|
||||
describe('Auth API', () => {
|
||||
beforeAll(async () => {
|
||||
strapi = await createStrapiInstance({ bypassAuth: false });
|
||||
|
||||
const { jwt, user } = await createAuthenticatedUser({ strapi, userInfo: internals.user });
|
||||
|
||||
data.user = user;
|
||||
|
||||
rq = createRequest({ strapi })
|
||||
.setURLPrefix('/api/auth')
|
||||
.setToken(jwt);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await strapi.destroy();
|
||||
});
|
||||
|
||||
describe('Change Password', () => {
|
||||
test('Fails on unauthenticated request', async () => {
|
||||
const nonAuthRequest = createRequest({ strapi });
|
||||
|
||||
const res = await nonAuthRequest({
|
||||
method: 'POST',
|
||||
url: '/api/auth/change-password',
|
||||
body: {},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
test('Fails on invalid confirmation password', async () => {
|
||||
const res = await rq({
|
||||
method: 'POST',
|
||||
url: '/change-password',
|
||||
body: {
|
||||
password: 'newPassword',
|
||||
passwordConfirmation: 'somethingElse',
|
||||
currentPassword: internals.user.password,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(res.body.error.name).toBe('ValidationError');
|
||||
expect(res.body.error.message).toBe('Passwords do not match');
|
||||
});
|
||||
|
||||
test('Fails on invalid current password', async () => {
|
||||
const res = await rq({
|
||||
method: 'POST',
|
||||
url: '/change-password',
|
||||
body: {
|
||||
password: 'newPassword',
|
||||
passwordConfirmation: 'newPassword',
|
||||
currentPassword: 'badPassword',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(res.body.error.name).toBe('ValidationError');
|
||||
expect(res.body.error.message).toBe('The provided current password is invalid');
|
||||
});
|
||||
|
||||
test('Fails when current and new password are the same', async () => {
|
||||
const res = await rq({
|
||||
method: 'POST',
|
||||
url: '/change-password',
|
||||
body: {
|
||||
password: internals.user.password,
|
||||
passwordConfirmation: internals.user.password,
|
||||
currentPassword: internals.user.password,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(res.body.error.name).toBe('ValidationError');
|
||||
expect(res.body.error.message).toBe(
|
||||
'Your new password must be different than your current password'
|
||||
);
|
||||
});
|
||||
|
||||
test('Returns user info and jwt token on success', async () => {
|
||||
const res = await rq({
|
||||
method: 'POST',
|
||||
url: '/change-password',
|
||||
body: {
|
||||
password: internals.newPassword,
|
||||
passwordConfirmation: internals.newPassword,
|
||||
currentPassword: internals.user.password,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
jwt: expect.any(String),
|
||||
user: {
|
||||
id: data.user.id,
|
||||
email: data.user.email,
|
||||
username: data.user.username,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Can login with new password after success', async () => {
|
||||
const rq = createRequest({ strapi }).setURLPrefix('/api/auth');
|
||||
|
||||
const res = await rq({
|
||||
method: 'POST',
|
||||
url: '/local',
|
||||
body: {
|
||||
identifier: internals.user.email,
|
||||
password: internals.newPassword,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
20
packages/plugins/users-permissions/tests/utils.js
Normal file
20
packages/plugins/users-permissions/tests/utils.js
Normal file
@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const createAuthenticatedUser = async ({ strapi, userInfo }) => {
|
||||
const defaultRole = await strapi
|
||||
.query('plugin::users-permissions.role')
|
||||
.findOne({ where: { type: 'authenticated' } });
|
||||
|
||||
const user = await strapi.service('plugin::users-permissions.user').add({
|
||||
role: defaultRole.id,
|
||||
...userInfo,
|
||||
});
|
||||
|
||||
const jwt = strapi.service('plugin::users-permissions.jwt').issue({ id: user.id });
|
||||
|
||||
return { user, jwt };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createAuthenticatedUser,
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user