strapi-plugin-magic-sessionmanager 4.2.4 → 4.2.6

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 (62) hide show
  1. package/README.md +0 -2
  2. package/dist/server/index.js +1 -1
  3. package/dist/server/index.mjs +1 -1
  4. package/package.json +1 -3
  5. package/admin/jsconfig.json +0 -10
  6. package/admin/src/components/Initializer.jsx +0 -11
  7. package/admin/src/components/LicenseGuard.jsx +0 -591
  8. package/admin/src/components/OnlineUsersWidget.jsx +0 -212
  9. package/admin/src/components/PluginIcon.jsx +0 -8
  10. package/admin/src/components/SessionDetailModal.jsx +0 -449
  11. package/admin/src/components/SessionInfoCard.jsx +0 -151
  12. package/admin/src/components/SessionInfoPanel.jsx +0 -385
  13. package/admin/src/components/index.jsx +0 -5
  14. package/admin/src/hooks/useLicense.js +0 -103
  15. package/admin/src/index.js +0 -149
  16. package/admin/src/pages/ActiveSessions.jsx +0 -12
  17. package/admin/src/pages/Analytics.jsx +0 -735
  18. package/admin/src/pages/App.jsx +0 -12
  19. package/admin/src/pages/HomePage.jsx +0 -1212
  20. package/admin/src/pages/License.jsx +0 -603
  21. package/admin/src/pages/Settings.jsx +0 -1646
  22. package/admin/src/pages/SettingsNew.jsx +0 -1204
  23. package/admin/src/pages/UpgradePage.jsx +0 -448
  24. package/admin/src/pages/index.jsx +0 -3
  25. package/admin/src/pluginId.js +0 -4
  26. package/admin/src/translations/de.json +0 -299
  27. package/admin/src/translations/en.json +0 -299
  28. package/admin/src/translations/es.json +0 -287
  29. package/admin/src/translations/fr.json +0 -287
  30. package/admin/src/translations/pt.json +0 -287
  31. package/admin/src/utils/getTranslation.js +0 -5
  32. package/admin/src/utils/index.js +0 -2
  33. package/admin/src/utils/parseUserAgent.js +0 -79
  34. package/admin/src/utils/theme.js +0 -85
  35. package/server/jsconfig.json +0 -10
  36. package/server/src/bootstrap.js +0 -492
  37. package/server/src/config/index.js +0 -23
  38. package/server/src/content-types/index.js +0 -9
  39. package/server/src/content-types/session/schema.json +0 -84
  40. package/server/src/controllers/controller.js +0 -11
  41. package/server/src/controllers/index.js +0 -11
  42. package/server/src/controllers/license.js +0 -266
  43. package/server/src/controllers/session.js +0 -433
  44. package/server/src/controllers/settings.js +0 -122
  45. package/server/src/destroy.js +0 -22
  46. package/server/src/index.js +0 -23
  47. package/server/src/middlewares/index.js +0 -5
  48. package/server/src/middlewares/last-seen.js +0 -62
  49. package/server/src/policies/index.js +0 -3
  50. package/server/src/register.js +0 -36
  51. package/server/src/routes/admin.js +0 -149
  52. package/server/src/routes/content-api.js +0 -60
  53. package/server/src/routes/index.js +0 -9
  54. package/server/src/services/geolocation.js +0 -182
  55. package/server/src/services/index.js +0 -13
  56. package/server/src/services/license-guard.js +0 -316
  57. package/server/src/services/notifications.js +0 -319
  58. package/server/src/services/service.js +0 -7
  59. package/server/src/services/session.js +0 -393
  60. package/server/src/utils/encryption.js +0 -121
  61. package/server/src/utils/getClientIp.js +0 -118
  62. 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
-