feat: improve users module
This commit is contained in:
parent
1db71b166d
commit
78f75f5a2f
@ -1,6 +1,7 @@
|
|||||||
import { observable } from '@formily/reactive';
|
import { observable } from '@formily/reactive';
|
||||||
import { Result } from 'ahooks/lib/useRequest/src/types';
|
import { Result } from 'ahooks/lib/useRequest/src/types';
|
||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
import qs from 'qs';
|
import qs from 'qs';
|
||||||
|
|
||||||
export interface ActionParams {
|
export interface ActionParams {
|
||||||
@ -54,18 +55,25 @@ export class APIClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
authMiddleware() {
|
authMiddleware() {
|
||||||
this.axios.interceptors.request.use((config) => {
|
this.axios.interceptors.request.use((config) => {
|
||||||
const token = localStorage.getItem(this.tokenKey);
|
const token = localStorage.getItem(this.tokenKey);
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers['Authorization'] = `Bearer ${token}`;
|
config.headers['Authorization'] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
const currentRoleName = Cookies.get('currentRoleName');
|
||||||
|
if (currentRoleName) {
|
||||||
|
config.headers['X-Role'] = currentRoleName;
|
||||||
|
}
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
setBearerToken(token: any) {
|
setBearerToken(token: any) {
|
||||||
localStorage.setItem(this.tokenKey, token || '');
|
localStorage.setItem(this.tokenKey, token || '');
|
||||||
|
Cookies.remove('currentRoleName');
|
||||||
}
|
}
|
||||||
|
|
||||||
service(uid: string): Result<any, any> {
|
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 { Button, Dropdown, Menu } from 'antd';
|
||||||
import React from 'react';
|
import React, { createContext, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { useAPIClient, useDesignable } from '..';
|
import { useAPIClient, useCurrentUserContext } from '..';
|
||||||
import { ProfileAction } from './ProfileAction';
|
import { ChangePassword } from './ChangePassword';
|
||||||
|
import { EditProfile } from './EditProfile';
|
||||||
|
import { LanguageSettings } from './LanguageSettings';
|
||||||
|
import { SwitchRole } from './SwitchRole';
|
||||||
|
|
||||||
|
export const DropdownVisibleContext = createContext(null);
|
||||||
|
|
||||||
export const CurrentUser = () => {
|
export const CurrentUser = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { reset } = useDesignable();
|
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const { i18n } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const { data } = useCurrentUserContext();
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'inline-block' }}>
|
<div style={{ display: 'inline-block', verticalAlign: 'top' }}>
|
||||||
<Menu selectable={false} mode={'horizontal'} theme={'dark'}>
|
<DropdownVisibleContext.Provider value={{ visible, setVisible }}>
|
||||||
<Menu.SubMenu key={'current-user'} title={'超级管理员'}>
|
<Dropdown
|
||||||
<ProfileAction />
|
visible={visible}
|
||||||
<Menu.Item>切换角色</Menu.Item>
|
onVisibleChange={(visible) => {
|
||||||
<Menu.Item
|
setVisible(visible);
|
||||||
onClick={() => {
|
}}
|
||||||
i18n.changeLanguage(i18n.language === 'en-US' ? 'zh-CN' : 'en-US');
|
overlay={
|
||||||
// reset();
|
<Menu>
|
||||||
window.location.reload();
|
<EditProfile />
|
||||||
}}
|
<ChangePassword />
|
||||||
>
|
<SwitchRole />
|
||||||
语言设置
|
<LanguageSettings />
|
||||||
</Menu.Item>
|
<Menu.Divider />
|
||||||
<Menu.Divider />
|
<Menu.Item
|
||||||
<Menu.Item
|
onClick={() => {
|
||||||
onClick={() => {
|
api.setBearerToken(null);
|
||||||
api.setBearerToken(null);
|
history.push('/signin');
|
||||||
history.push('/signin');
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{t('Sign out')}
|
||||||
注销
|
</Menu.Item>
|
||||||
</Menu.Item>
|
</Menu>
|
||||||
</Menu.SubMenu>
|
}
|
||||||
</Menu>
|
>
|
||||||
|
<Button ghost style={{ border: 0 }}>
|
||||||
|
{data?.data?.nickname || data?.data?.email}
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
</DropdownVisibleContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import React, { createContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { useRequest } from '../api-client';
|
import { useRequest } from '../api-client';
|
||||||
|
|
||||||
export const CurrentUserContext = createContext(null);
|
export const CurrentUserContext = createContext(null);
|
||||||
|
|
||||||
|
export const useCurrentUserContext = () => {
|
||||||
|
return useContext(CurrentUserContext);
|
||||||
|
}
|
||||||
|
|
||||||
export const CurrentUserProvider = (props) => {
|
export const CurrentUserProvider = (props) => {
|
||||||
const result = useRequest({
|
const result = useRequest({
|
||||||
url: 'users:check',
|
url: 'users:check',
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { ISchema, useForm } from '@formily/react';
|
import { ISchema, useForm } from '@formily/react';
|
||||||
import { uid } from '@formily/shared';
|
import { uid } from '@formily/shared';
|
||||||
import { Menu } from 'antd';
|
import { Menu } from 'antd';
|
||||||
import React, { useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { ActionContext, SchemaComponent, useActionContext, useRequest } from '../';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ActionContext, DropdownVisibleContext, SchemaComponent, useActionContext, useCurrentUserContext, useRequest } from '../';
|
||||||
|
|
||||||
const useCloseAction = () => {
|
const useCloseAction = () => {
|
||||||
const { setVisible } = useActionContext();
|
const { setVisible } = useActionContext();
|
||||||
@ -17,16 +18,9 @@ const useCloseAction = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const useCurrentUserValues = (props, options) => {
|
const useCurrentUserValues = (options) => {
|
||||||
return useRequest(
|
const ctx = useCurrentUserContext();
|
||||||
() =>
|
return useRequest(() => Promise.resolve(ctx.data), options);
|
||||||
Promise.resolve({
|
|
||||||
data: {},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
...options,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const useSaveCurrentUserValues = () => {
|
const useSaveCurrentUserValues = () => {
|
||||||
@ -87,17 +81,20 @@ const schema: ISchema = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProfileAction = () => {
|
export const EditProfile = () => {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const ctx = useContext(DropdownVisibleContext);
|
||||||
return (
|
return (
|
||||||
<ActionContext.Provider value={{ visible, setVisible }}>
|
<ActionContext.Provider value={{ visible, setVisible }}>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
eventKey={'ProfileAction'}
|
eventKey={'ProfileAction'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
|
ctx.setVisible(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
个人资料
|
{t('Edit profile')}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<SchemaComponent scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }} schema={schema} />
|
<SchemaComponent scope={{ useCurrentUserValues, useCloseAction, useSaveCurrentUserValues }} schema={schema} />
|
||||||
</ActionContext.Provider>
|
</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 { resolve } from 'path';
|
||||||
import { availableActionResource } from './actions/available-actions';
|
import { availableActionResource } from './actions/available-actions';
|
||||||
import { roleCollectionsResource } from './actions/role-collections';
|
import { roleCollectionsResource } from './actions/role-collections';
|
||||||
|
import { RoleModel } from './model/RoleModel';
|
||||||
import { RoleResourceActionModel } from './model/RoleResourceActionModel';
|
import { RoleResourceActionModel } from './model/RoleResourceActionModel';
|
||||||
import { RoleResourceModel } from './model/RoleResourceModel';
|
import { RoleResourceModel } from './model/RoleResourceModel';
|
||||||
import { RoleModel } from './model/RoleModel';
|
|
||||||
|
|
||||||
export interface AssociationFieldAction {
|
export interface AssociationFieldAction {
|
||||||
associationActions: string[];
|
associationActions: string[];
|
||||||
@ -237,6 +237,23 @@ export class PluginACL extends Plugin {
|
|||||||
this.app.on('beforeStart', async () => {
|
this.app.on('beforeStart', async () => {
|
||||||
await this.writeRolesToACL();
|
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() {
|
async load() {
|
||||||
|
@ -8,10 +8,11 @@ export function parseToken(options?: any) {
|
|||||||
return async function parseToken(ctx: Context, next: Next) {
|
return async function parseToken(ctx: Context, next: Next) {
|
||||||
const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
|
const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
|
||||||
const User = ctx.db.getCollection('users');
|
const User = ctx.db.getCollection('users');
|
||||||
const user = await User.model.findOne({
|
const user = await User.repository.findOne({
|
||||||
where: {
|
filter: {
|
||||||
token,
|
token,
|
||||||
},
|
},
|
||||||
|
appends: ['roles'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
@ -12,13 +12,19 @@ export default class UsersPlugin extends Plugin {
|
|||||||
adminPassword = 'admin123',
|
adminPassword = 'admin123',
|
||||||
} = this.options;
|
} = 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');
|
const User = this.db.getCollection('users');
|
||||||
await User.model.create({
|
await User.repository.create({
|
||||||
nickname: adminNickname,
|
values: {
|
||||||
email: adminEmail,
|
nickname: adminNickname,
|
||||||
password: adminPassword,
|
email: adminEmail,
|
||||||
|
password: adminPassword,
|
||||||
|
roles: ['admin'],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
await this.app.emitAsync('installing.afterUsersPlugin', ...args);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.db.on('users.afterCreateWithAssociations', async (model, options) => {
|
this.db.on('users.afterCreateWithAssociations', async (model, options) => {
|
||||||
|
Loading…
Reference in New Issue
Block a user