feat: improve users module

This commit is contained in:
chenos 2022-02-28 14:25:50 +08:00
parent 1db71b166d
commit 78f75f5a2f
10 changed files with 304 additions and 54 deletions

View File

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

View 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>
);
};

View File

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

View File

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

View File

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

View 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>
);
};

View 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>
);
};

View File

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

View File

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

View File

@ -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) => {