Password Change Graph Support

This commit is contained in:
Wyatt Johnson
2018-04-16 12:29:37 -06:00
parent f23f962adb
commit 0fcb0be360
7 changed files with 107 additions and 21 deletions
+38
View File
@@ -9,6 +9,7 @@ const {
SET_USER_SUSPENSION_STATUS,
UPDATE_USER_ROLES,
DELETE_USER,
CHANGE_PASSWORD,
} = require('../../perms/constants');
const setUserUsernameStatus = async (ctx, id, status) => {
@@ -143,6 +144,38 @@ const delUser = async (ctx, id) => {
await user.remove();
};
const changeUserPassword = async (ctx, oldPassword, newPassword) => {
const {
user,
loaders: { Settings },
connectors: { services: { I18n } },
} = ctx;
// Verify the old password.
const validPassword = await user.verifyPassword(oldPassword);
if (!validPassword) {
throw new ErrNotAuthorized();
}
// Change the users password now.
await Users.changePassword(user.id, newPassword);
// Get some context for the email to be sent.
const { organizationName, organizationContactEmail } = await Settings.load(
'organizationName',
'organizationContactEmail'
);
// Send the password change email.
await Users.sendEmail(user, {
template: 'plain',
locals: {
body: I18n.t('email.password_change.body', organizationName),
},
subject: I18n.t('email.password_change.subject', organizationContactEmail),
});
};
module.exports = ctx => {
let mutators = {
User: {
@@ -194,6 +227,11 @@ module.exports = ctx => {
if (ctx.user.can(DELETE_USER)) {
mutators.User.del = id => delUser(ctx, id);
}
if (ctx.user.can(CHANGE_PASSWORD)) {
mutators.User.changePassword = ({ oldPassword, newPassword }) =>
changeUserPassword(ctx, oldPassword, newPassword);
}
}
return mutators;
+19
View File
@@ -1436,6 +1436,21 @@ type DelUserResponse implements Response {
errors: [UserError!]
}
input ChangePasswordInput {
# oldPassword is the previous password set on the account. An incorrect
# password here will result in an unauthorized error being thrown.
oldPassword: String!
# newPassword is the password we're changing it to.
newPassword: String!
}
type ChangePasswordResponse implements Response {
# An array of errors relating to the mutation that occurred.
errors: [UserError!]
}
# All mutations for the application are defined on this object.
type RootMutation {
@@ -1536,6 +1551,10 @@ type RootMutation {
# delUser will delete the user with the specified id.
delUser(id: ID!): DelUserResponse
# changePassword allows the current user to change their password that have an
# associated local user account.
changePassword(input: ChangePasswordInput!): ChangePasswordResponse
}
type UsernameChangedPayload {
+3
View File
@@ -210,6 +210,9 @@ en:
we_received_a_request: "We received a request to reset your password. If you did not request this change, you can ignore this email."
if_you_did: "If you did,"
please_click: "please click here to reset password"
password_change:
subject: "{0} password change"
body: "Password was changed on your account.\n\nIf you did not request this change, please contact us at {0}."
embedlink:
copy: "Copy to Clipboard"
error:
+1
View File
@@ -19,4 +19,5 @@ module.exports = {
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
DELETE_USER: 'DELETE_USER',
CHANGE_PASSWORD: 'CHANGE_PASSWORD',
};
+9
View File
@@ -1,8 +1,17 @@
const { isString } = require('lodash');
const { check } = require('../utils');
const types = require('../constants');
module.exports = (user, perm) => {
switch (perm) {
case types.CHANGE_PASSWORD:
// Only users with a local account where they have a password set can
// actually change their password.
return (
user.profiles.some(({ provider }) => provider === 'local') &&
isString(user.password) &&
user.password.length > 0
);
case types.CHANGE_USERNAME:
return user.status.username.status === 'REJECTED';
+3 -12
View File
@@ -109,20 +109,11 @@ router.put(
async (req, res, next) => {
const { token, password } = req.body;
if (!password || password.length < 8) {
return next(errors.ErrPasswordTooShort);
}
try {
let [user, redirect] = await UsersService.verifyPasswordResetToken(token);
// Change the users' password.
await UsersService.changePassword(user.id, password);
const { redirect } = await UsersService.resetPassword(token, password);
res.json({ redirect });
} catch (e) {
console.error(e);
return next(errors.ErrNotAuthorized);
} catch (err) {
return next(err);
}
}
);
+34 -9
View File
@@ -132,7 +132,7 @@ class Users {
locals: {
body: message,
},
subject: 'Your account has been suspended',
subject: 'Your account has been suspended', // TODO: replace with translation
});
}
@@ -490,6 +490,10 @@ class Users {
}
static async changePassword(id, password) {
if (!password || password.length < 8) {
throw new ErrPasswordTooShort();
}
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
return User.update(
@@ -725,18 +729,13 @@ class Users {
});
}
/**
* Verifies a jwt and returns the associated user. Throws an error when the
* token isn't valid.
*
* @param {String} token the JSON Web Token to verify
*/
// TODO: update doc
static async verifyPasswordResetToken(token) {
if (!token) {
throw new Error('cannot verify an empty token');
}
const { userId, loc, version } = await Users.verifyToken(token, {
const { userId, loc: redirect, version } = await Users.verifyToken(token, {
subject: PASSWORD_RESET_JWT_SUBJECT,
});
@@ -746,7 +745,33 @@ class Users {
throw new Error('password reset token has expired');
}
return [user, loc];
return { user, redirect, version };
}
// TODO: update doc
static async resetPassword(token, password) {
const { user, redirect, version } = await this.verifyPasswordResetToken(
token
);
if (!password || password.length < 8) {
throw new ErrPasswordTooShort();
}
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
// Update the user's password.
await User.update(
{ id: user.id, __v: version },
{
$inc: { __v: 1 },
$set: {
password: hashedPassword,
},
}
);
return { user, redirect };
}
/**