mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
when admin is changing users emails check if its avaiable and remove old email of user first upgrade script to cleanup email:uid, email:sorted, will remove entries if user doesn't exist or doesn't have email or if entry in user hash doesn't match entry in email:uid fix missing ! in email interstitial fix missing await in canSendValidation, fix broken tests dont pass sessionId to email.remove if admin is changing/removing email
This commit is contained in:
committed by
GitHub
parent
7a5bcc2171
commit
845c8013b6
46
src/upgrades/2.8.7/fix-email-sorted-sets.js
Normal file
46
src/upgrades/2.8.7/fix-email-sorted-sets.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
const db = require('../../database');
|
||||||
|
const batch = require('../../batch');
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'Fix user email sorted sets',
|
||||||
|
timestamp: Date.UTC(2023, 1, 4),
|
||||||
|
method: async function () {
|
||||||
|
const { progress } = this;
|
||||||
|
const bulkRemove = [];
|
||||||
|
await batch.processSortedSet('email:uid', async (data) => {
|
||||||
|
progress.incr(data.length);
|
||||||
|
const usersData = await db.getObjects(data.map(d => `user:${d.score}`));
|
||||||
|
data.forEach((emailData, index) => {
|
||||||
|
const { score: uid, value: email } = emailData;
|
||||||
|
const userData = usersData[index];
|
||||||
|
// user no longer exists or doesn't have email set in user hash
|
||||||
|
// remove the email/uid pair from email:uid, email:sorted
|
||||||
|
if (!userData || !userData.email) {
|
||||||
|
bulkRemove.push(['email:uid', email]);
|
||||||
|
bulkRemove.push(['email:sorted', `${email.toLowerCase()}:${uid}`]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// user has email but doesn't match whats stored in user hash, gh#11259
|
||||||
|
if (userData.email && userData.email.toLowerCase() !== email.toLowerCase()) {
|
||||||
|
bulkRemove.push(['email:uid', email]);
|
||||||
|
bulkRemove.push(['email:sorted', `${email.toLowerCase()}:${uid}`]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
batch: 500,
|
||||||
|
withScores: true,
|
||||||
|
progress: progress,
|
||||||
|
});
|
||||||
|
|
||||||
|
await batch.processArray(bulkRemove, async (bulk) => {
|
||||||
|
await db.sortedSetRemoveBulk(bulk);
|
||||||
|
}, {
|
||||||
|
batch: 500,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -39,7 +39,7 @@ UserEmail.remove = async function (uid, sessionId) {
|
|||||||
db.sortedSetRemove('email:uid', email.toLowerCase()),
|
db.sortedSetRemove('email:uid', email.toLowerCase()),
|
||||||
db.sortedSetRemove('email:sorted', `${email.toLowerCase()}:${uid}`),
|
db.sortedSetRemove('email:sorted', `${email.toLowerCase()}:${uid}`),
|
||||||
user.email.expireValidation(uid),
|
user.email.expireValidation(uid),
|
||||||
user.auth.revokeAllSessions(uid, sessionId),
|
sessionId ? user.auth.revokeAllSessions(uid, sessionId) : Promise.resolve(),
|
||||||
events.log({ type: 'email-change', email, newEmail: '' }),
|
events.log({ type: 'email-change', email, newEmail: '' }),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
@@ -69,7 +69,7 @@ UserEmail.expireValidation = async (uid) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
UserEmail.canSendValidation = async (uid, email) => {
|
UserEmail.canSendValidation = async (uid, email) => {
|
||||||
const pending = UserEmail.isValidationPending(uid, email);
|
const pending = await UserEmail.isValidationPending(uid, email);
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -196,6 +196,20 @@ UserEmail.confirmByUid = async function (uid) {
|
|||||||
throw new Error('[[error:invalid-email]]');
|
throw new Error('[[error:invalid-email]]');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If another uid has the same email throw error
|
||||||
|
const oldUid = await db.sortedSetScore('email:uid', currentEmail.toLowerCase());
|
||||||
|
if (oldUid && oldUid !== parseInt(uid, 10)) {
|
||||||
|
throw new Error('[[error:email-taken]]');
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmedEmails = await db.getSortedSetRangeByScore(`email:uid`, 0, -1, uid, uid);
|
||||||
|
if (confirmedEmails.length) {
|
||||||
|
// remove old email of user by uid
|
||||||
|
await db.sortedSetsRemoveRangeByScore([`email:uid`], uid, uid);
|
||||||
|
await db.sortedSetRemoveBulk(
|
||||||
|
confirmedEmails.map(email => [`email:sorted`, `${email.toLowerCase()}:${uid}`])
|
||||||
|
);
|
||||||
|
}
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.sortedSetAddBulk([
|
db.sortedSetAddBulk([
|
||||||
['email:uid', uid, currentEmail.toLowerCase()],
|
['email:uid', uid, currentEmail.toLowerCase()],
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ Interstitials.email = async (data) => {
|
|||||||
callback: async (userData, formData) => {
|
callback: async (userData, formData) => {
|
||||||
// Validate and send email confirmation
|
// Validate and send email confirmation
|
||||||
if (userData.uid) {
|
if (userData.uid) {
|
||||||
|
const isSelf = parseInt(userData.uid, 10) === parseInt(data.req.uid, 10);
|
||||||
const [isPasswordCorrect, canEdit, { email: current, 'email:confirmed': confirmed }, { allowed, error }] = await Promise.all([
|
const [isPasswordCorrect, canEdit, { email: current, 'email:confirmed': confirmed }, { allowed, error }] = await Promise.all([
|
||||||
user.isPasswordCorrect(userData.uid, formData.password, data.req.ip),
|
user.isPasswordCorrect(userData.uid, formData.password, data.req.ip),
|
||||||
privileges.users.canEdit(data.req.uid, userData.uid),
|
privileges.users.canEdit(data.req.uid, userData.uid),
|
||||||
@@ -68,13 +69,17 @@ Interstitials.email = async (data) => {
|
|||||||
if (formData.email === current) {
|
if (formData.email === current) {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
throw new Error('[[error:email-nochange]]');
|
throw new Error('[[error:email-nochange]]');
|
||||||
} else if (await user.email.canSendValidation(userData.uid, current)) {
|
} else if (!await user.email.canSendValidation(userData.uid, current)) {
|
||||||
throw new Error(`[[error:confirm-email-already-sent, ${meta.config.emailConfirmInterval}]]`);
|
throw new Error(`[[error:confirm-email-already-sent, ${meta.config.emailConfirmInterval}]]`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admins editing will auto-confirm, unless editing their own email
|
// Admins editing will auto-confirm, unless editing their own email
|
||||||
if (isAdminOrGlobalMod && userData.uid !== data.req.uid) {
|
if (isAdminOrGlobalMod && userData.uid !== data.req.uid) {
|
||||||
|
if (!await user.email.available(formData.email)) {
|
||||||
|
throw new Error('[[error:email-taken]]');
|
||||||
|
}
|
||||||
|
await user.email.remove(userData.uid);
|
||||||
await user.setUserField(userData.uid, 'email', formData.email);
|
await user.setUserField(userData.uid, 'email', formData.email);
|
||||||
await user.email.confirmByUid(userData.uid);
|
await user.email.confirmByUid(userData.uid);
|
||||||
} else if (canEdit) {
|
} else if (canEdit) {
|
||||||
@@ -99,8 +104,8 @@ Interstitials.email = async (data) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (current.length && (!hasPassword || (hasPassword && isPasswordCorrect) || isAdminOrGlobalMod)) {
|
if (current.length && (!hasPassword || (hasPassword && isPasswordCorrect) || isAdminOrGlobalMod)) {
|
||||||
// User explicitly clearing their email
|
// User or admin explicitly clearing their email
|
||||||
await user.email.remove(userData.uid, data.req.session.id);
|
await user.email.remove(userData.uid, isSelf ? data.req.session.id : null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ describe('email confirmation (library methods)', () => {
|
|||||||
await user.email.sendValidationEmail(uid, {
|
await user.email.sendValidationEmail(uid, {
|
||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
const ok = await user.email.canSendValidation(uid, 'test@example.com');
|
const ok = await user.email.canSendValidation(uid, email);
|
||||||
|
|
||||||
assert.strictEqual(ok, false);
|
assert.strictEqual(ok, false);
|
||||||
});
|
});
|
||||||
@@ -131,7 +131,7 @@ describe('email confirmation (library methods)', () => {
|
|||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
await db.pexpire(`confirm:byUid:${uid}`, 1000);
|
await db.pexpire(`confirm:byUid:${uid}`, 1000);
|
||||||
const ok = await user.email.canSendValidation(uid, 'test@example.com');
|
const ok = await user.email.canSendValidation(uid, email);
|
||||||
|
|
||||||
assert(ok);
|
assert(ok);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user