mirror of
https://github.com/wassname/talk.git
synced 2026-07-17 11:33:39 +08:00
GDPR Email Support
- Email and password can be used to create a new local profile - Email can be changed with accounts that already have a local profile
This commit is contained in:
@@ -7,6 +7,7 @@ plugin:
|
||||
default: true
|
||||
provides:
|
||||
- Client
|
||||
- Server
|
||||
---
|
||||
|
||||
This provides the base plugin that is the basis for all auth based plugins that
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
module.exports = {};
|
||||
const typeDefs = require('./server/typeDefs');
|
||||
const resolvers = require('./server/resolvers');
|
||||
const mutators = require('./server/mutators');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
translations: path.join(__dirname, 'server', 'translations.yml'),
|
||||
typeDefs,
|
||||
mutators,
|
||||
resolvers,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const { TalkError } = require('errors');
|
||||
|
||||
// ErrNoLocalProfile is returned when there is no existing local profile
|
||||
// attached to a user.
|
||||
class ErrNoLocalProfile extends TalkError {
|
||||
constructor() {
|
||||
super('No local profile associated with account', {
|
||||
translation_key: 'NO_LOCAL_PROFILE',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ErrLocalProfile is returned when a profile is already attached to a user and
|
||||
// the user is trying to attach a new profile to it.
|
||||
class ErrLocalProfile extends TalkError {
|
||||
constructor() {
|
||||
super('Local profile already associated with account', {
|
||||
translation_key: 'LOCAL_PROFILE',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ErrLocalProfile, ErrNoLocalProfile };
|
||||
@@ -0,0 +1,158 @@
|
||||
const {
|
||||
ErrNotAuthorized,
|
||||
ErrPasswordTooShort,
|
||||
ErrNotFound,
|
||||
ErrEmailTaken,
|
||||
} = require('errors');
|
||||
const { ErrNoLocalProfile, ErrLocalProfile } = require('./errors');
|
||||
const { get } = require('lodash');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
function hasLocalProfile(user) {
|
||||
return Boolean(
|
||||
get(user, 'profiles', []).find(({ provider }) => provider === 'local')
|
||||
);
|
||||
}
|
||||
|
||||
// updateUserEmailAddress will verify that the user has sent the correct
|
||||
// password followed by executing the email change and notifying the emails
|
||||
// about that change.
|
||||
async function updateUserEmailAddress(ctx, email, confirmPassword) {
|
||||
const {
|
||||
user,
|
||||
loaders: { Settings },
|
||||
connectors: { models: { User }, services: { Mailer, I18n, Users } },
|
||||
} = ctx;
|
||||
|
||||
// Ensure that the user has a local profile associated with their account.
|
||||
if (!hasLocalProfile(user)) {
|
||||
throw new ErrNoLocalProfile();
|
||||
}
|
||||
|
||||
// Ensure that the password provided matches what we have on file.
|
||||
if (!await user.verifyPassword(confirmPassword)) {
|
||||
throw new ErrNotAuthorized();
|
||||
}
|
||||
|
||||
// Cleanup the email address.
|
||||
email = email.toLowerCase().trim();
|
||||
|
||||
// Update the Users email address.
|
||||
await User.update(
|
||||
{
|
||||
id: user.id,
|
||||
profiles: { $elemMatch: { provider: 'local' } },
|
||||
},
|
||||
{
|
||||
$set: { 'profiles.$.id': email },
|
||||
}
|
||||
);
|
||||
|
||||
// Get some context for the email to be sent.
|
||||
const { organizationContactEmail } = await Settings.load([
|
||||
'organizationContactEmail',
|
||||
]);
|
||||
|
||||
// Send off the email to the old email address that we have changed it.
|
||||
await Mailer.send({
|
||||
email: user.firstEmail,
|
||||
template: 'plain',
|
||||
locals: {
|
||||
body: I18n.t(
|
||||
'email.email_change_original.body',
|
||||
user.email,
|
||||
email,
|
||||
organizationContactEmail
|
||||
),
|
||||
},
|
||||
subject: I18n.t('email.email_change_original.subject'),
|
||||
});
|
||||
|
||||
// Send off the email to the new email address that we need to verify the new
|
||||
// address.
|
||||
await Users.sendEmailConfirmation(user, email);
|
||||
}
|
||||
|
||||
// attachUserLocalAuth will attach a new local profile to an existing user.
|
||||
async function attachUserLocalAuth(ctx, email, password) {
|
||||
const { user, connectors: { models: { User }, services: { Users } } } = ctx;
|
||||
|
||||
// Ensure that the current user doesn't already have a local account
|
||||
// associated with them.
|
||||
if (hasLocalProfile(user)) {
|
||||
throw new ErrLocalProfile();
|
||||
}
|
||||
|
||||
// Cleanup the email address.
|
||||
email = email.toLowerCase().trim();
|
||||
|
||||
// Validate the password.
|
||||
if (!password || password.length < 8) {
|
||||
throw new ErrPasswordTooShort();
|
||||
}
|
||||
|
||||
// Hash the new password.
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
try {
|
||||
// Associate the account with the user.
|
||||
const updatedUser = await User.findOneAndUpdate(
|
||||
{
|
||||
id: user.id,
|
||||
'profiles.provider': { $ne: 'local' },
|
||||
},
|
||||
{
|
||||
$push: {
|
||||
profiles: {
|
||||
provider: 'local',
|
||||
id: email,
|
||||
},
|
||||
},
|
||||
$set: {
|
||||
password: hashedPassword,
|
||||
},
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
if (!updatedUser) {
|
||||
const foundUser = await User.findOne({ id: user.id });
|
||||
if (foundUser === null) {
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
if (hasLocalProfile(foundUser)) {
|
||||
throw new ErrLocalProfile();
|
||||
}
|
||||
|
||||
throw new Error('local auth attachment failed due to unexpected reason');
|
||||
}
|
||||
|
||||
// Send off the email to the new email address that we need to verify the
|
||||
// new address.
|
||||
await Users.sendEmailConfirmation(updatedUser, email);
|
||||
} catch (err) {
|
||||
if (err.code === 11000) {
|
||||
throw new ErrEmailTaken();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ctx => {
|
||||
const mutators = {
|
||||
User: {
|
||||
updateEmailAddress: () => Promise.reject(new ErrNotAuthorized()),
|
||||
attachLocalAuth: () => Promise.reject(new ErrNotAuthorized()),
|
||||
},
|
||||
};
|
||||
|
||||
if (ctx.user) {
|
||||
mutators.User.updateEmailAddress = ({ email, confirmPassword }) =>
|
||||
updateUserEmailAddress(ctx, email, confirmPassword);
|
||||
|
||||
mutators.User.attachLocalAuth = ({ email, password }) =>
|
||||
attachUserLocalAuth(ctx, email, password);
|
||||
}
|
||||
|
||||
return mutators;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
RootMutation: {
|
||||
updateEmailAddress: async (root, { input }, { mutators: { User } }) => {
|
||||
await User.updateEmailAddress(input);
|
||||
},
|
||||
attachLocalAuth: async (root, { input }, { mutators: { User } }) => {
|
||||
await User.attachLocalAuth(input);
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
en:
|
||||
email:
|
||||
email_change_original:
|
||||
subject: Email change
|
||||
body: Your email address has been changed from {0} to {1}. If you did not initiate this change, please contact {2}.
|
||||
error:
|
||||
NO_LOCAL_PROFILE: No existing email address is associated with this account.
|
||||
LOCAL_PROFILE: An email address is already associated with this account.
|
||||
@@ -0,0 +1,47 @@
|
||||
# UpdateEmailAddressInput provides input for changing a users email address
|
||||
# associated with their account.
|
||||
input UpdateEmailAddressInput {
|
||||
|
||||
# email is the Users email address that they want to update to.
|
||||
email: String!
|
||||
|
||||
# confirmPassword is the Users current password.
|
||||
confirmPassword: String!
|
||||
}
|
||||
|
||||
# UpdateEmailAddressResponse is returned when you try to update a users email
|
||||
# address.
|
||||
type UpdateEmailAddressResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# AttachLocalAuthInput provides the input for attaching a new local
|
||||
# authentication profile.
|
||||
input AttachLocalAuthInput {
|
||||
|
||||
# email is the Users email address that they want to add.
|
||||
email: String!
|
||||
|
||||
# password is the Users password that they want to add.
|
||||
password: String!
|
||||
}
|
||||
|
||||
# AttachLocalAuthResponse returns any errors for when the user attempts to
|
||||
# attach a new local authentication profile.
|
||||
type AttachLocalAuthResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
|
||||
# updateEmailAddress changes the email address of the current logged in user.
|
||||
updateEmailAddress(input: UpdateEmailAddressInput!): UpdateEmailAddressResponse
|
||||
|
||||
# attachLocalAuth will attach a new local authentication profile to an
|
||||
# account.
|
||||
attachLocalAuth(input: AttachLocalAuthInput!): AttachLocalAuthResponse
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = fs.readFileSync(
|
||||
path.join(__dirname, 'typeDefs.graphql'),
|
||||
'utf8'
|
||||
);
|
||||
Reference in New Issue
Block a user