@blocklet/ui-react 2.9.51 → 2.9.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/@types/index.d.ts +3 -5
- package/es/Dashboard/index.d.ts +5 -6
- package/es/Dashboard/index.js +5 -5
- package/es/Footer/index.js +2 -2
- package/es/Header/index.js +5 -5
- package/es/UserCenter/components/notification.js +1 -1
- package/es/UserCenter/components/passport.js +1 -2
- package/es/UserCenter/components/privacy.js +1 -1
- package/es/UserCenter/components/settings.js +9 -1
- package/es/UserCenter/components/storage/connect-to.d.ts +1 -1
- package/es/UserCenter/components/storage/connect-to.js +9 -3
- package/es/UserCenter/components/storage/connected.d.ts +1 -1
- package/es/UserCenter/components/storage/connected.js +5 -2
- package/es/UserCenter/components/storage/delete.d.ts +1 -1
- package/es/UserCenter/components/storage/delete.js +4 -1
- package/es/UserCenter/components/storage/index.js +4 -2
- package/es/UserCenter/components/storage/item.js +7 -12
- package/es/UserCenter/components/user-center.d.ts +2 -2
- package/es/UserCenter/components/user-center.js +15 -6
- package/es/UserCenter/libs/locales.d.ts +54 -0
- package/es/UserCenter/libs/locales.js +56 -2
- package/es/UserSessions/components/user-session-info.d.ts +6 -0
- package/es/UserSessions/components/user-session-info.js +58 -0
- package/es/UserSessions/components/user-sessions.d.ts +9 -0
- package/es/UserSessions/components/user-sessions.js +255 -0
- package/es/UserSessions/index.d.ts +1 -0
- package/es/UserSessions/index.js +1 -0
- package/es/UserSessions/libs/locales.d.ts +52 -0
- package/es/UserSessions/libs/locales.js +52 -0
- package/es/UserSessions/libs/utils.d.ts +2 -0
- package/es/UserSessions/libs/utils.js +73 -0
- package/es/blocklets.js +6 -6
- package/es/common/header-addons.d.ts +3 -4
- package/es/common/header-addons.js +4 -4
- package/es/contexts/config-user-space.js +2 -2
- package/es/index.d.ts +1 -0
- package/es/index.js +1 -0
- package/es/types.d.ts +2 -2
- package/es/types.js +2 -2
- package/lib/@types/index.d.ts +3 -5
- package/lib/Dashboard/index.d.ts +5 -6
- package/lib/Dashboard/index.js +4 -4
- package/lib/Footer/index.js +1 -1
- package/lib/Header/index.js +4 -4
- package/lib/UserCenter/components/notification.js +1 -1
- package/lib/UserCenter/components/passport.js +1 -2
- package/lib/UserCenter/components/privacy.js +1 -1
- package/lib/UserCenter/components/settings.js +10 -1
- package/lib/UserCenter/components/storage/connect-to.d.ts +1 -1
- package/lib/UserCenter/components/storage/connect-to.js +3 -3
- package/lib/UserCenter/components/storage/connected.d.ts +1 -1
- package/lib/UserCenter/components/storage/connected.js +1 -1
- package/lib/UserCenter/components/storage/delete.d.ts +1 -1
- package/lib/UserCenter/components/storage/index.js +22 -17
- package/lib/UserCenter/components/storage/item.js +1 -13
- package/lib/UserCenter/components/user-center.d.ts +2 -2
- package/lib/UserCenter/components/user-center.js +20 -10
- package/lib/UserCenter/libs/locales.d.ts +54 -0
- package/lib/UserCenter/libs/locales.js +56 -2
- package/lib/UserSessions/components/user-session-info.d.ts +6 -0
- package/lib/UserSessions/components/user-session-info.js +68 -0
- package/lib/UserSessions/components/user-sessions.d.ts +9 -0
- package/lib/UserSessions/components/user-sessions.js +282 -0
- package/lib/UserSessions/index.d.ts +1 -0
- package/lib/UserSessions/index.js +13 -0
- package/lib/UserSessions/libs/locales.d.ts +52 -0
- package/lib/UserSessions/libs/locales.js +58 -0
- package/lib/UserSessions/libs/utils.d.ts +2 -0
- package/lib/UserSessions/libs/utils.js +80 -0
- package/lib/blocklets.js +6 -6
- package/lib/common/header-addons.d.ts +3 -4
- package/lib/common/header-addons.js +3 -3
- package/lib/contexts/config-user-space.js +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +12 -0
- package/lib/types.d.ts +2 -2
- package/lib/types.js +3 -3
- package/package.json +14 -6
- package/src/@types/index.ts +3 -5
- package/src/Dashboard/index.jsx +7 -3
- package/src/Footer/index.jsx +2 -2
- package/src/Header/index.jsx +5 -3
- package/src/Icon/index.jsx +1 -0
- package/src/UserCenter/components/notification.tsx +2 -2
- package/src/UserCenter/components/passport.tsx +1 -2
- package/src/UserCenter/components/privacy.tsx +1 -1
- package/src/UserCenter/components/settings.tsx +15 -2
- package/src/UserCenter/components/storage/connect-to.tsx +17 -11
- package/src/UserCenter/components/storage/connected.tsx +9 -3
- package/src/UserCenter/components/storage/delete.tsx +8 -2
- package/src/UserCenter/components/storage/index.tsx +17 -13
- package/src/UserCenter/components/storage/item.tsx +8 -15
- package/src/UserCenter/components/storage/preview-nft.tsx +1 -1
- package/src/UserCenter/components/user-center.tsx +21 -14
- package/src/UserCenter/components/webhook-item.tsx +1 -1
- package/src/UserCenter/libs/locales.ts +54 -0
- package/src/UserSessions/components/user-session-info.tsx +52 -0
- package/src/UserSessions/components/user-sessions.tsx +276 -0
- package/src/UserSessions/index.tsx +1 -0
- package/src/UserSessions/libs/locales.ts +52 -0
- package/src/UserSessions/libs/utils.ts +82 -0
- package/src/blocklets.js +6 -6
- package/src/common/header-addons.jsx +2 -2
- package/src/contexts/config-user-space.tsx +12 -11
- package/src/index.ts +1 -0
- package/src/{UserCenter/libs → libs}/client.ts +1 -0
- package/src/libs/spaces.tsx +2 -2
- package/src/types.js +2 -2
- /package/es/{UserCenter/libs → libs}/client.d.ts +0 -0
- /package/es/{UserCenter/libs → libs}/client.js +0 -0
- /package/lib/{UserCenter/libs → libs}/client.d.ts +0 -0
- /package/lib/{UserCenter/libs → libs}/client.js +0 -0
|
@@ -41,6 +41,33 @@ export const translations = {
|
|
|
41
41
|
myInfo: '我的信息',
|
|
42
42
|
loginNow: '立即登录',
|
|
43
43
|
viewAfterLogin: '登录后才可以查看',
|
|
44
|
+
sessionManagement: '会话管理',
|
|
45
|
+
storage: {
|
|
46
|
+
spaces: {
|
|
47
|
+
tips: '提示',
|
|
48
|
+
label: '请输入 DID Spaces 服务地址',
|
|
49
|
+
connectedWithName: '你已成功连接至 {name}',
|
|
50
|
+
gateway: {
|
|
51
|
+
delete: {
|
|
52
|
+
failed: '删除 DID Spaces 失败',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
provideNFT: {
|
|
56
|
+
title: '请为 {appName} 出示 DID Spaces NFT',
|
|
57
|
+
scan: '使用你的 DID Wallet 扫描下面的二维码为应用 {appName} 出示 DID Spaces NFT',
|
|
58
|
+
success: '连接成功',
|
|
59
|
+
},
|
|
60
|
+
connect: {
|
|
61
|
+
useWallet: '使用 DID Wallet 连接',
|
|
62
|
+
useWalletReconnect: '使用 DID Wallet 重新连接',
|
|
63
|
+
providerForStorage: '请连接您的 DID Spaces',
|
|
64
|
+
},
|
|
65
|
+
connected: {
|
|
66
|
+
title: '已连接的 DID Spaces',
|
|
67
|
+
tag: '已连接',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
44
71
|
},
|
|
45
72
|
en: {
|
|
46
73
|
settings: 'Settings',
|
|
@@ -84,5 +111,32 @@ export const translations = {
|
|
|
84
111
|
myInfo: 'My Info',
|
|
85
112
|
loginNow: 'Login',
|
|
86
113
|
viewAfterLogin: 'View after login',
|
|
114
|
+
sessionManagement: 'Session Management',
|
|
115
|
+
storage: {
|
|
116
|
+
spaces: {
|
|
117
|
+
tips: 'Tips',
|
|
118
|
+
label: 'Enter DID Spaces Gateway',
|
|
119
|
+
connectedWithName: 'You have successfully connected to {name}',
|
|
120
|
+
gateway: {
|
|
121
|
+
delete: {
|
|
122
|
+
failed: 'Delete gateway failed',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
provideNFT: {
|
|
126
|
+
title: 'Please show the DID Spaces NFT for {appName}',
|
|
127
|
+
scan: 'Use your DID Wallet to scan the QR code below to allow the app {appName} show DID space NFT',
|
|
128
|
+
success: 'Connected successfully',
|
|
129
|
+
},
|
|
130
|
+
connect: {
|
|
131
|
+
useWallet: 'Connect with DID Wallet',
|
|
132
|
+
useWalletReconnect: 'Reconnect with DID Wallet',
|
|
133
|
+
providerForStorage: 'Please connect your DID Spaces',
|
|
134
|
+
},
|
|
135
|
+
connected: {
|
|
136
|
+
title: 'Connected DID Spaces',
|
|
137
|
+
tag: 'Connected',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
87
141
|
},
|
|
88
142
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Box, Chip, Typography } from '@mui/material';
|
|
2
|
+
import Avatar from '@arcblock/ux/lib/Avatar';
|
|
3
|
+
import { useCreation } from 'ahooks';
|
|
4
|
+
import { temp as colors } from '@arcblock/ux/lib/Colors';
|
|
5
|
+
|
|
6
|
+
import { User } from '../../@types';
|
|
7
|
+
|
|
8
|
+
export default function UserSessionInfo({ user, sessionUser }: { readonly user: User; readonly sessionUser: any }) {
|
|
9
|
+
const currentRole = useCreation(() => {
|
|
10
|
+
return (user?.passports || [])?.find((item) => item.name === sessionUser.role);
|
|
11
|
+
}, [user?.passports, sessionUser?.role]);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Box
|
|
15
|
+
sx={{
|
|
16
|
+
display: 'flex',
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
gap: 1,
|
|
19
|
+
}}>
|
|
20
|
+
<Avatar size={40} variant="circle" shape="circle" src={sessionUser.avatar} did={sessionUser.did} />
|
|
21
|
+
<Box>
|
|
22
|
+
<Box
|
|
23
|
+
sx={{
|
|
24
|
+
display: 'flex',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
gap: 1,
|
|
27
|
+
}}>
|
|
28
|
+
<Typography sx={{ whiteSpace: 'normal', wordBreak: 'break-all' }}>{sessionUser.fullName}</Typography>
|
|
29
|
+
<Chip
|
|
30
|
+
label={currentRole?.title || currentRole?.name || 'Guest'}
|
|
31
|
+
size="small"
|
|
32
|
+
variant="outlined"
|
|
33
|
+
sx={{
|
|
34
|
+
flexShrink: 0,
|
|
35
|
+
fontWeight: 'bold',
|
|
36
|
+
fontSize: '12px',
|
|
37
|
+
color: colors.textBase,
|
|
38
|
+
borderColor: colors.strokeBorderStrong,
|
|
39
|
+
backgroundColor: 'transparent',
|
|
40
|
+
textTransform: 'capitalize',
|
|
41
|
+
pr: 1,
|
|
42
|
+
pl: 0.5,
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
</Box>
|
|
46
|
+
<Typography variant="body2" color="grey">
|
|
47
|
+
{sessionUser.email}
|
|
48
|
+
</Typography>
|
|
49
|
+
</Box>
|
|
50
|
+
</Box>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/* eslint-disable react/no-unstable-nested-components */
|
|
2
|
+
import Datatable from '@arcblock/ux/lib/Datatable';
|
|
3
|
+
import { useCreation, useMemoizedFn, useRequest } from 'ahooks';
|
|
4
|
+
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
5
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
6
|
+
import RelativeTime from '@arcblock/ux/lib/RelativeTime';
|
|
7
|
+
import sortBy from 'lodash/sortBy';
|
|
8
|
+
import UAParser from 'ua-parser-js';
|
|
9
|
+
import { getVisitorId } from '@arcblock/ux/lib/Util';
|
|
10
|
+
import { useConfirm } from '@arcblock/ux/lib/Dialog';
|
|
11
|
+
import pAll from 'p-all';
|
|
12
|
+
import { Box, Button, Tooltip, Typography } from '@mui/material';
|
|
13
|
+
import { ReactElement } from 'react';
|
|
14
|
+
import { UserSession } from '@blocklet/js-sdk';
|
|
15
|
+
|
|
16
|
+
import UserSessionInfo from './user-session-info';
|
|
17
|
+
import { User } from '../../@types';
|
|
18
|
+
import { client } from '../../libs/client';
|
|
19
|
+
import { translations } from '../libs/locales';
|
|
20
|
+
import { batchIp2Region } from '../libs/utils';
|
|
21
|
+
|
|
22
|
+
const parseUa = (ua: string) => {
|
|
23
|
+
const parser = new UAParser(ua, {
|
|
24
|
+
// eslint-disable-next-line no-useless-escape
|
|
25
|
+
browser: [[/(ArcWallet)\/([\w\.]+)/i], [UAParser.BROWSER.NAME, UAParser.BROWSER.VERSION]],
|
|
26
|
+
});
|
|
27
|
+
const result = parser.getResult();
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default function UserSessions({
|
|
32
|
+
user,
|
|
33
|
+
showAction = true,
|
|
34
|
+
showUser = true,
|
|
35
|
+
}: {
|
|
36
|
+
readonly user: User & {
|
|
37
|
+
userSessions?: any[];
|
|
38
|
+
};
|
|
39
|
+
readonly showAction?: boolean;
|
|
40
|
+
readonly showUser?: boolean;
|
|
41
|
+
}) {
|
|
42
|
+
const currentVisitorId = getVisitorId();
|
|
43
|
+
const { locale } = useLocaleContext();
|
|
44
|
+
const { confirmApi, confirmHolder } = useConfirm();
|
|
45
|
+
const t = useMemoizedFn((key, data = {}) => {
|
|
46
|
+
return translate(translations, key, locale, 'en', data);
|
|
47
|
+
});
|
|
48
|
+
const getData: () => Promise<(UserSession & { ipRegion?: string })[]> = useMemoizedFn(async () => {
|
|
49
|
+
let data = user?.userSessions as (UserSession & { ipRegion?: string })[];
|
|
50
|
+
if (!data) {
|
|
51
|
+
data = await client.userSession.getUserSessions({ did: user.did });
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const ipIndexList = data?.map((x, index) => [index, x.lastLoginIp] as [number, string]).filter((x) => !!x[1]);
|
|
55
|
+
const ipList = ipIndexList?.map((x) => x[1]);
|
|
56
|
+
const result = await batchIp2Region(ipList);
|
|
57
|
+
for (let index = 0; index < result.length; index++) {
|
|
58
|
+
const x = result[index];
|
|
59
|
+
const ipIndexItem = ipIndexList[index];
|
|
60
|
+
const dataItem = data[ipIndexItem[0]];
|
|
61
|
+
dataItem.ipRegion = x;
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.warn('Failed to convert ip to region');
|
|
65
|
+
console.error(e);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const now = new Date().getTime();
|
|
69
|
+
return sortBy(data, (x) => {
|
|
70
|
+
if (x.visitorId === currentVisitorId) {
|
|
71
|
+
return -1;
|
|
72
|
+
}
|
|
73
|
+
return now - new Date(x.updatedAt).getTime();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const pageState = useRequest(getData);
|
|
78
|
+
|
|
79
|
+
const safeData = useCreation(() => {
|
|
80
|
+
return pageState.data || [];
|
|
81
|
+
}, [pageState.data]);
|
|
82
|
+
|
|
83
|
+
const ipRegionMap = useCreation(() => {
|
|
84
|
+
return safeData.reduce((acc, x) => {
|
|
85
|
+
acc[x.lastLoginIp] = x.ipRegion;
|
|
86
|
+
return acc;
|
|
87
|
+
}, {} as { [key: string]: string | undefined });
|
|
88
|
+
}, [safeData]);
|
|
89
|
+
|
|
90
|
+
const logout = useMemoizedFn(({ visitorId }) => {
|
|
91
|
+
confirmApi.open({
|
|
92
|
+
title: t('logoutThisSession'),
|
|
93
|
+
content: t('logoutThisSessionConfirm'),
|
|
94
|
+
confirmButtonText: t('confirm'),
|
|
95
|
+
cancelButtonText: t('cancel'),
|
|
96
|
+
onConfirm: async () => {
|
|
97
|
+
await client.user.logout({ visitorId });
|
|
98
|
+
pageState.refresh();
|
|
99
|
+
confirmApi.close();
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
const otherUserSessions = useCreation(() => {
|
|
104
|
+
const list = safeData.filter((x) => x.visitorId !== currentVisitorId);
|
|
105
|
+
return list;
|
|
106
|
+
}, [safeData]);
|
|
107
|
+
const logoutAll = useMemoizedFn(() => {
|
|
108
|
+
confirmApi.open({
|
|
109
|
+
title: t('logoutAllSession'),
|
|
110
|
+
content: t('logoutAllSessionConfirm'),
|
|
111
|
+
confirmButtonText: t('confirm'),
|
|
112
|
+
cancelButtonText: t('cancel'),
|
|
113
|
+
onConfirm: async () => {
|
|
114
|
+
const list = otherUserSessions.map((x) => {
|
|
115
|
+
return () => client.user.logout({ visitorId: x.visitorId });
|
|
116
|
+
});
|
|
117
|
+
await pAll(list, {
|
|
118
|
+
concurrency: 3,
|
|
119
|
+
stopOnError: false,
|
|
120
|
+
});
|
|
121
|
+
pageState.refresh();
|
|
122
|
+
confirmApi.close();
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
const customButtons: ReactElement[] = [];
|
|
127
|
+
if (showAction) {
|
|
128
|
+
customButtons.push(
|
|
129
|
+
<Tooltip key="logoutAll" title={t('logoutAllTips')}>
|
|
130
|
+
<Button
|
|
131
|
+
sx={{ ml: 0.5 }}
|
|
132
|
+
size="small"
|
|
133
|
+
variant="contained"
|
|
134
|
+
color="error"
|
|
135
|
+
onClick={logoutAll}
|
|
136
|
+
disabled={otherUserSessions.length === 0}>
|
|
137
|
+
{t('logoutAll')}
|
|
138
|
+
</Button>
|
|
139
|
+
</Tooltip>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const tableOptions = useCreation(() => {
|
|
143
|
+
return {
|
|
144
|
+
// viewColumns: false,
|
|
145
|
+
search: false,
|
|
146
|
+
sort: false,
|
|
147
|
+
download: false,
|
|
148
|
+
filter: false,
|
|
149
|
+
print: false,
|
|
150
|
+
expandableRowsOnClick: false,
|
|
151
|
+
searchDebounceTime: 600,
|
|
152
|
+
};
|
|
153
|
+
}, []);
|
|
154
|
+
const columns = [
|
|
155
|
+
{
|
|
156
|
+
label: t('platform'),
|
|
157
|
+
name: 'platform',
|
|
158
|
+
options: {
|
|
159
|
+
customBodyRenderLite: (rawIndex: number) => {
|
|
160
|
+
const x = safeData[rawIndex];
|
|
161
|
+
const result = parseUa(x.ua);
|
|
162
|
+
return <Box>{[result.os?.name, result.os?.version].filter(Boolean).join('/') || t('unknown')}</Box>;
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
label: t('deviceType'),
|
|
168
|
+
name: 'deviceType',
|
|
169
|
+
options: {
|
|
170
|
+
customBodyRenderLite: (rawIndex: number) => {
|
|
171
|
+
const x = safeData[rawIndex];
|
|
172
|
+
const result = parseUa(x.ua);
|
|
173
|
+
return <Box>{[result.browser?.name, result.browser?.version].filter(Boolean).join('/') || t('unknown')}</Box>;
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
label: t('walletOS'),
|
|
179
|
+
name: 'walletOS',
|
|
180
|
+
options: {
|
|
181
|
+
customBodyRenderLite: (rawIndex: number) => {
|
|
182
|
+
const x = safeData[rawIndex];
|
|
183
|
+
return <Box>{x.extra?.walletOS || t('unknown')}</Box>;
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
showUser && {
|
|
188
|
+
label: t('user'),
|
|
189
|
+
name: 'user',
|
|
190
|
+
options: {
|
|
191
|
+
customBodyRenderLite: (rawIndex: number) => {
|
|
192
|
+
const x = safeData[rawIndex];
|
|
193
|
+
return <UserSessionInfo sessionUser={x.user} user={user} />;
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
label: t('updatedAt'),
|
|
199
|
+
name: 'updatedAt',
|
|
200
|
+
options: {
|
|
201
|
+
customBodyRenderLite: (rawIndex: number) => {
|
|
202
|
+
const x = safeData[rawIndex];
|
|
203
|
+
return <RelativeTime value={x.updatedAt} relativeRange={3 * 86400 * 1000} locale={locale} />;
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
label: t('lastLoginIp'),
|
|
209
|
+
name: 'lastLoginIp',
|
|
210
|
+
options: {
|
|
211
|
+
customBodyRenderLite: (rawIndex: number) => {
|
|
212
|
+
const x = safeData[rawIndex];
|
|
213
|
+
return (
|
|
214
|
+
<Box>
|
|
215
|
+
{ipRegionMap[x.lastLoginIp] ? (
|
|
216
|
+
<>
|
|
217
|
+
<Typography variant="body2">{ipRegionMap[x.lastLoginIp]}</Typography>
|
|
218
|
+
<Typography variant="body2" color="grey">
|
|
219
|
+
{x.lastLoginIp || t('unknown')}
|
|
220
|
+
</Typography>
|
|
221
|
+
</>
|
|
222
|
+
) : (
|
|
223
|
+
<Typography>{x.lastLoginIp || t('unknown')}</Typography>
|
|
224
|
+
)}
|
|
225
|
+
</Box>
|
|
226
|
+
);
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
showAction && {
|
|
231
|
+
label: t('actions'),
|
|
232
|
+
name: 'actions',
|
|
233
|
+
options: {
|
|
234
|
+
customBodyRenderLite: (rawIndex: number) => {
|
|
235
|
+
const x = safeData[rawIndex];
|
|
236
|
+
return (
|
|
237
|
+
<Box>
|
|
238
|
+
<Button
|
|
239
|
+
sx={{
|
|
240
|
+
whiteSpace: 'nowrap',
|
|
241
|
+
fontSize: '12px',
|
|
242
|
+
px: 1,
|
|
243
|
+
}}
|
|
244
|
+
disabled={currentVisitorId === x.visitorId}
|
|
245
|
+
variant="outlined"
|
|
246
|
+
size="small"
|
|
247
|
+
color="error"
|
|
248
|
+
onClick={() => logout({ visitorId: x.visitorId })}>
|
|
249
|
+
{currentVisitorId === x.visitorId ? t('currentSession') : t('logout')}
|
|
250
|
+
</Button>
|
|
251
|
+
</Box>
|
|
252
|
+
);
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
].filter(Boolean);
|
|
257
|
+
return (
|
|
258
|
+
<Box
|
|
259
|
+
sx={{
|
|
260
|
+
'.MuiTableCell-head': {
|
|
261
|
+
whiteSpace: 'nowrap',
|
|
262
|
+
fontWeight: 'bold',
|
|
263
|
+
},
|
|
264
|
+
}}>
|
|
265
|
+
{confirmHolder}
|
|
266
|
+
<Datatable
|
|
267
|
+
locale={locale}
|
|
268
|
+
data={safeData}
|
|
269
|
+
columns={columns}
|
|
270
|
+
customButtons={customButtons}
|
|
271
|
+
options={tableOptions}
|
|
272
|
+
loading={pageState.loading}
|
|
273
|
+
/>
|
|
274
|
+
</Box>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as UserSessions } from './components/user-sessions';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const translations = {
|
|
2
|
+
zh: {
|
|
3
|
+
confirm: '确认',
|
|
4
|
+
cancel: '取消',
|
|
5
|
+
visitorId: '设备标识',
|
|
6
|
+
deviceType: '设备类型',
|
|
7
|
+
platform: '操作系统',
|
|
8
|
+
walletOS: '钱包类型',
|
|
9
|
+
role: '角色',
|
|
10
|
+
fullname: '姓名',
|
|
11
|
+
email: '邮箱',
|
|
12
|
+
avatar: '头像',
|
|
13
|
+
user: '用户',
|
|
14
|
+
updatedAt: '最近活动时间',
|
|
15
|
+
lastLoginIp: '最近活动地址',
|
|
16
|
+
actions: '操作',
|
|
17
|
+
logoutAll: '注销所有会话',
|
|
18
|
+
logoutAllTips: '不会注销当前会话',
|
|
19
|
+
logout: '注销',
|
|
20
|
+
currentSession: '当前会话',
|
|
21
|
+
unknown: '未知',
|
|
22
|
+
logoutThisSession: '注销指定会话',
|
|
23
|
+
logoutThisSessionConfirm: '确定要注销此会话吗?',
|
|
24
|
+
logoutAllSession: '注销所有会话',
|
|
25
|
+
logoutAllSessionConfirm: '确定要注销所有会话吗?',
|
|
26
|
+
},
|
|
27
|
+
en: {
|
|
28
|
+
confirm: 'Confirm',
|
|
29
|
+
cancel: 'Cancel',
|
|
30
|
+
visitorId: 'Device ID',
|
|
31
|
+
deviceType: 'Device Type',
|
|
32
|
+
platform: 'Platform',
|
|
33
|
+
walletOS: 'Wallet OS',
|
|
34
|
+
role: 'Role',
|
|
35
|
+
fullname: 'Fullname',
|
|
36
|
+
email: 'Email',
|
|
37
|
+
avatar: 'Avatar',
|
|
38
|
+
user: 'User',
|
|
39
|
+
updatedAt: 'Last Active Time',
|
|
40
|
+
actions: 'Actions',
|
|
41
|
+
lastLoginIp: 'Last Login IP',
|
|
42
|
+
logoutAll: 'Logout all',
|
|
43
|
+
logoutAllTips: 'Will not logout current session',
|
|
44
|
+
logout: 'Logout',
|
|
45
|
+
currentSession: 'Current Session',
|
|
46
|
+
unknown: 'Unknown',
|
|
47
|
+
logoutThisSession: 'Logout this session',
|
|
48
|
+
logoutThisSessionConfirm: 'Are you sure to logout this session?',
|
|
49
|
+
logoutAllSession: 'Logout all sessions',
|
|
50
|
+
logoutAllSessionConfirm: 'Are you sure to logout all sessions?',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const RESERVED_IP = 'Reserved IP';
|
|
2
|
+
const IP_REGION_CACHE = 'ip-region-cache';
|
|
3
|
+
|
|
4
|
+
async function getIpRegionFromIpApi(ip: string): Promise<string> {
|
|
5
|
+
const url = `https://ipapi.co/${ip}/json/`;
|
|
6
|
+
const result = await fetch(url);
|
|
7
|
+
const data = await result.json();
|
|
8
|
+
|
|
9
|
+
let region = '';
|
|
10
|
+
if (data.error) {
|
|
11
|
+
if (data.reserved) {
|
|
12
|
+
region = RESERVED_IP;
|
|
13
|
+
}
|
|
14
|
+
} else {
|
|
15
|
+
region = [data.country_name, data.region, data.city].filter(Boolean).join('/');
|
|
16
|
+
}
|
|
17
|
+
return region;
|
|
18
|
+
}
|
|
19
|
+
async function getIpRegionFromIpSb(ip: string): Promise<string> {
|
|
20
|
+
const url = `https://api.ip.sb/geoip/${ip}`;
|
|
21
|
+
const result = await fetch(url);
|
|
22
|
+
const data = await result.json();
|
|
23
|
+
|
|
24
|
+
let region = '';
|
|
25
|
+
if (data.error) {
|
|
26
|
+
if (data.reserved) {
|
|
27
|
+
region = RESERVED_IP;
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
region = [data.country, data.region, data.city].filter(Boolean).join('/');
|
|
31
|
+
}
|
|
32
|
+
return region;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function ip2Region(ip: string): Promise<string> {
|
|
36
|
+
let region = '';
|
|
37
|
+
let ipRegionCache: Record<string, string> = {};
|
|
38
|
+
try {
|
|
39
|
+
const tmpCache = localStorage.getItem(IP_REGION_CACHE);
|
|
40
|
+
if (tmpCache) {
|
|
41
|
+
ipRegionCache = JSON.parse(tmpCache);
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
ipRegionCache = {};
|
|
45
|
+
}
|
|
46
|
+
if (ipRegionCache[ip]) {
|
|
47
|
+
region = ipRegionCache[ip];
|
|
48
|
+
} else {
|
|
49
|
+
try {
|
|
50
|
+
region = await getIpRegionFromIpSb(ip);
|
|
51
|
+
} catch {
|
|
52
|
+
console.warn('Fail to get ip region from ip.sb');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!region) {
|
|
56
|
+
try {
|
|
57
|
+
region = await getIpRegionFromIpApi(ip);
|
|
58
|
+
} catch {
|
|
59
|
+
console.warn('Fail to get ip region from ip-api.co');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (region) {
|
|
65
|
+
// NOTICE: 为了防止 cache 过大,将在 size 超过 100 时,删除最旧的数据
|
|
66
|
+
const ipList = Object.keys(ipRegionCache);
|
|
67
|
+
if (ipList.length > 100) {
|
|
68
|
+
delete ipRegionCache[ipList[0]];
|
|
69
|
+
}
|
|
70
|
+
if (ipRegionCache[ip]) {
|
|
71
|
+
delete ipRegionCache[ip];
|
|
72
|
+
}
|
|
73
|
+
ipRegionCache[ip] = region;
|
|
74
|
+
localStorage.setItem(IP_REGION_CACHE, JSON.stringify(ipRegionCache));
|
|
75
|
+
}
|
|
76
|
+
return region;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// eslint-disable-next-line require-await
|
|
80
|
+
export async function batchIp2Region(ips: string[]): Promise<string[]> {
|
|
81
|
+
return Promise.all(ips.map((ip) => ip2Region(ip)));
|
|
82
|
+
}
|
package/src/blocklets.js
CHANGED
|
@@ -28,30 +28,30 @@ export const getLocalizedNavigation = (navigation, locale = 'en') => {
|
|
|
28
28
|
return navigation;
|
|
29
29
|
}
|
|
30
30
|
// eslint-disable-next-line no-shadow
|
|
31
|
-
const getTitle = (title,
|
|
31
|
+
const getTitle = (title, _locale) => {
|
|
32
32
|
if (typeof title === 'string') {
|
|
33
33
|
return title;
|
|
34
34
|
}
|
|
35
35
|
if (typeof title === 'object') {
|
|
36
|
-
return title[
|
|
36
|
+
return title[_locale] || title?.en || title?.zh;
|
|
37
37
|
}
|
|
38
38
|
return title;
|
|
39
39
|
};
|
|
40
40
|
// eslint-disable-next-line no-shadow
|
|
41
|
-
const getLink = (link,
|
|
41
|
+
const getLink = (link, _locale) => {
|
|
42
42
|
if (typeof link === 'string') {
|
|
43
43
|
// http[s] 开头的 url
|
|
44
44
|
if (isUrl(link)) {
|
|
45
45
|
const url = new URL(link);
|
|
46
|
-
url.searchParams.set('locale',
|
|
46
|
+
url.searchParams.set('locale', _locale);
|
|
47
47
|
return url.href;
|
|
48
48
|
}
|
|
49
49
|
const url = new URL(link, window.location.origin);
|
|
50
|
-
url.searchParams.set('locale',
|
|
50
|
+
url.searchParams.set('locale', _locale);
|
|
51
51
|
return url.pathname + url.search;
|
|
52
52
|
}
|
|
53
53
|
if (typeof link === 'object') {
|
|
54
|
-
return link[
|
|
54
|
+
return link[_locale] || link?.en || link?.zh;
|
|
55
55
|
}
|
|
56
56
|
return link;
|
|
57
57
|
};
|
|
@@ -9,7 +9,7 @@ import SessionUser from '@arcblock/ux/lib/SessionUser';
|
|
|
9
9
|
import SessionBlocklet from '@arcblock/ux/lib/SessionBlocklet';
|
|
10
10
|
import LocaleSelector from '@arcblock/ux/lib/Locale/selector';
|
|
11
11
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
12
|
-
import {
|
|
12
|
+
import { SessionManagerProps } from '../types';
|
|
13
13
|
import { getLocalizedNavigation, filterNavByRole } from '../blocklets';
|
|
14
14
|
|
|
15
15
|
// eslint-disable-next-line no-shadow
|
|
@@ -81,7 +81,7 @@ HeaderAddons.propTypes = {
|
|
|
81
81
|
// - PropTypes.func: 可以把自定义 addons 插在 session-manager 或 locale-selector (如果存在的话) 前/中/后
|
|
82
82
|
// - PropTypes.node: 将 addons 原样传给 UX Header 组件
|
|
83
83
|
addons: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
|
|
84
|
-
sessionManagerProps,
|
|
84
|
+
sessionManagerProps: SessionManagerProps,
|
|
85
85
|
};
|
|
86
86
|
|
|
87
87
|
HeaderAddons.defaultProps = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createContext, useContext, useMemo, useState, useEffect } from 'react';
|
|
2
|
-
import { SessionContext as TSessionContext } from '../@types';
|
|
3
2
|
import { SessionContext } from '@arcblock/did-connect/lib/Session';
|
|
4
3
|
|
|
4
|
+
import { SessionContext as TSessionContext } from '../@types';
|
|
5
5
|
|
|
6
6
|
export interface SpaceGateway {
|
|
7
7
|
did: string;
|
|
@@ -12,13 +12,13 @@ export interface SpaceGateway {
|
|
|
12
12
|
|
|
13
13
|
export interface SettingStorageEndpoint {
|
|
14
14
|
(endpoint: string | undefined): void | Promise<void>;
|
|
15
|
-
}
|
|
15
|
+
}
|
|
16
16
|
|
|
17
17
|
interface ConfigUserSpaceContextType {
|
|
18
18
|
loading: boolean;
|
|
19
19
|
spaceGateway: SpaceGateway | undefined;
|
|
20
20
|
deleteSpaceGateway: (spaceGateway: SpaceGateway) => Promise<void>;
|
|
21
|
-
updateSpaceGateway: (updateSpaceGateway: SpaceGateway) =>
|
|
21
|
+
updateSpaceGateway: (updateSpaceGateway: SpaceGateway) => Promise<void>;
|
|
22
22
|
storageEndpoint: string;
|
|
23
23
|
settingStorageEndpoint: SettingStorageEndpoint;
|
|
24
24
|
hasStorageEndpoint: boolean;
|
|
@@ -27,9 +27,9 @@ interface ConfigUserSpaceContextType {
|
|
|
27
27
|
const ConfigUserSpaceContext = createContext<ConfigUserSpaceContextType>({} as ConfigUserSpaceContextType);
|
|
28
28
|
const { Provider, Consumer } = ConfigUserSpaceContext;
|
|
29
29
|
|
|
30
|
-
function ConfigUserSpaceProvider({ children }: {children: React.ReactNode }) {
|
|
30
|
+
function ConfigUserSpaceProvider({ children }: { children: React.ReactNode }) {
|
|
31
31
|
const [loading] = useState(false);
|
|
32
|
-
const {session} = useContext<TSessionContext>(SessionContext);
|
|
32
|
+
const { session } = useContext<TSessionContext>(SessionContext);
|
|
33
33
|
const { user } = session;
|
|
34
34
|
|
|
35
35
|
const [spaceGateway, setSpaceGateway] = useState<SpaceGateway | undefined>();
|
|
@@ -38,13 +38,17 @@ function ConfigUserSpaceProvider({ children }: {children: React.ReactNode }) {
|
|
|
38
38
|
}, [user?.didSpace]);
|
|
39
39
|
|
|
40
40
|
useEffect(() => {
|
|
41
|
-
setSpaceGateway(user?.didSpace as SpaceGateway)
|
|
42
|
-
}, [user?.didSpace])
|
|
41
|
+
setSpaceGateway(user?.didSpace as SpaceGateway);
|
|
42
|
+
}, [user?.didSpace]);
|
|
43
43
|
|
|
44
|
+
// eslint-disable-next-line require-await
|
|
44
45
|
const deleteSpaceGateway = async (): Promise<void> => {
|
|
45
46
|
setSpaceGateway(undefined);
|
|
46
47
|
};
|
|
47
|
-
|
|
48
|
+
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
50
|
+
const settingStorageEndpoint = (endpoint: string | undefined) => {};
|
|
51
|
+
|
|
48
52
|
const updateSpaceGateway = async (x: SpaceGateway) => {
|
|
49
53
|
setSpaceGateway(x);
|
|
50
54
|
session.refresh();
|
|
@@ -52,9 +56,6 @@ function ConfigUserSpaceProvider({ children }: {children: React.ReactNode }) {
|
|
|
52
56
|
await settingStorageEndpoint(x.endpoint);
|
|
53
57
|
};
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
const settingStorageEndpoint = (endpoint: string | undefined) => {};
|
|
57
|
-
|
|
58
59
|
const hasStorageEndpoint = Boolean(storageEndpoint && spaceGateway);
|
|
59
60
|
|
|
60
61
|
return (
|
package/src/index.ts
CHANGED
package/src/libs/spaces.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { joinURL } from
|
|
1
|
+
import { joinURL } from 'ufo';
|
|
2
2
|
|
|
3
3
|
export function getSpaceNftDisplayUrlFromEndpoint(endpoint: string): string {
|
|
4
4
|
const prefix = endpoint.replace(/\/api\/space\/.+/, '');
|
|
@@ -7,4 +7,4 @@ export function getSpaceNftDisplayUrlFromEndpoint(endpoint: string): string {
|
|
|
7
7
|
const spaceDid = strArray.at(-4);
|
|
8
8
|
|
|
9
9
|
return joinURL(prefix, `/api/space/nft/display?spaceDid=${spaceDid}`);
|
|
10
|
-
}
|
|
10
|
+
}
|