Skip to content

Commit ab93b53

Browse files
committed
feat: add external account and API key management
- Add IFLOW.md to gitignore - Add bind-account route for external account binding - Add API key management page with list, create, enable, disable, and delete functionality - Add English and Chinese localization files for API key management - Create external account binding page for linking social logins to existing accounts - Add login service methods for binding and unbinding external accounts - Fix error response handling in request interceptor - Add TypeScript types for API key and external account binding features Signed-off-by: wecoding <wecoding@yeah.net>
1 parent 89b4206 commit ab93b53

File tree

24 files changed

+1135
-1
lines changed

24 files changed

+1135
-1
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,5 @@ screenshot
4040

4141
build
4242
.aider*
43+
44+
IFLOW.md

config/routes.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ export default [
2525
hideInMenu: true,
2626
component: './Login/Callback',
2727
},
28+
{
29+
name: 'bind-account',
30+
path: '/auth/bind-account',
31+
layout: false,
32+
hideInMenu: true,
33+
component: './Login/BindAccount',
34+
},
2835
{
2936
path: '/welcome',
3037
name: 'welcome',
@@ -160,6 +167,11 @@ export default [
160167
component: './Role/Edit',
161168
hideInMenu: true,
162169
},
170+
{
171+
path: '/resource/apikey',
172+
name: 'apikey.list',
173+
component: './ApiKey',
174+
},
163175
],
164176
},
165177
// {

src/locales/en-US.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import menu from './en-US/menu';
2+
import message from './zh-CN/message';
3+
import pages from './zh-CN/pages';
4+
import pwa from './zh-CN/pwa';
5+
import settings from './zh-CN/settings';
6+
import apikey from '@/pages/ApiKey/locales/en-US';
7+
8+
export default {
9+
'navBar.lang': 'Language',
10+
'layout.user.link.help': 'Help',
11+
'layout.user.link.privacy': 'Privacy',
12+
'layout.user.link.terms': 'Terms',
13+
'app.copyright.produced': 'WECODING. All Rights Reserved.',
14+
'app.copyright.website': 'Online Preview',
15+
'app.copyright.helpDoc': 'Help Documentation',
16+
'app.preview.down.block': 'Download this page to local project',
17+
'app.welcome.link.fetch-blocks': 'Get all blocks',
18+
'app.welcome.link.block-list': 'Develop based on block, quickly build standard pages',
19+
...pages,
20+
...menu,
21+
...settings,
22+
...pwa,
23+
...message,
24+
...apikey,
25+
};

src/locales/en-US/menu.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
export default {
2+
'menu.welcome': 'Welcome',
3+
'menu.more-blocks': 'More Blocks',
4+
'menu.home': 'Home',
5+
'menu.login': 'Login',
6+
'menu.register': 'Register',
7+
'menu.register-result': 'Register Result',
8+
'menu.dashboard': 'Dashboard',
9+
'menu.dashboard.analysis': 'Analysis',
10+
'menu.dashboard.monitor': 'Monitor',
11+
'menu.dashboard.workplace': 'Workplace',
12+
'menu.exception.403': '403',
13+
'menu.exception.404': '404',
14+
'menu.exception.500': '500',
15+
'menu.result.fail': 'Fail',
16+
'menu.exception': 'Exception',
17+
'menu.exception.not-permission': '403',
18+
'menu.exception.not-find': '404',
19+
'menu.exception.server-error': '500',
20+
'menu.exception.trigger': 'Trigger Error',
21+
'menu.account': 'Account',
22+
'menu.account.center': 'Account Center',
23+
'menu.account.settings': 'Account Settings',
24+
'menu.account.trigger': 'Trigger Error',
25+
'menu.account.logout': 'Logout',
26+
27+
'menu.org-management': 'Organization',
28+
'menu.org-management.org.list': 'Organization Management',
29+
'menu.org-management.org.edit': 'Edit Organization',
30+
'menu.org-management.user.list': 'User Management',
31+
'menu.org-management.user.edit': 'Edit User',
32+
33+
'menu.app-management': 'Applications',
34+
'menu.app-management.app.list': 'Application Center',
35+
'menu.app-management.app.create': 'Create Application',
36+
'menu.app-management.app.edit': 'Edit Application',
37+
38+
'menu.authn': 'Authentication',
39+
'menu.authn.identity-source.social.list': 'Social Identity Providers',
40+
'menu.authn.identity-source.social.create': 'Create Social Identity Provider',
41+
'menu.authn.identity-source.social.edit': 'Edit Social Identity Provider',
42+
'menu.authn.identity-source.enterprise': 'Enterprise Identity Providers',
43+
44+
'menu.resource': 'Resource Permissions',
45+
'menu.resource.resource-list': 'Resource Management',
46+
'menu.resource.resource-create': 'Create Resource',
47+
'menu.resource.resource-edit': 'Edit Resource',
48+
'menu.resource.policy-list': 'Permission Policies',
49+
'menu.resource.policy-create': 'Create Policy',
50+
'menu.resource.policy-edit': 'Edit Policy',
51+
'menu.resource.role-list': 'Role Management',
52+
'menu.resource.role-edit': 'Edit Role',
53+
'menu.resource.apikey.list': 'API Keys',
54+
55+
'menu.system': 'System Settings',
56+
};

src/locales/zh-CN.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import message from './zh-CN/message';
33
import pages from './zh-CN/pages';
44
import pwa from './zh-CN/pwa';
55
import settings from './zh-CN/settings';
6+
import apikey from '@/pages/ApiKey/locales/zh-CN';
67

78
export default {
89
'navBar.lang': '语言',
@@ -20,4 +21,5 @@ export default {
2021
...settings,
2122
...pwa,
2223
...message,
24+
...apikey,
2325
};

src/locales/zh-CN/menu.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export default {
5050
'menu.resource.policy-edit': '编辑策略',
5151
'menu.resource.role-list': '角色管理',
5252
'menu.resource.role-edit': '编辑角色',
53+
'menu.resource.apikey.list': 'API Keys',
5354

5455
'menu.system': '系统设置',
5556
};

src/pages/ApiKey/List/index.tsx

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import React, { useRef } from 'react';
2+
import CreateApiKeyModal from '@/pages/ApiKey/components/CreateApiKeyModal';
3+
import { ListApiKeyOptions, listApiKeys } from '@/services/apikey/listApiKeys';
4+
import { deleteApiKey } from '@/services/apikey/deleteApiKey';
5+
import { enableApiKey } from '@/services/apikey/enableApiKey';
6+
import { disableApiKey } from '@/services/apikey/disableApiKey';
7+
import {
8+
DeleteOutlined,
9+
EllipsisOutlined,
10+
PauseCircleOutlined,
11+
PlayCircleOutlined,
12+
} from '@ant-design/icons';
13+
import type { ActionType, ProColumns } from '@ant-design/pro-components';
14+
import { ProTable } from '@ant-design/pro-components';
15+
import { FormattedMessage, useIntl, useRequest } from '@umijs/max';
16+
import { App, Button, Dropdown, message } from 'antd';
17+
import { BASIC_INTL } from '@/constant';
18+
import { transformSearchParams } from '@/utils';
19+
20+
import useStyle from './style';
21+
22+
export type Props = Record<string, never>;
23+
24+
const INTL = {
25+
TABLE_TITLE: {
26+
id: 'apikeys.table.title',
27+
},
28+
KEY: {
29+
id: 'apikeys.table.key',
30+
},
31+
STATUS: {
32+
id: 'apikeys.table.status',
33+
},
34+
EXPIRES_AT: {
35+
id: 'apikeys.table.expiresAt',
36+
},
37+
ENABLE: {
38+
id: 'apikeys.action.enable',
39+
},
40+
DISABLE: {
41+
id: 'apikeys.action.disable',
42+
},
43+
};
44+
45+
const ApiKeyList: React.FC<Props> = () => {
46+
const { styles } = useStyle();
47+
48+
const actionRef = useRef<ActionType>();
49+
const { modal } = App.useApp();
50+
const intl = useIntl();
51+
52+
const reloadTable = () => {
53+
if (actionRef.current) {
54+
actionRef.current.reload();
55+
}
56+
};
57+
58+
const { run: doDeleteApiKey } = useRequest(deleteApiKey, {
59+
manual: true,
60+
onSuccess: () => {
61+
reloadTable();
62+
message.success(intl.formatMessage(BASIC_INTL.DELETE_SUCCESS));
63+
},
64+
});
65+
66+
const { run: doEnableApiKey } = useRequest(enableApiKey, {
67+
manual: true,
68+
onSuccess: () => {
69+
reloadTable();
70+
message.success('Operation successful');
71+
},
72+
});
73+
74+
const { run: doDisableApiKey } = useRequest(disableApiKey, {
75+
manual: true,
76+
onSuccess: () => {
77+
reloadTable();
78+
message.success('Operation successful');
79+
},
80+
});
81+
82+
const handleListApiKeys = async (opts: ListApiKeyOptions) => {
83+
opts.fieldSelector = transformSearchParams(opts, ['status', 'name']).join(',');
84+
const apiKeyList = await listApiKeys(opts);
85+
return {
86+
data: apiKeyList.items,
87+
total: apiKeyList.total,
88+
};
89+
};
90+
91+
const deleteModalConfig = (records: API.ApiKey[]) => {
92+
let title =
93+
records.length === 1
94+
? intl.formatMessage(BASIC_INTL.DELETE_CONFIRM_TITLE, {
95+
name: records[0].metadata.name,
96+
})
97+
: intl.formatMessage(BASIC_INTL.MULTI_DELETE_CONFIRM_TITLE, {
98+
count: records.length,
99+
});
100+
return {
101+
title: title,
102+
content: (
103+
<span>
104+
<FormattedMessage {...BASIC_INTL.DELETE_CONFIRM_CONTENT} />
105+
</span>
106+
),
107+
centered: true,
108+
onOk: () => {
109+
doDeleteApiKey(records[0].metadata.instanceId);
110+
},
111+
okText: intl.formatMessage(BASIC_INTL.BTN_DELETE),
112+
okButtonProps: { danger: true },
113+
};
114+
};
115+
116+
const columns: ProColumns<API.ApiKey>[] = [
117+
{
118+
title: <FormattedMessage {...BASIC_INTL.NAME} />,
119+
dataIndex: ['metadata', 'name'],
120+
width: 200,
121+
},
122+
{
123+
title: <FormattedMessage {...INTL.KEY} />,
124+
dataIndex: 'key',
125+
renderText: (text: string) => {
126+
if (!text) return '-';
127+
const visibleStart = text.substring(0, 7);
128+
const visibleEnd = text.substring(text.length - 4);
129+
return `${visibleStart}***********************${visibleEnd}`;
130+
},
131+
width: 300,
132+
},
133+
{
134+
title: <FormattedMessage {...INTL.STATUS} />,
135+
dataIndex: 'status',
136+
valueEnum: {
137+
1: {
138+
text: <FormattedMessage {...BASIC_INTL.ACTIVED} />,
139+
status: 'Success',
140+
},
141+
0: {
142+
text: <FormattedMessage {...BASIC_INTL.DISABLED} />,
143+
status: 'Error',
144+
},
145+
2: {
146+
text: <FormattedMessage id="apikeys.status.expired" />,
147+
status: 'Warning',
148+
},
149+
},
150+
width: 100,
151+
},
152+
{
153+
title: <FormattedMessage {...BASIC_INTL.CREATED_AT} />,
154+
dataIndex: ['metadata', 'createdAt'],
155+
valueType: 'date',
156+
width: 120,
157+
renderText: (text: string) => {
158+
if (!text) return '-';
159+
const date = new Date(text);
160+
return date.toLocaleDateString(intl.locale);
161+
},
162+
},
163+
{
164+
title: <FormattedMessage {...INTL.EXPIRES_AT} />,
165+
dataIndex: 'expiresAt',
166+
valueType: 'date',
167+
width: 120,
168+
renderText: (text: string) => {
169+
if (!text) return intl.formatMessage({ id: 'apikeys.expiresAt.never' });
170+
const date = new Date(text);
171+
return date.toLocaleDateString(intl.locale);
172+
},
173+
},
174+
{
175+
title: <FormattedMessage {...BASIC_INTL.TITLE_OPTION} />,
176+
dataIndex: 'option',
177+
valueType: 'option',
178+
width: 80,
179+
fixed: 'right',
180+
render: (_, record: API.ApiKey) => [
181+
<Dropdown
182+
key="dropdown"
183+
trigger={['click']}
184+
placement="bottom"
185+
menu={{
186+
items: [
187+
{
188+
key: 'enable',
189+
icon: <PlayCircleOutlined />,
190+
label: intl.formatMessage(INTL.ENABLE),
191+
onClick: () => {
192+
doEnableApiKey(record.metadata.instanceId);
193+
},
194+
disabled: record.status === 1,
195+
},
196+
{
197+
key: 'disable',
198+
icon: <PauseCircleOutlined />,
199+
label: intl.formatMessage(INTL.DISABLE),
200+
onClick: () => {
201+
doDisableApiKey(record.metadata.instanceId);
202+
},
203+
disabled: record.status === 0 || record.status === 2,
204+
},
205+
{
206+
type: 'divider',
207+
},
208+
{
209+
key: 'delete',
210+
icon: <DeleteOutlined />,
211+
label: intl.formatMessage(BASIC_INTL.BTN_DELETE),
212+
onClick: () => {
213+
modal.confirm(deleteModalConfig([record]));
214+
},
215+
},
216+
],
217+
}}
218+
>
219+
<Button shape="default" type="text" icon={<EllipsisOutlined />}></Button>
220+
</Dropdown>,
221+
],
222+
},
223+
];
224+
225+
return (
226+
<div>
227+
<ProTable<API.ApiKey, ListApiKeyOptions>
228+
size="small"
229+
className={styles.container}
230+
headerTitle={intl.formatMessage(INTL.TABLE_TITLE)}
231+
actionRef={actionRef}
232+
columns={columns}
233+
rowKey={(record) => record?.metadata?.instanceId ?? ''}
234+
search={{ labelWidth: 'auto' }}
235+
request={handleListApiKeys}
236+
rowSelection={{}}
237+
pagination={{
238+
defaultPageSize: 10,
239+
showSizeChanger: true,
240+
pageSizeOptions: [10, 20, 30, 50],
241+
}}
242+
toolBarRender={() => [
243+
<CreateApiKeyModal
244+
key="create"
245+
onFinish={async () => {
246+
reloadTable();
247+
return true;
248+
}}
249+
/>,
250+
]}
251+
/>
252+
</div>
253+
);
254+
};
255+
256+
export default ApiKeyList;

0 commit comments

Comments
 (0)