refactor(emails): interstitial for adding/updating email

v1.18.x
Julian Lam 4 years ago
parent 74aaa0a926
commit f365bc4600

@ -33,6 +33,7 @@
"username-taken": "Username taken",
"email-taken": "Email taken",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "You are unable to post until your email is confirmed, please click here to confirm your email.",
"email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.",

@ -22,7 +22,7 @@
"registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.",
"interstitial.intro": "We'd like some additional information in order to update your account…",
"interstitial.intro-new": "We'd like some additional information before we can create your account…",
"interstitial.errors-found": "We could not complete your registration:",
"interstitial.errors-found": "Please review the entered information:",
"gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.",
"gdpr_agree_email": "I consent to receive digest and notification emails from this website.",
"gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.",

@ -201,5 +201,9 @@
"consent.export_uploads": "Export Uploaded Content (.zip)",
"consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.",
"consent.export_posts": "Export Posts (.csv)",
"consent.export-posts-success": "Exporting posts, you will get a notification when it is complete."
"consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.",
"emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.",
"emailUpdate.optional": "<strong>This field is optional</strong>. You are not obligated to provide your email address, but without a validated email, you will not be able to recover your account.",
"emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page."
}

@ -77,8 +77,11 @@ editController.username = async function (req, res, next) {
await renderRoute('username', req, res, next);
};
editController.email = async function (req, res, next) {
await renderRoute('email', req, res, next);
editController.email = async function (req, res) {
req.session.registration = req.session.registration || {};
req.session.registration.updateEmail = true;
req.session.registration.uid = req.uid;
helpers.redirect(res, '/register/complete');
};
async function renderRoute(name, req, res, next) {

@ -33,6 +33,7 @@ async function registerAndLoginUser(req, res, userData) {
if (deferRegistration) {
userData.register = true;
userData.updateEmail = true;
req.session.registration = userData;
if (req.body.noscript === 'true') {
@ -209,11 +210,17 @@ authenticationController.registerComplete = function (req, res, next) {
};
authenticationController.registerAbort = function (req, res) {
// End the session and redirect to home
req.session.destroy(() => {
res.clearCookie(nconf.get('sessionKey'), meta.configs.cookie.get());
if (req.uid) {
// Clear interstitial data and go home
delete req.session.registration;
res.redirect(`${nconf.get('relative_path')}/`);
});
} else {
// End the session and redirect to home
req.session.destroy(() => {
res.clearCookie(nconf.get('sessionKey'), meta.configs.cookie.get());
res.redirect(`${nconf.get('relative_path')}/`);
});
}
};
authenticationController.login = async (req, res, next) => {

@ -117,7 +117,7 @@ module.exports = function (User) {
User.updateDigestSetting(userData.uid, meta.config.dailyDigestFreq),
]);
if (userData.email && userData.uid > 1 && meta.config.requireEmailConfirmation) {
if (userData.email && userData.uid > 1) {
User.email.sendValidationEmail(userData.uid, {
email: userData.email,
}).catch(err => winston.error(`[user.create] Validation email failed to send\n[emailer.send] ${err.stack}`));

@ -93,15 +93,27 @@ UserEmail.confirmByCode = async function (code) {
if (!confirmObj || !confirmObj.uid || !confirmObj.email) {
throw new Error('[[error:invalid-data]]');
}
const currentEmail = await user.getUserField(confirmObj.uid, 'email');
if (!currentEmail || currentEmail.toLowerCase() !== confirmObj.email) {
throw new Error('[[error:invalid-email]]');
let oldEmail = await user.getUserField(confirmObj.uid, 'email');
if (oldEmail) {
oldEmail = oldEmail || '';
if (oldEmail === confirmObj.email) {
return;
}
await db.sortedSetRemove('email:uid', oldEmail.toLowerCase());
await db.sortedSetRemove('email:sorted', `${oldEmail.toLowerCase()}:${confirmObj.uid}`);
await user.auth.revokeAllSessions(confirmObj.uid);
}
await UserEmail.confirmByUid(confirmObj.uid);
await db.delete(`confirm:${code}`);
await Promise.all([
user.setUserField('email', confirmObj.email),
UserEmail.confirmByUid(confirmObj.uid),
db.delete(`confirm:${code}`),
]);
};
// confirm uid's email
// confirm uid's email via ACP
UserEmail.confirmByUid = async function (uid) {
if (!(parseInt(uid, 10) > 0)) {
throw new Error('[[error:invalid-uid]]');
@ -110,11 +122,18 @@ UserEmail.confirmByUid = async function (uid) {
if (!currentEmail) {
throw new Error('[[error:invalid-email]]');
}
await Promise.all([
db.sortedSetAddBulk([
['email:uid', uid, currentEmail.toLowerCase()],
['email:sorted', 0, `${currentEmail.toLowerCase()}:${uid}`],
[`user:${uid}:emails`, Date.now(), `${currentEmail}:${Date.now()}`],
]),
user.setUserField(uid, 'email:confirmed', 1),
groups.join('verified-users', uid),
groups.leave('unverified-users', uid),
db.delete(`uid:${uid}:confirm:email:sent`),
user.reset.cleanByUid(uid),
]);
await plugins.hooks.fire('action:user.email.confirmed', { uid: uid, email: currentEmail });
};

@ -229,6 +229,53 @@ User.addInterstitials = function (callback) {
plugins.hooks.register('core', {
hook: 'filter:register.interstitial',
method: [
// Email address (for password reset + digest)
async (data) => {
if (!data.userData) {
throw new Error('[[error:invalid-data]]');
}
if (!data.userData.updateEmail) {
return data;
}
let email;
if (data.userData.uid) {
email = await User.getUserField(data.userData.uid, 'email');
}
data.interstitials.push({
template: 'partials/email_update',
data: { email },
callback: async (userData, formData) => {
// Validate and send email confirmation
if (formData.email && formData.email.length) {
if (!utils.isEmailValid(formData.email)) {
throw new Error('[[error:invalid-email]]');
}
if (userData.uid) {
const current = await User.getUserField(userData.uid, 'email');
if (formData.email === current) {
throw new Error('[[error:email-nochange]]');
}
await User.email.sendValidationEmail(userData.uid, {
email: formData.email,
force: true,
});
} else {
// New registrants have the confirm email sent from user.create()
userData.email = formData.email;
}
}
delete userData.updateEmail;
},
});
return data;
},
// GDPR information collection/processing consent + email consent
async function (data) {
if (!meta.config.gdpr_enabled || (data.userData && data.userData.gdpr_consent)) {

@ -243,23 +243,7 @@ module.exports = function (User) {
return;
}
await db.sortedSetRemove('email:uid', oldEmail.toLowerCase());
await db.sortedSetRemove('email:sorted', `${oldEmail.toLowerCase()}:${uid}`);
await User.auth.revokeAllSessions(uid);
await Promise.all([
db.sortedSetAddBulk([
['email:uid', uid, newEmail.toLowerCase()],
['email:sorted', 0, `${newEmail.toLowerCase()}:${uid}`],
[`user:${uid}:emails`, Date.now(), `${newEmail}:${Date.now()}`],
]),
User.setUserFields(uid, { email: newEmail, 'email:confirmed': 0 }),
groups.leave('verified-users', uid),
groups.join('unverified-users', uid),
User.reset.cleanByUid(uid),
]);
if (meta.config.requireEmailConfirmation && newEmail) {
if (newEmail) {
await User.email.sendValidationEmail(uid, {
email: newEmail,
subject: '[[email:email.verify-your-email.subject]]',

@ -0,0 +1,9 @@
<div class="form-group">
<p>[[user:emailUpdate.intro]]</p>
<p>[[user:emailUpdate.optional]]</p>
<div class="form-group">
<label for="email">[[global:email]]</label>
<input class="form-control" type="text" id="email" name="email" />
</div>
<p class="help-block">[[user:emailUpdate.change-instructions]]</p>
</div>
Loading…
Cancel
Save