strapi-plugin-magic-sessionmanager 2.0.1 → 2.0.3
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.
- package/admin/jsconfig.json +10 -0
- package/admin/src/components/Initializer.jsx +11 -0
- package/admin/src/components/LicenseGuard.jsx +591 -0
- package/admin/src/components/OnlineUsersWidget.jsx +208 -0
- package/admin/src/components/PluginIcon.jsx +8 -0
- package/admin/src/components/SessionDetailModal.jsx +445 -0
- package/admin/src/components/SessionInfoCard.jsx +151 -0
- package/admin/src/components/SessionInfoPanel.jsx +375 -0
- package/admin/src/components/index.jsx +5 -0
- package/admin/src/hooks/useLicense.js +103 -0
- package/admin/src/index.js +137 -0
- package/admin/src/pages/ActiveSessions.jsx +12 -0
- package/admin/src/pages/Analytics.jsx +735 -0
- package/admin/src/pages/App.jsx +12 -0
- package/admin/src/pages/HomePage.jsx +1248 -0
- package/admin/src/pages/License.jsx +603 -0
- package/admin/src/pages/Settings.jsx +1497 -0
- package/admin/src/pages/SettingsNew.jsx +1204 -0
- package/admin/src/pages/index.jsx +3 -0
- package/admin/src/pluginId.js +3 -0
- package/admin/src/translations/de.json +20 -0
- package/admin/src/translations/en.json +20 -0
- package/admin/src/utils/getTranslation.js +5 -0
- package/admin/src/utils/index.js +2 -0
- package/admin/src/utils/parseUserAgent.js +79 -0
- package/dist/server/index.js +91 -2
- package/dist/server/index.mjs +91 -2
- package/package.json +3 -1
- package/server/jsconfig.json +10 -0
- package/server/src/bootstrap.js +297 -0
- package/server/src/config/index.js +20 -0
- package/server/src/content-types/index.js +9 -0
- package/server/src/content-types/session/schema.json +76 -0
- package/server/src/controllers/controller.js +11 -0
- package/server/src/controllers/index.js +11 -0
- package/server/src/controllers/license.js +266 -0
- package/server/src/controllers/session.js +362 -0
- package/server/src/controllers/settings.js +122 -0
- package/server/src/destroy.js +18 -0
- package/server/src/index.js +23 -0
- package/server/src/middlewares/index.js +5 -0
- package/server/src/middlewares/last-seen.js +56 -0
- package/server/src/policies/index.js +3 -0
- package/server/src/register.js +32 -0
- package/server/src/routes/admin.js +149 -0
- package/server/src/routes/content-api.js +51 -0
- package/server/src/routes/index.js +9 -0
- package/server/src/services/geolocation.js +180 -0
- package/server/src/services/index.js +13 -0
- package/server/src/services/license-guard.js +308 -0
- package/server/src/services/notifications.js +319 -0
- package/server/src/services/service.js +7 -0
- package/server/src/services/session.js +345 -0
- package/server/src/utils/getClientIp.js +118 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
type: 'admin',
|
|
5
|
+
routes: [
|
|
6
|
+
{
|
|
7
|
+
method: 'GET',
|
|
8
|
+
path: '/sessions',
|
|
9
|
+
handler: 'session.getAllSessionsAdmin',
|
|
10
|
+
config: {
|
|
11
|
+
policies: ['admin::isAuthenticatedAdmin'],
|
|
12
|
+
description: 'Get all sessions - active and inactive (admin)',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
method: 'GET',
|
|
17
|
+
path: '/sessions/active',
|
|
18
|
+
handler: 'session.getActiveSessions',
|
|
19
|
+
config: {
|
|
20
|
+
policies: ['admin::isAuthenticatedAdmin'],
|
|
21
|
+
description: 'Get only active sessions (admin)',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
method: 'GET',
|
|
26
|
+
path: '/user/:userId/sessions',
|
|
27
|
+
handler: 'session.getUserSessions',
|
|
28
|
+
config: {
|
|
29
|
+
policies: ['admin::isAuthenticatedAdmin'],
|
|
30
|
+
description: 'Get user sessions (admin)',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
method: 'POST',
|
|
35
|
+
path: '/sessions/:sessionId/terminate',
|
|
36
|
+
handler: 'session.terminateSingleSession',
|
|
37
|
+
config: {
|
|
38
|
+
policies: ['admin::isAuthenticatedAdmin'],
|
|
39
|
+
description: 'Terminate a specific session (admin)',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
method: 'DELETE',
|
|
44
|
+
path: '/sessions/:sessionId',
|
|
45
|
+
handler: 'session.deleteSession',
|
|
46
|
+
config: {
|
|
47
|
+
policies: ['admin::isAuthenticatedAdmin'],
|
|
48
|
+
description: 'Delete a single session permanently (admin)',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
method: 'POST',
|
|
53
|
+
path: '/sessions/clean-inactive',
|
|
54
|
+
handler: 'session.cleanInactiveSessions',
|
|
55
|
+
config: {
|
|
56
|
+
policies: ['admin::isAuthenticatedAdmin'],
|
|
57
|
+
description: 'Delete all inactive sessions from database (admin)',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
method: 'POST',
|
|
62
|
+
path: '/user/:userId/terminate-all',
|
|
63
|
+
handler: 'session.terminateAllUserSessions',
|
|
64
|
+
config: {
|
|
65
|
+
policies: ['admin::isAuthenticatedAdmin'],
|
|
66
|
+
description: 'Terminate all sessions for a user (admin)',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
method: 'POST',
|
|
71
|
+
path: '/user/:userId/toggle-block',
|
|
72
|
+
handler: 'session.toggleUserBlock',
|
|
73
|
+
config: {
|
|
74
|
+
policies: ['admin::isAuthenticatedAdmin'],
|
|
75
|
+
description: 'Toggle user blocked status (admin)',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
// License Management
|
|
79
|
+
{
|
|
80
|
+
method: 'GET',
|
|
81
|
+
path: '/license/status',
|
|
82
|
+
handler: 'license.getStatus',
|
|
83
|
+
config: {
|
|
84
|
+
policies: [],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
method: 'POST',
|
|
89
|
+
path: '/license/auto-create',
|
|
90
|
+
handler: 'license.autoCreate',
|
|
91
|
+
config: {
|
|
92
|
+
policies: [],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
method: 'POST',
|
|
97
|
+
path: '/license/create',
|
|
98
|
+
handler: 'license.createAndActivate',
|
|
99
|
+
config: {
|
|
100
|
+
policies: [],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
method: 'POST',
|
|
105
|
+
path: '/license/ping',
|
|
106
|
+
handler: 'license.ping',
|
|
107
|
+
config: {
|
|
108
|
+
policies: [],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
method: 'POST',
|
|
113
|
+
path: '/license/store-key',
|
|
114
|
+
handler: 'license.storeKey',
|
|
115
|
+
config: {
|
|
116
|
+
policies: [],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
// Geolocation (Premium Feature)
|
|
120
|
+
{
|
|
121
|
+
method: 'GET',
|
|
122
|
+
path: '/geolocation/:ipAddress',
|
|
123
|
+
handler: 'session.getIpGeolocation',
|
|
124
|
+
config: {
|
|
125
|
+
policies: ['admin::isAuthenticatedAdmin'],
|
|
126
|
+
description: 'Get IP geolocation data (Premium feature)',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
// Settings Management
|
|
130
|
+
{
|
|
131
|
+
method: 'GET',
|
|
132
|
+
path: '/settings',
|
|
133
|
+
handler: 'settings.getSettings',
|
|
134
|
+
config: {
|
|
135
|
+
policies: ['admin::isAuthenticatedAdmin'],
|
|
136
|
+
description: 'Get plugin settings',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
method: 'PUT',
|
|
141
|
+
path: '/settings',
|
|
142
|
+
handler: 'settings.updateSettings',
|
|
143
|
+
config: {
|
|
144
|
+
policies: ['admin::isAuthenticatedAdmin'],
|
|
145
|
+
description: 'Update plugin settings',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Content API Routes for Magic Session Manager
|
|
5
|
+
*
|
|
6
|
+
* SECURITY: All routes require authentication
|
|
7
|
+
* - User can only access their own sessions
|
|
8
|
+
* - Admin routes are in admin.js
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
type: 'content-api',
|
|
13
|
+
routes: [
|
|
14
|
+
// ============================================================
|
|
15
|
+
// LOGOUT ENDPOINTS
|
|
16
|
+
// ============================================================
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
method: 'POST',
|
|
20
|
+
path: '/logout',
|
|
21
|
+
handler: 'session.logout',
|
|
22
|
+
config: {
|
|
23
|
+
auth: { strategies: ['users-permissions'] },
|
|
24
|
+
description: 'Logout current session (requires JWT)',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
method: 'POST',
|
|
29
|
+
path: '/logout-all',
|
|
30
|
+
handler: 'session.logoutAll',
|
|
31
|
+
config: {
|
|
32
|
+
auth: { strategies: ['users-permissions'] },
|
|
33
|
+
description: 'Logout from all devices (requires JWT)',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// ============================================================
|
|
38
|
+
// SESSION QUERIES
|
|
39
|
+
// ============================================================
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
method: 'GET',
|
|
43
|
+
path: '/user/:userId/sessions',
|
|
44
|
+
handler: 'session.getUserSessions',
|
|
45
|
+
config: {
|
|
46
|
+
auth: { strategies: ['users-permissions'] },
|
|
47
|
+
description: 'Get own sessions (controller validates user can only see own sessions)',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IP Geolocation Service
|
|
3
|
+
* Uses ipapi.co for accurate IP information
|
|
4
|
+
* Free tier: 30,000 requests/month
|
|
5
|
+
*
|
|
6
|
+
* Premium features:
|
|
7
|
+
* - Country, City, Region
|
|
8
|
+
* - Timezone
|
|
9
|
+
* - VPN/Proxy/Threat detection
|
|
10
|
+
* - Security scoring
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
module.exports = ({ strapi }) => ({
|
|
14
|
+
/**
|
|
15
|
+
* Get IP information from ipapi.co
|
|
16
|
+
* @param {string} ipAddress - IP to lookup
|
|
17
|
+
* @returns {Promise<Object>} Geolocation data
|
|
18
|
+
*/
|
|
19
|
+
async getIpInfo(ipAddress) {
|
|
20
|
+
try {
|
|
21
|
+
// Skip localhost/private IPs
|
|
22
|
+
if (!ipAddress || ipAddress === '127.0.0.1' || ipAddress === '::1' || ipAddress.startsWith('192.168.') || ipAddress.startsWith('10.')) {
|
|
23
|
+
return {
|
|
24
|
+
ip: ipAddress,
|
|
25
|
+
country: 'Local Network',
|
|
26
|
+
country_code: 'XX',
|
|
27
|
+
country_flag: '🏠',
|
|
28
|
+
city: 'Localhost',
|
|
29
|
+
region: 'Private Network',
|
|
30
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
31
|
+
latitude: null,
|
|
32
|
+
longitude: null,
|
|
33
|
+
isVpn: false,
|
|
34
|
+
isProxy: false,
|
|
35
|
+
isThreat: false,
|
|
36
|
+
securityScore: 100,
|
|
37
|
+
riskLevel: 'None',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Call ipapi.co API
|
|
42
|
+
const response = await fetch(`https://ipapi.co/${ipAddress}/json/`, {
|
|
43
|
+
method: 'GET',
|
|
44
|
+
headers: {
|
|
45
|
+
'User-Agent': 'Strapi-Magic-SessionManager/1.0',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`API returned ${response.status}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = await response.json();
|
|
54
|
+
|
|
55
|
+
// Check for rate limit
|
|
56
|
+
if (data.error) {
|
|
57
|
+
strapi.log.warn(`[magic-sessionmanager/geolocation] API Error: ${data.reason}`);
|
|
58
|
+
return this.getFallbackData(ipAddress);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Parse and return structured data
|
|
62
|
+
const result = {
|
|
63
|
+
ip: data.ip,
|
|
64
|
+
country: data.country_name,
|
|
65
|
+
country_code: data.country_code,
|
|
66
|
+
country_flag: this.getCountryFlag(data.country_code),
|
|
67
|
+
city: data.city,
|
|
68
|
+
region: data.region,
|
|
69
|
+
timezone: data.timezone,
|
|
70
|
+
latitude: data.latitude,
|
|
71
|
+
longitude: data.longitude,
|
|
72
|
+
postal: data.postal,
|
|
73
|
+
org: data.org,
|
|
74
|
+
asn: data.asn,
|
|
75
|
+
|
|
76
|
+
// Security features (available in ipapi.co response)
|
|
77
|
+
isVpn: data.threat?.is_vpn || false,
|
|
78
|
+
isProxy: data.threat?.is_proxy || false,
|
|
79
|
+
isThreat: data.threat?.is_threat || false,
|
|
80
|
+
isAnonymous: data.threat?.is_anonymous || false,
|
|
81
|
+
isTor: data.threat?.is_tor || false,
|
|
82
|
+
|
|
83
|
+
// Calculate security score (0-100, higher is safer)
|
|
84
|
+
securityScore: this.calculateSecurityScore({
|
|
85
|
+
isVpn: data.threat?.is_vpn,
|
|
86
|
+
isProxy: data.threat?.is_proxy,
|
|
87
|
+
isThreat: data.threat?.is_threat,
|
|
88
|
+
isAnonymous: data.threat?.is_anonymous,
|
|
89
|
+
isTor: data.threat?.is_tor,
|
|
90
|
+
}),
|
|
91
|
+
|
|
92
|
+
riskLevel: this.getRiskLevel({
|
|
93
|
+
isVpn: data.threat?.is_vpn,
|
|
94
|
+
isProxy: data.threat?.is_proxy,
|
|
95
|
+
isThreat: data.threat?.is_threat,
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
strapi.log.debug(`[magic-sessionmanager/geolocation] IP ${ipAddress}: ${result.city}, ${result.country} (Score: ${result.securityScore})`);
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
strapi.log.error(`[magic-sessionmanager/geolocation] Error fetching IP info for ${ipAddress}:`, error.message);
|
|
104
|
+
return this.getFallbackData(ipAddress);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Calculate security score based on threat indicators
|
|
110
|
+
*/
|
|
111
|
+
calculateSecurityScore({ isVpn, isProxy, isThreat, isAnonymous, isTor }) {
|
|
112
|
+
let score = 100;
|
|
113
|
+
|
|
114
|
+
if (isTor) score -= 50; // Tor = sehr verdächtig
|
|
115
|
+
if (isThreat) score -= 40; // Known threat
|
|
116
|
+
if (isVpn) score -= 20; // VPN = moderate risk
|
|
117
|
+
if (isProxy) score -= 15; // Proxy = low-moderate risk
|
|
118
|
+
if (isAnonymous) score -= 10; // Anonymous service
|
|
119
|
+
|
|
120
|
+
return Math.max(0, score);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get risk level based on indicators
|
|
125
|
+
*/
|
|
126
|
+
getRiskLevel({ isVpn, isProxy, isThreat }) {
|
|
127
|
+
if (isThreat) return 'High';
|
|
128
|
+
if (isVpn && isProxy) return 'Medium-High';
|
|
129
|
+
if (isVpn || isProxy) return 'Medium';
|
|
130
|
+
return 'Low';
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get country flag emoji
|
|
135
|
+
*/
|
|
136
|
+
getCountryFlag(countryCode) {
|
|
137
|
+
if (!countryCode) return '🏳️';
|
|
138
|
+
|
|
139
|
+
// Convert country code to flag emoji
|
|
140
|
+
const codePoints = countryCode
|
|
141
|
+
.toUpperCase()
|
|
142
|
+
.split('')
|
|
143
|
+
.map(char => 127397 + char.charCodeAt());
|
|
144
|
+
|
|
145
|
+
return String.fromCodePoint(...codePoints);
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Fallback data when API fails
|
|
150
|
+
*/
|
|
151
|
+
getFallbackData(ipAddress) {
|
|
152
|
+
return {
|
|
153
|
+
ip: ipAddress,
|
|
154
|
+
country: 'Unknown',
|
|
155
|
+
country_code: 'XX',
|
|
156
|
+
country_flag: '🌍',
|
|
157
|
+
city: 'Unknown',
|
|
158
|
+
region: 'Unknown',
|
|
159
|
+
timezone: 'Unknown',
|
|
160
|
+
latitude: null,
|
|
161
|
+
longitude: null,
|
|
162
|
+
isVpn: false,
|
|
163
|
+
isProxy: false,
|
|
164
|
+
isThreat: false,
|
|
165
|
+
securityScore: 50,
|
|
166
|
+
riskLevel: 'Unknown',
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Batch lookup multiple IPs (for efficiency)
|
|
172
|
+
*/
|
|
173
|
+
async batchGetIpInfo(ipAddresses) {
|
|
174
|
+
const results = await Promise.all(
|
|
175
|
+
ipAddresses.map(ip => this.getIpInfo(ip))
|
|
176
|
+
);
|
|
177
|
+
return results;
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const session = require('./session');
|
|
4
|
+
const licenseGuard = require('./license-guard');
|
|
5
|
+
const geolocation = require('./geolocation');
|
|
6
|
+
const notifications = require('./notifications');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
session,
|
|
10
|
+
'license-guard': licenseGuard,
|
|
11
|
+
geolocation,
|
|
12
|
+
notifications,
|
|
13
|
+
};
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License Guard Service for Magic Session Manager
|
|
3
|
+
* Handles license creation, verification, and ping tracking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
// FIXED LICENSE SERVER URL
|
|
10
|
+
const LICENSE_SERVER_URL = 'https://magicapi.fitlex.me';
|
|
11
|
+
|
|
12
|
+
module.exports = ({ strapi }) => ({
|
|
13
|
+
/**
|
|
14
|
+
* Get license server URL
|
|
15
|
+
*/
|
|
16
|
+
getLicenseServerUrl() {
|
|
17
|
+
return LICENSE_SERVER_URL;
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate device ID
|
|
22
|
+
*/
|
|
23
|
+
generateDeviceId() {
|
|
24
|
+
try {
|
|
25
|
+
const networkInterfaces = os.networkInterfaces();
|
|
26
|
+
const macAddresses = [];
|
|
27
|
+
|
|
28
|
+
Object.values(networkInterfaces).forEach(interfaces => {
|
|
29
|
+
interfaces?.forEach(iface => {
|
|
30
|
+
if (iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
|
31
|
+
macAddresses.push(iface.mac);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const identifier = `${macAddresses.join('-')}-${os.hostname()}`;
|
|
37
|
+
return crypto.createHash('sha256').update(identifier).digest('hex').substring(0, 32);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return crypto.randomBytes(16).toString('hex');
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
getDeviceName() {
|
|
44
|
+
try {
|
|
45
|
+
return os.hostname() || 'Unknown Device';
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return 'Unknown Device';
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
getIpAddress() {
|
|
52
|
+
try {
|
|
53
|
+
const networkInterfaces = os.networkInterfaces();
|
|
54
|
+
for (const name of Object.keys(networkInterfaces)) {
|
|
55
|
+
const interfaces = networkInterfaces[name];
|
|
56
|
+
if (interfaces) {
|
|
57
|
+
for (const iface of interfaces) {
|
|
58
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
59
|
+
return iface.address;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return '127.0.0.1';
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return '127.0.0.1';
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
getUserAgent() {
|
|
71
|
+
return `Strapi/${strapi.config.get('info.strapi') || '5.0.0'} Node/${process.version} ${os.platform()}/${os.release()}`;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async createLicense({ email, firstName, lastName }) {
|
|
75
|
+
try {
|
|
76
|
+
const deviceId = this.generateDeviceId();
|
|
77
|
+
const deviceName = this.getDeviceName();
|
|
78
|
+
const ipAddress = this.getIpAddress();
|
|
79
|
+
const userAgent = this.getUserAgent();
|
|
80
|
+
|
|
81
|
+
const licenseServerUrl = this.getLicenseServerUrl();
|
|
82
|
+
const response = await fetch(`${licenseServerUrl}/api/licenses/create`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'Content-Type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
email,
|
|
87
|
+
firstName,
|
|
88
|
+
lastName,
|
|
89
|
+
deviceName,
|
|
90
|
+
deviceId,
|
|
91
|
+
ipAddress,
|
|
92
|
+
userAgent,
|
|
93
|
+
pluginName: 'magic-sessionmanager',
|
|
94
|
+
productName: 'Magic Session Manager - Premium Session Tracking',
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const data = await response.json();
|
|
99
|
+
|
|
100
|
+
if (data.success) {
|
|
101
|
+
strapi.log.info('[magic-sessionmanager] ✅ License created:', data.data.licenseKey);
|
|
102
|
+
return data.data;
|
|
103
|
+
} else {
|
|
104
|
+
strapi.log.error('[magic-sessionmanager] ❌ License creation failed:', data);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
strapi.log.error('[magic-sessionmanager] ❌ Error creating license:', error);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async verifyLicense(licenseKey, allowGracePeriod = false) {
|
|
114
|
+
try {
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
117
|
+
|
|
118
|
+
const licenseServerUrl = this.getLicenseServerUrl();
|
|
119
|
+
const response = await fetch(`${licenseServerUrl}/api/licenses/verify`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/json' },
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
licenseKey,
|
|
124
|
+
pluginName: 'magic-sessionmanager',
|
|
125
|
+
productName: 'Magic Session Manager - Premium Session Tracking',
|
|
126
|
+
}),
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
clearTimeout(timeoutId);
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
|
|
133
|
+
if (data.success && data.data) {
|
|
134
|
+
return { valid: true, data: data.data, gracePeriod: false };
|
|
135
|
+
} else {
|
|
136
|
+
return { valid: false, data: null };
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (allowGracePeriod) {
|
|
140
|
+
return { valid: true, data: null, gracePeriod: true };
|
|
141
|
+
}
|
|
142
|
+
return { valid: false, data: null };
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
async getLicenseByKey(licenseKey) {
|
|
147
|
+
try {
|
|
148
|
+
const licenseServerUrl = this.getLicenseServerUrl();
|
|
149
|
+
const url = `${licenseServerUrl}/api/licenses/key/${licenseKey}`;
|
|
150
|
+
|
|
151
|
+
strapi.log.debug(`[magic-sessionmanager/license-guard] Fetching license from: ${url}`);
|
|
152
|
+
|
|
153
|
+
const response = await fetch(url, {
|
|
154
|
+
method: 'GET',
|
|
155
|
+
headers: { 'Content-Type': 'application/json' },
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const data = await response.json();
|
|
159
|
+
|
|
160
|
+
if (data.success && data.data) {
|
|
161
|
+
strapi.log.debug(`[magic-sessionmanager/license-guard] License fetched: ${data.data.email}, featurePremium: ${data.data.featurePremium}`);
|
|
162
|
+
return data.data;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
strapi.log.warn(`[magic-sessionmanager/license-guard] License API returned no data`);
|
|
166
|
+
return null;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
strapi.log.error('[magic-sessionmanager/license-guard] Error fetching license by key:', error);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
async pingLicense(licenseKey) {
|
|
174
|
+
try {
|
|
175
|
+
const deviceId = this.generateDeviceId();
|
|
176
|
+
const deviceName = this.getDeviceName();
|
|
177
|
+
const ipAddress = this.getIpAddress();
|
|
178
|
+
const userAgent = this.getUserAgent();
|
|
179
|
+
|
|
180
|
+
const licenseServerUrl = this.getLicenseServerUrl();
|
|
181
|
+
const response = await fetch(`${licenseServerUrl}/api/licenses/ping`, {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: { 'Content-Type': 'application/json' },
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
licenseKey,
|
|
186
|
+
deviceId,
|
|
187
|
+
deviceName,
|
|
188
|
+
ipAddress,
|
|
189
|
+
userAgent,
|
|
190
|
+
pluginName: 'magic-sessionmanager',
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const data = await response.json();
|
|
195
|
+
return data.success ? data.data : null;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
async storeLicenseKey(licenseKey) {
|
|
202
|
+
const pluginStore = strapi.store({
|
|
203
|
+
type: 'plugin',
|
|
204
|
+
name: 'magic-sessionmanager'
|
|
205
|
+
});
|
|
206
|
+
await pluginStore.set({ key: 'licenseKey', value: licenseKey });
|
|
207
|
+
strapi.log.info(`[magic-sessionmanager] ✅ License key stored: ${licenseKey.substring(0, 8)}...`);
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
startPinging(licenseKey, intervalMinutes = 15) {
|
|
211
|
+
strapi.log.info(`[magic-sessionmanager] ⏰ Starting license pings every ${intervalMinutes} minutes`);
|
|
212
|
+
|
|
213
|
+
// Immediate ping
|
|
214
|
+
this.pingLicense(licenseKey);
|
|
215
|
+
|
|
216
|
+
const interval = setInterval(async () => {
|
|
217
|
+
try {
|
|
218
|
+
await this.pingLicense(licenseKey);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
strapi.log.error('[magic-sessionmanager] Ping error:', error);
|
|
221
|
+
}
|
|
222
|
+
}, intervalMinutes * 60 * 1000);
|
|
223
|
+
|
|
224
|
+
return interval;
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Initialize license guard
|
|
229
|
+
* Checks for existing license and starts pinging
|
|
230
|
+
*/
|
|
231
|
+
async initialize() {
|
|
232
|
+
try {
|
|
233
|
+
strapi.log.info('[magic-sessionmanager] 🔐 Initializing License Guard...');
|
|
234
|
+
|
|
235
|
+
// Check if license key exists in plugin store
|
|
236
|
+
const pluginStore = strapi.store({
|
|
237
|
+
type: 'plugin',
|
|
238
|
+
name: 'magic-sessionmanager'
|
|
239
|
+
});
|
|
240
|
+
const licenseKey = await pluginStore.get({ key: 'licenseKey' });
|
|
241
|
+
|
|
242
|
+
if (!licenseKey) {
|
|
243
|
+
strapi.log.info('[magic-sessionmanager] ℹ️ No license found - Running in demo mode');
|
|
244
|
+
return {
|
|
245
|
+
valid: false,
|
|
246
|
+
demo: true,
|
|
247
|
+
data: null,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check last validation timestamp for grace period
|
|
252
|
+
const lastValidated = await pluginStore.get({ key: 'lastValidated' });
|
|
253
|
+
const now = new Date();
|
|
254
|
+
const gracePeriodHours = 24;
|
|
255
|
+
let withinGracePeriod = false;
|
|
256
|
+
|
|
257
|
+
if (lastValidated) {
|
|
258
|
+
const lastValidatedDate = new Date(lastValidated);
|
|
259
|
+
const hoursSinceValidation = (now.getTime() - lastValidatedDate.getTime()) / (1000 * 60 * 60);
|
|
260
|
+
withinGracePeriod = hoursSinceValidation < gracePeriodHours;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Verify license (allow grace period if we have a last validation)
|
|
264
|
+
const verification = await this.verifyLicense(licenseKey, withinGracePeriod);
|
|
265
|
+
|
|
266
|
+
if (verification.valid) {
|
|
267
|
+
// Update last validated timestamp
|
|
268
|
+
await pluginStore.set({
|
|
269
|
+
key: 'lastValidated',
|
|
270
|
+
value: now.toISOString()
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Start automatic pinging
|
|
274
|
+
const pingInterval = this.startPinging(licenseKey, 15);
|
|
275
|
+
|
|
276
|
+
// Store interval globally so we can clean it up
|
|
277
|
+
strapi.licenseGuard = {
|
|
278
|
+
licenseKey,
|
|
279
|
+
pingInterval,
|
|
280
|
+
data: verification.data,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
valid: true,
|
|
285
|
+
demo: false,
|
|
286
|
+
data: verification.data,
|
|
287
|
+
gracePeriod: verification.gracePeriod || false,
|
|
288
|
+
};
|
|
289
|
+
} else {
|
|
290
|
+
strapi.log.error('[magic-sessionmanager] ❌ License validation failed');
|
|
291
|
+
return {
|
|
292
|
+
valid: false,
|
|
293
|
+
demo: true,
|
|
294
|
+
error: 'Invalid or expired license',
|
|
295
|
+
data: null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
strapi.log.error('[magic-sessionmanager] ❌ Error initializing License Guard:', error);
|
|
300
|
+
return {
|
|
301
|
+
valid: false,
|
|
302
|
+
demo: true,
|
|
303
|
+
error: error.message,
|
|
304
|
+
data: null,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
});
|