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,1212 +0,0 @@
1
- import { useState, useEffect } from 'react';
2
- import { useIntl } from 'react-intl';
3
- import { useFetchClient, useNotification } from '@strapi/strapi/admin';
4
- import styled, { keyframes, css } from 'styled-components';
5
- import { getTranslation } from '../utils/getTranslation';
6
- import { theme } from '../utils/theme';
7
- import {
8
- Box,
9
- Button,
10
- Flex,
11
- Typography,
12
- Loader,
13
- Table,
14
- Thead,
15
- Tbody,
16
- Tr,
17
- Td,
18
- Th,
19
- Badge,
20
- SingleSelect,
21
- SingleSelectOption,
22
- } from '@strapi/design-system';
23
- import {
24
- Check,
25
- Cross,
26
- Clock,
27
- User,
28
- Monitor,
29
- Phone,
30
- Server,
31
- Sparkle,
32
- Trash,
33
- Search,
34
- Eye,
35
- Download,
36
- } from '@strapi/icons';
37
- import pluginId from '../pluginId';
38
- import parseUserAgent from '../utils/parseUserAgent';
39
- import SessionDetailModal from '../components/SessionDetailModal';
40
- import { useLicense } from '../hooks/useLicense';
41
-
42
- // ================ ANIMATIONS ================
43
- const fadeIn = keyframes`
44
- from { opacity: 0; transform: translateY(10px); }
45
- to { opacity: 1; transform: translateY(0); }
46
- `;
47
-
48
- const shimmer = keyframes`
49
- 0% { background-position: -200% 0; }
50
- 100% { background-position: 200% 0; }
51
- `;
52
-
53
- const float = keyframes`
54
- 0%, 100% { transform: translateY(0px); }
55
- 50% { transform: translateY(-5px); }
56
- `;
57
-
58
- const pulse = keyframes`
59
- 0%, 100% { opacity: 1; }
60
- 50% { opacity: 0.5; }
61
- `;
62
-
63
- const FloatingEmoji = styled.div`
64
- position: absolute;
65
- bottom: 40px;
66
- right: 40px;
67
- font-size: 72px;
68
- opacity: 0.08;
69
- ${css`animation: ${float} 4s ease-in-out infinite;`}
70
- `;
71
-
72
- // ================ RESPONSIVE BREAKPOINTS ================
73
- const breakpoints = {
74
- mobile: '768px',
75
- tablet: '1024px',
76
- };
77
-
78
- // ================ STYLED COMPONENTS ================
79
- const Container = styled(Box)`
80
- ${css`animation: ${fadeIn} ${theme.transitions.slow};`}
81
- min-height: 100vh;
82
- max-width: 1440px;
83
- margin: 0 auto;
84
- padding: ${theme.spacing.xl} ${theme.spacing.lg} 0;
85
-
86
- @media screen and (max-width: ${breakpoints.mobile}) {
87
- padding: 16px 12px 0;
88
- }
89
- `;
90
-
91
- const Header = styled(Box)`
92
- background: linear-gradient(135deg,
93
- ${theme.colors.primary[600]} 0%,
94
- ${theme.colors.secondary[600]} 100%
95
- );
96
- border-radius: ${theme.borderRadius.xl};
97
- padding: ${theme.spacing.xl} ${theme.spacing['2xl']};
98
- margin-bottom: ${theme.spacing.xl};
99
- position: relative;
100
- overflow: hidden;
101
- box-shadow: ${theme.shadows.xl};
102
-
103
- @media screen and (max-width: ${breakpoints.mobile}) {
104
- padding: 24px 20px;
105
- border-radius: 12px;
106
- }
107
-
108
- &::before {
109
- content: '';
110
- position: absolute;
111
- top: 0;
112
- left: -100%;
113
- width: 200%;
114
- height: 100%;
115
- background: linear-gradient(
116
- 90deg,
117
- transparent,
118
- rgba(255, 255, 255, 0.15),
119
- transparent
120
- );
121
- ${css`animation: ${shimmer} 3s infinite;`}
122
- }
123
-
124
- &::after {
125
- content: '';
126
- position: absolute;
127
- top: 0;
128
- right: 0;
129
- width: 100%;
130
- height: 100%;
131
- background-image: radial-gradient(circle at 20% 80%, transparent 50%, rgba(255, 255, 255, 0.1) 50%);
132
- background-size: 15px 15px;
133
- opacity: 0.3;
134
- }
135
- `;
136
-
137
- const HeaderContent = styled(Flex)`
138
- position: relative;
139
- z-index: 1;
140
- `;
141
-
142
- const Title = styled(Typography)`
143
- color: white;
144
- font-size: 2rem;
145
- font-weight: 700;
146
- letter-spacing: -0.025em;
147
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
148
- display: flex;
149
- align-items: center;
150
- gap: ${theme.spacing.sm};
151
-
152
- svg {
153
- width: 28px;
154
- height: 28px;
155
- ${css`animation: ${float} 3s ease-in-out infinite;`}
156
- }
157
-
158
- @media screen and (max-width: ${breakpoints.mobile}) {
159
- font-size: 1.5rem;
160
-
161
- svg {
162
- width: 22px;
163
- height: 22px;
164
- }
165
- }
166
- `;
167
-
168
- const Subtitle = styled(Typography)`
169
- color: rgba(255, 255, 255, 0.95);
170
- font-size: 0.95rem;
171
- font-weight: 400;
172
- margin-top: ${theme.spacing.xs};
173
- letter-spacing: 0.01em;
174
-
175
- @media screen and (max-width: ${breakpoints.mobile}) {
176
- font-size: 0.85rem;
177
- }
178
- `;
179
-
180
- const StatsGrid = styled.div`
181
- margin-bottom: ${theme.spacing.xl};
182
- display: grid;
183
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
184
- gap: ${theme.spacing.lg};
185
- justify-content: center;
186
- max-width: 1200px;
187
- margin-left: auto;
188
- margin-right: auto;
189
-
190
- @media screen and (max-width: ${breakpoints.mobile}) {
191
- grid-template-columns: repeat(2, 1fr);
192
- gap: 12px;
193
- margin-bottom: 24px;
194
- }
195
- `;
196
-
197
- const StatCard = styled(Box)`
198
- background: ${props => props.theme.colors.neutral0};
199
- border-radius: ${theme.borderRadius.lg};
200
- padding: 28px ${theme.spacing.lg};
201
- position: relative;
202
- overflow: hidden;
203
- transition: all ${theme.transitions.normal};
204
- ${css`animation: ${fadeIn} ${theme.transitions.slow} backwards;`}
205
- animation-delay: ${props => props.$delay || '0s'};
206
- box-shadow: ${theme.shadows.sm};
207
- border: 1px solid ${props => props.theme.colors.neutral200};
208
- min-width: 200px;
209
- flex: 1;
210
- text-align: center;
211
- display: flex;
212
- flex-direction: column;
213
- align-items: center;
214
- justify-content: center;
215
-
216
- @media screen and (max-width: ${breakpoints.mobile}) {
217
- min-width: unset;
218
- padding: 20px 12px;
219
-
220
- &:hover {
221
- transform: none;
222
- }
223
- }
224
-
225
- &:hover {
226
- transform: translateY(-6px);
227
- box-shadow: ${theme.shadows.xl};
228
- border-color: ${props => props.$color || props.theme.colors.primary600};
229
-
230
- .stat-icon {
231
- transform: scale(1.15) rotate(5deg);
232
- }
233
-
234
- .stat-value {
235
- transform: scale(1.08);
236
- color: ${props => props.$color || props.theme.colors.primary600};
237
- }
238
- }
239
- `;
240
-
241
- const StatIcon = styled(Box)`
242
- width: 68px;
243
- height: 68px;
244
- border-radius: ${theme.borderRadius.lg};
245
- display: flex;
246
- align-items: center;
247
- justify-content: center;
248
- background: ${props => props.$bg || props.theme.colors.primary100};
249
- transition: all ${theme.transitions.normal};
250
- margin: 0 auto 20px;
251
- box-shadow: ${theme.shadows.sm};
252
-
253
- svg {
254
- width: 34px;
255
- height: 34px;
256
- color: ${props => props.$color || props.theme.colors.primary600};
257
- }
258
-
259
- @media screen and (max-width: ${breakpoints.mobile}) {
260
- width: 48px;
261
- height: 48px;
262
- margin-bottom: 12px;
263
-
264
- svg {
265
- width: 24px;
266
- height: 24px;
267
- }
268
- }
269
- `;
270
-
271
- const StatValue = styled(Typography)`
272
- font-size: 2.75rem;
273
- font-weight: 700;
274
- color: ${props => props.theme.colors.neutral800};
275
- line-height: 1;
276
- margin-bottom: 10px;
277
- transition: all ${theme.transitions.normal};
278
- text-align: center;
279
-
280
- @media screen and (max-width: ${breakpoints.mobile}) {
281
- font-size: 2rem;
282
- margin-bottom: 6px;
283
- }
284
- `;
285
-
286
- const StatLabel = styled(Typography)`
287
- font-size: 0.95rem;
288
- color: ${props => props.theme.colors.neutral600};
289
- font-weight: 500;
290
- letter-spacing: 0.025em;
291
- text-align: center;
292
-
293
- @media screen and (max-width: ${breakpoints.mobile}) {
294
- font-size: 0.8rem;
295
- }
296
- `;
297
-
298
- const DataTable = styled(Box)`
299
- background: ${props => props.theme.colors.neutral0};
300
- border-radius: ${theme.borderRadius.lg};
301
- overflow: hidden;
302
- box-shadow: ${theme.shadows.sm};
303
- border: 1px solid ${props => props.theme.colors.neutral200};
304
- margin-bottom: ${theme.spacing.xl};
305
- `;
306
-
307
- const StyledTable = styled(Table)`
308
- thead {
309
- background: ${props => props.theme.colors.neutral100};
310
- border-bottom: 2px solid ${props => props.theme.colors.neutral200};
311
-
312
- th {
313
- font-weight: 600;
314
- color: ${props => props.theme.colors.neutral800};
315
- font-size: 0.875rem;
316
- text-transform: uppercase;
317
- letter-spacing: 0.025em;
318
- padding: ${theme.spacing.lg} ${theme.spacing.lg};
319
- }
320
- }
321
-
322
- tbody tr {
323
- transition: all ${theme.transitions.fast};
324
- border-bottom: 1px solid ${props => props.theme.colors.neutral150};
325
-
326
- &:last-child {
327
- border-bottom: none;
328
- }
329
-
330
- &:hover {
331
- background: ${props => props.theme.colors.primary100};
332
-
333
- .action-buttons {
334
- opacity: 1;
335
- }
336
- }
337
-
338
- td {
339
- padding: ${theme.spacing.lg} ${theme.spacing.lg};
340
- color: ${props => props.theme.colors.neutral800};
341
- vertical-align: middle;
342
- }
343
- }
344
- `;
345
-
346
- const OnlineIndicator = styled.div`
347
- width: 10px;
348
- height: 10px;
349
- border-radius: 50%;
350
- background: ${props => props.$online ? theme.colors.success[500] : props.theme.colors.neutral400};
351
- display: inline-block;
352
- margin-right: 8px;
353
- ${css`animation: ${props => props.$online ? pulse : 'none'} 2s ease-in-out infinite;`}
354
- `;
355
-
356
- const FilterBar = styled(Flex)`
357
- background: ${props => props.theme.colors.neutral0};
358
- padding: ${theme.spacing.md} ${theme.spacing.lg};
359
- border-radius: ${theme.borderRadius.lg};
360
- margin-bottom: ${theme.spacing.lg};
361
- box-shadow: ${theme.shadows.sm};
362
- border: 1px solid ${props => props.theme.colors.neutral200};
363
- gap: ${theme.spacing.md};
364
- align-items: center;
365
- `;
366
-
367
- const SearchInputWrapper = styled.div`
368
- position: relative;
369
- flex: 1;
370
- display: flex;
371
- align-items: center;
372
- `;
373
-
374
- const SearchIcon = styled(Search)`
375
- position: absolute;
376
- left: 12px;
377
- width: 16px;
378
- height: 16px;
379
- color: ${props => props.theme.colors.neutral600};
380
- pointer-events: none;
381
- `;
382
-
383
- const StyledSearchInput = styled.input`
384
- width: 100%;
385
- padding: ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm} 36px;
386
- border: 1px solid ${props => props.theme.colors.neutral200};
387
- border-radius: ${theme.borderRadius.md};
388
- font-size: 0.875rem;
389
- transition: all ${theme.transitions.fast};
390
- background: ${props => props.theme.colors.neutral0};
391
- color: ${props => props.theme.colors.neutral800};
392
-
393
- &:focus {
394
- outline: none;
395
- border-color: ${props => props.theme.colors.primary600};
396
- box-shadow: 0 0 0 3px ${props => props.theme.colors.primary100};
397
- }
398
-
399
- &::placeholder {
400
- color: ${props => props.theme.colors.neutral500};
401
- }
402
- `;
403
-
404
- const ActionButtons = styled(Flex)`
405
- opacity: 0.7;
406
- transition: all ${theme.transitions.fast};
407
- gap: ${theme.spacing.xs};
408
- justify-content: flex-end;
409
- `;
410
-
411
- const ClickableRow = styled(Tr)`
412
- cursor: pointer;
413
-
414
- &:hover {
415
- background: ${props => props.theme.colors.primary100} !important;
416
- }
417
- `;
418
-
419
- // Empty state background that works in dark mode
420
- const EmptyStateBox = styled(Box)`
421
- background: ${props => props.theme.colors.neutral0};
422
- border-radius: ${theme.borderRadius.xl};
423
- border: 2px dashed ${props => props.theme.colors.neutral300};
424
- padding: 80px 32px;
425
- text-align: center;
426
- position: relative;
427
- overflow: hidden;
428
- min-height: 400px;
429
- display: flex;
430
- align-items: center;
431
- justify-content: center;
432
- `;
433
-
434
- const EmptyStateGradient = styled.div`
435
- position: absolute;
436
- top: 0;
437
- left: 0;
438
- right: 0;
439
- bottom: 0;
440
- background: linear-gradient(135deg, ${theme.colors.primary[50]} 0%, ${theme.colors.secondary[50]} 100%);
441
- opacity: 0.3;
442
- z-index: 0;
443
- `;
444
-
445
- const HomePage = () => {
446
- const { formatMessage } = useIntl();
447
- const { get, post, del } = useFetchClient();
448
- const { toggleNotification } = useNotification();
449
- const { isPremium } = useLicense();
450
- const t = (id, defaultMessage, values) => formatMessage({ id: getTranslation(id), defaultMessage }, values);
451
- const [sessions, setSessions] = useState([]);
452
- const [loading, setLoading] = useState(true);
453
- const [filterStatus, setFilterStatus] = useState('active'); // Default: Active Only
454
- const [entriesPerPage, setEntriesPerPage] = useState('25');
455
- const [searchQuery, setSearchQuery] = useState('');
456
- const [selectedSession, setSelectedSession] = useState(null);
457
- const [showDetailModal, setShowDetailModal] = useState(false);
458
-
459
- useEffect(() => {
460
- fetchSessions();
461
-
462
- // Auto-refresh every 10 minutes (silent background update)
463
- // But ONLY if modal is not open (to avoid interrupting user)
464
- const interval = setInterval(() => {
465
- if (!showDetailModal) {
466
- fetchSessions();
467
- }
468
- }, 10 * 60 * 1000);
469
-
470
- return () => clearInterval(interval);
471
- }, [showDetailModal]);
472
-
473
- const fetchSessions = async () => {
474
- setLoading(true);
475
- try {
476
- const { data } = await get(`/${pluginId}/sessions`);
477
- setSessions(data.data || []);
478
- } catch (err) {
479
- console.error('[SessionManager] Error fetching sessions:', err);
480
- } finally {
481
- setLoading(false);
482
- }
483
- };
484
-
485
- const handleTerminateSession = async (sessionId) => {
486
- if (!confirm(t('homepage.confirm.terminate', 'Are you sure you want to terminate this session?\n\nThis will set isActive to false (user will be logged out).'))) {
487
- return;
488
- }
489
-
490
- try {
491
- await post(`/${pluginId}/sessions/${sessionId}/terminate`);
492
- fetchSessions();
493
- } catch (err) {
494
- console.error('[SessionManager] Error terminating session:', err);
495
- }
496
- };
497
-
498
- const handleDeleteSession = async (sessionId) => {
499
- if (!confirm(t('homepage.confirm.delete', '[WARNING] This will PERMANENTLY delete this session from the database!\n\nThis action cannot be undone.\n\nAre you sure?'))) {
500
- return;
501
- }
502
-
503
- try {
504
- await del(`/${pluginId}/sessions/${sessionId}`);
505
- fetchSessions();
506
- toggleNotification({
507
- type: 'success',
508
- message: t('notifications.success.deleted', 'Session permanently deleted'),
509
- });
510
- } catch (err) {
511
- console.error('[SessionManager] Error deleting session:', err);
512
- toggleNotification({
513
- type: 'danger',
514
- message: t('notifications.error.delete', 'Failed to delete session'),
515
- });
516
- }
517
- };
518
-
519
- const handleExportCSV = () => {
520
- if (!isPremium) {
521
- toggleNotification({
522
- type: 'warning',
523
- message: t('notifications.warning.premiumRequired', 'Premium license required for export functionality'),
524
- });
525
- return;
526
- }
527
-
528
- try {
529
- // CSV Header
530
- const headers = ['ID', 'Status', 'User Email', 'Username', 'Device', 'Browser', 'OS', 'IP Address', 'Login Time', 'Last Active', 'Logout Time', 'Minutes Idle'];
531
-
532
- // CSV Rows
533
- const rows = filteredSessions.map(session => {
534
- const deviceInfo = parseUserAgent(session.userAgent);
535
- const status = getSessionStatus(session);
536
-
537
- return [
538
- session.id,
539
- status,
540
- session.user?.email || '',
541
- session.user?.username || '',
542
- deviceInfo.device,
543
- deviceInfo.browser,
544
- deviceInfo.os,
545
- session.ipAddress,
546
- new Date(session.loginTime).toISOString(),
547
- new Date(session.lastActive || session.loginTime).toISOString(),
548
- session.logoutTime ? new Date(session.logoutTime).toISOString() : '',
549
- session.minutesSinceActive,
550
- ];
551
- });
552
-
553
- // Create CSV content
554
- const csvContent = [
555
- headers.join(','),
556
- ...rows.map(row => row.map(cell => `"${cell}"`).join(','))
557
- ].join('\n');
558
-
559
- // Download
560
- const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
561
- const link = document.createElement('a');
562
- link.href = URL.createObjectURL(blob);
563
- link.download = `sessions-export-${new Date().toISOString().split('T')[0]}.csv`;
564
- link.click();
565
-
566
- toggleNotification({
567
- type: 'success',
568
- message: t('notifications.success.exported', 'Exported {count} sessions to {format}', { count: filteredSessions.length, format: 'CSV' }),
569
- });
570
- } catch (err) {
571
- console.error('[SessionManager] Export error:', err);
572
- toggleNotification({
573
- type: 'danger',
574
- message: t('notifications.error.export', 'Failed to export sessions'),
575
- });
576
- }
577
- };
578
-
579
- const handleExportJSON = () => {
580
- if (!isPremium) {
581
- toggleNotification({
582
- type: 'warning',
583
- message: t('notifications.warning.premiumRequired', 'Premium license required for export functionality'),
584
- });
585
- return;
586
- }
587
-
588
- try {
589
- const exportData = {
590
- exportedAt: new Date().toISOString(),
591
- filter: filterStatus,
592
- totalSessions: sessions.length,
593
- exportedSessions: filteredSessions.length,
594
- sessions: filteredSessions.map(session => {
595
- const deviceInfo = parseUserAgent(session.userAgent);
596
- return {
597
- id: session.id,
598
- status: getSessionStatus(session),
599
- user: {
600
- id: session.user?.id,
601
- email: session.user?.email,
602
- username: session.user?.username,
603
- },
604
- device: {
605
- type: deviceInfo.device,
606
- browser: deviceInfo.browser,
607
- browserVersion: deviceInfo.browserVersion,
608
- os: deviceInfo.os,
609
- },
610
- ipAddress: session.ipAddress,
611
- loginTime: session.loginTime,
612
- lastActive: session.lastActive,
613
- logoutTime: session.logoutTime,
614
- minutesSinceActive: session.minutesSinceActive,
615
- isActive: session.isActive,
616
- isTrulyActive: session.isTrulyActive,
617
- };
618
- }),
619
- };
620
-
621
- const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
622
- const link = document.createElement('a');
623
- link.href = URL.createObjectURL(blob);
624
- link.download = `sessions-export-${new Date().toISOString().split('T')[0]}.json`;
625
- link.click();
626
-
627
- toggleNotification({
628
- type: 'success',
629
- message: t('notifications.success.exported', 'Exported {count} sessions to {format}', { count: filteredSessions.length, format: 'JSON' }),
630
- });
631
- } catch (err) {
632
- console.error('[SessionManager] Export error:', err);
633
- toggleNotification({
634
- type: 'danger',
635
- message: t('notifications.error.export', 'Failed to export sessions'),
636
- });
637
- }
638
- };
639
-
640
- const getDeviceIcon = (deviceType) => {
641
- if (deviceType === 'Mobile' || deviceType === 'Tablet') return Phone;
642
- if (deviceType === 'Desktop' || deviceType === 'Laptop') return Monitor;
643
- return Server;
644
- };
645
-
646
- // Calculate stats based on new 4-status logic
647
- const activeSessions = sessions.filter(s => s.isActive && s.isTrulyActive);
648
- const idleSessions = sessions.filter(s => s.isActive && !s.isTrulyActive);
649
- const loggedOutSessions = sessions.filter(s => !s.isActive && s.logoutTime);
650
- const terminatedSessions = sessions.filter(s => !s.isActive && !s.logoutTime);
651
-
652
- const handleSessionClick = (session) => {
653
- setSelectedSession(session);
654
- setShowDetailModal(true);
655
- };
656
-
657
- const handleModalClose = () => {
658
- setShowDetailModal(false);
659
- setSelectedSession(null);
660
- };
661
-
662
- const handleSessionTerminated = () => {
663
- fetchSessions();
664
- };
665
-
666
- // Helper function to get session status
667
- const getSessionStatus = (session) => {
668
- if (!session.isActive) {
669
- return session.logoutTime ? 'loggedout' : 'terminated';
670
- }
671
- return session.isTrulyActive ? 'active' : 'idle';
672
- };
673
-
674
- // Filter sessions
675
- const filteredSessions = sessions
676
- .filter(session => {
677
- // Filter by status
678
- const sessionStatus = getSessionStatus(session);
679
- if (filterStatus === 'active' && sessionStatus !== 'active') return false;
680
- if (filterStatus === 'idle' && sessionStatus !== 'idle') return false;
681
- if (filterStatus === 'loggedout' && sessionStatus !== 'loggedout') return false;
682
- if (filterStatus === 'terminated' && sessionStatus !== 'terminated') return false;
683
-
684
- // Filter by search query
685
- if (searchQuery) {
686
- const query = searchQuery.toLowerCase();
687
- const matchesUser = session.user?.email?.toLowerCase().includes(query) ||
688
- session.user?.username?.toLowerCase().includes(query);
689
- const matchesIp = session.ipAddress?.toLowerCase().includes(query);
690
- const deviceInfo = parseUserAgent(session.userAgent);
691
- const matchesDevice = deviceInfo.device?.toLowerCase().includes(query) ||
692
- deviceInfo.browser?.toLowerCase().includes(query) ||
693
- deviceInfo.os?.toLowerCase().includes(query);
694
-
695
- return matchesUser || matchesIp || matchesDevice;
696
- }
697
-
698
- return true;
699
- })
700
- .slice(0, parseInt(entriesPerPage));
701
-
702
- return (
703
- <Container padding={8}>
704
- {/* Gradient Header */}
705
- <Header>
706
- <HeaderContent justifyContent="space-between" alignItems="center">
707
- <Flex direction="column" alignItems="flex-start" gap={2}>
708
- <Title>
709
- <Monitor /> {t('homepage.title', 'Session Manager')}
710
- </Title>
711
- <Subtitle>
712
- {t('homepage.subtitle', 'Monitor and manage user sessions in real-time')}
713
- </Subtitle>
714
- </Flex>
715
-
716
- {isPremium && filteredSessions.length > 0 && (
717
- <Flex gap={2}>
718
- <Button
719
- onClick={handleExportCSV}
720
- startIcon={<Download />}
721
- size="M"
722
- variant="secondary"
723
- style={{
724
- backgroundColor: 'rgba(255,255,255,0.2)',
725
- color: 'white',
726
- border: '1px solid rgba(255,255,255,0.3)',
727
- fontWeight: '600',
728
- }}
729
- >
730
- {t('homepage.export.csv', 'Export CSV')}
731
- </Button>
732
- <Button
733
- onClick={handleExportJSON}
734
- startIcon={<Download />}
735
- size="M"
736
- variant="secondary"
737
- style={{
738
- backgroundColor: 'rgba(255,255,255,0.2)',
739
- color: 'white',
740
- border: '1px solid rgba(255,255,255,0.3)',
741
- fontWeight: '600',
742
- }}
743
- >
744
- {t('homepage.export.json', 'Export JSON')}
745
- </Button>
746
- </Flex>
747
- )}
748
- </HeaderContent>
749
- </Header>
750
-
751
- {/* Stats Cards */}
752
- <StatsGrid>
753
- <StatCard $delay="0.1s" $color={theme.colors.success[500]}>
754
- <StatIcon className="stat-icon" $bg={theme.colors.success[100]} $color={theme.colors.success[600]}>
755
- <Check />
756
- </StatIcon>
757
- <StatValue className="stat-value">{activeSessions.length}</StatValue>
758
- <StatLabel>{t('homepage.stats.active', 'Active')}</StatLabel>
759
- </StatCard>
760
-
761
- <StatCard $delay="0.2s" $color={theme.colors.warning[500]}>
762
- <StatIcon className="stat-icon" $bg={theme.colors.warning[100]} $color={theme.colors.warning[600]}>
763
- <Clock />
764
- </StatIcon>
765
- <StatValue className="stat-value">{idleSessions.length}</StatValue>
766
- <StatLabel>{t('homepage.stats.idle', 'Idle')}</StatLabel>
767
- </StatCard>
768
-
769
- <StatCard $delay="0.3s" $color={theme.colors.danger[500]}>
770
- <StatIcon className="stat-icon" $bg={theme.colors.danger[100]} $color={theme.colors.danger[600]}>
771
- <Cross />
772
- </StatIcon>
773
- <StatValue className="stat-value">{loggedOutSessions.length}</StatValue>
774
- <StatLabel>{t('homepage.stats.loggedOut', 'Logged Out')}</StatLabel>
775
- </StatCard>
776
-
777
- <StatCard $delay="0.4s" $color="#4B5563">
778
- <StatIcon className="stat-icon" $bg="#F3F4F6" $color="#4B5563">
779
- <Cross />
780
- </StatIcon>
781
- <StatValue className="stat-value">{terminatedSessions.length}</StatValue>
782
- <StatLabel>{t('homepage.stats.terminated', 'Terminated')}</StatLabel>
783
- </StatCard>
784
-
785
- <StatCard $delay="0.5s" $color="#A855F7">
786
- <StatIcon className="stat-icon" $bg="#EDE9FE" $color="#9333EA">
787
- <User />
788
- </StatIcon>
789
- <StatValue className="stat-value">{sessions.length}</StatValue>
790
- <StatLabel>{t('homepage.stats.total', 'Total')}</StatLabel>
791
- </StatCard>
792
- </StatsGrid>
793
-
794
- {/* Loading */}
795
- {loading && (
796
- <Flex justifyContent="center" padding={8}>
797
- <Loader>{t('homepage.loading', 'Loading sessions...')}</Loader>
798
- </Flex>
799
- )}
800
-
801
- {/* Sessions Table */}
802
- {!loading && sessions.length > 0 && (
803
- <Box>
804
- <Box style={{ marginBottom: theme.spacing.md }}>
805
- <Typography variant="delta" textColor="neutral700" style={{ marginBottom: theme.spacing.md }}>
806
- {t('homepage.allSessions', 'All Sessions')}
807
- </Typography>
808
- </Box>
809
-
810
- {/* Filter Bar */}
811
- <FilterBar>
812
- {/* Search Input */}
813
- <SearchInputWrapper>
814
- <SearchIcon />
815
- <StyledSearchInput
816
- value={searchQuery}
817
- onChange={(e) => setSearchQuery(e.target.value)}
818
- placeholder={t('homepage.search.placeholder', 'Search by user, IP address, or device...')}
819
- type="text"
820
- />
821
- </SearchInputWrapper>
822
-
823
- {/* Status Filter */}
824
- <Box style={{ minWidth: '180px' }}>
825
- <SingleSelect
826
- value={filterStatus}
827
- onChange={setFilterStatus}
828
- placeholder="Filter"
829
- size="S"
830
- >
831
- <SingleSelectOption value="all">{t('homepage.filter.all', 'All Sessions')}</SingleSelectOption>
832
- <SingleSelectOption value="active">{t('homepage.filter.active', 'Active (less than 15 min)')}</SingleSelectOption>
833
- <SingleSelectOption value="idle">{t('homepage.filter.idle', 'Idle (more than 15 min)')}</SingleSelectOption>
834
- <SingleSelectOption value="loggedout">{t('homepage.filter.loggedout', 'Logged Out')}</SingleSelectOption>
835
- <SingleSelectOption value="terminated">{t('homepage.filter.terminated', 'Terminated')}</SingleSelectOption>
836
- </SingleSelect>
837
- </Box>
838
-
839
- {/* Entries per page */}
840
- <Box style={{ minWidth: '130px' }}>
841
- <SingleSelect
842
- value={entriesPerPage}
843
- onChange={setEntriesPerPage}
844
- placeholder="Entries"
845
- size="S"
846
- >
847
- <SingleSelectOption value="10">{t('homepage.entries.10', '10 entries')}</SingleSelectOption>
848
- <SingleSelectOption value="25">{t('homepage.entries.25', '25 entries')}</SingleSelectOption>
849
- <SingleSelectOption value="50">{t('homepage.entries.50', '50 entries')}</SingleSelectOption>
850
- <SingleSelectOption value="100">{t('homepage.entries.100', '100 entries')}</SingleSelectOption>
851
- </SingleSelect>
852
- </Box>
853
- </FilterBar>
854
-
855
- {/* Results count */}
856
- <Box style={{ marginBottom: theme.spacing.md }}>
857
- <Typography variant="pi" textColor="neutral600">
858
- {searchQuery
859
- ? t('homepage.showingFiltered', 'Showing {count} of {total} sessions (filtered by "{query}")', { count: filteredSessions.length, total: sessions.length, query: searchQuery })
860
- : t('homepage.showing', 'Showing {count} of {total} sessions', { count: filteredSessions.length, total: sessions.length })
861
- }
862
- </Typography>
863
- </Box>
864
-
865
- {/* Table or No Results */}
866
- {filteredSessions.length > 0 ? (
867
- <DataTable>
868
- <StyledTable>
869
- <Thead>
870
- <Tr>
871
- <Th>{t('homepage.table.status', 'Status')}</Th>
872
- <Th>{t('homepage.table.user', 'User')}</Th>
873
- <Th>{t('homepage.table.device', 'Device')}</Th>
874
- <Th>{t('homepage.table.ipAddress', 'IP Address')}</Th>
875
- <Th>{t('homepage.table.loginTime', 'Login Time')}</Th>
876
- <Th>{t('homepage.table.lastActive', 'Last Active')}</Th>
877
- <Th>{t('homepage.table.actions', 'Actions')}</Th>
878
- </Tr>
879
- </Thead>
880
- <Tbody>
881
- {filteredSessions.map((session) => {
882
- const deviceInfo = parseUserAgent(session.userAgent);
883
- const DeviceIcon = getDeviceIcon(deviceInfo.device);
884
- const sessionStatus = getSessionStatus(session);
885
-
886
- // Status colors and labels
887
- const statusConfig = {
888
- active: {
889
- bg: theme.colors.success[50],
890
- badgeColor: 'success600',
891
- label: t('homepage.status.active', 'Active'),
892
- indicator: true
893
- },
894
- idle: {
895
- bg: theme.colors.warning[50],
896
- badgeColor: 'warning600',
897
- label: t('homepage.status.idle', 'Idle'),
898
- indicator: false
899
- },
900
- loggedout: {
901
- bg: theme.colors.danger[50],
902
- badgeColor: 'danger600',
903
- label: t('homepage.status.loggedOut', 'Logged Out'),
904
- indicator: false,
905
- opacity: 0.7
906
- },
907
- terminated: {
908
- bg: '#F3F4F6',
909
- badgeColor: 'neutral600',
910
- label: t('homepage.status.terminated', 'Terminated'),
911
- indicator: false,
912
- opacity: 0.6
913
- },
914
- };
915
-
916
- const config = statusConfig[sessionStatus];
917
-
918
- return (
919
- <ClickableRow
920
- key={session.id}
921
- onClick={() => handleSessionClick(session)}
922
- style={{
923
- background: config.bg,
924
- opacity: config.opacity || 1,
925
- }}
926
- >
927
- {/* Status */}
928
- <Td>
929
- <Flex alignItems="center" gap={2}>
930
- <OnlineIndicator $online={config.indicator} />
931
- <Badge
932
- backgroundColor={config.badgeColor}
933
- textColor="neutral0"
934
- size="S"
935
- >
936
- {config.label}
937
- </Badge>
938
- </Flex>
939
- </Td>
940
-
941
- {/* User */}
942
- <Td>
943
- <Flex direction="column" alignItems="flex-start">
944
- <Typography fontWeight="semiBold" ellipsis>
945
- {session.user?.username || session.user?.email || t('homepage.user.unknown', 'Unknown')}
946
- </Typography>
947
- {session.user?.email && session.user?.username && (
948
- <Typography variant="pi" textColor="neutral600" ellipsis>
949
- {session.user.email}
950
- </Typography>
951
- )}
952
- </Flex>
953
- </Td>
954
-
955
- {/* Device */}
956
- <Td>
957
- <Flex alignItems="center" gap={2}>
958
- <DeviceIcon width="18px" height="18px" />
959
- <Flex direction="column" alignItems="flex-start">
960
- <Typography variant="omega" fontWeight="semiBold">
961
- {deviceInfo.device}
962
- </Typography>
963
- <Typography variant="pi" textColor="neutral600">
964
- {deviceInfo.browser} on {deviceInfo.os}
965
- </Typography>
966
- </Flex>
967
- </Flex>
968
- </Td>
969
-
970
- {/* IP Address */}
971
- <Td>
972
- <Typography variant="omega" style={{ fontFamily: 'monospace' }}>
973
- {session.ipAddress}
974
- </Typography>
975
- </Td>
976
-
977
- {/* Login Time */}
978
- <Td>
979
- <Typography variant="pi" textColor="neutral700">
980
- {new Date(session.loginTime).toLocaleString()}
981
- </Typography>
982
- </Td>
983
-
984
- {/* Last Active */}
985
- <Td>
986
- <Flex direction="column" alignItems="flex-start">
987
- <Typography variant="pi" textColor="neutral700">
988
- {new Date(session.lastActive || session.loginTime).toLocaleString()}
989
- </Typography>
990
- <Typography variant="pi" textColor={sessionStatus === 'active' ? 'success600' : 'neutral500'}>
991
- {t('homepage.time.minAgo', '{minutes} min ago', { minutes: session.minutesSinceActive })}
992
- </Typography>
993
- </Flex>
994
- </Td>
995
-
996
- {/* Actions */}
997
- <Td onClick={(e) => e.stopPropagation()}>
998
- <ActionButtons className="action-buttons">
999
- <Button
1000
- variant="secondary"
1001
- size="S"
1002
- onClick={(e) => {
1003
- e.stopPropagation();
1004
- handleSessionClick(session);
1005
- }}
1006
- title={t('homepage.actions.viewDetails', 'View Details')}
1007
- >
1008
- <Eye />
1009
- </Button>
1010
- <Button
1011
- variant="danger-light"
1012
- size="S"
1013
- onClick={(e) => {
1014
- e.stopPropagation();
1015
- handleTerminateSession(session.id);
1016
- }}
1017
- disabled={sessionStatus !== 'active' && sessionStatus !== 'idle'}
1018
- title={session.isActive ? t('homepage.actions.terminate', 'Terminate (Logout)') : t('homepage.actions.alreadyInactive', 'Already inactive')}
1019
- >
1020
- <Cross />
1021
- </Button>
1022
- <Button
1023
- variant="danger"
1024
- size="S"
1025
- onClick={(e) => {
1026
- e.stopPropagation();
1027
- handleDeleteSession(session.id);
1028
- }}
1029
- title={t('homepage.actions.deletePermanently', 'Delete Permanently')}
1030
- >
1031
- <Trash />
1032
- </Button>
1033
- </ActionButtons>
1034
- </Td>
1035
- </ClickableRow>
1036
- );
1037
- })}
1038
- </Tbody>
1039
- </StyledTable>
1040
- </DataTable>
1041
- ) : (
1042
- /* No results found */
1043
- <Box
1044
- background="neutral0"
1045
- style={{
1046
- borderRadius: theme.borderRadius.xl,
1047
- border: '2px dashed #E5E7EB',
1048
- padding: '60px 32px',
1049
- textAlign: 'center',
1050
- position: 'relative',
1051
- overflow: 'hidden',
1052
- minHeight: '300px',
1053
- display: 'flex',
1054
- alignItems: 'center',
1055
- justifyContent: 'center',
1056
- }}
1057
- >
1058
- {/* Background Gradient */}
1059
- <Box
1060
- style={{
1061
- position: 'absolute',
1062
- top: 0,
1063
- left: 0,
1064
- right: 0,
1065
- bottom: 0,
1066
- background: `linear-gradient(135deg, ${theme.colors.primary[50]} 0%, ${theme.colors.secondary[50]} 100%)`,
1067
- opacity: 0.3,
1068
- zIndex: 0,
1069
- }}
1070
- />
1071
-
1072
- {/* Floating Emoji */}
1073
- <FloatingEmoji>
1074
- 🔍
1075
- </FloatingEmoji>
1076
-
1077
- {/* Content */}
1078
- <Flex direction="column" alignItems="center" gap={4} style={{ position: 'relative', zIndex: 1 }}>
1079
- {/* Icon Circle */}
1080
- <Box
1081
- style={{
1082
- width: '100px',
1083
- height: '100px',
1084
- borderRadius: '50%',
1085
- background: `linear-gradient(135deg, ${theme.colors.primary[100]} 0%, ${theme.colors.secondary[100]} 100%)`,
1086
- display: 'flex',
1087
- alignItems: 'center',
1088
- justifyContent: 'center',
1089
- boxShadow: theme.shadows.xl,
1090
- }}
1091
- >
1092
- <Search style={{ width: '50px', height: '50px', color: '#0284C7' }} />
1093
- </Box>
1094
-
1095
- <Typography
1096
- variant="alpha"
1097
- textColor="neutral800"
1098
- style={{
1099
- fontSize: '1.5rem',
1100
- fontWeight: '700',
1101
- marginBottom: '4px',
1102
- }}
1103
- >
1104
- {t('homepage.noResults.title', 'No sessions found')}
1105
- </Typography>
1106
-
1107
- <Typography
1108
- variant="omega"
1109
- textColor="neutral600"
1110
- style={{
1111
- fontSize: '1rem',
1112
- maxWidth: '400px',
1113
- lineHeight: '1.6',
1114
- }}
1115
- >
1116
- {t('homepage.noResults.description', 'Try adjusting your search query or filters to find sessions')}
1117
- </Typography>
1118
- </Flex>
1119
- </Box>
1120
- )}
1121
- </Box>
1122
- )}
1123
-
1124
- {/* Empty State */}
1125
- {!loading && sessions.length === 0 && (
1126
- <Box
1127
- background="neutral0"
1128
- style={{
1129
- borderRadius: theme.borderRadius.xl,
1130
- border: '2px dashed #E5E7EB',
1131
- padding: '80px 32px',
1132
- textAlign: 'center',
1133
- position: 'relative',
1134
- overflow: 'hidden',
1135
- minHeight: '400px',
1136
- display: 'flex',
1137
- alignItems: 'center',
1138
- justifyContent: 'center',
1139
- }}
1140
- >
1141
- {/* Background Gradient */}
1142
- <Box
1143
- style={{
1144
- position: 'absolute',
1145
- top: 0,
1146
- left: 0,
1147
- right: 0,
1148
- bottom: 0,
1149
- background: `linear-gradient(135deg, ${theme.colors.primary[50]} 0%, ${theme.colors.secondary[50]} 100%)`,
1150
- opacity: 0.3,
1151
- zIndex: 0,
1152
- }}
1153
- />
1154
-
1155
- {/* Floating Icon (removed emoji) */}
1156
-
1157
- <Flex direction="column" alignItems="center" gap={6} style={{ position: 'relative', zIndex: 1 }}>
1158
- <Box
1159
- style={{
1160
- width: '120px',
1161
- height: '120px',
1162
- borderRadius: '50%',
1163
- background: `linear-gradient(135deg, ${theme.colors.primary[100]} 0%, ${theme.colors.secondary[100]} 100%)`,
1164
- display: 'flex',
1165
- alignItems: 'center',
1166
- justifyContent: 'center',
1167
- boxShadow: theme.shadows.xl,
1168
- }}
1169
- >
1170
- <Monitor style={{ width: '60px', height: '60px', color: '#0284C7' }} />
1171
- </Box>
1172
-
1173
- <Typography
1174
- variant="alpha"
1175
- textColor="neutral800"
1176
- style={{
1177
- fontSize: '1.75rem',
1178
- fontWeight: '700',
1179
- marginBottom: '8px',
1180
- }}
1181
- >
1182
- {t('homepage.empty.title', 'No sessions yet')}
1183
- </Typography>
1184
-
1185
- <Typography
1186
- variant="omega"
1187
- textColor="neutral600"
1188
- style={{
1189
- fontSize: '1rem',
1190
- maxWidth: '500px',
1191
- lineHeight: '1.6',
1192
- }}
1193
- >
1194
- {t('homepage.empty.description', 'Sessions will appear here when users log in to your application')}
1195
- </Typography>
1196
- </Flex>
1197
- </Box>
1198
- )}
1199
-
1200
- {/* Session Detail Modal */}
1201
- {showDetailModal && selectedSession && (
1202
- <SessionDetailModal
1203
- session={selectedSession}
1204
- onClose={handleModalClose}
1205
- onSessionTerminated={handleSessionTerminated}
1206
- />
1207
- )}
1208
- </Container>
1209
- );
1210
- };
1211
-
1212
- export default HomePage;