Merge remote-tracking branch 'origin/develop' into activitypub

This commit is contained in:
Julian Lam
2024-12-11 10:12:33 -05:00
59 changed files with 771 additions and 177 deletions

View File

@@ -1,3 +1,227 @@
#### v3.11.0 (2024-11-27)
##### Chores
* **i18n:**
* fallback strings for new resources: nodebb.admin-settings-reputation, nodebb.error (985e5e3d)
* fallback strings for new resources: nodebb.admin-manage-user-custom-fields, nodebb.error (174be398)
* fallback strings for new resources: nodebb.error (b676c220)
* fallback strings for new resources: nodebb.admin-manage-user-custom-fields (b89036cd)
* fallback strings for new resources: nodebb.admin-settings-uploads (e3ef858b)
* fallback strings for new resources: nodebb.admin-manage-users, nodebb.error (15e16704)
* fallback strings for new resources: nodebb.topic (9fd5ca27)
* fallback strings for new resources: nodebb.pages, nodebb.user (1c26048f)
* migrate admin/settings/social.json into admin/settings/general.json (206613dd)
* migrate admin/settings/languages.json into admin/settings/general.json (ab143b1d)
* migrate admin/settings/homepage.json into admin/settings/general.json (a7678185)
* combine admin/settings/guest.json language file into admin/settings/user.json (3f14373c)
* up themes (388a156d)
* up harmony (2324a5bf)
* up harmony (03cea237)
* up themes (2ec3d1af)
* up themes (965ca636)
* up persona (1a4a9d08)
* up peace (acd42c23)
* up peace (7179a280)
* up peace (bcf8dee2)
* up peace (524b2b76)
* up themes (4ad082b4)
* up peace (953ea609)
* up widgets (6a71fd24)
* up harmony (f13b7f8d)
* up persona (6f89bf6b)
* up persona (79c93c85)
* up themes (d63a828d)
* up themes (6aa8f1b6)
* up peace (a4d8baf3)
* up themes (24e9adf6)
* up persona (baff68b4)
* up peace (c4b50607)
* up lavender (c32c63aa)
* up lavender (66214bea)
* update tx config (4ce387b9)
* up themes (65f64eba)
* up themes (280e7613)
* up widgets (b13bd803)
* incrementing version number - v3.10.3 (57d54224)
* update changelog for v3.10.3 (83965385)
* up harmony (f6f1d906)
* incrementing version number - v3.10.2 (2f15f464)
* incrementing version number - v3.10.1 (cca3a644)
* up harmony (9426fd1a)
* incrementing version number - v3.10.0 (b60a9b4e)
* incrementing version number - v3.9.1 (f120c91c)
* incrementing version number - v3.9.0 (4880f32d)
* incrementing version number - v3.8.4 (4833f9a6)
* incrementing version number - v3.8.3 (97ce2c44)
* incrementing version number - v3.8.2 (72d91251)
* incrementing version number - v3.8.1 (527326f7)
* incrementing version number - v3.8.0 (e228a6eb)
* incrementing version number - v3.7.5 (6882894d)
* incrementing version number - v3.7.4 (6678744c)
* incrementing version number - v3.7.3 (2d62b6f6)
* incrementing version number - v3.7.2 (cc257e7e)
* incrementing version number - v3.7.1 (712365a5)
* incrementing version number - v3.7.0 (9a6153d7)
* incrementing version number - v3.6.7 (86a17e38)
* incrementing version number - v3.6.6 (6604bf37)
* incrementing version number - v3.6.5 (6c653625)
* incrementing version number - v3.6.4 (83d131b4)
* incrementing version number - v3.6.3 (fc7d2bfd)
* incrementing version number - v3.6.2 (0f577a57)
* incrementing version number - v3.6.1 (f1a69468)
* incrementing version number - v3.6.0 (4cdf85f8)
* incrementing version number - v3.5.3 (ed0e8783)
* incrementing version number - v3.5.2 (52fbb2da)
* incrementing version number - v3.5.1 (4c543488)
* incrementing version number - v3.5.0 (d06fb4f0)
* incrementing version number - v3.4.3 (5c984250)
* incrementing version number - v3.4.2 (3f0dac38)
* incrementing version number - v3.4.1 (01e69574)
* incrementing version number - v3.4.0 (fd9247c5)
* incrementing version number - v3.3.9 (5805e770)
* incrementing version number - v3.3.8 (a5603565)
* incrementing version number - v3.3.7 (b26f1744)
* incrementing version number - v3.3.6 (7fb38792)
* incrementing version number - v3.3.4 (a67f84ea)
* incrementing version number - v3.3.3 (f94d239b)
* incrementing version number - v3.3.2 (ec9dac97)
* incrementing version number - v3.3.1 (151cc68f)
* incrementing version number - v3.3.0 (fc1ad70f)
* incrementing version number - v3.2.3 (b06d3e63)
* incrementing version number - v3.2.2 (758ecfcd)
* incrementing version number - v3.2.1 (20145074)
* incrementing version number - v3.2.0 (9ecac38e)
* incrementing version number - v3.1.7 (0b4e81ab)
* incrementing version number - v3.1.6 (b3a3b130)
* incrementing version number - v3.1.5 (ec19343a)
* incrementing version number - v3.1.4 (2452783c)
* incrementing version number - v3.1.3 (3b4e9d3f)
* incrementing version number - v3.1.2 (40fa3489)
* incrementing version number - v3.1.1 (40250733)
* incrementing version number - v3.1.0 (0cb386bd)
* incrementing version number - v3.0.1 (26f6ea49)
* incrementing version number - v3.0.0 (224e08cd)
* **deps:**
* update commitlint monorepo to v19.6.0 (#12920) (151e0164)
* update postgres docker tag to v17.2 (#12931) (036f4564)
* update postgres docker tag to v17.1 (#12915) (0d0f9144)
* update dependency sass-embedded to v1.81.0 (#12914) (ff258028)
* update dependency sass-embedded to v1.80.6 (#12894) (37f877b3)
* update dependency sass-embedded to v1.80.5 (#12889) (383f332c)
* update dependency mocha to v10.8.2 (#12884) (d86bedf8)
* update coverallsapp/github-action action to v2.3.4 (#12867) (351bcdbf)
* update dependency sass-embedded to v1.80.4 (#12861) (be0c92ec)
* update dependency smtp-server to v3.13.6 (#12859) (a0804485)
* update coverallsapp/github-action action to v2.3.3 (#12854) (48b09f1d)
* update coverallsapp/github-action action to v2.3.2 (#12846) (c16192dc)
* update dependency sass-embedded to v1.79.5 (#12850) (6f227264)
* update dependency eslint-plugin-import to v2.31.0 (#12832) (cc2e0e9f)
* update redis docker tag to v7.4.1 (#12836) (e7cba341)
* update mongo docker tag to v8 (#12810) (2fd89558)
* update dependency sass-embedded to v1.79.4 (#12825) (24928b17)
* update postgres docker tag to v17 (#12829) (d7299f90)
##### New Features
* use displayname in chat system messages, closes #12937 (e6f78d25)
* move website/location fields into custom user fields (669c9c50)
* add pagination to groups page, api routes (49e0e1ab)
* add date and multiselect custom fields (9cf85ced)
* new language file for user custom fields (05fb8aa5)
* closes #12902, allow adding users as post editors (bc00df3c)
* added nn_NO, takk\! (b9eff72d)
* add read topics route (757d7101)
##### Bug Fixes
* update acp template to use new lang keys (bc5b91aa)
* spec, dont show registered-users, verified-users, unverified-users in manage groups (8605584f)
* #12927, give more right padding on FF (5b3b003d)
* dont show chat upload btn if maxFileSize is 0, #12926 (14125858)
* dont allow core user fields to be used as custom fields (836e8458)
* button to match category dropdown (5e9b7b44)
* filter undefined posts/topics (db291ecd)
* don't crash if post is undefined (d4770908)
* spec (d5c9c0ba)
* block search showing old matches (a9fc13dd)
* missing template (008ad1e3)
* naive checking to handle uploaded images whose filenames end with '-resized' (1268ed50)
* update copy to reflect real default value of 2000px for `resizeImageWidthThreshold` (4614958b)
* editor removal (977d80c1)
* add warning txt file for nn-NO (23b3a64a)
* incorrect folder syntax for nn_NO (396c7766)
* use postData.topic.title instead of topicData.title (d24bc5c5)
* another missing await (10a85e94)
* **deps:**
* update dependency sortablejs to v1.15.4 (#12934) (2db5b815)
* update dependency nodebb-plugin-2factor to v7.5.7 (#12930) (f9c4815a)
* update dependency @fortawesome/fontawesome-free to v6.7.1 (#12929) (80b4d10b)
* update dependency mongodb to v6.11.0 (#12932) (c3b42c85)
* update dependency @fortawesome/fontawesome-free to v6.7.0 (#12921) (a3042479)
* update dependency ace-builds to v1.36.5 (#12913) (68637633)
* update dependency sass to v1.81.0 (#12916) (ee91e516)
* update dependency cron to v3.2.1 (#12911) (42791287)
* update dependency postcss to v8.4.49 (#12910) (85f55695)
* update dependency winston to v3.17.0 (#12906) (87c276dd)
* update dependency postcss to v8.4.48 (#12907) (aa57b248)
* update dependency nodebb-theme-peace to v2.2.8 (#12901) (8af37a7d)
* update dependency cron to v3.1.9 (#12899) (87ec8637)
* update dependency ace-builds to v1.36.4 (#12898) (3aa0fa61)
* update dependency winston to v3.16.0 (#12896) (a4f75ca1)
* update dependency compression to v1.7.5 (#12890) (f5439d95)
* update dependency webpack to v5.96.1 (#12895) (3d847a5a)
* update dependency sass to v1.80.6 (#12892) (c5dd8b67)
* update dependency jquery-ui to v1.14.1 (#12891) (3fb6587a)
* update dependency nodebb-plugin-composer-default to v10.2.42 (#12888) (2cb2ac3a)
* update dependency cron to v3.1.8 (#12881) (8ae2f503)
* update dependency chart.js to v4.4.6 (#12876) (89bd1a24)
* update dependency nodemailer to v6.9.16 (#12874) (51cb249e)
* update dependency pg-cursor to v2.12.1 (#12870) (e3203b66)
* update dependency pg to v8.13.1 (#12869) (b628013d)
* update socket.io packages to v4.8.1 (#12871) (cdd9caa7)
* update dependency nodebb-theme-harmony to v1.2.77 (#12868) (d30e1f91)
* update dependency mongodb to v6.10.0 (#12862) (6da24583)
* update dependency ace-builds to v1.36.3 (#12860) (8d8243c4)
* update dependency sass to v1.80.4 (#12855) (80034ae8)
* update socket.io packages to v4.8.0 (#12815) (4788e2a3)
* update dependency chart.js to v4.4.5 (#12852) (c504b49f)
* update dependency workerpool to v9.2.0 (#12851) (8dfb2237)
* update dependency sass to v1.79.5 (#12847) (ab859aec)
* update dependency cookie-parser to v1.4.7 (#12842) (526faf07)
* update dependency express to v4.21.1 (#12843) (0e7f11bd)
* update dependency express-session to v1.18.1 (#12844) (38c1072f)
* update dependency sanitize-html to v2.13.1 (#12839) (5159552d)
* update dependency winston to v3.15.0 (#12840) (29826cf2)
* update dependency webpack to v5.95.0 (#12828) (55faa8a2)
* update dependency sass to v1.79.4 (#12826) (376ac905)
* update dependency helmet to v7.2.0 (#12827) (5acf0398)
* update dependency nodebb-theme-harmony to v1.2.71 (#12820) (b1993ff0)
##### Other Changes
* remove unused utils (0e4fc531)
* remove unused utils (b6a79365)
* //github.com/NodeBB/NodeBB/issues/12824 (c4a60dbb)
##### Refactors
* shorter check for showfullname (6ebff2e1)
* remove unused uids (20da7148)
* no category sharing for a long time (6af5cef7)
* move flags into core (3dff083d)
* add some margin bottom to bs tags (40758b32)
* post queue btn and dropdown (038fb71e)
* search logic to use switch..case (bd76ccf0)
##### Tests
* fix tests (173c604a)
* fix crash due to excludeGroups (61d43ee1)
* fix spec (70d99501)
* fix spec (c586854a)
* update spec on plugins (f9178a39)
#### v3.10.3 (2024-10-23)
##### Chores

View File

@@ -1,5 +1,3 @@
version: '3.8'
services:
nodebb:
build: .

View File

@@ -50,7 +50,7 @@
"bootstrap": "5.3.3",
"bootswatch": "5.3.3",
"chalk": "4.1.2",
"chart.js": "4.4.6",
"chart.js": "4.4.7",
"cli-graph": "3.2.2",
"clipboard": "2.0.11",
"colors": "1.4.0",
@@ -69,7 +69,7 @@
"daemon": "1.1.0",
"diff": "7.0.0",
"esbuild": "0.24.0",
"express": "4.21.1",
"express": "4.21.2",
"express-session": "1.18.1",
"express-useragent": "1.0.15",
"fetch-cookie": "3.0.1",
@@ -105,12 +105,12 @@
"nodebb-plugin-emoji-android": "4.1.1",
"nodebb-plugin-markdown": "13.0.0-pre.10",
"nodebb-plugin-mentions": "4.6.10",
"nodebb-plugin-spam-be-gone": "2.2.2",
"nodebb-plugin-spam-be-gone": "2.3.0",
"nodebb-plugin-web-push": "0.7.0",
"nodebb-rewards-essentials": "1.0.0",
"nodebb-theme-harmony": "2.0.0-pre.43",
"nodebb-theme-lavender": "7.1.16",
"nodebb-theme-peace": "2.2.23",
"nodebb-theme-lavender": "7.1.17",
"nodebb-theme-peace": "2.2.28",
"nodebb-theme-persona": "14.0.0-pre.5",
"nodebb-widget-essentials": "7.0.31",
"nodemailer": "6.9.16",
@@ -129,7 +129,7 @@
"rss": "1.2.2",
"rtlcss": "4.3.0",
"sanitize-html": "2.13.1",
"sass": "1.81.0",
"sass": "1.82.0",
"satori": "^0.11.1",
"semver": "7.6.3",
"serve-favicon": "2.5.0",
@@ -148,7 +148,7 @@
"toobusy-js": "0.5.1",
"tough-cookie": "5.0.0",
"validator": "13.12.0",
"webpack": "5.96.1",
"webpack": "5.97.1",
"webpack-merge": "6.0.1",
"winston": "3.17.0",
"workerpool": "9.2.0",
@@ -169,15 +169,15 @@
"grunt-contrib-watch": "1.1.0",
"husky": "8.0.3",
"jsdom": "25.0.1",
"lint-staged": "15.2.10",
"mocha": "10.8.2",
"lint-staged": "15.2.11",
"mocha": "11.0.1",
"mocha-lcov-reporter": "1.3.0",
"mockdate": "3.0.5",
"nyc": "17.1.0",
"smtp-server": "3.13.6"
},
"optionalDependencies": {
"sass-embedded": "1.81.0"
"sass-embedded": "1.82.0"
},
"resolutions": {
"*/jquery": "3.7.1"

View File

@@ -2,6 +2,7 @@
const winston = require('winston');
const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');
@@ -13,7 +14,10 @@ const nconf = require('nconf');
const Benchpress = require('benchpressjs');
const { mkdirp } = require('mkdirp');
const { paths } = require('../src/constants');
const sass = require('../src/utils').getSass();
const utils = require('../src/utils');
const sass = utils.getSass();
const { generateToken, csrfSynchronisedProtection } = require('../src/middleware/csrf');
const app = express();
let server;
@@ -73,6 +77,13 @@ web.install = async function (port) {
app.use(bodyParser.urlencoded({
extended: true,
}));
app.use(session({
secret: utils.generateUUID(),
resave: false,
saveUninitialized: false,
}));
try {
await Promise.all([
compileTemplate(),
@@ -103,8 +114,8 @@ function launchExpress(port) {
}
function setupRoutes() {
app.get('/', welcome);
app.post('/', install);
app.get('/', csrfSynchronisedProtection, welcome);
app.post('/', csrfSynchronisedProtection, install);
app.get('/testdb', testDatabase);
app.get('/ping', ping);
app.get('/sping', ping);
@@ -160,6 +171,7 @@ function welcome(req, res) {
minimumPasswordStrength: defaults.minimumPasswordStrength,
installing: installing,
percentInstalled: installing ? ((Date.now() - timeStart) / totalTime * 100).toFixed(2) : 0,
csrf_token: generateToken(req),
});
}

View File

@@ -1,5 +1,5 @@
{
"post-sort-option": "帖子分类选项,1%",
"post-sort-option": "帖子分类选项,%1",
"topic-sort-option": "主题分类选项,%1",
"user-avatar-for": "用户头像%1",
"user-watched-tags": "用户关注标签",

View File

@@ -38,8 +38,8 @@
"chat.no-pinned-messages": "这里没有已置顶消息",
"chat.pin-message": "置顶消息",
"chat.unpin-message": "取消置顶消息",
"chat.public-rooms": "公开房间(1%",
"chat.private-rooms": "私有房间(1%",
"chat.public-rooms": "公开房间(%1",
"chat.private-rooms": "私有房间(%1",
"chat.create-room": "创建聊天室",
"chat.private.option": "私有(仅已加入房间用户可见)",
"chat.public.option": "公开(对选中组里的所有用户可见)",

View File

@@ -117,9 +117,9 @@
"thread-tools.purge-confirm": "确认清除此主题吗?",
"thread-tools.merge-topics": "合并主题",
"thread-tools.merge": "合并主题",
"topic-move-success": "注意:此主题将会被移动到“1%”。点击此处可取消。",
"topic-move-success": "注意:此主题将会被移动到“%1”。点击此处可取消。",
"topic-move-multiple-success": "注意:以下主题将会被移动到“%1”点击此处可取消。",
"topic-move-all-success": "注意 :全部主题都将被移动到“1%”,点击此处可取消。",
"topic-move-all-success": "注意 :全部主题都将被移动到“%1”,点击此处可取消。",
"topic-move-undone": "撤销主题移动",
"topic-move-posts-success": "此帖子将马上移动。点击此处撤销",
"topic-move-posts-undone": "撤销帖子移动",
@@ -170,7 +170,7 @@
"composer.schedule": "定时",
"composer.replying-to": "正在回复 %1",
"composer.new-topic": "新主题",
"composer.editing-in": "在 1% 中编辑帖子",
"composer.editing-in": "在 %1 中编辑帖子",
"composer.uploading": "正在上传...",
"composer.thumb-url-label": "粘贴主题缩略图网址",
"composer.thumb-title": "给此主题添加缩略图",

View File

@@ -80,20 +80,38 @@ paths:
$ref: 'read/admin/dashboard/topics.yaml'
/api/admin/dashboard/searches:
$ref: 'read/admin/dashboard/searches.yaml'
"/api/admin/settings/{term}":
$ref: 'read/admin/settings/term.yaml'
"/api/admin/settings/general":
$ref: 'read/admin/settings/general.yaml'
/api/admin/settings/navigation:
$ref: 'read/admin/settings/navigation.yaml'
/api/admin/settings/api:
$ref: 'read/admin/settings/api.yaml'
/api/admin/settings/email:
$ref: 'read/admin/settings/email.yaml'
/api/admin/settings/user:
$ref: 'read/admin/settings/user.yaml'
/api/admin/settings/reputation:
$ref: 'read/admin/settings/reputation.yaml'
/api/admin/settings/group:
$ref: 'read/admin/settings/group.yaml'
/api/admin/settings/tags:
$ref: 'read/admin/settings/tags.yaml'
/api/admin/settings/post:
$ref: 'read/admin/settings/post.yaml'
/api/admin/settings/activitypub:
$ref: 'read/admin/settings/activitypub.yaml'
/api/admin/settings/uploads:
$ref: 'read/admin/settings/uploads.yaml'
/api/admin/settings/email:
$ref: 'read/admin/settings/email.yaml'
/api/admin/settings/chat:
$ref: 'read/admin/settings/chat.yaml'
/api/admin/settings/pagination:
$ref: 'read/admin/settings/pagination.yaml'
/api/admin/settings/notifications:
$ref: 'read/admin/settings/notifications.yaml'
/api/admin/settings/api:
$ref: 'read/admin/settings/api.yaml'
/api/admin/settings/cookies:
$ref: 'read/admin/settings/cookies.yaml'
/api/admin/settings/web-crawler:
$ref: 'read/admin/settings/web-crawler.yaml'
/api/admin/settings/advanced:
$ref: 'read/admin/settings/advanced.yaml'
/api/admin/manage/categories:
@@ -124,8 +142,12 @@ paths:
$ref: 'read/admin/manage/uploads.yaml'
/api/admin/manage/digest:
$ref: 'read/admin/manage/digest.yaml'
"/api/admin/appearance/{term}":
$ref: 'read/admin/appearance/term.yaml'
"/api/admin/appearance/themes":
$ref: 'read/admin/appearance/themes.yaml'
"/api/admin/appearance/skins":
$ref: 'read/admin/appearance/skins.yaml'
"/api/admin/appearance/customise":
$ref: 'read/admin/appearance/customise.yaml'
/api/admin/extend/plugins:
$ref: 'read/admin/extend/plugins.yaml'
/api/admin/extend/widgets:

View File

@@ -2,13 +2,6 @@ get:
tags:
- admin
summary: Get appearance settings
parameters:
- name: term
in: path
required: true
schema:
type: string
example: themes
responses:
"200":
description: ""

View File

@@ -0,0 +1,11 @@
get:
tags:
- admin
summary: Get appearance settings
responses:
"200":
description: ""
content:
application/json:
schema:
$ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

View File

@@ -0,0 +1,11 @@
get:
tags:
- admin
summary: Get appearance settings
responses:
"200":
description: ""
content:
application/json:
schema:
$ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

View File

@@ -0,0 +1,16 @@
get:
tags:
- admin
summary: Get chat settings
responses:
"200":
description: ""
content:
application/json:
schema:
allOf:
- type: object
properties:
title:
type: string
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

View File

@@ -0,0 +1,16 @@
get:
tags:
- admin
summary: Get cookie settings
responses:
"200":
description: ""
content:
application/json:
schema:
allOf:
- type: object
properties:
title:
type: string
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

View File

@@ -2,13 +2,6 @@ get:
tags:
- admin
summary: Get system settings
parameters:
- name: term
in: path
required: true
schema:
type: string
example: general
responses:
"200":
description: ""

View File

@@ -0,0 +1,16 @@
get:
tags:
- admin
summary: Get group settings
responses:
"200":
description: ""
content:
application/json:
schema:
allOf:
- type: object
properties:
title:
type: string
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

View File

@@ -0,0 +1,16 @@
get:
tags:
- admin
summary: Get notification settings
responses:
"200":
description: ""
content:
application/json:
schema:
allOf:
- type: object
properties:
title:
type: string
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

View File

@@ -0,0 +1,16 @@
get:
tags:
- admin
summary: Get pagination settings
responses:
"200":
description: ""
content:
application/json:
schema:
allOf:
- type: object
properties:
title:
type: string
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

View File

@@ -0,0 +1,16 @@
get:
tags:
- admin
summary: Get reputation settings
responses:
"200":
description: ""
content:
application/json:
schema:
allOf:
- type: object
properties:
title:
type: string
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

View File

@@ -0,0 +1,16 @@
get:
tags:
- admin
summary: Get tag settings
responses:
"200":
description: ""
content:
application/json:
schema:
allOf:
- type: object
properties:
title:
type: string
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

View File

@@ -0,0 +1,16 @@
get:
tags:
- admin
summary: Get upload settings
responses:
"200":
description: ""
content:
application/json:
schema:
allOf:
- type: object
properties:
title:
type: string
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

View File

@@ -0,0 +1,16 @@
get:
tags:
- admin
summary: Get web crawler settings
responses:
"200":
description: ""
content:
application/json:
schema:
allOf:
- type: object
properties:
title:
type: string
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps

View File

@@ -32,13 +32,16 @@ define('forum/users', [
// Populate box with query if present
const searchEl = document.getElementById('search-user');
if (searchEl) {
const search = new URLSearchParams(document.location.search);
const query = search.get('query');
if (query) {
searchEl.value = query;
}
if (!utils.isMobile()) {
searchEl.focus();
}
}
};
function doSearch() {

View File

@@ -67,8 +67,8 @@ usersAPI.update = async function (caller, data) {
privileges.users.canEdit(caller.uid, data.uid),
]);
// Changing own email/username requires password confirmation
if (data.hasOwnProperty('email') || data.hasOwnProperty('username')) {
const isChangingEmailOrUsername = data.hasOwnProperty('email') || data.hasOwnProperty('username');
if (isChangingEmailOrUsername) {
await isPrivilegedOrSelfAndPasswordMatch(caller, data);
}
@@ -531,7 +531,6 @@ async function processDeletion({ uid, method, password, caller }) {
throw new Error('[[error:no-privileges]]');
}
// Self-deletions require a password
const hasPassword = await user.hasPassword(uid);
if (isSelf && hasPassword) {
const ok = await user.isPasswordCorrect(uid, password, caller.ip);

10
src/cache/lru.js vendored
View File

@@ -2,6 +2,8 @@
module.exports = function (opts) {
const { LRUCache } = require('lru-cache');
const os = require('os');
const pubsub = require('../pubsub');
// lru-cache@7 deprecations
@@ -99,7 +101,9 @@ module.exports = function (opts) {
cache.delete = cache.del;
cache.reset = function () {
pubsub.publish(`${cache.name}:lruCache:reset`);
pubsub.publish(`${cache.name}:lruCache:reset`, {
id: `${os.hostname()}:${process.pid}`,
});
localReset();
};
cache.clear = cache.reset;
@@ -110,8 +114,10 @@ module.exports = function (opts) {
cache.misses = 0;
}
pubsub.on(`${cache.name}:lruCache:reset`, () => {
pubsub.on(`${cache.name}:lruCache:reset`, ({ id }) => {
if (id !== `${os.hostname()}:${process.pid}`) {
localReset();
}
});
pubsub.on(`${cache.name}:lruCache:del`, (keys) => {

10
src/cache/ttl.js vendored
View File

@@ -2,6 +2,8 @@
module.exports = function (opts) {
const TTLCache = require('@isaacs/ttlcache');
const os = require('os');
const pubsub = require('../pubsub');
const ttlCache = new TTLCache(opts);
@@ -72,7 +74,9 @@ module.exports = function (opts) {
cache.delete = cache.del;
cache.reset = function () {
pubsub.publish(`${cache.name}:ttlCache:reset`);
pubsub.publish(`${cache.name}:ttlCache:reset`, {
id: `${os.hostname()}:${process.pid}`,
});
localReset();
};
cache.clear = cache.reset;
@@ -83,8 +87,10 @@ module.exports = function (opts) {
cache.misses = 0;
}
pubsub.on(`${cache.name}:ttlCache:reset`, () => {
pubsub.on(`${cache.name}:ttlCache:reset`, ({ id }) => {
if (id !== `${os.hostname()}:${process.pid}`) {
localReset();
}
});
pubsub.on(`${cache.name}:ttlCache:del`, (keys) => {

View File

@@ -165,7 +165,7 @@ module.exports = function (Categories) {
Categories.assignColours = function () {
const backgrounds = ['#AB4642', '#DC9656', '#F7CA88', '#A1B56C', '#86C1B9', '#7CAFC2', '#BA8BAF', '#A16946'];
const text = ['#ffffff', '#ffffff', '#333333', '#ffffff', '#333333', '#ffffff', '#ffffff', '#ffffff'];
const index = Math.floor(Math.random() * backgrounds.length);
const index = utils.secureRandom(0, backgrounds.length - 1);
return [backgrounds[index], text[index]];
};

View File

@@ -51,8 +51,8 @@ async function setup(initConfig) {
}
console.log('NodeBB Setup Completed. Run "./nodebb start" to manually start your NodeBB server.');
// If I am a child process, notify the parent of the returned data before exiting (useful for notifying
// hosts of auto-generated username/password during headless setups)
// If I am a child process, notify the parent of the returned data before exiting
// (useful for notifying hosts during headless setups)
if (process.send) {
process.send(data);
}

View File

@@ -8,6 +8,7 @@ const meta = require('../meta');
const plugins = require('../plugins');
const middleware = require('../middleware');
const helpers = require('../middleware/helpers');
const { secureRandom } = require('../utils');
exports.handle404 = helpers.try(async (req, res) => {
const relativePath = nconf.get('relative_path');
@@ -64,6 +65,6 @@ exports.send404 = helpers.try(async (req, res) => {
path: validator.escape(path),
title: '[[global:404.title]]',
bodyClass: helpers.buildBodyClass(req, res),
icon: icons[Math.floor(Math.random() * icons.length)],
icon: icons[secureRandom(0, icons.length - 1)],
});
});

View File

@@ -1,11 +1,15 @@
'use strict';
const nconf = require('nconf');
const user = require('../../user');
const helpers = require('../helpers');
const pagination = require('../../pagination');
const followController = module.exports;
const url = nconf.get('url');
followController.getFollowing = async function (req, res, next) {
await getFollow('account/following', 'following', req, res, next);
};
@@ -39,5 +43,12 @@ async function getFollow(tpl, name, req, res, next) {
payload.breadcrumbs = helpers.buildBreadcrumbs([{ text: username, url: `/user/${userslug}` }, { text: `[[user:${name}]]` }]);
res.locals.linkTags = [
{
rel: 'canonical',
href: `${url}${req.url.replace(/^\/api/, '')}`,
},
];
res.render(tpl, payload);
}

View File

@@ -1,11 +1,15 @@
'use strict';
const nconf = require('nconf');
const user = require('../../user');
const groups = require('../../groups');
const helpers = require('../helpers');
const groupsController = module.exports;
const url = nconf.get('url');
groupsController.get = async function (req, res) {
const { username, userslug } = await user.getUserFields(res.locals.uid, ['username', 'userslug']);
@@ -21,5 +25,11 @@ groupsController.get = async function (req, res) {
payload.groups = groupsData;
payload.title = `[[pages:account/groups, ${username}]]`;
payload.breadcrumbs = helpers.buildBreadcrumbs([{ text: username, url: `/user/${userslug}` }, { text: '[[global:header.groups]]' }]);
res.locals.linkTags = [
{
rel: 'canonical',
href: `${url}${req.url.replace(/^\/api/, '')}`,
},
];
res.render('account/groups', payload);
};

View File

@@ -1,5 +1,7 @@
'use strict';
const nconf = require('nconf');
const db = require('../../database');
const user = require('../../user');
const posts = require('../../posts');
@@ -13,6 +15,8 @@ const utils = require('../../utils');
const postsController = module.exports;
const url = nconf.get('url');
const templateToData = {
'account/bookmarks': {
type: 'posts',
@@ -247,6 +251,13 @@ async function getPostsFromUserSet(template, req, res) {
option.selected = option.url.includes(`sort=${req.query.sort}`);
});
res.locals.linkTags = [
{
rel: 'canonical',
href: `${url}${req.url.replace(/^\/api/, '')}`,
},
];
res.render(template, payload);
}

View File

@@ -16,6 +16,8 @@ const utils = require('../../utils');
const profileController = module.exports;
const url = nconf.get('url');
profileController.get = async function (req, res, next) {
const { userData } = res.locals;
if (!userData) {
@@ -159,11 +161,18 @@ function addTags(res, userData) {
);
}
res.locals.linkTags = [];
res.locals.linkTags.push({
rel: 'canonical',
href: `${url}/user/${userData.userslug}`,
});
if (meta.config.activitypubEnabled) {
res.locals.linkTags = [{
res.locals.linkTags.push({
rel: 'alternate',
type: 'application/activity+json',
href: `${nconf.get('url')}/uid/${userData.uid}`,
}];
});
}
}

View File

@@ -2,8 +2,14 @@
const appearanceController = module.exports;
appearanceController.get = function (req, res) {
const term = req.params.term ? req.params.term : 'themes';
res.render(`admin/appearance/${term}`, {});
appearanceController.themes = function (req, res) {
res.render(`admin/appearance/themes`, {});
};
appearanceController.skins = function (req, res) {
res.render(`admin/appearance/skins`, {});
};
appearanceController.customise = function (req, res) {
res.render(`admin/appearance/customise`, {});
};

View File

@@ -52,7 +52,7 @@ cacheController.dump = async function (req, res, next) {
local: require('../../cache'),
};
caches = await plugins.hooks.fire('filter:admin.cache.get', caches);
if (!caches[req.query.name]) {
if (!caches.hasOwnProperty(req.query.name)) {
return next();
}

View File

@@ -275,7 +275,7 @@ dashboardController.getLogins = async (req, res) => {
res.render('admin/dashboard/logins', {
set: 'logins',
query: req.query,
query: _.pick(req.query, ['units', 'until', 'count']),
stats,
summary,
sessions,
@@ -303,7 +303,7 @@ dashboardController.getUsers = async (req, res) => {
res.render('admin/dashboard/users', {
set: 'registrations',
query: req.query,
query: _.pick(req.query, ['units', 'until', 'count']),
stats,
summary,
users,
@@ -330,7 +330,7 @@ dashboardController.getTopics = async (req, res) => {
res.render('admin/dashboard/topics', {
set: 'topics',
query: req.query,
query: _.pick(req.query, ['units', 'until', 'count']),
stats,
summary,
topics: topicData,

View File

@@ -17,64 +17,20 @@ const translator = require('../../translator');
const settingsController = module.exports;
settingsController.get = async function (req, res) {
const term = req.params.term || 'general';
const payload = {
title: `[[admin/menu:settings/${term}]]`,
};
if (term === 'general') {
payload.routes = await helpers.getHomePageRoutes(req.uid);
payload.postSharing = await social.getPostSharing();
settingsController.general = async (req, res) => {
const routes = await helpers.getHomePageRoutes(req.uid);
const postSharing = await social.getPostSharing();
const languageData = await languages.list();
languageData.forEach((language) => {
language.selected = language.code === meta.config.defaultLang;
});
payload.languages = languageData;
payload.autoDetectLang = meta.config.autoDetectLang;
}
res.render(`admin/settings/${term}`, payload);
};
settingsController.email = async (req, res) => {
const emails = await emailer.getTemplates(meta.config);
res.render('admin/settings/email', {
title: '[[admin/menu:settings/email]]',
emails: emails,
sendable: emails.filter(e => !e.path.includes('_plaintext') && !e.path.includes('partials')).map(tpl => tpl.path),
services: emailer.listServices(),
});
};
settingsController.user = async (req, res) => {
const [notificationTypes, groupData] = await Promise.all([
notifications.getAllNotificationTypes(),
groups.getNonPrivilegeGroups('groups:createtime', 0, -1),
]);
const notificationSettings = notificationTypes.map(type => ({
name: type,
label: `[[notifications:${type.replace(/_/g, '-')}]]`,
}));
res.render('admin/settings/user', {
title: '[[admin/menu:settings/user]]',
notificationSettings: notificationSettings,
groupsExemptFromNewUserRestrictions: groupData,
});
};
settingsController.post = async (req, res) => {
const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1);
res.render('admin/settings/post', {
title: '[[admin/menu:settings/post]]',
groupsExemptFromPostQueue: groupData,
});
};
settingsController.advanced = async (req, res) => {
const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1);
res.render('admin/settings/advanced', {
title: '[[admin/menu:settings/advanced]]',
groupsExemptFromMaintenanceMode: groupData,
res.render('admin/settings/general', {
title: `[[admin/menu:settings/general]]`,
routes,
postSharing,
languages: languageData,
autoDetectLang: meta.config.autoDetectLang,
});
};
@@ -108,6 +64,83 @@ settingsController.navigation = async function (req, res) {
res.render('admin/settings/navigation', admin);
};
settingsController.user = async (req, res) => {
const [notificationTypes, groupData] = await Promise.all([
notifications.getAllNotificationTypes(),
groups.getNonPrivilegeGroups('groups:createtime', 0, -1),
]);
const notificationSettings = notificationTypes.map(type => ({
name: type,
label: `[[notifications:${type.replace(/_/g, '-')}]]`,
}));
res.render('admin/settings/user', {
title: '[[admin/menu:settings/user]]',
notificationSettings: notificationSettings,
groupsExemptFromNewUserRestrictions: groupData,
});
};
settingsController.reputation = async (req, res) => {
res.render(`admin/settings/reputation`, {
title: `[[admin/menu:settings/reputation]]`,
});
};
settingsController.group = async (req, res) => {
res.render(`admin/settings/group`, {
title: `[[admin/menu:settings/group]]`,
});
};
settingsController.tags = async (req, res) => {
res.render(`admin/settings/tags`, {
title: `[[admin/menu:settings/tags]]`,
});
};
settingsController.post = async (req, res) => {
const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1);
res.render('admin/settings/post', {
title: '[[admin/menu:settings/post]]',
groupsExemptFromPostQueue: groupData,
});
};
settingsController.uploads = async (req, res) => {
res.render(`admin/settings/uploads`, {
title: `[[admin/menu:settings/uploads]]`,
});
};
settingsController.email = async (req, res) => {
const emails = await emailer.getTemplates(meta.config);
res.render('admin/settings/email', {
title: '[[admin/menu:settings/email]]',
emails: emails,
sendable: emails.filter(e => !e.path.includes('_plaintext') && !e.path.includes('partials')).map(tpl => tpl.path),
services: emailer.listServices(),
});
};
settingsController.chat = async (req, res) => {
res.render(`admin/settings/chat`, {
title: `[[admin/menu:settings/chat]]`,
});
};
settingsController.pagination = async (req, res) => {
res.render(`admin/settings/pagination`, {
title: `[[admin/menu:settings/pagination]]`,
});
};
settingsController.notifications = async (req, res) => {
res.render(`admin/settings/notifications`, {
title: `[[admin/menu:settings/notifications]]`,
});
};
settingsController.api = async (req, res) => {
const page = parseInt(req.query.page, 10) || 1;
const resultsPerPage = 50;
@@ -133,3 +166,27 @@ settingsController.activitypub = async (req, res) => {
instanceCount,
});
};
settingsController.cookies = async (req, res) => {
res.render(`admin/settings/cookies`, {
title: `[[admin/menu:settings/cookies]]`,
});
};
settingsController.webCrawler = async (req, res) => {
res.render(`admin/settings/web-crawler`, {
title: `[[admin/menu:settings/web-crawler]]`,
});
};
settingsController.advanced = async (req, res) => {
const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1);
res.render('admin/settings/advanced', {
title: '[[admin/menu:settings/advanced]]',
groupsExemptFromMaintenanceMode: groupData,
});
};

View File

@@ -20,12 +20,18 @@ themesController.get = async function (req, res, next) {
themeConfig = JSON.parse(themeConfig);
} catch (err) {
if (err.code === 'ENOENT') {
return next(Error('invalid-data'));
return next(Error('[[error:invalid-data]]'));
}
return next(err);
}
const screenshotPath = themeConfig.screenshot ? path.join(themeDir, themeConfig.screenshot) : defaultScreenshotPath;
const exists = await file.exists(screenshotPath);
const screenshotPath = themeConfig.screenshot ?
path.join(themeDir, themeConfig.screenshot) :
'';
if (screenshotPath && !screenshotPath.startsWith(themeDir)) {
throw new Error('[[error:invalid-path]]');
}
const exists = screenshotPath ? await file.exists(screenshotPath) : false;
res.sendFile(exists ? screenshotPath : defaultScreenshotPath);
};

View File

@@ -12,6 +12,8 @@ const privileges = require('../privileges');
const groupsController = module.exports;
const url = nconf.get('url');
groupsController.list = async function (req, res) {
const sort = req.query.sort || 'alpha';
const page = parseInt(req.query.page, 10) || 1;
@@ -20,6 +22,13 @@ groupsController.list = async function (req, res) {
getGroups(req, sort, page),
]);
res.locals.linkTags = [
{
rel: 'canonical',
href: `${url}${req.url.replace(/^\/api/, '')}`,
},
];
res.render('groups/list', {
groups: groupData,
allowGroupCreation: allowGroupCreation,
@@ -101,6 +110,13 @@ groupsController.details = async function (req, res, next) {
return next();
}
res.locals.linkTags = [
{
rel: 'canonical',
href: `${url}/groups/${lowercaseSlug}`,
},
];
res.render('groups/details', {
title: `[[pages:group, ${groupData.displayName}]]`,
group: groupData,

View File

@@ -14,6 +14,8 @@ const helpers = require('./helpers');
const tagsController = module.exports;
const url = nconf.get('url');
tagsController.getTag = async function (req, res) {
const tag = validator.escape(utils.cleanUpTag(req.params.tag, meta.config.maximumTagLength));
const page = parseInt(req.query.page, 10) || 1;
@@ -98,6 +100,13 @@ tagsController.getTags = async function (req, res) {
topics.getCategoryTagsData(cids, 0, 99),
]);
res.locals.linkTags = [
{
rel: 'canonical',
href: `${url}/tags`,
},
];
res.render('tags', {
tags: tags.filter(Boolean),
displayTagSearch: canSearch,

View File

@@ -1,5 +1,7 @@
'use strict';
const nconf = require('nconf');
const user = require('../user');
const meta = require('../meta');
@@ -12,6 +14,8 @@ const utils = require('../utils');
const usersController = module.exports;
const url = nconf.get('url');
usersController.index = async function (req, res, next) {
const section = req.query.section || 'joindate';
const sectionToController = {
@@ -206,6 +210,13 @@ async function render(req, res, data) {
data['reputation:disabled'] = meta.config['reputation:disabled'];
res.locals.linkTags = [
{
rel: 'canonical',
href: `${url}${req.url.replace(/^\/api/, '')}`,
},
];
res.append('X-Total-Count', data.userCount);
res.render('users', data);
}

View File

@@ -3,6 +3,7 @@
module.exports = function (module) {
const _ = require('lodash');
const helpers = require('./helpers');
const { secureRandom } = require('../../utils');
module.setAdd = async function (key, value) {
if (!Array.isArray(value)) {
@@ -200,7 +201,7 @@ module.exports = function (module) {
return;
}
const randomIndex = Math.floor(Math.random() * data.members.length);
const randomIndex = secureRandom(0, data.members.length - 1);
const value = data.members[randomIndex];
await module.setRemove(data._key, value);
return value;

View File

@@ -687,6 +687,7 @@ Flags.canFlag = async function (type, id, uid, skipLimitCheck = false) {
Flags.getTarget = async function (type, id, uid) {
if (type === 'user') {
const userData = await user.getUserData(id);
userData.aboutme = validator.escape(String(userData.aboutme));
return userData && userData.uid ? userData : {};
}
if (type === 'post') {

View File

@@ -370,26 +370,21 @@ async function createAdmin() {
}
async function retryPassword(originalResults) {
// Ask only the password questions
const results = await prompt.get(passwordQuestions);
// Update the original data with newly collected password
originalResults.password = results.password;
originalResults['password:confirm'] = results['password:confirm'];
// Send back to success to handle
return await success(originalResults);
}
// Add the password questions
questions = questions.concat(passwordQuestions);
if (!install.values) {
const results = await prompt.get(questions);
return await success(results);
}
// If automated setup did not provide a user password, generate one,
// it will be shown to the user upon setup completion
if (!install.values.hasOwnProperty('admin:password') && !nconf.get('admin:password')) {
console.log('Password was not provided during automated setup, generating one...');
password = utils.generateUUID().slice(0, 8);

View File

@@ -11,7 +11,11 @@ let cached;
// cache buster is an 11-character, lowercase, alphanumeric string
function generate() {
return (Math.random() * 1e18).toString(32).slice(0, 11);
const crypto = require('crypto');
const length = 11;
const generated = crypto.randomBytes(Math.ceil(length / 2))
.toString('hex').slice(0, length);
return generated;
}
exports.write = async function write() {

View File

@@ -47,7 +47,6 @@ middleware.checkPrivileges = helpers.try(async (req, res, next) => {
}
}
// If user does not have password
const hasPassword = await user.hasPassword(req.uid);
if (!hasPassword) {
return next();

View File

@@ -29,7 +29,9 @@ async function getFakeHash() {
if (fakeHashCache) {
return fakeHashCache;
}
fakeHashCache = await exports.hash(12, Math.random().toString());
const length = 18;
fakeHashCache = crypto.randomBytes(Math.ceil(length / 2))
.toString('hex').slice(0, length);
return fakeHashCache;
}

View File

@@ -33,16 +33,28 @@ module.exports = function (app, name, middleware, controllers) {
helpers.setupAdminPageRoute(app, `/${name}/manage/uploads`, middlewares, controllers.admin.uploads.get);
helpers.setupAdminPageRoute(app, `/${name}/manage/digest`, middlewares, controllers.admin.digest.get);
helpers.setupAdminPageRoute(app, `/${name}/settings/email`, middlewares, controllers.admin.settings.email);
helpers.setupAdminPageRoute(app, `/${name}/settings/user`, middlewares, controllers.admin.settings.user);
helpers.setupAdminPageRoute(app, `/${name}/settings/post`, middlewares, controllers.admin.settings.post);
helpers.setupAdminPageRoute(app, `/${name}/settings/advanced`, middlewares, controllers.admin.settings.advanced);
helpers.setupAdminPageRoute(app, `/${name}/settings/general`, middlewares, controllers.admin.settings.general);
helpers.setupAdminPageRoute(app, `/${name}/settings/navigation`, middlewares, controllers.admin.settings.navigation);
helpers.setupAdminPageRoute(app, `/${name}/settings/user`, middlewares, controllers.admin.settings.user);
helpers.setupAdminPageRoute(app, `/${name}/settings/reputation`, middlewares, controllers.admin.settings.reputation);
helpers.setupAdminPageRoute(app, `/${name}/settings/group`, middlewares, controllers.admin.settings.group);
helpers.setupAdminPageRoute(app, `/${name}/settings/tags`, middlewares, controllers.admin.settings.tags);
helpers.setupAdminPageRoute(app, `/${name}/settings/post`, middlewares, controllers.admin.settings.post);
helpers.setupAdminPageRoute(app, `/${name}/settings/uploads`, middlewares, controllers.admin.settings.uploads);
helpers.setupAdminPageRoute(app, `/${name}/settings/email`, middlewares, controllers.admin.settings.email);
helpers.setupAdminPageRoute(app, `/${name}/settings/chat`, middlewares, controllers.admin.settings.chat);
helpers.setupAdminPageRoute(app, `/${name}/settings/pagination`, middlewares, controllers.admin.settings.pagination);
helpers.setupAdminPageRoute(app, `/${name}/settings/notifications`, middlewares, controllers.admin.settings.notifications);
helpers.setupAdminPageRoute(app, `/${name}/settings/api`, middlewares, controllers.admin.settings.api);
helpers.setupAdminPageRoute(app, `/${name}/settings/activitypub`, middlewares, controllers.admin.settings.activitypub);
helpers.setupAdminPageRoute(app, `/${name}/settings/:term?`, middlewares, controllers.admin.settings.get);
helpers.setupAdminPageRoute(app, `/${name}/settings/cookies`, middlewares, controllers.admin.settings.cookies);
helpers.setupAdminPageRoute(app, `/${name}/settings/web-crawler`, middlewares, controllers.admin.settings.webCrawler);
helpers.setupAdminPageRoute(app, `/${name}/settings/advanced`, middlewares, controllers.admin.settings.advanced);
helpers.setupAdminPageRoute(app, `/${name}/appearance/themes`, middlewares, controllers.admin.appearance.themes);
helpers.setupAdminPageRoute(app, `/${name}/appearance/skins`, middlewares, controllers.admin.appearance.skins);
helpers.setupAdminPageRoute(app, `/${name}/appearance/customise`, middlewares, controllers.admin.appearance.customise);
helpers.setupAdminPageRoute(app, `/${name}/appearance/:term?`, middlewares, controllers.admin.appearance.get);
helpers.setupAdminPageRoute(app, `/${name}/extend/plugins`, middlewares, controllers.admin.plugins.get);
helpers.setupAdminPageRoute(app, `/${name}/extend/widgets`, middlewares, controllers.admin.extend.widgets.get);

View File

@@ -24,7 +24,6 @@ require('./user/status')(SocketUser);
require('./user/picture')(SocketUser);
require('./user/registration')(SocketUser);
// Password Reset
SocketUser.reset = {};
SocketUser.reset.send = async function (socket, email) {
@@ -47,10 +46,10 @@ SocketUser.reset.send = async function (socket, email) {
try {
await user.reset.send(email);
await logEvent('[[success:success]]');
await sleep(2500 + ((Math.random() * 500) - 250));
await sleep(2500 + (utils.secureRandom(0, 500) - 250));
} catch (err) {
await logEvent(err.message);
await sleep(2500 + ((Math.random() * 500) - 250));
await sleep(2500 + (utils.secureRandom(0, 500) - 250));
const internalErrors = ['[[error:invalid-email]]'];
if (!internalErrors.includes(err.message)) {
throw err;

View File

@@ -93,7 +93,6 @@ module.exports = function (User) {
if (!fields.length) {
fields = results.whitelist;
} else {
// Never allow password retrieval via this method
fields = fields.filter(value => value !== 'password');
}

View File

@@ -282,7 +282,7 @@ User.addInterstitials = function (callback) {
plugins.hooks.register('core', {
hook: 'filter:register.interstitial',
method: [
User.interstitials.email, // Email address (for password reset + digest)
User.interstitials.email, // Email address
User.interstitials.gdpr, // GDPR information collection/processing consent + email consent
User.interstitials.tou, // Forum Terms of Use
],

View File

@@ -108,7 +108,6 @@ UserReset.commit = async function (code, password) {
'password:shaWrapped': 1,
};
// don't verify email if password reset is due to expiry
const isPasswordExpired = userData.passwordExpiry && userData.passwordExpiry < Date.now();
if (!isPasswordExpired) {
data['email:confirmed'] = 1;

View File

@@ -31,6 +31,16 @@ utils.generateUUID = function () {
return rnd.join('-');
};
utils.secureRandom = function (low, high) {
if (low > high) {
throw new Error("The 'low' parameter must be less than or equal to the 'high' parameter.");
}
const randomBuffer = crypto.randomBytes(4);
const randomInt = randomBuffer.readUInt32BE(0);
const range = high - low + 1;
return low + (randomInt % range);
};
utils.getSass = function () {
try {
const sass = require('sass-embedded');

View File

@@ -103,7 +103,7 @@
<div id="smtp-transport" class="mb-4">
<h5 class="fw-bold tracking-tight settings-header">[[admin/settings/email:smtp-transport]]</h5>
<div class="alert alert-warning">
<div class="alert alert-warning text-sm p-2">
[[admin/settings/email:smtp-transport-help]]
</div>

View File

@@ -293,7 +293,7 @@
<hr/>
<div id="post-sharing" class="mb-4">
<h5 class="fw-bold tracking-tight settings-header">[[admin/settings/social:post-sharing]]</h5>
<h5 class="fw-bold tracking-tight settings-header">[[admin/settings/general:post-sharing]]</h5>
<div class="mb-3">
<div class="form-group" id="postSharingNetworks">
{{{ each postSharing }}}
@@ -304,7 +304,7 @@
</label>
</div>
{{{ end }}}
<p class="form-text">[[admin/settings/social:info-plugins-additional]]</p>
<p class="form-text">[[admin/settings/general:info-plugins-additional]]</p>
</div>
</div>
</div>

View File

@@ -123,7 +123,7 @@
{{{ end }}}
{{{ if type_bool.user }}}
<div class="d-flex gap-2">
<div class="d-flex gap-2 align-items-center lh-1 mb-2">
<a href="{config.relative_path}/user/{./target.userslug}">{buildAvatar(target, "16px", true)}</a>
<a href="{config.relative_path}/user/{./target.userslug}">{target.username}</a>
</div>

View File

@@ -35,6 +35,8 @@
You are just a few steps away from launching your own NodeBB forum!
</p>
<form id="install" action="/" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="{csrf_token}" />
{{{ if !skipGeneralSetup }}}
<div class="general">
<p>

View File

@@ -9,7 +9,7 @@
</div>
<div class="flex-grow-1 ms-3">
<p>
<code>{./name}</code>
<code style="word-break: break-all;">{./name}</code>
</p>
<button class="btn btn-danger btn-sm text-nowrap" data-action="remove"><i class="fa fa-times"></i> [[modules:thumbs.modal.remove]]</button>
</div>

View File

@@ -138,7 +138,7 @@ describe('Admin Controllers', () => {
});
it('should load general settings page', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/admin/settings`, { jar: jar });
const { response, body } = await request.get(`${nconf.get('url')}/admin/settings/general`, { jar: jar });
assert.equal(response.statusCode, 200);
assert(body);
});
@@ -423,23 +423,6 @@ describe('Admin Controllers', () => {
assert(body);
});
it('should load /admin/settings/languages', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/admin/settings/languages`, { jar });
assert.equal(response.statusCode, 200);
assert(body);
});
it('should load /admin/settings/social', async () => {
const { body } = await request.get(`${nconf.get('url')}/api/admin/settings/general`, { jar });
assert(body);
const sharing = body.postSharing.map(network => network && network.id);
assert(sharing.includes('facebook'));
assert(sharing.includes('twitter'));
assert(sharing.includes('linkedin'));
assert(sharing.includes('whatsapp'));
assert(sharing.includes('telegram'));
});
it('should load /admin/manage/tags', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/admin/manage/tags`, { jar });
assert.equal(response.statusCode, 200);
@@ -476,6 +459,12 @@ describe('Admin Controllers', () => {
assert(body);
});
it('should load /admin/appearance/skins', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/admin/appearance/skins`, { jar });
assert.equal(response.statusCode, 200);
assert(body);
});
it('should load /admin/appearance/customise', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/admin/appearance/customise`, { jar });
assert.equal(response.statusCode, 200);

View File

@@ -102,7 +102,7 @@ describe('Utility Methods', () => {
});
});
describe('UUID generation', () => {
describe('UUID generation / secureRandom', () => {
it('return unique random value every time', () => {
delete require.cache[require.resolve('../src/utils')];
const { generateUUID } = require('../src/utils');
@@ -110,6 +110,19 @@ describe('Utility Methods', () => {
const uuid2 = generateUUID();
assert.notEqual(uuid1, uuid2, 'matches');
});
it('should return a random number between 1-10 inclusive', () => {
const { secureRandom } = require('../src/utils');
const r1 = secureRandom(1, 10);
assert(r1 >= 1);
assert(r1 <= 10);
});
it('should always return 3', () => {
const { secureRandom } = require('../src/utils');
const r1 = secureRandom(3, 3);
assert.strictEqual(r1, 3);
});
});
describe('cleanUpTag', () => {