mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 12:06:47 +00:00
feat: improve users module
This commit is contained in:
parent
1db71b166d
commit
78f75f5a2f
@ -1,6 +1,7 @@
|
||||
import { observable } from '@formily/reactive';
|
||||
import { Result } from 'ahooks/lib/useRequest/src/types';
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import Cookies from 'js-cookie';
|
||||
import qs from 'qs';
|
||||
|
||||
export interface ActionParams {
|
||||
@ -54,18 +55,25 @@ export class APIClient {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO
|
||||
authMiddleware() {
|
||||
this.axios.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem(this.tokenKey);
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
const currentRoleName = Cookies.get('currentRoleName');
|
||||
if (currentRoleName) {
|
||||
config.headers['X-Role'] = currentRoleName;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
}
|
||||
|
||||
// TODO
|
||||
setBearerToken(token: any) {
|
||||
localStorage.setItem(this.tokenKey, token || '');
|
||||
Cookies.remove('currentRoleName');
|
||||
}
|
||||
|
||||
service(uid: string): Result<any, any> {
|
||||
|
134
packages/client/src/user/ChangePassword.tsx
Normal file
134
packages/client/src/user/ChangePassword.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Menu } from 'antd';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActionContext, SchemaComponent, useActionContext } from '../';
|
||||
import { useAPIClient } from '../api-client';
|
||||
import { DropdownVisibleContext } from './CurrentUser';
|
||||
|
||||
const useCloseAction = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
const form = useForm();
|
||||
return {
|
||||
async run() {
|
||||
setVisible(false);
|
||||
form.submit((values) => {
|
||||
console.log(values);
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const useSaveCurrentUserValues = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
const form = useForm();
|
||||
const api = useAPIClient();
|
||||
return {
|
||||
async run() {
|
||||
await form.submit();
|
||||
await api.resource('users').changePassword({
|
||||
values: form.values,
|
||||
});
|
||||
setVisible(false);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const schema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
[uid()]: {
|
||||
'x-decorator': 'Form',
|
||||
'x-component': 'Action.Drawer',
|
||||
type: 'void',
|
||||
title: '修改密码',
|
||||
properties: {
|
||||
oldPassword: {
|
||||
type: 'string',
|
||||
title: '{{t("Old Password")}}',
|
||||
required: true,
|
||||
'x-component': 'Password',
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
newPassword: {
|
||||
type: 'string',
|
||||
title: '{{t("New Password")}}',
|
||||
required: true,
|
||||
'x-component': 'Password',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': { placeholder: '{{t("New Password")}}', checkStrength: true, style: {} },
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.confirmPassword'],
|
||||
fulfill: {
|
||||
state: {
|
||||
selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? t("Password mismatch") : ""}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
confirmPassword: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
title: '{{t("Confirm Password")}}',
|
||||
'x-component': 'Password',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': { placeholder: '{{t("Confirm password")}}', checkStrength: true, style: {} },
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['.newPassword'],
|
||||
fulfill: {
|
||||
state: {
|
||||
selfErrors: '{{$deps[0] && $self.value && $self.value !== $deps[0] ? t("Password mismatch") : ""}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
type: 'void',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: '{{t("Cancel")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ useCloseAction }}',
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
title: '{{t("Submit")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: '{{ useSaveCurrentUserValues }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ChangePassword = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const ctx = useContext(DropdownVisibleContext);
|
||||
return (
|
||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||
<Menu.Item
|
||||
eventKey={'ChangePassword'}
|
||||
onClick={() => {
|
||||
ctx?.setVisible?.(false);
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
{t('Change password')}
|
||||
</Menu.Item>
|
||||
<SchemaComponent scope={{ useCloseAction, useSaveCurrentUserValues }} schema={schema} />
|
||||
</ActionContext.Provider>
|
||||
);
|
||||
};
|
@ -1,41 +1,52 @@
|
||||
import { Menu } from 'antd';
|
||||
import React from 'react';
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import React, { createContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useAPIClient, useDesignable } from '..';
|
||||
import { ProfileAction } from './ProfileAction';
|
||||
import { useAPIClient, useCurrentUserContext } from '..';
|
||||
import { ChangePassword } from './ChangePassword';
|
||||
import { EditProfile } from './EditProfile';
|
||||
import { LanguageSettings } from './LanguageSettings';
|
||||
import { SwitchRole } from './SwitchRole';
|
||||
|
||||
export const DropdownVisibleContext = createContext(null);
|
||||
|
||||
export const CurrentUser = () => {
|
||||
const history = useHistory();
|
||||
const { reset } = useDesignable();
|
||||
const api = useAPIClient();
|
||||
const { i18n } = useTranslation();
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { data } = useCurrentUserContext();
|
||||
return (
|
||||
<div style={{ display: 'inline-block' }}>
|
||||
<Menu selectable={false} mode={'horizontal'} theme={'dark'}>
|
||||
<Menu.SubMenu key={'current-user'} title={'超级管理员'}>
|
||||
<ProfileAction />
|
||||
<Menu.Item>切换角色</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
i18n.changeLanguage(i18n.language === 'en-US' ? 'zh-CN' : 'en-US');
|
||||
// reset();
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
语言设置
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
api.setBearerToken(null);
|
||||
history.push('/signin');
|
||||
}}
|
||||
>
|
||||
注销
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
</Menu>
|
||||
<div style={{ display: 'inline-block', verticalAlign: 'top' }}>
|
||||
<DropdownVisibleContext.Provider value={{ visible, setVisible }}>
|
||||
<Dropdown
|
||||
visible={visible}
|
||||
onVisibleChange={(visible) => {
|
||||
setVisible(visible);
|
||||
}}
|
||||
overlay={
|
||||
<Menu>
|
||||
<EditProfile />
|
||||
<ChangePassword />
|
||||
<SwitchRole />
|
||||
<LanguageSettings />
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
api.setBearerToken(null);
|
||||
history.push('/signin');
|
||||
}}
|
||||
>
|
||||
{t('Sign out')}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button ghost style={{ border: 0 }}>
|
||||
{data?.data?.nickname || data?.data?.email}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</DropdownVisibleContext.Provider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,10 +1,14 @@
|
||||
import { Spin } from 'antd';
|
||||
import React, { createContext } from 'react';
|
||||
import React, { createContext, useContext } from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { useRequest } from '../api-client';
|
||||
|
||||
export const CurrentUserContext = createContext(null);
|
||||
|
||||
export const useCurrentUserContext = () => {
|
||||
return useContext(CurrentUserContext);
|
||||
}
|
||||
|
||||
export const CurrentUserProvider = (props) => {
|
||||
const result = useRequest({
|
||||
url: 'users:check',
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { ISchema, useForm } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Menu } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { ActionContext, SchemaComponent, useActionContext, useRequest } from '../';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActionContext, DropdownVisibleContext, SchemaComponent, useActionContext, useCurrentUserContext, useRequest } from '../';
|
||||
|
||||
const useCloseAction = () => {
|
||||
const { setVisible } = useActionContext();
|
||||
@ -17,16 +18,9 @@ const useCloseAction = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const useCurrentUserValues = (props, options) => {
|
||||
return useRequest(
|
||||
() =>
|
||||
Promise.resolve({
|
||||
data: {},
|
||||
}),
|
||||
{
|
||||
...options,
|
||||
},
|
||||
);
|
||||
const useCurrentUserValues = (options) => {
|
||||
const ctx = useCurrentUserContext();
|
||||
return useRequest(() => Promise.resolve(ctx.data), options);
|
||||
};
|
||||
|
||||
const useSaveCurrentUserValues = () => {
|
||||
@ -87,17 +81,20 @@ const schema: ISchema = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ProfileAction = () => {
|
||||
export const EditProfile = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const ctx = useContext(DropdownVisibleContext);
|
||||
return (
|
||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||
<Menu.Item
|
||||
eventKey={'ProfileAction'}
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
ctx.setVisible(false);
|
||||
}}
|
||||
>
|
||||
个人资料
|
||||
{t('Edit profile')}
|
||||
</Menu.Item>
|
||||
<SchemaComponent scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }} schema={schema} />
|
||||
</ActionContext.Provider>
|
34
packages/client/src/user/LanguageSettings.tsx
Normal file
34
packages/client/src/user/LanguageSettings.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Menu, Select } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const LanguageSettings = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{t('Language')}{' '}
|
||||
<Select
|
||||
style={{ minWidth: 100 }}
|
||||
bordered={false}
|
||||
open={open}
|
||||
onDropdownVisibleChange={(open) => {
|
||||
setOpen(open);
|
||||
}}
|
||||
options={[
|
||||
{ label: '简体中文', value: 'zh-CN' },
|
||||
{ label: 'English', value: 'en-US' },
|
||||
]}
|
||||
value={i18n.language}
|
||||
onChange={async (lang) => {
|
||||
await i18n.changeLanguage(lang);
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
38
packages/client/src/user/SwitchRole.tsx
Normal file
38
packages/client/src/user/SwitchRole.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { useCookieState } from 'ahooks';
|
||||
import { Menu, Select } from 'antd';
|
||||
import React from 'react';
|
||||
import { useCurrentUserContext } from './CurrentUserProvider';
|
||||
|
||||
const useCurrentRoles = () => {
|
||||
const { data } = useCurrentUserContext();
|
||||
return data?.data?.roles || [];
|
||||
};
|
||||
|
||||
export const SwitchRole = () => {
|
||||
const roles = useCurrentRoles();
|
||||
const [roleName, setRoleName] = useCookieState('currentRoleName', {
|
||||
defaultValue: roles?.find((role) => role.default)?.name,
|
||||
});
|
||||
if (roles.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Menu.Item>
|
||||
切换角色{' '}
|
||||
<Select
|
||||
style={{ minWidth: 100 }}
|
||||
bordered={false}
|
||||
fieldNames={{
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
}}
|
||||
options={roles}
|
||||
value={roleName}
|
||||
onChange={(roleName) => {
|
||||
setRoleName(roleName);
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
@ -2,9 +2,9 @@ import { Plugin } from '@nocobase/server';
|
||||
import { resolve } from 'path';
|
||||
import { availableActionResource } from './actions/available-actions';
|
||||
import { roleCollectionsResource } from './actions/role-collections';
|
||||
import { RoleModel } from './model/RoleModel';
|
||||
import { RoleResourceActionModel } from './model/RoleResourceActionModel';
|
||||
import { RoleResourceModel } from './model/RoleResourceModel';
|
||||
import { RoleModel } from './model/RoleModel';
|
||||
|
||||
export interface AssociationFieldAction {
|
||||
associationActions: string[];
|
||||
@ -237,6 +237,23 @@ export class PluginACL extends Plugin {
|
||||
this.app.on('beforeStart', async () => {
|
||||
await this.writeRolesToACL();
|
||||
});
|
||||
|
||||
this.app.on('installing.beforeUsersPlugin', async () => {
|
||||
const repository = this.app.db.getRepository('roles');
|
||||
await repository.createMany({
|
||||
records: [
|
||||
{
|
||||
name: 'admin',
|
||||
title: 'Admin',
|
||||
},
|
||||
{
|
||||
name: 'member',
|
||||
title: 'Member',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
|
@ -8,10 +8,11 @@ export function parseToken(options?: any) {
|
||||
return async function parseToken(ctx: Context, next: Next) {
|
||||
const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
|
||||
const User = ctx.db.getCollection('users');
|
||||
const user = await User.model.findOne({
|
||||
where: {
|
||||
const user = await User.repository.findOne({
|
||||
filter: {
|
||||
token,
|
||||
},
|
||||
appends: ['roles'],
|
||||
});
|
||||
|
||||
if (user) {
|
||||
|
@ -12,13 +12,19 @@ export default class UsersPlugin extends Plugin {
|
||||
adminPassword = 'admin123',
|
||||
} = this.options;
|
||||
|
||||
this.app.on('installing', async () => {
|
||||
this.app.on('installing', async (...args) => {
|
||||
// TODO 暂时先这么写着,理想状态应该由 app.emitAsync('installing') 内部处理
|
||||
await this.app.emitAsync('installing.beforeUsersPlugin', ...args);
|
||||
const User = this.db.getCollection('users');
|
||||
await User.model.create({
|
||||
nickname: adminNickname,
|
||||
email: adminEmail,
|
||||
password: adminPassword,
|
||||
await User.repository.create({
|
||||
values: {
|
||||
nickname: adminNickname,
|
||||
email: adminEmail,
|
||||
password: adminPassword,
|
||||
roles: ['admin'],
|
||||
},
|
||||
});
|
||||
await this.app.emitAsync('installing.afterUsersPlugin', ...args);
|
||||
});
|
||||
|
||||
this.db.on('users.afterCreateWithAssociations', async (model, options) => {
|
||||
|
Loading…
Reference in New Issue
Block a user