Merge branch 'master' into develop

isekai-main
Julian Lam 2 years ago
commit dbc84f7571

@ -19,6 +19,15 @@ post:
content: content:
type: string type: string
example: This is the test topic's content example: This is the test topic's content
timestamp:
type: number
description: |
A UNIX timestamp of the topic's creation date (i.e. when it will be posted).
Specifically, this value can only be set to a value in the future if the calling user has the `topics:schedule` privilege for the passed-in category.
Otherwise, the current date and time are always assumed.
In some scenarios (e.g. forum migrations), you may want to backdate topics and posts.
Please see [this Developer FAQ topic](https://community.nodebb.org/topic/16983/how-can-i-backdate-topics-and-posts-for-migration-purposes) for more information.
example: 556084800000
tags: tags:
type: array type: array
items: items:

@ -46,8 +46,6 @@ post:
content: content:
type: string type: string
example: This is a test reply example: This is a test reply
timestamp:
type: number
toPid: toPid:
type: number type: number
required: required:

@ -133,11 +133,11 @@ modsController.flags.detail = async function (req, res, next) {
uids = _.uniq(admins.concat(uids)); uids = _.uniq(admins.concat(uids));
} else if (flagData.type === 'post') { } else if (flagData.type === 'post') {
const cid = await posts.getCidByPid(flagData.targetId); const cid = await posts.getCidByPid(flagData.targetId);
if (!cid) { uids = _.uniq(admins.concat(globalMods));
return []; if (cid) {
const modUids = (await privileges.categories.getUidsWithPrivilege([cid], 'moderate'))[0];
uids = _.uniq(uids.concat(modUids));
} }
uids = (await privileges.categories.getUidsWithPrivilege([cid], 'moderate'))[0];
uids = _.uniq(admins.concat(globalMods).concat(uids));
} }
const userData = await user.getUsersData(uids); const userData = await user.getUsersData(uids);
return userData.filter(u => u && u.userslug); return userData.filter(u => u && u.userslug);

@ -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;
} }
@ -134,13 +134,13 @@ UserEmail.sendValidationEmail = async function (uid, options) {
await UserEmail.expireValidation(uid); await UserEmail.expireValidation(uid);
await db.set(`confirm:byUid:${uid}`, confirm_code); await db.set(`confirm:byUid:${uid}`, confirm_code);
await db.pexpire(`confirm:byUid:${uid}`, emailConfirmExpiry * 24 * 60 * 60 * 1000); await db.pexpire(`confirm:byUid:${uid}`, emailConfirmExpiry * 60 * 60 * 1000);
await db.setObject(`confirm:${confirm_code}`, { await db.setObject(`confirm:${confirm_code}`, {
email: options.email.toLowerCase(), email: options.email.toLowerCase(),
uid: uid, uid: uid,
}); });
await db.pexpire(`confirm:${confirm_code}`, emailConfirmExpiry * 24 * 60 * 60 * 1000); await db.pexpire(`confirm:${confirm_code}`, emailConfirmExpiry * 60 * 60 * 1000);
winston.verbose(`[user/email] Validation email for uid ${uid} sent to ${options.email}`); winston.verbose(`[user/email] Validation email for uid ${uid} sent to ${options.email}`);
events.log({ events.log({
@ -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);
}); });

Loading…
Cancel
Save