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