(feat)Support for managing large models with Dify #1830;2、add user access token; #1829; 3、support change password #1824 (#1839)

This commit is contained in:
zhaodongsheng
2024-10-22 13:58:58 +08:00
committed by GitHub
parent bdb20ca462
commit 0ddcdf93ec
34 changed files with 1341 additions and 45 deletions

View File

@@ -1,7 +1,7 @@
export default {
dev: {
'/api/': {
target: 'http://10.91.217.39:9080',
target: 'http://127.0.0.1:9080',
changeOrigin: true,
},
},

View File

@@ -0,0 +1,195 @@
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { Button, Form, Input, message, Modal, Table } from 'antd';
import { useBoolean, useDynamicList, useRequest } from 'ahooks';
import {
changePassword,
generateAccessToken,
getUserAccessTokens,
removeAccessToken,
} from '@/services/user';
import { encryptPassword, encryptKey } from '@/utils/utils';
import { API } from '@/services/API';
import { EditableProTable, ProColumns } from '@ant-design/pro-components';
import { CopyOutlined } from '@ant-design/icons';
type DataSourceType = {
id: React.Key;
name?: string;
token?: string;
expireDate?: string;
createDate?: string;
toBeSaved?: boolean;
};
export interface IRef {
open: () => void;
close: () => void;
}
const ChangePasswordModal = forwardRef<IRef>((_, ref) => {
const [open, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false);
const [dataSource, setDataSource] = useState<readonly DataSourceType[]>([]);
const getAccessTokens = async () => {
try {
const res = await getUserAccessTokens();
if (res && res.code === 200) {
return res.data;
} else {
message.error(res.msg);
return [];
}
} catch (error) {
message.error('获取数据失败,原因:' + error);
return [];
}
};
useImperativeHandle(ref, () => ({
open: () => {
openModal();
},
close: () => {
closeModal();
},
}));
const columns: ProColumns<DataSourceType>[] = [
{
title: '名称',
dataIndex: 'name',
width: '15%',
formItemProps: {
rules: [{ required: true, message: '此项为必填项' }],
},
editable: (text, record, index) => !!record.toBeSaved,
},
{
title: '访问令牌',
dataIndex: 'token',
width: '25%',
formItemProps: (form, { rowIndex }) => {
return {
rules: [{ required: true, message: '此项为必填项' }],
};
},
render: (text, record, index) => {
// 脱敏处理, 点击图标可完整token
return record.toBeSaved ? (
text
) : (
<>
{record.token ? record.token.slice(0, 5) + '********' + record.token.slice(-5) : ''}
<Button
type="link"
size="small"
onClick={() => {
navigator.clipboard.writeText(record.token || '');
message.info('已复制到剪贴板');
}}
>
<CopyOutlined />
</Button>
</>
);
},
editable: false,
},
{
title: '过期时间',
dataIndex: 'expireDate',
valueType: 'date',
width: '20%',
formItemProps: {
rules: [{ required: true, message: '此项为必填项' }],
},
editable: (text, record, index) => !!record.toBeSaved,
},
{
title: '创建时间',
dataIndex: 'createDate',
valueType: 'date',
editable: false,
width: '20%',
},
{
title: '操作',
valueType: 'option',
width: '15%',
render: (text, record, _, action) => [
<a
key="delete"
onClick={() => {
Modal.confirm({
title: '删除访问令牌',
content: '确定删除此访问令牌吗?',
onOk: async () => {
const res = await removeAccessToken(record.id as number);
if (res && res.code !== 200) {
message.error('删除失败,原因:' + res.msg);
return;
}
setDataSource(dataSource.filter((item) => item.id !== record.id));
message.success('删除成功');
},
});
}}
>
</a>,
],
},
];
return (
<Modal
title="访问令牌"
open={open}
onClose={closeModal}
onCancel={closeModal}
width={1200}
footer={false}
destroyOnClose
>
<EditableProTable<DataSourceType>
rowKey="id"
recordCreatorProps={{
position: 'bottom',
creatorButtonText: '新增访问令牌',
record: () => ({ id: (Math.random() * 1000000).toFixed(0), toBeSaved: true }),
}}
loading={false}
columns={columns}
request={async () => {
const data = await getAccessTokens();
return {
data,
total: data.length,
success: true,
};
}}
value={dataSource}
onChange={setDataSource}
editable={{
type: 'single',
onSave: async (rowKey, data, row) => {
console.log(rowKey, data, row);
await generateAccessToken({
name: data.name!,
expireTime: new Date(data.expireDate!).getTime() - new Date().getTime(),
});
const newTokens = await getAccessTokens();
setTimeout(() => {
setDataSource(newTokens);
}, 100);
},
}}
/>
</Modal>
);
});
export default ChangePasswordModal;

View File

@@ -1,10 +1,12 @@
import React from 'react';
import { LogoutOutlined } from '@ant-design/icons';
import React, { useRef } from 'react';
import { LogoutOutlined, KeyOutlined, UnlockOutlined } from '@ant-design/icons';
import { useModel } from 'umi';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
import TMEAvatar from '../TMEAvatar';
import { AUTH_TOKEN_KEY } from '@/common/constants';
import ChangePasswordModal, { IRef as IRefChangePasswordModal } from './ChangePasswordModal';
import AccessTokensModal, { IRef as IAccessTokensModalRef } from './AccessTokensModal';
import { history } from 'umi';
export type GlobalHeaderRightProps = {
@@ -27,7 +29,38 @@ const { APP_TARGET } = process.env;
const AvatarDropdown: React.FC<GlobalHeaderRightProps> = () => {
const { initialState = {}, setInitialState } = useModel('@@initialState');
const { currentUser = {} } = initialState as any;
const changePasswordModalRef = useRef<IRefChangePasswordModal>(null);
const accessTokensModalRef = useRef<IAccessTokensModalRef>(null);
const handleAccessToken = () => {
accessTokensModalRef.current?.open();
};
const handleChangePassword = () => {
changePasswordModalRef.current?.open();
};
const items = [
{
label: (
<>
<KeyOutlined />
访
</>
),
key: 'accessToken',
onClick: handleAccessToken,
},
{
label: (
<>
<UnlockOutlined />
</>
),
key: 'changePassword',
onClick: handleChangePassword,
},
{
label: (
<>
@@ -48,12 +81,21 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = () => {
},
];
return (
<HeaderDropdown menu={{ items }} disabled={APP_TARGET === 'inner'}>
<span className={`${styles.action} ${styles.account}`}>
<TMEAvatar className={styles.avatar} size="small" staffName={currentUser.staffName} />
<span className={styles.userName}>{currentUser.staffName}</span>
</span>
</HeaderDropdown>
<>
<HeaderDropdown
menu={{
items,
}}
disabled={APP_TARGET === 'inner'}
>
<span className={`${styles.action} ${styles.account}`}>
<TMEAvatar className={styles.avatar} size="small" staffName={currentUser.staffName} />
<span className={styles.userName}>{currentUser.staffName}</span>
</span>
</HeaderDropdown>
<ChangePasswordModal ref={changePasswordModalRef} />
<AccessTokensModal ref={accessTokensModalRef} />
</>
);
};

View File

@@ -0,0 +1,121 @@
import React, { forwardRef, useImperativeHandle } from 'react';
import { Form, Input, message, Modal } from 'antd';
import { useBoolean } from 'ahooks';
import { changePassword } from '@/services/user';
import { pick } from 'lodash';
import { encryptPassword, encryptKey } from '@/utils/utils';
export interface IRef {
open: () => void;
close: () => void;
}
const ChangePasswordModal = forwardRef<IRef>((_, ref) => {
const [form] = Form.useForm();
const [open, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false);
const [confirmLoading, { set: setConfirmLoading }] = useBoolean(false);
useImperativeHandle(ref, () => ({
open: () => {
openModal();
form.resetFields();
},
close: () => {
closeModal();
form.resetFields();
},
}));
const handleOk = async () => {
try {
const values = await form.validateFields();
console.log(values);
setConfirmLoading(true);
// Call API to change password
const res = await changePassword({
oldPassword: encryptPassword(values.oldPassword, encryptKey),
newPassword: encryptPassword(values.newPassword, encryptKey),
});
if (res && res.code !== 200) {
return message.warning(res.msg);
}
closeModal();
} catch (error) {
console.log('Failed:', error);
} finally {
setConfirmLoading(false);
}
};
return (
<Modal
title="修改密码"
open={open}
onOk={handleOk}
onClose={closeModal}
onCancel={closeModal}
confirmLoading={confirmLoading}
>
<Form form={form}>
<Form.Item
name="oldPassword"
label="原密码"
rules={[
{
required: true,
message: '请输入原密码!',
},
]}
hasFeedback
>
<Input.Password />
</Form.Item>
<Form.Item
name="newPassword"
label="新密码"
rules={[
{
required: true,
message: '请输入新密码!',
},
{
min: 6,
max: 10,
message: '密码须在6-10字符之间!',
},
]}
hasFeedback
>
<Input.Password />
</Form.Item>
<Form.Item
name="confirm"
label="确认密码"
dependencies={['newPassword']}
hasFeedback
rules={[
{
required: true,
message: '请确认密码!',
},
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('newPassword') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次输入的密码不一致!'));
},
}),
]}
>
<Input.Password />
</Form.Item>
</Form>
</Modal>
);
});
export default ChangePasswordModal;

View File

@@ -23,6 +23,23 @@ declare namespace API {
access?: 'user' | 'guest' | 'admin';
};
export interface UserItem {
id: number;
name: string;
displayName: string;
email: string;
}
export interface UserAccessToken {
createDate: string;
expireDate: string;
expireTime: number;
id: number;
name: string;
token: string;
userName: string;
}
export type LoginStateType = {
status?: 'ok' | 'error';
type?: string;

View File

@@ -20,3 +20,31 @@ export function saveSystemConfig(data: any): Promise<any> {
data,
});
}
export function changePassword(data: { newPassword: string; oldPassword: string }): Promise<any> {
return request(`${process.env.AUTH_API_BASE_URL}user/resetPassword`, {
method: 'post',
data: {
newPassword: data.newPassword,
password: data.oldPassword,
},
});
}
// 获取用户accessTokens
export async function getUserAccessTokens(): Promise<Result<API.UserItem[]>> {
return request.get(`${process.env.AUTH_API_BASE_URL}user/getUserTokens`);
}
export function generateAccessToken(data: { expireTime: number; name: string }): Promise<any> {
return request(`${process.env.AUTH_API_BASE_URL}user/generateToken`, {
method: 'post',
data,
});
}
export function removeAccessToken(id: number): Promise<any> {
return request(`${process.env.AUTH_API_BASE_URL}user/deleteUserToken?tokenId=${id}`, {
method: 'post',
});
}

View File

@@ -472,7 +472,7 @@ export const objToArray = (_obj: ObjToArrayParams, keyType: string = 'string') =
});
};
const encryptKey = CryptoJS.enc.Hex.parse(
export const encryptKey = CryptoJS.enc.Hex.parse(
'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
);