strapi-plugin-magic-sessionmanager 4.2.4 → 4.2.5

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