mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-07 14:39:25 +08:00
Merge branch 'main' into next
This commit is contained in:
commit
702bdbe3d9
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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": "密码不允许修改"
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
|
@ -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({
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
@ -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();
|
||||
}
|
@ -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:*'],
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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": "用户资料不允许修改"
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
|
@ -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({
|
||||
|
Loading…
x
Reference in New Issue
Block a user