mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-02-24 23:51:18 +01:00
Merge commit '791551098cb4a56edbae824e45b6f0a10138695b' into v2.x
This commit is contained in:
51
CHANGELOG.md
51
CHANGELOG.md
@@ -1,3 +1,54 @@
|
||||
#### v2.8.6 (2023-02-03)
|
||||
|
||||
##### Chores
|
||||
|
||||
* **i18n:** fallback strings for new resources: nodebb.error (8335f90a)
|
||||
* incrementing version number - v2.8.5 (bff5ce2d)
|
||||
* update changelog for v2.8.5 (24e58c28)
|
||||
* incrementing version number - v2.8.4 (a46b2bbc)
|
||||
* incrementing version number - v2.8.3 (c20b20a7)
|
||||
* incrementing version number - v2.8.2 (050e43f8)
|
||||
* incrementing version number - v2.8.1 (727f879e)
|
||||
* incrementing version number - v2.8.0 (8e77673d)
|
||||
* incrementing version number - v2.7.0 (96cc0617)
|
||||
* incrementing version number - v2.6.1 (7e52a7a5)
|
||||
* incrementing version number - v2.6.0 (e7fcf482)
|
||||
* incrementing version number - v2.5.8 (dec0e7de)
|
||||
* incrementing version number - v2.5.7 (5836bf4a)
|
||||
* incrementing version number - v2.5.6 (c7bd7dbf)
|
||||
* incrementing version number - v2.5.5 (3509ed94)
|
||||
* incrementing version number - v2.5.4 (e83260ca)
|
||||
* incrementing version number - v2.5.3 (7e922936)
|
||||
* incrementing version number - v2.5.2 (babcd17e)
|
||||
* incrementing version number - v2.5.1 (ce3aa950)
|
||||
* incrementing version number - v2.5.0 (01d276cb)
|
||||
* incrementing version number - v2.4.5 (dd3e1a28)
|
||||
* incrementing version number - v2.4.4 (d5525c87)
|
||||
* incrementing version number - v2.4.3 (9c647c6c)
|
||||
* incrementing version number - v2.4.2 (3aa7b855)
|
||||
* incrementing version number - v2.4.1 (60cbd148)
|
||||
* incrementing version number - v2.4.0 (4834cde3)
|
||||
* incrementing version number - v2.3.1 (d2425942)
|
||||
* incrementing version number - v2.3.0 (046ea120)
|
||||
|
||||
##### New Features
|
||||
|
||||
* add sitemap filter hooks for categories/topic pages (bf92ee0e)
|
||||
* closes #11241, add missing error lang keys (c241baf6)
|
||||
* #11240, only show relevant users in flags assignee list (0713482b)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* #11254, return check for reroll property (202378b9)
|
||||
* closes #11249, notification uses displayname (705cd13a)
|
||||
* wrong link to topics in acp dashboard (b5598a6e)
|
||||
* https://github.com/NodeBB/NodeBB/issues/11239 (1d3c0e5a)
|
||||
* notif filter selecte field (6d819b05)
|
||||
|
||||
##### Other Changes
|
||||
|
||||
* remove unused (d68352cc)
|
||||
|
||||
#### v2.8.5 (2023-01-27)
|
||||
|
||||
##### Chores
|
||||
|
||||
@@ -19,6 +19,15 @@ post:
|
||||
content:
|
||||
type: string
|
||||
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:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -46,8 +46,6 @@ post:
|
||||
content:
|
||||
type: string
|
||||
example: This is a test reply
|
||||
timestamp:
|
||||
type: number
|
||||
toPid:
|
||||
type: number
|
||||
required:
|
||||
|
||||
@@ -9,15 +9,23 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function
|
||||
configureEmailTester();
|
||||
configureEmailEditor();
|
||||
handleDigestHourChange();
|
||||
handleSmtpServiceChange();
|
||||
|
||||
$(window).on('action:admin.settingsLoaded action:admin.settingsSaved', handleDigestHourChange);
|
||||
$(window).on('action:admin.settingsSaved', function () {
|
||||
socket.emit('admin.user.restartJobs');
|
||||
});
|
||||
$('[id="email:smtpTransport:service"]').change(handleSmtpServiceChange);
|
||||
$(window).off('action:admin.settingsLoaded', onSettingsLoaded)
|
||||
.on('action:admin.settingsLoaded', onSettingsLoaded);
|
||||
$(window).off('action:admin.settingsSaved', onSettingsSaved)
|
||||
.on('action:admin.settingsSaved', onSettingsSaved);
|
||||
};
|
||||
|
||||
function onSettingsLoaded() {
|
||||
handleDigestHourChange();
|
||||
handleSmtpServiceChange();
|
||||
}
|
||||
|
||||
function onSettingsSaved() {
|
||||
handleDigestHourChange();
|
||||
socket.emit('admin.user.restartJobs');
|
||||
}
|
||||
|
||||
function configureEmailTester() {
|
||||
$('button[data-action="email.test"]').off('click').on('click', function () {
|
||||
socket.emit('admin.email.test', { template: $('#test-email').val() }, function (err) {
|
||||
@@ -106,20 +114,26 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function
|
||||
}
|
||||
|
||||
function handleSmtpServiceChange() {
|
||||
const isCustom = $('[id="email:smtpTransport:service"]').val() === 'nodebb-custom-smtp';
|
||||
$('[id="email:smtpTransport:custom-service"]')[isCustom ? 'slideDown' : 'slideUp'](isCustom);
|
||||
|
||||
const enabledEl = document.getElementById('email:smtpTransport:enabled');
|
||||
if (enabledEl) {
|
||||
if (!enabledEl.checked) {
|
||||
enabledEl.closest('label').classList.toggle('is-checked', true);
|
||||
enabledEl.checked = true;
|
||||
alerts.alert({
|
||||
message: '[[admin/settings/email:smtp-transport.auto-enable-toast]]',
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
function toggleCustomService() {
|
||||
const isCustom = $('[id="email:smtpTransport:service"]').val() === 'nodebb-custom-smtp';
|
||||
$('[id="email:smtpTransport:custom-service"]')[isCustom ? 'slideDown' : 'slideUp'](isCustom);
|
||||
}
|
||||
toggleCustomService();
|
||||
$('[id="email:smtpTransport:service"]').change(function () {
|
||||
toggleCustomService();
|
||||
|
||||
const enabledEl = document.getElementById('email:smtpTransport:enabled');
|
||||
if (enabledEl) {
|
||||
if (!enabledEl.checked) {
|
||||
$('label[for="email:smtpTransport:enabled"]').toggleClass('is-checked', true);
|
||||
enabledEl.checked = true;
|
||||
alerts.alert({
|
||||
message: '[[admin/settings/email:smtp-transport.auto-enable-toast]]',
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return module;
|
||||
|
||||
@@ -77,6 +77,7 @@ define('forum/account/edit/password', [
|
||||
ajaxify.go('user/' + ajaxify.data.userslug + '/edit');
|
||||
}
|
||||
})
|
||||
.catch(alerts.error)
|
||||
.finally(() => {
|
||||
btn.removeClass('disabled').find('i').addClass('hide');
|
||||
currentPassword.val('');
|
||||
|
||||
@@ -443,6 +443,10 @@ usersAPI.changePicture = async (caller, data) => {
|
||||
};
|
||||
|
||||
usersAPI.generateExport = async (caller, { uid, type }) => {
|
||||
const validTypes = ['profile', 'posts', 'uploads'];
|
||||
if (!validTypes.includes(type)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
const count = await db.incrObjectField('locks', `export:${uid}${type}`);
|
||||
if (count > 1) {
|
||||
throw new Error('[[error:already-exporting]]');
|
||||
|
||||
@@ -132,11 +132,11 @@ modsController.flags.detail = async function (req, res, next) {
|
||||
uids = _.uniq(admins.concat(uids));
|
||||
} else if (flagData.type === 'post') {
|
||||
const cid = await posts.getCidByPid(flagData.targetId);
|
||||
if (!cid) {
|
||||
return [];
|
||||
uids = _.uniq(admins.concat(globalMods));
|
||||
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);
|
||||
return userData.filter(u => u && u.userslug);
|
||||
|
||||
@@ -154,7 +154,7 @@ Auth.reloadRoutes = async function (params) {
|
||||
}, Auth.middleware.validateAuth, (req, res, next) => {
|
||||
async.waterfall([
|
||||
async.apply(req.login.bind(req), res.locals.user, { keepSessionInfo: true }),
|
||||
async.apply(controllers.authentication.onSuccessfulLogin, req, req.uid),
|
||||
async.apply(controllers.authentication.onSuccessfulLogin, req, res.locals.user.uid),
|
||||
], (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
|
||||
@@ -127,7 +127,7 @@ async function generateForCategory(req, res, next) {
|
||||
db.getSortedSetRevIntersect({
|
||||
sets: ['topics:tid', `cid:${cid}:tids:lastposttime`],
|
||||
start: 0,
|
||||
stop: 25,
|
||||
stop: 24,
|
||||
weights: [1, 0],
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -74,6 +74,6 @@ module.exports = function (SocketUser) {
|
||||
|
||||
await user.isAdminOrSelf(socket.uid, data.uid);
|
||||
|
||||
api.users.generateExport(socket, { type, ...data });
|
||||
api.users.generateExport(socket, { type, uid: data.uid });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,6 +60,7 @@ Scheduled.pin = async function (tid, topicData) {
|
||||
};
|
||||
|
||||
Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) {
|
||||
const mainPid = await topics.getTopicField(tid, 'mainPid');
|
||||
await Promise.all([
|
||||
db.sortedSetsAdd([
|
||||
'topics:scheduled',
|
||||
@@ -67,6 +68,7 @@ Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) {
|
||||
'topics:tid',
|
||||
`cid:${cid}:uid:${uid}:tids`,
|
||||
], timestamp, tid),
|
||||
posts.setPostField(mainPid, 'timestamp', timestamp),
|
||||
shiftPostTimes(tid, timestamp),
|
||||
]);
|
||||
return topics.updateLastPostTimeFromLastPid(tid);
|
||||
|
||||
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:sorted', `${email.toLowerCase()}:${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: '' }),
|
||||
]);
|
||||
};
|
||||
@@ -69,7 +69,7 @@ UserEmail.expireValidation = async (uid) => {
|
||||
};
|
||||
|
||||
UserEmail.canSendValidation = async (uid, email) => {
|
||||
const pending = UserEmail.isValidationPending(uid, email);
|
||||
const pending = await UserEmail.isValidationPending(uid, email);
|
||||
if (!pending) {
|
||||
return true;
|
||||
}
|
||||
@@ -134,13 +134,13 @@ UserEmail.sendValidationEmail = async function (uid, options) {
|
||||
|
||||
await UserEmail.expireValidation(uid);
|
||||
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}`, {
|
||||
email: options.email.toLowerCase(),
|
||||
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}`);
|
||||
events.log({
|
||||
@@ -196,6 +196,20 @@ UserEmail.confirmByUid = async function (uid) {
|
||||
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([
|
||||
db.sortedSetAddBulk([
|
||||
['email:uid', uid, currentEmail.toLowerCase()],
|
||||
|
||||
@@ -42,6 +42,7 @@ Interstitials.email = async (data) => {
|
||||
callback: async (userData, formData) => {
|
||||
// Validate and send email confirmation
|
||||
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([
|
||||
user.isPasswordCorrect(userData.uid, formData.password, data.req.ip),
|
||||
privileges.users.canEdit(data.req.uid, userData.uid),
|
||||
@@ -68,13 +69,17 @@ Interstitials.email = async (data) => {
|
||||
if (formData.email === current) {
|
||||
if (confirmed) {
|
||||
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}]]`);
|
||||
}
|
||||
}
|
||||
|
||||
// Admins editing will auto-confirm, unless editing their own email
|
||||
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.email.confirmByUid(userData.uid);
|
||||
} else if (canEdit) {
|
||||
@@ -99,8 +104,8 @@ Interstitials.email = async (data) => {
|
||||
}
|
||||
|
||||
if (current.length && (!hasPassword || (hasPassword && isPasswordCorrect) || isAdminOrGlobalMod)) {
|
||||
// User explicitly clearing their email
|
||||
await user.email.remove(userData.uid, data.req.session.id);
|
||||
// User or admin explicitly clearing their email
|
||||
await user.email.remove(userData.uid, isSelf ? data.req.session.id : null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
[[admin/settings/email:smtp-transport.gmail-warning2]]
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group well" id="email:smtpTransport:custom-service" style="display: none">
|
||||
<div class="form-group well" id="email:smtpTransport:custom-service">
|
||||
<h5>Custom Service</h5>
|
||||
|
||||
<label for="email:smtpTransport:host">[[admin/settings/email:smtp-transport.host]]</label>
|
||||
|
||||
@@ -12,13 +12,22 @@ const db = require('./mocks/databasemock');
|
||||
const user = require('../src/user');
|
||||
const utils = require('../src/utils');
|
||||
const meta = require('../src/meta');
|
||||
const plugins = require('../src/plugins');
|
||||
const privileges = require('../src/privileges');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
describe('authentication', () => {
|
||||
const jar = request.jar();
|
||||
let regularUid;
|
||||
const dummyEmailerHook = async (data) => {};
|
||||
|
||||
before((done) => {
|
||||
// Attach an emailer hook so related requests do not error
|
||||
plugins.hooks.register('authentication-test', {
|
||||
hook: 'filter:email.send',
|
||||
method: dummyEmailerHook,
|
||||
});
|
||||
|
||||
user.create({ username: 'regular', password: 'regularpwd', email: 'regular@nodebb.org' }, (err, uid) => {
|
||||
assert.ifError(err);
|
||||
regularUid = uid;
|
||||
@@ -27,6 +36,10 @@ describe('authentication', () => {
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
plugins.hooks.unregister('authentication-test', 'filter:email.send');
|
||||
});
|
||||
|
||||
it('should allow login with email for uid 1', async () => {
|
||||
const oldValue = meta.config.allowLoginWith;
|
||||
meta.config.allowLoginWith = 'username-email';
|
||||
|
||||
@@ -120,7 +120,7 @@ describe('email confirmation (library methods)', () => {
|
||||
await user.email.sendValidationEmail(uid, {
|
||||
email,
|
||||
});
|
||||
const ok = await user.email.canSendValidation(uid, 'test@example.com');
|
||||
const ok = await user.email.canSendValidation(uid, email);
|
||||
|
||||
assert.strictEqual(ok, false);
|
||||
});
|
||||
@@ -131,7 +131,7 @@ describe('email confirmation (library methods)', () => {
|
||||
email,
|
||||
});
|
||||
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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user