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')); });