diff --git a/packages/core/client/src/user/EditProfile.tsx b/packages/core/client/src/user/EditProfile.tsx index 034a5a8473..8f78348cbd 100644 --- a/packages/core/client/src/user/EditProfile.tsx +++ b/packages/core/client/src/user/EditProfile.tsx @@ -83,11 +83,7 @@ const schema: ISchema = { title: "{{t('Nickname')}}", 'x-decorator': 'FormItem', 'x-component': 'Input', - 'x-reactions': (field) => { - if (field.initialValue) { - field.disabled = true; - } - }, + 'x-disabled': '{{ enableEditProfile === false }}', }, username: { type: 'string', @@ -96,11 +92,7 @@ const schema: ISchema = { 'x-component': 'Input', 'x-validator': { username: true }, required: true, - 'x-reactions': (field) => { - if (field.initialValue) { - field.disabled = true; - } - }, + 'x-disabled': '{{ enableEditProfile === false }}', }, email: { type: 'string', @@ -108,22 +100,14 @@ const schema: ISchema = { 'x-decorator': 'FormItem', 'x-component': 'Input', 'x-validator': 'email', - 'x-reactions': (field) => { - if (field.initialValue) { - field.disabled = true; - } - }, + 'x-disabled': '{{ enableEditProfile === false }}', }, phone: { type: 'string', title: '{{t("Phone")}}', 'x-decorator': 'FormItem', 'x-component': 'Input', - 'x-reactions': (field) => { - if (field.initialValue) { - field.disabled = true; - } - }, + 'x-disabled': '{{ enableEditProfile === false }}', }, footer: { 'x-component': 'Action.Drawer.Footer', @@ -139,6 +123,7 @@ const schema: ISchema = { submit: { title: 'Submit', 'x-component': 'Action', + 'x-disabled': '{{ enableEditProfile === false }}', 'x-component-props': { type: 'primary', useAction: '{{ useSaveCurrentUserValues }}', @@ -171,7 +156,7 @@ export const useEditProfile = () => {
e.stopPropagation()}>
@@ -180,8 +165,10 @@ export const useEditProfile = () => { ), }; }, [visible]); + if (enableEditProfile === false) { return null; } + return result; }; diff --git a/packages/core/client/src/user/LanguageSettings.tsx b/packages/core/client/src/user/LanguageSettings.tsx index 4eb87f132a..6f257d0bb2 100644 --- a/packages/core/client/src/user/LanguageSettings.tsx +++ b/packages/core/client/src/user/LanguageSettings.tsx @@ -35,7 +35,7 @@ export const useLanguageSettings = () => { })} defaultValue={i18n.language} onChange={async (lang) => { - await api.resource('users').updateProfile({ + await api.resource('users').updateLang({ values: { appLang: lang, }, diff --git a/packages/plugins/@nocobase/plugin-auth/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-auth/src/locale/en-US.json index 70ce40becd..35afad8c48 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/locale/en-US.json +++ b/packages/plugins/@nocobase/plugin-auth/src/locale/en-US.json @@ -28,5 +28,6 @@ "Show": "Show", "Sign up settings": "Sign up settings", "Sign up form": "Sign up form", - "At least one of the username or email fields is required": "At least one of the username or email fields is required" + "At least one of the username or email fields is required": "At least one of the username or email fields is required", + "Password is not allowed to be changed": "Password is not allowed to be changed" } diff --git a/packages/plugins/@nocobase/plugin-auth/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-auth/src/locale/zh-CN.json index 86e19bc222..82a817e9b8 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-auth/src/locale/zh-CN.json @@ -28,5 +28,6 @@ "Show": "显示", "Sign up settings": "注册设置", "Sign up form": "注册表单", - "At least one of the username or email fields is required": "至少需要设置用户名或邮箱中的一个字段为必填字段" + "At least one of the username or email fields is required": "至少需要设置用户名或邮箱中的一个字段为必填字段", + "Password is not allowed to be changed": "密码不允许修改" } diff --git a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/actions.test.ts b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/actions.test.ts index 13e715d554..3dab29b06c 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/actions.test.ts +++ b/packages/plugins/@nocobase/plugin-auth/src/server/__tests__/actions.test.ts @@ -110,7 +110,7 @@ describe('actions', () => { process.env.INIT_ROOT_PASSWORD = '123456'; process.env.INIT_ROOT_NICKNAME = 'Test'; app = await createMockServer({ - plugins: ['field-sort', 'auth', 'users'], + plugins: ['field-sort', 'auth', 'users', 'system-settings'], }); db = app.db; agent = app.agent(); @@ -264,6 +264,31 @@ describe('actions', () => { expect(res3.statusCode).toEqual(200); }); + it('should not allow to change password', async () => { + await db.getRepository('systemSettings').update({ + filterByTk: 1, + values: { + enableChangePassword: false, + }, + }); + const userRepo = db.getRepository('users'); + const user = await userRepo.create({ + values: { + username: 'test', + password: '12345', + }, + }); + const userAgent = await agent.login(user); + + const res = await userAgent.post('/auth:changePassword').set({ 'X-Authenticator': 'basic' }).send({ + oldPassword: '12345', + newPassword: '123456', + confirmPassword: '123456', + }); + expect(res.statusCode).toEqual(403); + expect(res.error.text).toBe('Password is not allowed to be changed'); + }); + it('should check confirm password when signing up', async () => { const res = await agent.post('/auth:signUp').set({ 'X-Authenticator': 'basic' }).send({ username: 'new', diff --git a/packages/plugins/@nocobase/plugin-auth/src/server/actions/auth.ts b/packages/plugins/@nocobase/plugin-auth/src/server/actions/auth.ts index 71f18907db..6957b06bf6 100644 --- a/packages/plugins/@nocobase/plugin-auth/src/server/actions/auth.ts +++ b/packages/plugins/@nocobase/plugin-auth/src/server/actions/auth.ts @@ -26,6 +26,13 @@ export default { // await next(); // }, changePassword: async (ctx: Context, next: Next) => { + const systemSettings = ctx.db.getRepository('systemSettings'); + const settings = await systemSettings.findOne(); + const enableChangePassword = settings.get('enableChangePassword'); + if (enableChangePassword === false) { + ctx.throw(403, ctx.t('Password is not allowed to be changed', { ns: namespace })); + } + const { values: { oldPassword, newPassword, confirmPassword }, } = ctx.action.params; diff --git a/packages/plugins/@nocobase/plugin-theme-editor/src/client/hooks/useUpdateThemeSettings.tsx b/packages/plugins/@nocobase/plugin-theme-editor/src/client/hooks/useUpdateThemeSettings.tsx index fdba6f7209..4916cbe06a 100644 --- a/packages/plugins/@nocobase/plugin-theme-editor/src/client/hooks/useUpdateThemeSettings.tsx +++ b/packages/plugins/@nocobase/plugin-theme-editor/src/client/hooks/useUpdateThemeSettings.tsx @@ -21,12 +21,9 @@ export function useUpdateThemeSettings() { return; } try { - await api.resource('users').updateProfile({ + await api.resource('users').updateTheme({ values: { - systemSettings: { - ...(currentUser.data.data.systemSettings || {}), - themeId, - }, + themeId, }, }); currentUser.mutate({ diff --git a/packages/plugins/@nocobase/plugin-theme-editor/src/server/__tests__/actions.test.ts b/packages/plugins/@nocobase/plugin-theme-editor/src/server/__tests__/actions.test.ts new file mode 100644 index 0000000000..0ecd488f0e --- /dev/null +++ b/packages/plugins/@nocobase/plugin-theme-editor/src/server/__tests__/actions.test.ts @@ -0,0 +1,59 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import Database from '@nocobase/database'; +import { createMockServer, MockServer } from '@nocobase/test'; + +describe('actions', () => { + let app: MockServer; + let db: Database; + let adminUser; + let agent; + let adminAgent; + + beforeEach(async () => { + process.env.INIT_ROOT_EMAIL = 'test@nocobase.com'; + process.env.INIT_ROOT_PASSWORD = '123456'; + process.env.INIT_ROOT_NICKNAME = 'Test'; + app = await createMockServer({ + plugins: ['auth', 'users', 'acl', 'data-source-manager', 'system-settings', 'theme-editor'], + }); + db = app.db; + + adminUser = await db.getRepository('users').findOne({ + filter: { + email: process.env.INIT_ROOT_EMAIL, + }, + }); + + agent = app.agent(); + adminAgent = app.agent().login(adminUser); + }); + + afterEach(async () => { + await app.db.clean({ drop: true }); + await app.destroy(); + }); + + it('update theme', async () => { + const res = await adminAgent.resource('users').updateTheme({ + values: { + themeId: 2, + }, + }); + expect(res.status).toBe(200); + const user = await db.getRepository('users').findOne({ + filterByTk: adminUser.id, + }); + expect(user.systemSettings).toMatchObject({ + ...user.systemSettings, + themeId: 2, + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-theme-editor/src/server/actions/update-user-theme.ts b/packages/plugins/@nocobase/plugin-theme-editor/src/server/actions/update-user-theme.ts new file mode 100644 index 0000000000..ff793fe049 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-theme-editor/src/server/actions/update-user-theme.ts @@ -0,0 +1,30 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Context, Next } from '@nocobase/actions'; + +export async function updateTheme(ctx: Context, next: Next) { + const { themeId } = ctx.action.params.values || {}; + const { currentUser } = ctx.state; + if (!currentUser) { + ctx.throw(401); + } + const userRepo = ctx.db.getRepository('users'); + const user = await userRepo.findOne({ filter: { id: currentUser.id } }); + await userRepo.update({ + filterByTk: currentUser.id, + values: { + systemSettings: { + ...user.systemSettings, + themeId, + }, + }, + }); + await next(); +} diff --git a/packages/plugins/@nocobase/plugin-theme-editor/src/server/plugin.ts b/packages/plugins/@nocobase/plugin-theme-editor/src/server/plugin.ts index 14d8926ecb..bf7cbb830a 100644 --- a/packages/plugins/@nocobase/plugin-theme-editor/src/server/plugin.ts +++ b/packages/plugins/@nocobase/plugin-theme-editor/src/server/plugin.ts @@ -8,8 +8,8 @@ */ import { InstallOptions, Plugin } from '@nocobase/server'; -import path, { resolve } from 'path'; import { compact, compactDark, dark, defaultTheme } from './builtinThemes'; +import { updateTheme } from './actions/update-user-theme'; export class PluginThemeEditorServer extends Plugin { theme: any; @@ -19,17 +19,10 @@ export class PluginThemeEditorServer extends Plugin { async beforeLoad() {} async load() { - await this.importCollections(path.resolve(__dirname, './collections')); - this.db.addMigrations({ - namespace: 'theme-editor', - directory: resolve(__dirname, './migrations'), - context: { - plugin: this, - }, - }); + this.app.resourceManager.registerActionHandler('users:updateTheme', updateTheme); + this.app.acl.allow('users', 'updateTheme', 'loggedIn'); this.app.acl.allow('themeConfig', 'list', 'public'); - this.app.acl.registerSnippet({ name: `pm.${this.name}.themeConfig`, actions: ['themeConfig:*'], diff --git a/packages/plugins/@nocobase/plugin-users/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-users/src/locale/en-US.json index e81f513864..a65c1d425a 100644 --- a/packages/plugins/@nocobase/plugin-users/src/locale/en-US.json +++ b/packages/plugins/@nocobase/plugin-users/src/locale/en-US.json @@ -3,5 +3,6 @@ "Add users": "Add users", "Remove user": "Remove user", "Are you sure you want to remove it?": "Are you sure you want to remove it?", - "Random password": "Random password" + "Random password": "Random password", + "User profile is not allowed to be edited": "User profile is not allowed to be edited" } diff --git a/packages/plugins/@nocobase/plugin-users/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-users/src/locale/zh-CN.json index 2153c4fe3d..b5415cacdb 100644 --- a/packages/plugins/@nocobase/plugin-users/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-users/src/locale/zh-CN.json @@ -6,5 +6,6 @@ "Random password": "随机密码", "Users manager": "用户管理", "Allow edit profile": "允许修改个人资料", - "Allow change password": "允许修改密码" + "Allow change password": "允许修改密码", + "User profile is not allowed to be edited": "用户资料不允许修改" } diff --git a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/actions.test.ts b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/actions.test.ts index 831aac71f0..ce426504f5 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/__tests__/actions.test.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/__tests__/actions.test.ts @@ -23,7 +23,7 @@ describe('actions', () => { process.env.INIT_ROOT_PASSWORD = '123456'; process.env.INIT_ROOT_NICKNAME = 'Test'; app = await createMockServer({ - plugins: ['field-sort', 'auth', 'users', 'acl', 'data-source-manager'], + plugins: ['field-sort', 'auth', 'users', 'acl', 'data-source-manager', 'system-settings'], }); db = app.db; @@ -40,6 +40,7 @@ describe('actions', () => { }); afterEach(async () => { + await app.db.clean({ drop: true }); await app.destroy(); }); @@ -61,6 +62,23 @@ describe('actions', () => { expect(res2.status).toBe(200); }); + it('update profile not allowed', async () => { + await db.getRepository('systemSettings').update({ + filterByTk: 1, + values: { + enableEditProfile: false, + }, + }); + const res = await agent.resource('users').updateProfile({ + filterByTk: adminUser.id, + values: { + nickname: 'a', + }, + }); + expect(res.status).toBe(403); + expect(res.error.text).toBe('User profile is not allowed to be edited'); + }); + it('update profile, but not roles', async () => { expect(adminUser.roles.length).not.toBe(0); const res2 = await adminAgent.resource('users').updateProfile({ @@ -70,11 +88,6 @@ describe('actions', () => { username: 'a', email: 'test@nocobase.com', phone: '12345678901', - systemSettings: { - ...adminUser.systemSettings, - themeId: 1, - }, - appLang: 'zh-CN', roles: [], }, }); @@ -87,8 +100,19 @@ describe('actions', () => { expect(user.username).toBe('a'); expect(user.email).toBe('test@nocobase.com'); expect(user.phone).toBe('12345678901'); - expect(user.systemSettings.themeId).toBe(1); - expect(user.appLang).toBe('zh-CN'); expect(user.roles.length).not.toBe(0); }); + + it('update lang', async () => { + const res = await adminAgent.resource('users').updateLang({ + values: { + appLang: 'zh-CN', + }, + }); + expect(res.status).toBe(200); + const user = await db.getRepository('users').findOne({ + filterByTk: adminUser.id, + }); + expect(user.appLang).toBe('zh-CN'); + }); }); diff --git a/packages/plugins/@nocobase/plugin-users/src/server/actions/users.ts b/packages/plugins/@nocobase/plugin-users/src/server/actions/users.ts index d967b0e37c..af8f00112e 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/actions/users.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/actions/users.ts @@ -9,8 +9,16 @@ import { Context, DEFAULT_PAGE, DEFAULT_PER_PAGE, Next } from '@nocobase/actions'; import _ from 'lodash'; +import { namespace } from '..'; export async function updateProfile(ctx: Context, next: Next) { + const systemSettings = ctx.db.getRepository('systemSettings'); + const settings = await systemSettings.findOne(); + const enableEditProfile = settings.get('enableEditProfile'); + if (enableEditProfile === false) { + ctx.throw(403, ctx.t('User profile is not allowed to be edited', { ns: namespace })); + } + const values = ctx.action.params.values || {}; const { currentUser } = ctx.state; if (!currentUser) { @@ -19,12 +27,28 @@ export async function updateProfile(ctx: Context, next: Next) { const UserRepo = ctx.db.getRepository('users'); const result = await UserRepo.update({ filterByTk: currentUser.id, - values: _.pick(values, ['nickname', 'username', 'email', 'phone', 'systemSettings', 'appLang']), + values: _.pick(values, ['nickname', 'username', 'email', 'phone']), }); ctx.body = result; await next(); } +export async function updateLang(ctx: Context, next: Next) { + const { appLang } = ctx.action.params.values || {}; + const { currentUser } = ctx.state; + if (!currentUser) { + ctx.throw(401); + } + const userRepo = ctx.db.getRepository('users'); + await userRepo.update({ + filterByTk: currentUser.id, + values: { + appLang, + }, + }); + await next(); +} + export const listExcludeRole = async (ctx: Context, next: Next) => { const { roleName, page = DEFAULT_PAGE, pageSize = DEFAULT_PER_PAGE } = ctx.action.params; const repo = ctx.db.getRepository('users'); diff --git a/packages/plugins/@nocobase/plugin-users/src/server/server.ts b/packages/plugins/@nocobase/plugin-users/src/server/server.ts index 319d307eab..6116723582 100644 --- a/packages/plugins/@nocobase/plugin-users/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-users/src/server/server.ts @@ -143,7 +143,7 @@ export default class PluginUsersServer extends Plugin { }; }); - const loggedInActions = ['updateProfile']; + const loggedInActions = ['updateProfile', 'updateLang']; loggedInActions.forEach((action) => this.app.acl.allow('users', action, 'loggedIn')); this.app.acl.registerSnippet({