diff --git a/install/package.json b/install/package.json
index bef89c665d..2c80add9a3 100644
--- a/install/package.json
+++ b/install/package.json
@@ -77,7 +77,7 @@
"jquery-ui": "1.13.2",
"jsesc": "3.0.2",
"json2csv": "5.0.7",
- "jsonwebtoken": "8.5.1",
+ "jsonwebtoken": "9.0.0",
"less": "4.1.3",
"lodash": "4.17.21",
"logrotate-stream": "0.2.8",
@@ -131,7 +131,7 @@
"slideout": "1.0.1",
"socket.io": "4.5.4",
"socket.io-client": "4.5.4",
- "@socket.io/redis-adapter": "8.0.0",
+ "@socket.io/redis-adapter": "8.0.1",
"sortablejs": "1.15.0",
"spdx-license-list": "6.6.0",
"spider-detector": "2.0.0",
@@ -151,7 +151,7 @@
"zxcvbn": "4.4.2"
},
"devDependencies": {
- "@apidevtools/swagger-parser": "10.0.3",
+ "@apidevtools/swagger-parser": "9.0.0",
"@commitlint/cli": "17.4.1",
"@commitlint/config-angular": "17.4.0",
"coveralls": "3.1.1",
diff --git a/public/language/vi/admin/settings/advanced.json b/public/language/vi/admin/settings/advanced.json
index 4f4b14039f..961593854c 100644
--- a/public/language/vi/admin/settings/advanced.json
+++ b/public/language/vi/admin/settings/advanced.json
@@ -20,7 +20,7 @@
"headers.coep-help": "Khi được bật (mặc định), sẽ đặt tiêu đề thành require-corp",
"headers.coop": "Cross-Origin-Opener-Policy",
"headers.corp": "Cross-Origin-Resource-Policy",
- "headers.permissions-policy": "Permissions-Policy",
+ "headers.permissions-policy": "Quyền-Chính sách",
"headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.",
"hsts": "Bảo Vệ Truyền Tải Nghiêm Ngặt",
"hsts.enabled": "Đã bật HSTS (đề nghị)",
diff --git a/public/language/vi/admin/settings/email.json b/public/language/vi/admin/settings/email.json
index cdd5a01772..39332d35cb 100644
--- a/public/language/vi/admin/settings/email.json
+++ b/public/language/vi/admin/settings/email.json
@@ -6,7 +6,7 @@
"from-help": "Tên người gửi hiển thị trong email.",
"confirmation-settings": "Xác nhận",
- "confirmation.expiry": "Hours to keep email confirmation link valid",
+ "confirmation.expiry": "Số giờ để giữ cho liên kết xác nhận email hợp lệ",
"smtp-transport": "Truyền Tải SMTP",
"smtp-transport.enabled": "Bật truyền tải SMTP",
diff --git a/public/language/vi/admin/settings/reputation.json b/public/language/vi/admin/settings/reputation.json
index ba1d868e2b..968d57555d 100644
--- a/public/language/vi/admin/settings/reputation.json
+++ b/public/language/vi/admin/settings/reputation.json
@@ -27,5 +27,5 @@
"flags.action-on-resolve": "Do the following when a flag is resolved",
"flags.action-on-reject": "Do the following when a flag is rejected",
"flags.action.nothing": "Do nothing",
- "flags.action.rescind": "Rescind the notification send to moderators/administrators"
+ "flags.action.rescind": "Hủy bỏ gửi thông báo cho người điều hành/quản trị viên"
}
\ No newline at end of file
diff --git a/public/language/vi/admin/settings/user.json b/public/language/vi/admin/settings/user.json
index 2ed9abdd02..920b8135b3 100644
--- a/public/language/vi/admin/settings/user.json
+++ b/public/language/vi/admin/settings/user.json
@@ -1,7 +1,7 @@
{
"authentication": "Xác thực",
"email-confirm-interval": "Người dùng không thể gửi lại email xác nhận cho đến khi",
- "email-confirm-interval2": "minutes have elapsed",
+ "email-confirm-interval2": "phút đã trôi qua",
"allow-login-with": "Cho phép đăng nhập với",
"allow-login-with.username-email": "Tên Đăng Nhập hoặc Email",
"allow-login-with.username": "Chỉ Tên Đăng Nhập",
@@ -29,8 +29,8 @@
"session-time-days": "Ngày",
"session-time-seconds": "Giây",
"session-time-help": "Giá trị này dùng để điều chỉnh thời gian người dùng đăng nhập khi họ chọn "Nhớ Tôi" lúc đăng nhập. Lưu ý chỉ một trong những giá trị này sẽ được dùng. Nếu không có giá trị giây chúng tôi sẽ dùng ngày. Nếu không có ngày mặc định là 14 ngày.",
- "session-duration": "Session length if \"Remember Me\" is not checked (seconds)",
- "session-duration-help": "By default — or if set to 0 — a user will stay logged in for the duration of the session (e.g. however long the browser window/tab remains open). Set this value to explicitly invalidate the session after the specified number of seconds.",
+ "session-duration": "Thời lượng phiên nếu \"Ghi nhớ tôi\" không được chọn (giây)",
+ "session-duration-help": "Theo mặc định — hoặc nếu đặt thành 0 — người dùng sẽ duy trì trạng thái đăng nhập trong suốt thời gian của phiên (VD: cửa sổ/tab trình duyệt vẫn mở trong bao lâu). Đặt giá trị này để vô hiệu hóa rõ ràng phiên sau số giây đã chỉ định.",
"online-cutoff": "Số phút sau khi người dùng được coi là không hoạt động",
"online-cutoff-help": "Nếu người dùng không thao tác trong khoảng thời gian này, được coi là không hoạt động và không nhận được cập nhật theo thời gian thực.",
"registration": "Đăng Ký Người Dùng",
diff --git a/public/openapi/write/users/uid/emails.yaml b/public/openapi/write/users/uid/emails.yaml
index 8bd00809c0..c6b67acf9e 100644
--- a/public/openapi/write/users/uid/emails.yaml
+++ b/public/openapi/write/users/uid/emails.yaml
@@ -16,6 +16,57 @@ get:
responses:
'200':
description: user emails successfully listed
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ $ref: ../../../components/schemas/Status.yaml#/Status
+ response:
+ type: object
+ properties:
+ emails:
+ type: array
+ items:
+ type: string
+ description: An email address
+post:
+ tags:
+ - users
+ summary: add email to user
+ description: |
+ This operation adds an email to the user account, optionally bypassing the confirmation step if requested.
+
+ **Note**: The confirmation bypass can only be called by super administrators or users with the `admin:users` privilege.
+ parameters:
+ - in: path
+ name: uid
+ schema:
+ type: integer
+ required: true
+ description: uid of the account to add the email
+ example: 1
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ email:
+ type: string
+ description: A single email address
+ example: test@example.org
+ skipConfirmation:
+ type: boolean
+ description: If truthy, will automatically confirm the user's email.
+ example: 1
+ required:
+ - email
+ responses:
+ '200':
+ description: email successfully added to user account
content:
application/json:
schema:
diff --git a/src/api/users.js b/src/api/users.js
index c0bdf57cd6..f523da2ec3 100644
--- a/src/api/users.js
+++ b/src/api/users.js
@@ -307,18 +307,17 @@ async function isPrivilegedOrSelfAndPasswordMatch(caller, data) {
async function processDeletion({ uid, method, password, caller }) {
const isTargetAdmin = await user.isAdministrator(uid);
const isSelf = parseInt(uid, 10) === parseInt(caller.uid, 10);
- const isAdmin = await user.isAdministrator(caller.uid);
+ const hasAdminPrivilege = await privileges.admin.can('admin:users', caller.uid);
if (isSelf && meta.config.allowAccountDelete !== 1) {
throw new Error('[[error:account-deletion-disabled]]');
- } else if (!isSelf && !isAdmin) {
+ } else if (!isSelf && !hasAdminPrivilege) {
throw new Error('[[error:no-privileges]]');
} else if (isTargetAdmin) {
throw new Error('[[error:cant-delete-admin]');
}
// Privilege checks -- only deleteAccount is available for non-admins
- const hasAdminPrivilege = await privileges.admin.can('admin:users', caller.uid);
if (!hasAdminPrivilege && ['delete', 'deleteContent'].includes(method)) {
throw new Error('[[error:no-privileges]]');
}
diff --git a/src/controllers/write/users.js b/src/controllers/write/users.js
index 2610de5d1d..393481e809 100644
--- a/src/controllers/write/users.js
+++ b/src/controllers/write/users.js
@@ -253,6 +253,24 @@ Users.getInviteGroups = async function (req, res) {
return helpers.formatApiResponse(200, res, userInviteGroups.map(group => group.displayName));
};
+Users.addEmail = async (req, res) => {
+ const canManageUsers = await privileges.admin.can('admin:users', req.uid);
+ const skipConfirmation = canManageUsers && req.body.skipConfirmation;
+
+ if (skipConfirmation) {
+ await user.setUserField(req.params.uid, 'email', req.body.email);
+ await user.email.confirmByUid(req.params.uid);
+ } else {
+ await api.users.update(req, {
+ uid: req.params.uid,
+ email: req.body.email,
+ });
+ }
+
+ const emails = await db.getSortedSetRangeByScore('email:uid', 0, 500, req.params.uid, req.params.uid);
+ helpers.formatApiResponse(200, res, { emails });
+};
+
Users.listEmails = async (req, res) => {
const [isPrivileged, { showemail }] = await Promise.all([
user.isPrivileged(req.uid),
diff --git a/src/privileges/admin.js b/src/privileges/admin.js
index 5a733d30f4..166236ac76 100644
--- a/src/privileges/admin.js
+++ b/src/privileges/admin.js
@@ -66,6 +66,7 @@ privsAdmin.routeMap = {
uploadDefaultAvatar: 'admin:settings',
};
privsAdmin.routePrefixMap = {
+ 'dashboard/': 'admin:dashboard',
'manage/categories/': 'admin:categories',
'manage/privileges/': 'admin:privileges',
'manage/groups/': 'admin:groups',
diff --git a/src/routes/write/users.js b/src/routes/write/users.js
index 980eccc8a2..23d8d75ddd 100644
--- a/src/routes/write/users.js
+++ b/src/routes/write/users.js
@@ -48,6 +48,7 @@ function authenticatedRoutes() {
setupApiRoute(router, 'get', '/:uid/invites/groups', [...middlewares, middleware.assert.user], controllers.write.users.getInviteGroups);
setupApiRoute(router, 'get', '/:uid/emails', [...middlewares, middleware.assert.user], controllers.write.users.listEmails);
+ setupApiRoute(router, 'post', '/:uid/emails', [...middlewares, middleware.assert.user], controllers.write.users.addEmail);
setupApiRoute(router, 'get', '/:uid/emails/:email', [...middlewares, middleware.assert.user], controllers.write.users.getEmail);
setupApiRoute(router, 'post', '/:uid/emails/:email/confirm', [...middlewares, middleware.assert.user], controllers.write.users.confirmEmail);
diff --git a/test/categories.js b/test/categories.js
index 284d0a0696..4a92929864 100644
--- a/test/categories.js
+++ b/test/categories.js
@@ -826,7 +826,7 @@ describe('Categories', () => {
});
});
- describe.only('Categories.getModeratorUids', () => {
+ describe('Categories.getModeratorUids', () => {
let cid;
before(async () => {
@@ -865,7 +865,7 @@ describe('Categories', () => {
const payload = {};
payload[cid] = { disabled: 1 };
await Categories.update(payload);
- const uids = await Categories.getModeratorUids([1, 2]);
+ const uids = await Categories.getModeratorUids([cid, 2]);
assert(!uids[0].includes('1'));
});