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.
Files changed (52) hide show
  1. package/admin/jsconfig.json +10 -0
  2. package/admin/src/components/Initializer.jsx +11 -0
  3. package/admin/src/components/LicenseGuard.jsx +591 -0
  4. package/admin/src/components/OnlineUsersWidget.jsx +208 -0
  5. package/admin/src/components/PluginIcon.jsx +8 -0
  6. package/admin/src/components/SessionDetailModal.jsx +445 -0
  7. package/admin/src/components/SessionInfoCard.jsx +151 -0
  8. package/admin/src/components/SessionInfoPanel.jsx +375 -0
  9. package/admin/src/components/index.jsx +5 -0
  10. package/admin/src/hooks/useLicense.js +103 -0
  11. package/admin/src/index.js +137 -0
  12. package/admin/src/pages/ActiveSessions.jsx +12 -0
  13. package/admin/src/pages/Analytics.jsx +735 -0
  14. package/admin/src/pages/App.jsx +12 -0
  15. package/admin/src/pages/HomePage.jsx +1248 -0
  16. package/admin/src/pages/License.jsx +603 -0
  17. package/admin/src/pages/Settings.jsx +1497 -0
  18. package/admin/src/pages/SettingsNew.jsx +1204 -0
  19. package/admin/src/pages/index.jsx +3 -0
  20. package/admin/src/pluginId.js +3 -0
  21. package/admin/src/translations/de.json +20 -0
  22. package/admin/src/translations/en.json +20 -0
  23. package/admin/src/utils/getTranslation.js +5 -0
  24. package/admin/src/utils/index.js +2 -0
  25. package/admin/src/utils/parseUserAgent.js +79 -0
  26. package/package.json +3 -1
  27. package/server/jsconfig.json +10 -0
  28. package/server/src/bootstrap.js +297 -0
  29. package/server/src/config/index.js +20 -0
  30. package/server/src/content-types/index.js +9 -0
  31. package/server/src/content-types/session/schema.json +76 -0
  32. package/server/src/controllers/controller.js +11 -0
  33. package/server/src/controllers/index.js +11 -0
  34. package/server/src/controllers/license.js +266 -0
  35. package/server/src/controllers/session.js +362 -0
  36. package/server/src/controllers/settings.js +122 -0
  37. package/server/src/destroy.js +18 -0
  38. package/server/src/index.js +21 -0
  39. package/server/src/middlewares/index.js +5 -0
  40. package/server/src/middlewares/last-seen.js +56 -0
  41. package/server/src/policies/index.js +3 -0
  42. package/server/src/register.js +32 -0
  43. package/server/src/routes/admin.js +149 -0
  44. package/server/src/routes/content-api.js +51 -0
  45. package/server/src/routes/index.js +9 -0
  46. package/server/src/services/geolocation.js +180 -0
  47. package/server/src/services/index.js +13 -0
  48. package/server/src/services/license-guard.js +308 -0
  49. package/server/src/services/notifications.js +319 -0
  50. package/server/src/services/service.js +7 -0
  51. package/server/src/services/session.js +345 -0
  52. 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
+