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,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es6",
4
+ "jsx": "react",
5
+ "module": "esnext",
6
+ "allowSyntheticDefaultImports": true,
7
+ "esModuleInterop": true
8
+ },
9
+ "include": ["./src/**/*.js", "./src/**/*.jsx"]
10
+ }
@@ -0,0 +1,11 @@
1
+ import { useEffect } from 'react';
2
+
3
+ const Initializer = () => {
4
+ useEffect(() => {
5
+ console.log('[magic-sessionmanager] Plugin initialized');
6
+ }, []);
7
+
8
+ return null;
9
+ };
10
+
11
+ export default Initializer;
@@ -0,0 +1,591 @@
1
+ import { useState, useEffect } from 'react';
2
+ import styled, { keyframes } from 'styled-components';
3
+ import {
4
+ Typography,
5
+ Box,
6
+ Flex,
7
+ Button,
8
+ TextInput,
9
+ Loader,
10
+ } from '@strapi/design-system';
11
+ import { useFetchClient, useNotification } from '@strapi/strapi/admin';
12
+ import { Check, Key, Cross } from '@strapi/icons';
13
+ import { useNavigate } from 'react-router-dom';
14
+ import pluginId from '../pluginId';
15
+
16
+ // Animations
17
+ const fadeIn = keyframes`
18
+ from { opacity: 0; }
19
+ to { opacity: 1; }
20
+ `;
21
+
22
+ const slideUp = keyframes`
23
+ from {
24
+ opacity: 0;
25
+ transform: translateY(30px);
26
+ }
27
+ to {
28
+ opacity: 1;
29
+ transform: translateY(0);
30
+ }
31
+ `;
32
+
33
+ // Styled Components
34
+ const ModalOverlay = styled.div`
35
+ position: fixed;
36
+ top: 0;
37
+ left: 0;
38
+ right: 0;
39
+ bottom: 0;
40
+ background: rgba(4, 28, 47, 0.85);
41
+ backdrop-filter: blur(8px);
42
+ z-index: 9999;
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ animation: ${fadeIn} 0.3s ease-out;
47
+ padding: 20px;
48
+ `;
49
+
50
+ const ModalContent = styled(Box)`
51
+ background: white;
52
+ border-radius: 16px;
53
+ width: 100%;
54
+ max-width: 580px;
55
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
56
+ animation: ${slideUp} 0.4s cubic-bezier(0.4, 0, 0.2, 1);
57
+ overflow: hidden;
58
+ `;
59
+
60
+ const GradientHeader = styled(Box)`
61
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
62
+ padding: 32px 40px;
63
+ position: relative;
64
+ overflow: hidden;
65
+
66
+ &::before {
67
+ content: '';
68
+ position: absolute;
69
+ top: -50%;
70
+ right: -50%;
71
+ width: 200%;
72
+ height: 200%;
73
+ background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
74
+ }
75
+ `;
76
+
77
+ const IconWrapper = styled.div`
78
+ width: 72px;
79
+ height: 72px;
80
+ border-radius: 50%;
81
+ background: rgba(255, 255, 255, 0.2);
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ margin: 0 auto 16px;
86
+ backdrop-filter: blur(10px);
87
+ border: 2px solid rgba(255, 255, 255, 0.3);
88
+
89
+ svg {
90
+ width: 36px;
91
+ height: 36px;
92
+ color: white;
93
+ }
94
+ `;
95
+
96
+ const CloseButton = styled.button`
97
+ position: absolute;
98
+ top: 16px;
99
+ right: 16px;
100
+ background: rgba(255, 255, 255, 0.2);
101
+ border: 2px solid rgba(255, 255, 255, 0.3);
102
+ border-radius: 50%;
103
+ width: 36px;
104
+ height: 36px;
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ cursor: pointer;
109
+ transition: all 0.2s;
110
+ z-index: 10;
111
+
112
+ svg {
113
+ width: 20px;
114
+ height: 20px;
115
+ color: white;
116
+ }
117
+
118
+ &:hover {
119
+ background: rgba(255, 255, 255, 0.3);
120
+ transform: scale(1.1);
121
+ }
122
+
123
+ &:active {
124
+ transform: scale(0.95);
125
+ }
126
+ `;
127
+
128
+ const ToggleButton = styled.button`
129
+ background: none;
130
+ border: none;
131
+ color: #667eea;
132
+ font-size: 13px;
133
+ font-weight: 600;
134
+ cursor: pointer;
135
+ padding: 8px 0;
136
+ text-decoration: underline;
137
+ transition: color 0.2s;
138
+
139
+ &:hover {
140
+ color: #764ba2;
141
+ }
142
+ `;
143
+
144
+ const LicenseGuard = ({ children }) => {
145
+ const { get, post } = useFetchClient();
146
+ const { toggleNotification } = useNotification();
147
+ const navigate = useNavigate();
148
+
149
+ const [isChecking, setIsChecking] = useState(true);
150
+ const [needsLicense, setNeedsLicense] = useState(false);
151
+ const [isCreating, setIsCreating] = useState(false);
152
+ const [useExistingKey, setUseExistingKey] = useState(false);
153
+ const [useAutoCreate, setUseAutoCreate] = useState(true);
154
+ const [existingLicenseKey, setExistingLicenseKey] = useState('');
155
+ const [existingEmail, setExistingEmail] = useState('');
156
+ const [adminUser, setAdminUser] = useState(null);
157
+
158
+ const [formData, setFormData] = useState({
159
+ email: '',
160
+ firstName: '',
161
+ lastName: '',
162
+ });
163
+
164
+ useEffect(() => {
165
+ checkLicenseStatus();
166
+ fetchAdminUser();
167
+ }, []);
168
+
169
+ const fetchAdminUser = async () => {
170
+ try {
171
+ const response = await get('/admin/users/me');
172
+ const userData = response.data?.data || response.data;
173
+ if (userData) {
174
+ setAdminUser(userData);
175
+ setFormData({
176
+ email: userData.email || '',
177
+ firstName: userData.firstname || '',
178
+ lastName: userData.lastname || '',
179
+ });
180
+ }
181
+ } catch (error) {
182
+ // Silently fail - user might not be authenticated yet
183
+ console.debug('[LicenseGuard] Could not fetch admin user (not logged in yet)');
184
+ }
185
+ };
186
+
187
+ const checkLicenseStatus = async () => {
188
+ setIsChecking(true);
189
+ try {
190
+ const response = await get(`/${pluginId}/license/status`);
191
+
192
+ if (response.data.valid) {
193
+ setNeedsLicense(false);
194
+ } else {
195
+ setNeedsLicense(true);
196
+ }
197
+ } catch (error) {
198
+ console.error('[LicenseGuard] Error checking license:', error);
199
+ setNeedsLicense(true);
200
+ } finally {
201
+ setIsChecking(false);
202
+ }
203
+ };
204
+
205
+ const handleAutoCreateLicense = async (e) => {
206
+ e.preventDefault();
207
+ setIsCreating(true);
208
+
209
+ try {
210
+ const response = await post(`/${pluginId}/license/auto-create`, {});
211
+
212
+ if (response.data && response.data.success) {
213
+ toggleNotification({
214
+ type: 'success',
215
+ message: 'License automatically created! Reloading...',
216
+ });
217
+
218
+ setNeedsLicense(false);
219
+
220
+ setTimeout(() => {
221
+ window.location.reload();
222
+ }, 500);
223
+ } else {
224
+ throw new Error('Failed to auto-create license');
225
+ }
226
+ } catch (error) {
227
+ console.error('[LicenseGuard] Error auto-creating license:', error);
228
+ toggleNotification({
229
+ type: 'danger',
230
+ message: error?.response?.data?.error?.message || 'Failed to auto-create license. Try manual creation.',
231
+ });
232
+ setIsCreating(false);
233
+ setUseAutoCreate(false);
234
+ }
235
+ };
236
+
237
+ const handleCreateLicense = async (e) => {
238
+ e.preventDefault();
239
+
240
+ if (!formData.email || !formData.firstName || !formData.lastName) {
241
+ toggleNotification({
242
+ type: 'warning',
243
+ message: 'Please fill in all fields',
244
+ });
245
+ return;
246
+ }
247
+
248
+ setIsCreating(true);
249
+
250
+ try {
251
+ const response = await post(`/${pluginId}/license/create`, formData);
252
+
253
+ if (response.data && response.data.success) {
254
+ toggleNotification({
255
+ type: 'success',
256
+ message: 'License created successfully! Reloading...',
257
+ });
258
+
259
+ setNeedsLicense(false);
260
+
261
+ setTimeout(() => {
262
+ window.location.reload();
263
+ }, 500);
264
+ } else {
265
+ throw new Error('Failed to create license');
266
+ }
267
+ } catch (error) {
268
+ console.error('[LicenseGuard] Error creating license:', error);
269
+ toggleNotification({
270
+ type: 'danger',
271
+ message: error?.response?.data?.error?.message || 'Failed to create license. Please try again.',
272
+ });
273
+ setIsCreating(false);
274
+ }
275
+ };
276
+
277
+ const handleValidateExistingKey = async (e) => {
278
+ e.preventDefault();
279
+
280
+ if (!existingLicenseKey.trim() || !existingEmail.trim()) {
281
+ toggleNotification({
282
+ type: 'warning',
283
+ message: 'Please enter both license key and email address',
284
+ });
285
+ return;
286
+ }
287
+
288
+ setIsCreating(true);
289
+
290
+ try {
291
+ const response = await post(`/${pluginId}/license/store-key`, {
292
+ licenseKey: existingLicenseKey.trim(),
293
+ email: existingEmail.trim(),
294
+ });
295
+
296
+ if (response.data && response.data.success) {
297
+ toggleNotification({
298
+ type: 'success',
299
+ message: 'License key validated successfully! Reloading...',
300
+ });
301
+
302
+ setNeedsLicense(false);
303
+
304
+ setTimeout(() => {
305
+ window.location.reload();
306
+ }, 500);
307
+ } else {
308
+ throw new Error('Invalid license key or email');
309
+ }
310
+ } catch (error) {
311
+ console.error('[LicenseGuard] Error validating license key:', error);
312
+ toggleNotification({
313
+ type: 'danger',
314
+ message: error?.response?.data?.error?.message || 'Invalid license key or email address. Please check and try again.',
315
+ });
316
+ setIsCreating(false);
317
+ }
318
+ };
319
+
320
+ const handleClose = () => {
321
+ navigate('/content-manager');
322
+ };
323
+
324
+ if (isChecking) {
325
+ return (
326
+ <Box padding={8} style={{ textAlign: 'center' }}>
327
+ <Loader>Checking license...</Loader>
328
+ </Box>
329
+ );
330
+ }
331
+
332
+ if (needsLicense) {
333
+ return (
334
+ <ModalOverlay>
335
+ <ModalContent>
336
+ <GradientHeader>
337
+ <CloseButton onClick={handleClose} type="button">
338
+ <Cross />
339
+ </CloseButton>
340
+ <IconWrapper>
341
+ <Key />
342
+ </IconWrapper>
343
+ <Box style={{ textAlign: 'center', position: 'relative' }}>
344
+ <Typography
345
+ variant="alpha"
346
+ style={{
347
+ color: 'white',
348
+ fontSize: '24px',
349
+ fontWeight: '700',
350
+ marginBottom: '12px',
351
+ display: 'block',
352
+ }}
353
+ >
354
+ 🔐 Activate Session Manager
355
+ </Typography>
356
+ <Typography
357
+ variant="epsilon"
358
+ style={{
359
+ color: 'rgba(255, 255, 255, 0.9)',
360
+ fontSize: '14px',
361
+ display: 'block',
362
+ }}
363
+ >
364
+ {useExistingKey ? 'Enter your existing license key' : 'Create a license to start using the plugin'}
365
+ </Typography>
366
+ </Box>
367
+ </GradientHeader>
368
+
369
+ <form onSubmit={useExistingKey ? handleValidateExistingKey : (useAutoCreate ? handleAutoCreateLicense : handleCreateLicense)}>
370
+ <Box
371
+ padding={6}
372
+ paddingLeft={8}
373
+ paddingRight={8}
374
+ >
375
+ <Flex direction="column" gap={5} style={{ width: '100%' }}>
376
+ <Box style={{ textAlign: 'center', width: '100%' }}>
377
+ {!useExistingKey && (
378
+ <ToggleButton
379
+ type="button"
380
+ onClick={() => setUseAutoCreate(!useAutoCreate)}
381
+ disabled={isCreating}
382
+ style={{ marginBottom: '8px', display: 'block', margin: '0 auto' }}
383
+ >
384
+ {useAutoCreate ? '→ Manual entry' : '← Auto-create with my account'}
385
+ </ToggleButton>
386
+ )}
387
+ <ToggleButton
388
+ type="button"
389
+ onClick={() => {
390
+ setUseExistingKey(!useExistingKey);
391
+ if (!useExistingKey) setUseAutoCreate(false);
392
+ }}
393
+ disabled={isCreating}
394
+ >
395
+ {useExistingKey ? '← Create new license' : 'Have a license key? →'}
396
+ </ToggleButton>
397
+ </Box>
398
+
399
+ <Box
400
+ background="primary100"
401
+ padding={4}
402
+ style={{
403
+ borderRadius: '8px',
404
+ border: '2px solid #BAE6FD',
405
+ width: '100%',
406
+ }}
407
+ >
408
+ <Typography variant="omega" style={{ fontSize: '13px', lineHeight: '1.6' }}>
409
+ {useExistingKey
410
+ ? '🔑 Enter your email and license key to activate.'
411
+ : useAutoCreate && adminUser && adminUser.email
412
+ ? `✨ Click "Activate" to auto-create a license with your account (${adminUser.email})`
413
+ : useAutoCreate
414
+ ? '✨ Click "Activate" to auto-create a license with your admin account'
415
+ : '💡 A license will be created with the details below.'
416
+ }
417
+ </Typography>
418
+ </Box>
419
+
420
+ {useExistingKey ? (
421
+ // Existing License Key Input
422
+ <>
423
+ <Box style={{ width: '100%' }}>
424
+ <Typography
425
+ variant="pi"
426
+ fontWeight="bold"
427
+ style={{ marginBottom: '8px', display: 'block' }}
428
+ >
429
+ Email Address *
430
+ </Typography>
431
+ <TextInput
432
+ placeholder="admin@example.com"
433
+ type="email"
434
+ value={existingEmail}
435
+ onChange={(e) => setExistingEmail(e.target.value)}
436
+ required
437
+ disabled={isCreating}
438
+ />
439
+ <Typography variant="omega" textColor="neutral600" style={{ fontSize: '11px', marginTop: '4px' }}>
440
+ Enter the email address associated with this license
441
+ </Typography>
442
+ </Box>
443
+
444
+ <Box style={{ width: '100%' }}>
445
+ <Typography
446
+ variant="pi"
447
+ fontWeight="bold"
448
+ style={{ marginBottom: '8px', display: 'block' }}
449
+ >
450
+ License Key *
451
+ </Typography>
452
+ <TextInput
453
+ placeholder="67C5-40D2-7695-718C"
454
+ value={existingLicenseKey}
455
+ onChange={(e) => setExistingLicenseKey(e.target.value)}
456
+ required
457
+ disabled={isCreating}
458
+ />
459
+ <Typography variant="omega" textColor="neutral600" style={{ fontSize: '11px', marginTop: '4px' }}>
460
+ Enter the license key in the format: XXXX-XXXX-XXXX-XXXX
461
+ </Typography>
462
+ </Box>
463
+ </>
464
+ ) : useAutoCreate && adminUser ? (
465
+ // Auto-create mode - Show user info
466
+ <Box
467
+ background="success100"
468
+ padding={5}
469
+ style={{
470
+ borderRadius: '8px',
471
+ border: '2px solid #DCFCE7',
472
+ textAlign: 'center',
473
+ }}
474
+ >
475
+ <Typography variant="omega" fontWeight="bold" style={{ marginBottom: '12px', display: 'block' }}>
476
+ Ready to activate with your account:
477
+ </Typography>
478
+ <Typography variant="pi" style={{ marginBottom: '4px', display: 'block' }}>
479
+ 👤 {adminUser.firstname || 'Admin'} {adminUser.lastname || 'User'}
480
+ </Typography>
481
+ <Typography variant="pi" textColor="neutral600">
482
+ 📧 {adminUser.email || 'Loading...'}
483
+ </Typography>
484
+ </Box>
485
+ ) : (
486
+ // Manual Create License Fields
487
+ <>
488
+ <Box style={{ width: '100%' }}>
489
+ <Typography
490
+ variant="pi"
491
+ fontWeight="bold"
492
+ style={{ marginBottom: '8px', display: 'block' }}
493
+ >
494
+ Email Address *
495
+ </Typography>
496
+ <TextInput
497
+ placeholder="admin@example.com"
498
+ type="email"
499
+ value={formData.email}
500
+ onChange={(e) => setFormData({ ...formData, email: e.target.value })}
501
+ required
502
+ disabled={isCreating}
503
+ />
504
+ </Box>
505
+
506
+ <Box style={{ width: '100%' }}>
507
+ <Typography
508
+ variant="pi"
509
+ fontWeight="bold"
510
+ style={{ marginBottom: '8px', display: 'block' }}
511
+ >
512
+ First Name *
513
+ </Typography>
514
+ <TextInput
515
+ placeholder="John"
516
+ value={formData.firstName}
517
+ onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
518
+ required
519
+ disabled={isCreating}
520
+ />
521
+ </Box>
522
+
523
+ <Box style={{ width: '100%' }}>
524
+ <Typography
525
+ variant="pi"
526
+ fontWeight="bold"
527
+ style={{ marginBottom: '8px', display: 'block' }}
528
+ >
529
+ Last Name *
530
+ </Typography>
531
+ <TextInput
532
+ placeholder="Doe"
533
+ value={formData.lastName}
534
+ onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
535
+ required
536
+ disabled={isCreating}
537
+ />
538
+ </Box>
539
+ </>
540
+ )}
541
+
542
+ <Flex gap={3} justifyContent="center" style={{ marginTop: '16px' }}>
543
+ {useExistingKey ? (
544
+ <Button
545
+ type="submit"
546
+ size="L"
547
+ startIcon={<Check />}
548
+ loading={isCreating}
549
+ disabled={isCreating || !existingLicenseKey.trim() || !existingEmail.trim()}
550
+ style={{
551
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
552
+ color: 'white',
553
+ fontWeight: '600',
554
+ border: 'none',
555
+ boxShadow: '0 4px 12px rgba(102, 126, 234, 0.4)',
556
+ }}
557
+ >
558
+ Validate License
559
+ </Button>
560
+ ) : (
561
+ <Button
562
+ type="submit"
563
+ size="L"
564
+ startIcon={<Check />}
565
+ loading={isCreating}
566
+ disabled={isCreating || (!useAutoCreate && (!formData.email || !formData.firstName || !formData.lastName))}
567
+ style={{
568
+ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
569
+ color: 'white',
570
+ fontWeight: '600',
571
+ border: 'none',
572
+ boxShadow: '0 4px 12px rgba(102, 126, 234, 0.4)',
573
+ }}
574
+ >
575
+ {useAutoCreate ? 'Activate License' : 'Create License'}
576
+ </Button>
577
+ )}
578
+ </Flex>
579
+ </Flex>
580
+ </Box>
581
+ </form>
582
+ </ModalContent>
583
+ </ModalOverlay>
584
+ );
585
+ }
586
+
587
+ return <>{children}</>;
588
+ };
589
+
590
+ export default LicenseGuard;
591
+