strapi-plugin-magic-sessionmanager 2.0.0 → 2.0.2
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/admin/jsconfig.json +10 -0
- package/admin/src/components/Initializer.jsx +11 -0
- package/admin/src/components/LicenseGuard.jsx +591 -0
- package/admin/src/components/OnlineUsersWidget.jsx +208 -0
- package/admin/src/components/PluginIcon.jsx +8 -0
- package/admin/src/components/SessionDetailModal.jsx +445 -0
- package/admin/src/components/SessionInfoCard.jsx +151 -0
- package/admin/src/components/SessionInfoPanel.jsx +375 -0
- package/admin/src/components/index.jsx +5 -0
- package/admin/src/hooks/useLicense.js +103 -0
- package/admin/src/index.js +137 -0
- package/admin/src/pages/ActiveSessions.jsx +12 -0
- package/admin/src/pages/Analytics.jsx +735 -0
- package/admin/src/pages/App.jsx +12 -0
- package/admin/src/pages/HomePage.jsx +1248 -0
- package/admin/src/pages/License.jsx +603 -0
- package/admin/src/pages/Settings.jsx +1497 -0
- package/admin/src/pages/SettingsNew.jsx +1204 -0
- package/admin/src/pages/index.jsx +3 -0
- package/admin/src/pluginId.js +3 -0
- package/admin/src/translations/de.json +20 -0
- package/admin/src/translations/en.json +20 -0
- package/admin/src/utils/getTranslation.js +5 -0
- package/admin/src/utils/index.js +2 -0
- package/admin/src/utils/parseUserAgent.js +79 -0
- package/package.json +3 -1
- package/server/jsconfig.json +10 -0
- package/server/src/bootstrap.js +297 -0
- package/server/src/config/index.js +20 -0
- package/server/src/content-types/index.js +9 -0
- package/server/src/content-types/session/schema.json +76 -0
- package/server/src/controllers/controller.js +11 -0
- package/server/src/controllers/index.js +11 -0
- package/server/src/controllers/license.js +266 -0
- package/server/src/controllers/session.js +362 -0
- package/server/src/controllers/settings.js +122 -0
- package/server/src/destroy.js +18 -0
- package/server/src/index.js +21 -0
- package/server/src/middlewares/index.js +5 -0
- package/server/src/middlewares/last-seen.js +56 -0
- package/server/src/policies/index.js +3 -0
- package/server/src/register.js +32 -0
- package/server/src/routes/admin.js +149 -0
- package/server/src/routes/content-api.js +51 -0
- package/server/src/routes/index.js +9 -0
- package/server/src/services/geolocation.js +180 -0
- package/server/src/services/index.js +13 -0
- package/server/src/services/license-guard.js +308 -0
- package/server/src/services/notifications.js +319 -0
- package/server/src/services/service.js +7 -0
- package/server/src/services/session.js +345 -0
- package/server/src/utils/getClientIp.js +118 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { Box, Typography, Flex, Grid } from '@strapi/design-system';
|
|
3
|
+
import { Check, Cross, Clock, User, Monitor } from '@strapi/icons';
|
|
4
|
+
import { useFetchClient } from '@strapi/strapi/admin';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Online Users Widget - Dashboard widget showing user activity statistics
|
|
8
|
+
* Styled exactly like Project Statistics
|
|
9
|
+
*/
|
|
10
|
+
const OnlineUsersWidget = () => {
|
|
11
|
+
const { get } = useFetchClient();
|
|
12
|
+
const [stats, setStats] = useState({
|
|
13
|
+
onlineNow: 0,
|
|
14
|
+
offline: 0,
|
|
15
|
+
last15min: 0,
|
|
16
|
+
last30min: 0,
|
|
17
|
+
totalUsers: 0,
|
|
18
|
+
blocked: 0,
|
|
19
|
+
});
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
|
|
22
|
+
const fetchStats = useCallback(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const { data } = await get('/magic-sessionmanager/sessions');
|
|
25
|
+
const sessions = data.data || [];
|
|
26
|
+
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const fifteenMin = 15 * 60 * 1000;
|
|
29
|
+
const thirtyMin = 30 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
const onlineNow = new Set();
|
|
32
|
+
const last15min = new Set();
|
|
33
|
+
const last30min = new Set();
|
|
34
|
+
|
|
35
|
+
sessions.forEach(session => {
|
|
36
|
+
const userId = session.user?.id;
|
|
37
|
+
if (!userId) return;
|
|
38
|
+
|
|
39
|
+
const lastActive = session.lastActive ? new Date(session.lastActive) : new Date(session.loginTime);
|
|
40
|
+
const timeSinceActive = now - lastActive.getTime();
|
|
41
|
+
|
|
42
|
+
if (timeSinceActive < fifteenMin) {
|
|
43
|
+
onlineNow.add(userId);
|
|
44
|
+
last15min.add(userId);
|
|
45
|
+
last30min.add(userId);
|
|
46
|
+
} else if (timeSinceActive < thirtyMin) {
|
|
47
|
+
last15min.add(userId);
|
|
48
|
+
last30min.add(userId);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Get total users
|
|
54
|
+
const { data: usersData } = await get('/content-manager/collection-types/plugin::users-permissions.user?pageSize=1');
|
|
55
|
+
const totalUsers = usersData?.pagination?.total || 0;
|
|
56
|
+
|
|
57
|
+
// Get blocked users count
|
|
58
|
+
const { data: blockedData } = await get('/content-manager/collection-types/plugin::users-permissions.user?filters[$and][0][blocked][$eq]=true&pageSize=1');
|
|
59
|
+
const blockedUsers = blockedData?.pagination?.total || 0;
|
|
60
|
+
|
|
61
|
+
setStats({
|
|
62
|
+
onlineNow: onlineNow.size,
|
|
63
|
+
last15min: last15min.size,
|
|
64
|
+
last30min: last30min.size,
|
|
65
|
+
offline: totalUsers - onlineNow.size,
|
|
66
|
+
totalUsers,
|
|
67
|
+
blocked: blockedUsers,
|
|
68
|
+
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error('[OnlineUsersWidget] Error fetching user count:', err);
|
|
71
|
+
setStats({
|
|
72
|
+
onlineNow: onlineNow.size,
|
|
73
|
+
last15min: last15min.size,
|
|
74
|
+
last30min: last30min.size,
|
|
75
|
+
offline: 0,
|
|
76
|
+
totalUsers: onlineNow.size,
|
|
77
|
+
blocked: 0,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error('[OnlineUsersWidget] Error:', err);
|
|
82
|
+
} finally {
|
|
83
|
+
setLoading(false);
|
|
84
|
+
}
|
|
85
|
+
}, [get]);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
fetchStats();
|
|
89
|
+
|
|
90
|
+
// Refresh every 30 seconds
|
|
91
|
+
const interval = setInterval(fetchStats, 30000);
|
|
92
|
+
return () => clearInterval(interval);
|
|
93
|
+
}, [fetchStats]);
|
|
94
|
+
|
|
95
|
+
// Stat Card - styled like Project Statistics items
|
|
96
|
+
const StatCard = ({ icon: Icon, label, value, color }) => (
|
|
97
|
+
<Box
|
|
98
|
+
as="a"
|
|
99
|
+
padding={4}
|
|
100
|
+
background="neutral0"
|
|
101
|
+
hasRadius
|
|
102
|
+
shadow="tableShadow"
|
|
103
|
+
style={{
|
|
104
|
+
textDecoration: 'none',
|
|
105
|
+
cursor: 'default',
|
|
106
|
+
transition: 'box-shadow 0.2s',
|
|
107
|
+
border: '1px solid #f0f0ff',
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
<Flex justifyContent="space-between" alignItems="flex-start">
|
|
111
|
+
<Flex gap={3} alignItems="center">
|
|
112
|
+
<Box
|
|
113
|
+
padding={2}
|
|
114
|
+
background={`${color}100`}
|
|
115
|
+
hasRadius
|
|
116
|
+
style={{
|
|
117
|
+
display: 'flex',
|
|
118
|
+
alignItems: 'center',
|
|
119
|
+
justifyContent: 'center',
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<Icon width="16px" height="16px" fill={`${color}600`} />
|
|
123
|
+
</Box>
|
|
124
|
+
<Flex direction="column" gap={1} alignItems="flex-start">
|
|
125
|
+
<Typography variant="pi" textColor="neutral600">
|
|
126
|
+
{label}
|
|
127
|
+
</Typography>
|
|
128
|
+
<Typography variant="delta" fontWeight="bold" textColor="neutral800">
|
|
129
|
+
{value}
|
|
130
|
+
</Typography>
|
|
131
|
+
</Flex>
|
|
132
|
+
</Flex>
|
|
133
|
+
</Flex>
|
|
134
|
+
</Box>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (loading) {
|
|
138
|
+
return (
|
|
139
|
+
<Box padding={4}>
|
|
140
|
+
<Typography variant="pi" textColor="neutral600">Loading...</Typography>
|
|
141
|
+
</Box>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Box padding={0}>
|
|
147
|
+
<Flex direction="column" gap={3}>
|
|
148
|
+
<Grid.Root gap={3} gridCols={2}>
|
|
149
|
+
<Grid.Item col={1}>
|
|
150
|
+
<StatCard
|
|
151
|
+
icon={Check}
|
|
152
|
+
label="Online Now"
|
|
153
|
+
value={stats.onlineNow}
|
|
154
|
+
color="success"
|
|
155
|
+
/>
|
|
156
|
+
</Grid.Item>
|
|
157
|
+
|
|
158
|
+
<Grid.Item col={1}>
|
|
159
|
+
<StatCard
|
|
160
|
+
icon={Cross}
|
|
161
|
+
label="Offline"
|
|
162
|
+
value={stats.offline}
|
|
163
|
+
color="neutral"
|
|
164
|
+
/>
|
|
165
|
+
</Grid.Item>
|
|
166
|
+
|
|
167
|
+
<Grid.Item col={1}>
|
|
168
|
+
<StatCard
|
|
169
|
+
icon={Clock}
|
|
170
|
+
label="Last 15 min"
|
|
171
|
+
value={stats.last15min}
|
|
172
|
+
color="primary"
|
|
173
|
+
/>
|
|
174
|
+
</Grid.Item>
|
|
175
|
+
|
|
176
|
+
<Grid.Item col={1}>
|
|
177
|
+
<StatCard
|
|
178
|
+
icon={Clock}
|
|
179
|
+
label="Last 30 min"
|
|
180
|
+
value={stats.last30min}
|
|
181
|
+
color="secondary"
|
|
182
|
+
/>
|
|
183
|
+
</Grid.Item>
|
|
184
|
+
|
|
185
|
+
<Grid.Item col={1}>
|
|
186
|
+
<StatCard
|
|
187
|
+
icon={User}
|
|
188
|
+
label="Total Users"
|
|
189
|
+
value={stats.totalUsers}
|
|
190
|
+
color="neutral"
|
|
191
|
+
/>
|
|
192
|
+
</Grid.Item>
|
|
193
|
+
|
|
194
|
+
<Grid.Item col={1}>
|
|
195
|
+
<StatCard
|
|
196
|
+
icon={Cross}
|
|
197
|
+
label="Blocked"
|
|
198
|
+
value={stats.blocked}
|
|
199
|
+
color="danger"
|
|
200
|
+
/>
|
|
201
|
+
</Grid.Item>
|
|
202
|
+
</Grid.Root>
|
|
203
|
+
</Flex>
|
|
204
|
+
</Box>
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export default OnlineUsersWidget;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Monitor Icon - Session Manager
|
|
2
|
+
const PluginIcon = () => (
|
|
3
|
+
<svg viewBox="0 0 32 32" fill="currentColor" width="24" height="24">
|
|
4
|
+
<path d="M26 5H6a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h20a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3M20 27h-8a1 1 0 0 0 0 2h8a1 1 0 0 0 0-2" />
|
|
5
|
+
</svg>
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
export default PluginIcon;
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import {
|
|
4
|
+
Modal,
|
|
5
|
+
Box,
|
|
6
|
+
Flex,
|
|
7
|
+
Typography,
|
|
8
|
+
Button,
|
|
9
|
+
Badge,
|
|
10
|
+
Divider,
|
|
11
|
+
} from '@strapi/design-system';
|
|
12
|
+
import {
|
|
13
|
+
Monitor,
|
|
14
|
+
Phone,
|
|
15
|
+
Server,
|
|
16
|
+
Cross,
|
|
17
|
+
Check,
|
|
18
|
+
Clock,
|
|
19
|
+
Information,
|
|
20
|
+
Crown,
|
|
21
|
+
Earth,
|
|
22
|
+
Shield,
|
|
23
|
+
} from '@strapi/icons';
|
|
24
|
+
import { useFetchClient, useNotification } from '@strapi/strapi/admin';
|
|
25
|
+
import parseUserAgent from '../utils/parseUserAgent';
|
|
26
|
+
import pluginId from '../pluginId';
|
|
27
|
+
import { useLicense } from '../hooks/useLicense';
|
|
28
|
+
|
|
29
|
+
const TwoColumnGrid = styled.div`
|
|
30
|
+
display: grid;
|
|
31
|
+
grid-template-columns: 1fr 1fr;
|
|
32
|
+
gap: 24px;
|
|
33
|
+
|
|
34
|
+
@media (max-width: 768px) {
|
|
35
|
+
grid-template-columns: 1fr;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const SectionTitle = styled(Typography)`
|
|
40
|
+
text-transform: uppercase;
|
|
41
|
+
letter-spacing: 0.5px;
|
|
42
|
+
font-size: 11px;
|
|
43
|
+
font-weight: 700;
|
|
44
|
+
color: #374151;
|
|
45
|
+
margin-bottom: 16px;
|
|
46
|
+
padding-bottom: 8px;
|
|
47
|
+
border-bottom: 2px solid #e5e7eb;
|
|
48
|
+
display: block;
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const Section = styled(Box)`
|
|
52
|
+
margin-bottom: 24px;
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const SessionDetailModal = ({ session, onClose, onSessionTerminated }) => {
|
|
56
|
+
const { get, post } = useFetchClient();
|
|
57
|
+
const { toggleNotification } = useNotification();
|
|
58
|
+
const { isPremium, loading: licenseLoading } = useLicense();
|
|
59
|
+
const [terminating, setTerminating] = useState(false);
|
|
60
|
+
const [showUserAgent, setShowUserAgent] = useState(false);
|
|
61
|
+
const [geoData, setGeoData] = useState(null);
|
|
62
|
+
const [geoLoading, setGeoLoading] = useState(false);
|
|
63
|
+
|
|
64
|
+
if (!session) return null;
|
|
65
|
+
|
|
66
|
+
const deviceInfo = parseUserAgent(session.userAgent);
|
|
67
|
+
const isOnline = session.isTrulyActive;
|
|
68
|
+
|
|
69
|
+
// Fetch real geolocation data if premium
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (isPremium && session.ipAddress && !geoData) {
|
|
72
|
+
fetchGeolocationData();
|
|
73
|
+
}
|
|
74
|
+
}, [isPremium, session.ipAddress]);
|
|
75
|
+
|
|
76
|
+
const fetchGeolocationData = async () => {
|
|
77
|
+
setGeoLoading(true);
|
|
78
|
+
try {
|
|
79
|
+
const { data } = await get(`/${pluginId}/geolocation/${session.ipAddress}`);
|
|
80
|
+
setGeoData(data.data);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error('[SessionDetailModal] Error fetching geolocation:', err);
|
|
83
|
+
// Fallback to mock data if API fails
|
|
84
|
+
setGeoData({
|
|
85
|
+
country_flag: '🌍',
|
|
86
|
+
country: 'Unknown',
|
|
87
|
+
city: 'Unknown',
|
|
88
|
+
timezone: 'Unknown',
|
|
89
|
+
securityScore: 50,
|
|
90
|
+
riskLevel: 'Unknown',
|
|
91
|
+
isVpn: false,
|
|
92
|
+
isProxy: false,
|
|
93
|
+
});
|
|
94
|
+
} finally {
|
|
95
|
+
setGeoLoading(false);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Use real data if available, otherwise fallback
|
|
100
|
+
const premiumData = geoData || {
|
|
101
|
+
country_flag: '🌍',
|
|
102
|
+
country: 'Loading...',
|
|
103
|
+
city: 'Loading...',
|
|
104
|
+
timezone: 'Loading...',
|
|
105
|
+
securityScore: 0,
|
|
106
|
+
riskLevel: 'Unknown',
|
|
107
|
+
isVpn: false,
|
|
108
|
+
isProxy: false,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const getDeviceIcon = (deviceType) => {
|
|
112
|
+
if (deviceType === 'Mobile' || deviceType === 'Tablet') return Phone;
|
|
113
|
+
if (deviceType === 'Desktop' || deviceType === 'Laptop') return Monitor;
|
|
114
|
+
return Server;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const DeviceIcon = getDeviceIcon(deviceInfo.device);
|
|
118
|
+
|
|
119
|
+
const handleTerminate = async () => {
|
|
120
|
+
if (!confirm('Are you sure you want to terminate this session?')) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
setTerminating(true);
|
|
125
|
+
try {
|
|
126
|
+
await post(`/${pluginId}/sessions/${session.id}/terminate`);
|
|
127
|
+
|
|
128
|
+
toggleNotification({
|
|
129
|
+
type: 'success',
|
|
130
|
+
message: 'Session terminated successfully',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
onSessionTerminated();
|
|
134
|
+
onClose();
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error('[SessionDetailModal] Error:', err);
|
|
137
|
+
toggleNotification({
|
|
138
|
+
type: 'danger',
|
|
139
|
+
message: 'Failed to terminate session',
|
|
140
|
+
});
|
|
141
|
+
} finally {
|
|
142
|
+
setTerminating(false);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const DetailRow = ({ label, value, icon: Icon, compact }) => (
|
|
147
|
+
<Flex gap={3} alignItems="flex-start" style={{ marginBottom: compact ? '12px' : '16px' }}>
|
|
148
|
+
{Icon && (
|
|
149
|
+
<Box style={{
|
|
150
|
+
width: '36px',
|
|
151
|
+
height: '36px',
|
|
152
|
+
display: 'flex',
|
|
153
|
+
alignItems: 'center',
|
|
154
|
+
justifyContent: 'center',
|
|
155
|
+
background: '#f3f4f6',
|
|
156
|
+
borderRadius: '8px',
|
|
157
|
+
flexShrink: 0,
|
|
158
|
+
}}>
|
|
159
|
+
<Icon width="18px" height="18px" />
|
|
160
|
+
</Box>
|
|
161
|
+
)}
|
|
162
|
+
<Flex direction="column" alignItems="flex-start" style={{ flex: 1, minWidth: 0 }}>
|
|
163
|
+
<Typography variant="pi" textColor="neutral600" style={{ fontSize: '11px', fontWeight: '600', marginBottom: '4px' }}>
|
|
164
|
+
{label}
|
|
165
|
+
</Typography>
|
|
166
|
+
<Typography variant="omega" textColor="neutral800" style={{ fontSize: '14px', fontWeight: '500' }}>
|
|
167
|
+
{value}
|
|
168
|
+
</Typography>
|
|
169
|
+
</Flex>
|
|
170
|
+
</Flex>
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<Modal.Root open onOpenChange={onClose}>
|
|
175
|
+
<Modal.Content style={{ maxWidth: '900px' }}>
|
|
176
|
+
<Modal.Header>
|
|
177
|
+
<Flex gap={3} alignItems="center">
|
|
178
|
+
<Box style={{
|
|
179
|
+
width: '48px',
|
|
180
|
+
height: '48px',
|
|
181
|
+
borderRadius: '12px',
|
|
182
|
+
background: isOnline ? '#dcfce7' : '#f3f4f6',
|
|
183
|
+
display: 'flex',
|
|
184
|
+
alignItems: 'center',
|
|
185
|
+
justifyContent: 'center',
|
|
186
|
+
}}>
|
|
187
|
+
<DeviceIcon width="24px" height="24px" />
|
|
188
|
+
</Box>
|
|
189
|
+
<Flex direction="column" alignItems="flex-start">
|
|
190
|
+
<Typography variant="beta" fontWeight="bold">
|
|
191
|
+
Session Details
|
|
192
|
+
</Typography>
|
|
193
|
+
<Typography variant="pi" textColor="neutral600">
|
|
194
|
+
ID: {session.id}
|
|
195
|
+
</Typography>
|
|
196
|
+
</Flex>
|
|
197
|
+
</Flex>
|
|
198
|
+
</Modal.Header>
|
|
199
|
+
|
|
200
|
+
<Modal.Body>
|
|
201
|
+
<Box padding={6}>
|
|
202
|
+
{/* Status Badge */}
|
|
203
|
+
<Flex justifyContent="center" style={{ marginBottom: '24px' }}>
|
|
204
|
+
<Badge
|
|
205
|
+
backgroundColor={isOnline ? 'success600' : 'neutral600'}
|
|
206
|
+
textColor="neutral0"
|
|
207
|
+
size="M"
|
|
208
|
+
style={{ fontSize: '14px', padding: '8px 20px', fontWeight: '600' }}
|
|
209
|
+
>
|
|
210
|
+
{isOnline ? '🟢 ONLINE' : '⚫ OFFLINE'}
|
|
211
|
+
</Badge>
|
|
212
|
+
</Flex>
|
|
213
|
+
|
|
214
|
+
<Divider style={{ marginBottom: '24px' }} />
|
|
215
|
+
|
|
216
|
+
{/* Two Column Layout */}
|
|
217
|
+
<TwoColumnGrid>
|
|
218
|
+
{/* Left Column: User & Device */}
|
|
219
|
+
<Box>
|
|
220
|
+
{/* User Information */}
|
|
221
|
+
<Section>
|
|
222
|
+
<SectionTitle>
|
|
223
|
+
👤 User
|
|
224
|
+
</SectionTitle>
|
|
225
|
+
|
|
226
|
+
<DetailRow compact icon={Check} label="Username" value={session.user?.username || 'N/A'} />
|
|
227
|
+
<DetailRow compact icon={Information} label="Email" value={session.user?.email || 'N/A'} />
|
|
228
|
+
<DetailRow compact icon={Information} label="User ID" value={session.user?.id || 'N/A'} />
|
|
229
|
+
</Section>
|
|
230
|
+
|
|
231
|
+
{/* Device Information */}
|
|
232
|
+
<Section>
|
|
233
|
+
<SectionTitle>
|
|
234
|
+
💻 Device
|
|
235
|
+
</SectionTitle>
|
|
236
|
+
|
|
237
|
+
<DetailRow compact icon={DeviceIcon} label="Device" value={deviceInfo.device} />
|
|
238
|
+
<DetailRow compact icon={Monitor} label="Browser" value={`${deviceInfo.browser} ${deviceInfo.browserVersion || ''}`} />
|
|
239
|
+
<DetailRow compact icon={Server} label="OS" value={deviceInfo.os} />
|
|
240
|
+
<DetailRow compact icon={Information} label="IP" value={session.ipAddress} />
|
|
241
|
+
</Section>
|
|
242
|
+
</Box>
|
|
243
|
+
|
|
244
|
+
{/* Right Column: Timeline */}
|
|
245
|
+
<Box>
|
|
246
|
+
<Section>
|
|
247
|
+
<SectionTitle>
|
|
248
|
+
⏱️ Timeline
|
|
249
|
+
</SectionTitle>
|
|
250
|
+
|
|
251
|
+
<DetailRow
|
|
252
|
+
compact
|
|
253
|
+
icon={Clock}
|
|
254
|
+
label="Login"
|
|
255
|
+
value={new Date(session.loginTime).toLocaleString('de-DE', {
|
|
256
|
+
day: '2-digit',
|
|
257
|
+
month: 'short',
|
|
258
|
+
hour: '2-digit',
|
|
259
|
+
minute: '2-digit'
|
|
260
|
+
})}
|
|
261
|
+
/>
|
|
262
|
+
<DetailRow
|
|
263
|
+
compact
|
|
264
|
+
icon={Clock}
|
|
265
|
+
label="Last Active"
|
|
266
|
+
value={new Date(session.lastActive || session.loginTime).toLocaleString('de-DE', {
|
|
267
|
+
day: '2-digit',
|
|
268
|
+
month: 'short',
|
|
269
|
+
hour: '2-digit',
|
|
270
|
+
minute: '2-digit'
|
|
271
|
+
})}
|
|
272
|
+
/>
|
|
273
|
+
<DetailRow
|
|
274
|
+
compact
|
|
275
|
+
icon={Clock}
|
|
276
|
+
label="Idle Time"
|
|
277
|
+
value={`${session.minutesSinceActive} min`}
|
|
278
|
+
/>
|
|
279
|
+
{session.logoutTime && (
|
|
280
|
+
<DetailRow
|
|
281
|
+
compact
|
|
282
|
+
icon={Cross}
|
|
283
|
+
label="Logout"
|
|
284
|
+
value={new Date(session.logoutTime).toLocaleString('de-DE', {
|
|
285
|
+
day: '2-digit',
|
|
286
|
+
month: 'short',
|
|
287
|
+
hour: '2-digit',
|
|
288
|
+
minute: '2-digit'
|
|
289
|
+
})}
|
|
290
|
+
/>
|
|
291
|
+
)}
|
|
292
|
+
</Section>
|
|
293
|
+
</Box>
|
|
294
|
+
</TwoColumnGrid>
|
|
295
|
+
|
|
296
|
+
{/* PREMIUM: Geolocation & Security Information */}
|
|
297
|
+
{isPremium ? (
|
|
298
|
+
<Section>
|
|
299
|
+
<SectionTitle>
|
|
300
|
+
🌍 Location & Security
|
|
301
|
+
</SectionTitle>
|
|
302
|
+
|
|
303
|
+
{geoLoading ? (
|
|
304
|
+
<Box padding={4} style={{ textAlign: 'center' }}>
|
|
305
|
+
<Typography variant="pi" textColor="neutral600">
|
|
306
|
+
Loading location data...
|
|
307
|
+
</Typography>
|
|
308
|
+
</Box>
|
|
309
|
+
) : (
|
|
310
|
+
<TwoColumnGrid>
|
|
311
|
+
<Box>
|
|
312
|
+
<DetailRow
|
|
313
|
+
compact
|
|
314
|
+
icon={Earth}
|
|
315
|
+
label="Country"
|
|
316
|
+
value={`${premiumData.country_flag || '🌍'} ${premiumData.country}`}
|
|
317
|
+
/>
|
|
318
|
+
<DetailRow compact icon={Earth} label="City" value={premiumData.city} />
|
|
319
|
+
<DetailRow compact icon={Clock} label="Timezone" value={premiumData.timezone} />
|
|
320
|
+
</Box>
|
|
321
|
+
<Box>
|
|
322
|
+
<DetailRow
|
|
323
|
+
compact
|
|
324
|
+
icon={Shield}
|
|
325
|
+
label="Security"
|
|
326
|
+
value={`${premiumData.securityScore}/100 (${premiumData.riskLevel})`}
|
|
327
|
+
/>
|
|
328
|
+
<DetailRow
|
|
329
|
+
compact
|
|
330
|
+
icon={Shield}
|
|
331
|
+
label="VPN"
|
|
332
|
+
value={premiumData.isVpn ? '⚠️ Yes' : '✅ No'}
|
|
333
|
+
/>
|
|
334
|
+
<DetailRow
|
|
335
|
+
compact
|
|
336
|
+
icon={Shield}
|
|
337
|
+
label="Proxy"
|
|
338
|
+
value={premiumData.isProxy ? '⚠️ Yes' : '✅ No'}
|
|
339
|
+
/>
|
|
340
|
+
</Box>
|
|
341
|
+
</TwoColumnGrid>
|
|
342
|
+
)}
|
|
343
|
+
</Section>
|
|
344
|
+
) : (
|
|
345
|
+
<Section>
|
|
346
|
+
<Box
|
|
347
|
+
padding={5}
|
|
348
|
+
style={{
|
|
349
|
+
background: 'linear-gradient(135deg, #fef3c7 0%, #fed7aa 100%)',
|
|
350
|
+
borderRadius: '12px',
|
|
351
|
+
border: '2px solid #fbbf24',
|
|
352
|
+
textAlign: 'center',
|
|
353
|
+
}}
|
|
354
|
+
>
|
|
355
|
+
<Flex direction="column" alignItems="center" gap={3}>
|
|
356
|
+
<Crown style={{ width: '40px', height: '40px', color: '#d97706' }} />
|
|
357
|
+
<Typography variant="beta" style={{ color: '#92400e', fontWeight: '700' }}>
|
|
358
|
+
🌍 Location & Security Analysis
|
|
359
|
+
</Typography>
|
|
360
|
+
<Typography variant="omega" style={{ color: '#78350f', fontSize: '14px', lineHeight: '1.6' }}>
|
|
361
|
+
Unlock premium features to get IP geolocation, security scoring, and VPN/Proxy detection for every session
|
|
362
|
+
</Typography>
|
|
363
|
+
<Button
|
|
364
|
+
variant="secondary"
|
|
365
|
+
size="M"
|
|
366
|
+
onClick={() => window.open('https://magicapi.fitlex.me', '_blank')}
|
|
367
|
+
style={{
|
|
368
|
+
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
|
369
|
+
color: 'white',
|
|
370
|
+
border: 'none',
|
|
371
|
+
fontWeight: '600',
|
|
372
|
+
marginTop: '8px',
|
|
373
|
+
boxShadow: '0 4px 12px rgba(245, 158, 11, 0.3)',
|
|
374
|
+
}}
|
|
375
|
+
>
|
|
376
|
+
Upgrade to Premium
|
|
377
|
+
</Button>
|
|
378
|
+
</Flex>
|
|
379
|
+
</Box>
|
|
380
|
+
</Section>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{/* User Agent - Collapsible */}
|
|
384
|
+
<Section>
|
|
385
|
+
<Flex justifyContent="space-between" alignItems="center" style={{ marginBottom: '12px' }}>
|
|
386
|
+
<SectionTitle style={{ marginBottom: 0, paddingBottom: 0, border: 'none' }}>
|
|
387
|
+
🔧 Technical Details
|
|
388
|
+
</SectionTitle>
|
|
389
|
+
<Button
|
|
390
|
+
variant="tertiary"
|
|
391
|
+
size="S"
|
|
392
|
+
onClick={() => setShowUserAgent(!showUserAgent)}
|
|
393
|
+
style={{ fontSize: '12px' }}
|
|
394
|
+
>
|
|
395
|
+
{showUserAgent ? '▲ Hide Details' : '▼ Show Details'}
|
|
396
|
+
</Button>
|
|
397
|
+
</Flex>
|
|
398
|
+
|
|
399
|
+
{showUserAgent && (
|
|
400
|
+
<Box
|
|
401
|
+
padding={3}
|
|
402
|
+
background="neutral100"
|
|
403
|
+
hasRadius
|
|
404
|
+
style={{
|
|
405
|
+
fontFamily: 'monospace',
|
|
406
|
+
fontSize: '10px',
|
|
407
|
+
wordBreak: 'break-all',
|
|
408
|
+
maxHeight: '80px',
|
|
409
|
+
overflow: 'auto',
|
|
410
|
+
marginTop: '8px',
|
|
411
|
+
animation: 'fadeIn 0.3s ease-in-out',
|
|
412
|
+
}}
|
|
413
|
+
>
|
|
414
|
+
<Typography variant="pi" textColor="neutral600" style={{ lineHeight: '1.6' }}>
|
|
415
|
+
{session.userAgent}
|
|
416
|
+
</Typography>
|
|
417
|
+
</Box>
|
|
418
|
+
)}
|
|
419
|
+
</Section>
|
|
420
|
+
</Box>
|
|
421
|
+
</Modal.Body>
|
|
422
|
+
|
|
423
|
+
<Modal.Footer>
|
|
424
|
+
<Flex justifyContent="space-between" style={{ width: '100%' }}>
|
|
425
|
+
<Button onClick={onClose} variant="tertiary">
|
|
426
|
+
Close
|
|
427
|
+
</Button>
|
|
428
|
+
<Button
|
|
429
|
+
onClick={handleTerminate}
|
|
430
|
+
variant="danger"
|
|
431
|
+
disabled={!session.isActive || terminating}
|
|
432
|
+
loading={terminating}
|
|
433
|
+
startIcon={<Cross />}
|
|
434
|
+
>
|
|
435
|
+
Terminate Session
|
|
436
|
+
</Button>
|
|
437
|
+
</Flex>
|
|
438
|
+
</Modal.Footer>
|
|
439
|
+
</Modal.Content>
|
|
440
|
+
</Modal.Root>
|
|
441
|
+
);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
export default SessionDetailModal;
|
|
445
|
+
|