mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-11 03:58:14 +00:00
(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:
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
dev: {
|
||||
'/api/': {
|
||||
target: 'http://10.91.217.39:9080',
|
||||
target: 'http://127.0.0.1:9080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user