diff --git a/packages/client/src/api-client/APIClient.ts b/packages/client/src/api-client/APIClient.ts index 3908cb1096..64bb115bcf 100644 --- a/packages/client/src/api-client/APIClient.ts +++ b/packages/client/src/api-client/APIClient.ts @@ -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 { diff --git a/packages/client/src/user/ChangePassword.tsx b/packages/client/src/user/ChangePassword.tsx new file mode 100644 index 0000000000..2a5f7f3006 --- /dev/null +++ b/packages/client/src/user/ChangePassword.tsx @@ -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 ( + + { + ctx?.setVisible?.(false); + setVisible(true); + }} + > + {t('Change password')} + + + + ); +}; diff --git a/packages/client/src/user/CurrentUser.tsx b/packages/client/src/user/CurrentUser.tsx index caa3a0ca53..28fb5423b9 100644 --- a/packages/client/src/user/CurrentUser.tsx +++ b/packages/client/src/user/CurrentUser.tsx @@ -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 ( -
- - - - 切换角色 - { - i18n.changeLanguage(i18n.language === 'en-US' ? 'zh-CN' : 'en-US'); - // reset(); - window.location.reload(); - }} - > - 语言设置 - - - { - api.setBearerToken(null); - history.push('/signin'); - }} - > - 注销 - - - +
+ + { + setVisible(visible); + }} + overlay={ + + + + + + + { + api.setBearerToken(null); + history.push('/signin'); + }} + > + {t('Sign out')} + + + } + > + + +
); }; diff --git a/packages/client/src/user/CurrentUserProvider.tsx b/packages/client/src/user/CurrentUserProvider.tsx index 3c30949b03..f316be1123 100644 --- a/packages/client/src/user/CurrentUserProvider.tsx +++ b/packages/client/src/user/CurrentUserProvider.tsx @@ -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', diff --git a/packages/client/src/user/ProfileAction.tsx b/packages/client/src/user/EditProfile.tsx similarity index 79% rename from packages/client/src/user/ProfileAction.tsx rename to packages/client/src/user/EditProfile.tsx index 05b9d5d347..502ebc148a 100644 --- a/packages/client/src/user/ProfileAction.tsx +++ b/packages/client/src/user/EditProfile.tsx @@ -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 ( { setVisible(true); + ctx.setVisible(false); }} > - 个人资料 + {t('Edit profile')} diff --git a/packages/client/src/user/LanguageSettings.tsx b/packages/client/src/user/LanguageSettings.tsx new file mode 100644 index 0000000000..ec19cd4730 --- /dev/null +++ b/packages/client/src/user/LanguageSettings.tsx @@ -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 ( + { + setOpen(true); + }} + > + {t('Language')}{' '} + { + setRoleName(roleName); + window.location.reload(); + }} + /> + + ); +}; diff --git a/packages/plugin-acl/src/server.ts b/packages/plugin-acl/src/server.ts index a11e9efa0e..d6a76fc263 100644 --- a/packages/plugin-acl/src/server.ts +++ b/packages/plugin-acl/src/server.ts @@ -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() { diff --git a/packages/plugin-users/src/middlewares/parseToken.ts b/packages/plugin-users/src/middlewares/parseToken.ts index 63087c271f..1528c5df59 100644 --- a/packages/plugin-users/src/middlewares/parseToken.ts +++ b/packages/plugin-users/src/middlewares/parseToken.ts @@ -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) { diff --git a/packages/plugin-users/src/server.ts b/packages/plugin-users/src/server.ts index a929067fce..fbe85da317 100644 --- a/packages/plugin-users/src/server.ts +++ b/packages/plugin-users/src/server.ts @@ -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) => {