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:
Wyatt Johnson
2018-04-19 17:19:14 -06:00
parent 5c5cde535e
commit 2e013c33ac
8 changed files with 267 additions and 1 deletions
+1
View File
@@ -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
+11 -1
View File
@@ -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,
};
+25
View File
@@ -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 };
+158
View File
@@ -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'
);