mirror of
https://github.com/wassname/talk.git
synced 2026-07-01 18:40:35 +08:00
Password Change Graph Support
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -19,4 +19,5 @@ module.exports = {
|
||||
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
|
||||
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
|
||||
DELETE_USER: 'DELETE_USER',
|
||||
CHANGE_PASSWORD: 'CHANGE_PASSWORD',
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user