Merge branch 'main' into next

This commit is contained in:
xilesun 2024-12-19 10:31:07 +08:00
commit 702bdbe3d9
15 changed files with 202 additions and 52 deletions

View File

@ -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 = () => {
<ActionContextProvider value={{ visible, setVisible }}>
<div onClick={(e) => e.stopPropagation()}>
<SchemaComponent
scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }}
scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues, enableEditProfile }}
schema={schema}
/>
</div>
@ -180,8 +165,10 @@ export const useEditProfile = () => {
),
};
}, [visible]);
if (enableEditProfile === false) {
return null;
}
return result;
};

View File

@ -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,
},

View File

@ -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"
}

View File

@ -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": "密码不允许修改"
}

View File

@ -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',

View File

@ -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;

View File

@ -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({

View File

@ -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,
});
});
});

View File

@ -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();
}

View File

@ -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:*'],

View File

@ -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"
}

View File

@ -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": "用户资料不允许修改"
}

View File

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

View File

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

View File

@ -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({