strapi-plugin-magic-mark 1.1.0
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/COPYRIGHT_NOTICE.txt +20 -0
- package/LICENSE +28 -0
- package/README.md +372 -0
- package/dist/_chunks/App-7ZH1Reka.mjs +1390 -0
- package/dist/_chunks/App-CMSut1pt.js +1392 -0
- package/dist/_chunks/de-Bag-366k.mjs +49 -0
- package/dist/_chunks/de-Dic_hhjg.js +49 -0
- package/dist/_chunks/en-C5BvHqNo.js +54 -0
- package/dist/_chunks/en-zokEerzt.mjs +54 -0
- package/dist/_chunks/es-BlSQpU1z.js +54 -0
- package/dist/_chunks/es-Br1ucP3h.mjs +54 -0
- package/dist/_chunks/fr-BHciYPYG.js +54 -0
- package/dist/_chunks/fr-Dzo3kt_q.mjs +54 -0
- package/dist/_chunks/index-B-Cc7QNW.mjs +322 -0
- package/dist/_chunks/index-B11QBtag.js +324 -0
- package/dist/_chunks/index-DYHEyGJr.mjs +2020 -0
- package/dist/_chunks/index-DkkmdRgb.js +2024 -0
- package/dist/_chunks/pt-DQoGyzyD.mjs +54 -0
- package/dist/_chunks/pt-Dawo5aUA.js +54 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/admin/src/components/AdvancedFilterButton.d.ts +3 -0
- package/dist/admin/src/components/AdvancedFilterModal.d.ts +21 -0
- package/dist/admin/src/components/CreateEditModal.d.ts +11 -0
- package/dist/admin/src/components/CreateViewModal.d.ts +9 -0
- package/dist/admin/src/components/CustomSelect.d.ts +13 -0
- package/dist/admin/src/components/FilterPreview.d.ts +6 -0
- package/dist/admin/src/components/HomePage.d.ts +0 -0
- package/dist/admin/src/components/Initializer.d.ts +6 -0
- package/dist/admin/src/components/LicenseGuard.d.ts +6 -0
- package/dist/admin/src/components/PluginIcon.d.ts +3 -0
- package/dist/admin/src/components/QueryBuilder.d.ts +23 -0
- package/dist/admin/src/components/SimpleAdvancedFilterModal.d.ts +15 -0
- package/dist/admin/src/components/ViewsListPopover.d.ts +2 -0
- package/dist/admin/src/components/ViewsWidget.d.ts +8 -0
- package/dist/admin/src/index.d.ts +15 -0
- package/dist/admin/src/pages/App.d.ts +3 -0
- package/dist/admin/src/pages/HomePage.d.ts +3 -0
- package/dist/admin/src/pages/HomePageModern.d.ts +3 -0
- package/dist/admin/src/pages/License/index.d.ts +3 -0
- package/dist/admin/src/permissions.d.ts +15 -0
- package/dist/admin/src/pluginId.d.ts +2 -0
- package/dist/admin/src/utils/queryGenerator.d.ts +28 -0
- package/dist/admin/src/utils/queryParser.d.ts +25 -0
- package/dist/admin/src/utils/queryToStructure.d.ts +20 -0
- package/dist/server/index.js +1309 -0
- package/dist/server/index.mjs +1307 -0
- package/dist/server/src/bootstrap.d.ts +5 -0
- package/dist/server/src/config/index.d.ts +5 -0
- package/dist/server/src/content-types/index.d.ts +82 -0
- package/dist/server/src/controllers/controller.d.ts +11 -0
- package/dist/server/src/controllers/index.d.ts +20 -0
- package/dist/server/src/controllers/license.d.ts +27 -0
- package/dist/server/src/destroy.d.ts +5 -0
- package/dist/server/src/index.d.ts +195 -0
- package/dist/server/src/middlewares/index.d.ts +4 -0
- package/dist/server/src/middlewares/license-check.d.ts +6 -0
- package/dist/server/src/policies/index.d.ts +4 -0
- package/dist/server/src/policies/license-check.d.ts +6 -0
- package/dist/server/src/register.d.ts +5 -0
- package/dist/server/src/routes/admin.d.ts +12 -0
- package/dist/server/src/routes/content-api.d.ts +5 -0
- package/dist/server/src/routes/index.d.ts +18 -0
- package/dist/server/src/services/index.d.ts +57 -0
- package/dist/server/src/services/license-guard.d.ts +103 -0
- package/dist/server/src/services/service.d.ts +33 -0
- package/package.json +108 -0
- package/strapi-server.js +4 -0
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import os from "os";
|
|
3
|
+
const magicMarkActions = {
|
|
4
|
+
actions: [
|
|
5
|
+
{
|
|
6
|
+
section: "plugins",
|
|
7
|
+
displayName: "Read",
|
|
8
|
+
uid: "settings.read",
|
|
9
|
+
subCategory: "Settings",
|
|
10
|
+
pluginName: "magic-mark"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
section: "plugins",
|
|
14
|
+
displayName: "Edit",
|
|
15
|
+
uid: "settings.update",
|
|
16
|
+
subCategory: "Settings",
|
|
17
|
+
pluginName: "magic-mark"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
const register = async ({ strapi }) => {
|
|
22
|
+
await strapi.admin.services.permission.actionProvider.registerMany(
|
|
23
|
+
magicMarkActions.actions
|
|
24
|
+
);
|
|
25
|
+
strapi.log.info("[Magic-Mark] Plugin registered successfully");
|
|
26
|
+
};
|
|
27
|
+
const bootstrap = ({ strapi }) => {
|
|
28
|
+
strapi.log.info("[Magic-Mark] Plugin bootstrapping...");
|
|
29
|
+
try {
|
|
30
|
+
const licenseGuardService2 = strapi.plugin("magic-mark").service("license-guard");
|
|
31
|
+
setTimeout(async () => {
|
|
32
|
+
const licenseStatus = await licenseGuardService2.initialize();
|
|
33
|
+
if (!licenseStatus.valid) {
|
|
34
|
+
strapi.log.error("╔════════════════════════════════════════════════════════════════╗");
|
|
35
|
+
strapi.log.error("║ ❌ MAGICMARK PLUGIN - NO VALID LICENSE ║");
|
|
36
|
+
strapi.log.error("║ ║");
|
|
37
|
+
strapi.log.error("║ This plugin requires a valid license to operate. ║");
|
|
38
|
+
strapi.log.error("║ Please activate your license via Admin UI: ║");
|
|
39
|
+
strapi.log.error("║ Go to Settings → MagicMark → License ║");
|
|
40
|
+
strapi.log.error("║ ║");
|
|
41
|
+
strapi.log.error("║ The plugin will run with limited functionality until ║");
|
|
42
|
+
strapi.log.error("║ a valid license is activated. ║");
|
|
43
|
+
strapi.log.error("╚════════════════════════════════════════════════════════════════╝");
|
|
44
|
+
} else if (licenseStatus.valid) {
|
|
45
|
+
const pluginStore = strapi.store({
|
|
46
|
+
type: "plugin",
|
|
47
|
+
name: "magic-mark"
|
|
48
|
+
});
|
|
49
|
+
const storedKey = await pluginStore.get({ key: "licenseKey" });
|
|
50
|
+
strapi.log.info("╔════════════════════════════════════════════════════════════════╗");
|
|
51
|
+
strapi.log.info("║ ✅ MAGICMARK PLUGIN LICENSE ACTIVE ║");
|
|
52
|
+
strapi.log.info("║ ║");
|
|
53
|
+
if (licenseStatus.data) {
|
|
54
|
+
strapi.log.info(`║ License: ${licenseStatus.data.licenseKey} ║`);
|
|
55
|
+
strapi.log.info(`║ User: ${licenseStatus.data.firstName} ${licenseStatus.data.lastName}`.padEnd(66) + "║");
|
|
56
|
+
strapi.log.info(`║ Email: ${licenseStatus.data.email}`.padEnd(66) + "║");
|
|
57
|
+
} else if (storedKey) {
|
|
58
|
+
strapi.log.info(`║ License: ${storedKey} (Offline Mode) ║`);
|
|
59
|
+
strapi.log.info(`║ Status: Grace Period Active ║`);
|
|
60
|
+
}
|
|
61
|
+
strapi.log.info("║ ║");
|
|
62
|
+
strapi.log.info("║ 🔄 Auto-pinging every 15 minutes ║");
|
|
63
|
+
strapi.log.info("╚════════════════════════════════════════════════════════════════╝");
|
|
64
|
+
}
|
|
65
|
+
}, 3e3);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
strapi.log.error("❌ Error initializing License Guard:", error);
|
|
68
|
+
}
|
|
69
|
+
strapi.log.info("[Magic-Mark] Plugin bootstrapped successfully");
|
|
70
|
+
};
|
|
71
|
+
const destroy = ({ strapi }) => {
|
|
72
|
+
try {
|
|
73
|
+
const licenseGuardService2 = strapi.plugin("magic-mark")?.service("license-guard");
|
|
74
|
+
if (licenseGuardService2) {
|
|
75
|
+
licenseGuardService2.cleanup();
|
|
76
|
+
strapi.log.info("✅ License Guard cleanup completed");
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
strapi.log.error("❌ Error during License Guard cleanup:", error);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const config = {
|
|
83
|
+
default: {},
|
|
84
|
+
validator: () => {
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const contentTypes = {
|
|
88
|
+
bookmark: {
|
|
89
|
+
schema: {
|
|
90
|
+
kind: "collectionType",
|
|
91
|
+
collectionName: "magic_bookmarks",
|
|
92
|
+
info: {
|
|
93
|
+
singularName: "bookmark",
|
|
94
|
+
pluralName: "bookmarks",
|
|
95
|
+
displayName: "Magic Bookmark",
|
|
96
|
+
description: "Saved filter views for quick access with path and query parameters"
|
|
97
|
+
},
|
|
98
|
+
options: {
|
|
99
|
+
draftAndPublish: false,
|
|
100
|
+
comment: "Magic Bookmarks - Saved Views with Filters"
|
|
101
|
+
},
|
|
102
|
+
pluginOptions: {
|
|
103
|
+
"content-manager": { visible: false },
|
|
104
|
+
"content-type-builder": { visible: false }
|
|
105
|
+
},
|
|
106
|
+
attributes: {
|
|
107
|
+
name: {
|
|
108
|
+
type: "string",
|
|
109
|
+
required: true,
|
|
110
|
+
maxLength: 100,
|
|
111
|
+
configurable: false
|
|
112
|
+
},
|
|
113
|
+
// Path: /content-manager/collection-types/api::article.article
|
|
114
|
+
path: {
|
|
115
|
+
type: "string",
|
|
116
|
+
required: true,
|
|
117
|
+
configurable: false,
|
|
118
|
+
description: "Content Manager path (e.g., /content-manager/collection-types/api::article.article)"
|
|
119
|
+
},
|
|
120
|
+
// Query: filters[status][$eq]=published&sort=createdAt:DESC
|
|
121
|
+
query: {
|
|
122
|
+
type: "text",
|
|
123
|
+
configurable: false,
|
|
124
|
+
description: "URL query parameters (e.g., filters[status][$eq]=published&sort=createdAt:DESC)"
|
|
125
|
+
},
|
|
126
|
+
emoji: {
|
|
127
|
+
type: "string",
|
|
128
|
+
default: "🔖",
|
|
129
|
+
configurable: false,
|
|
130
|
+
maxLength: 2
|
|
131
|
+
},
|
|
132
|
+
isPinned: {
|
|
133
|
+
type: "boolean",
|
|
134
|
+
default: false,
|
|
135
|
+
configurable: false
|
|
136
|
+
},
|
|
137
|
+
order: {
|
|
138
|
+
type: "integer",
|
|
139
|
+
default: 0,
|
|
140
|
+
configurable: false
|
|
141
|
+
},
|
|
142
|
+
description: {
|
|
143
|
+
type: "text",
|
|
144
|
+
configurable: false
|
|
145
|
+
},
|
|
146
|
+
// Array of role IDs that have access to this bookmark
|
|
147
|
+
sharedWithRoles: {
|
|
148
|
+
type: "json",
|
|
149
|
+
configurable: false,
|
|
150
|
+
description: "Array of role IDs that have access to this bookmark"
|
|
151
|
+
},
|
|
152
|
+
// Array of user IDs that have direct access to this bookmark
|
|
153
|
+
sharedWithUsers: {
|
|
154
|
+
type: "json",
|
|
155
|
+
configurable: false,
|
|
156
|
+
description: "Array of user IDs that have direct access to this bookmark"
|
|
157
|
+
},
|
|
158
|
+
// Whether this bookmark is public to all admin users
|
|
159
|
+
isPublic: {
|
|
160
|
+
type: "boolean",
|
|
161
|
+
default: false,
|
|
162
|
+
configurable: false,
|
|
163
|
+
description: "If true, all admin users can see this bookmark"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const bookmarkController = ({ strapi }) => ({
|
|
170
|
+
async getAll(ctx) {
|
|
171
|
+
try {
|
|
172
|
+
const user = ctx.state.user;
|
|
173
|
+
if (!user) {
|
|
174
|
+
return ctx.throw(401, "User not authenticated");
|
|
175
|
+
}
|
|
176
|
+
const bookmarks = await strapi.plugin("magic-mark").service("bookmarks").findAll(user.id);
|
|
177
|
+
ctx.body = {
|
|
178
|
+
data: bookmarks,
|
|
179
|
+
meta: {
|
|
180
|
+
count: bookmarks.length
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
} catch (error) {
|
|
184
|
+
ctx.throw(500, `Error fetching bookmarks: ${error}`);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
async create(ctx) {
|
|
188
|
+
try {
|
|
189
|
+
const user = ctx.state.user;
|
|
190
|
+
if (!user) {
|
|
191
|
+
return ctx.throw(401, "User not authenticated");
|
|
192
|
+
}
|
|
193
|
+
const { name, path, query, emoji, description, sharedWithRoles, sharedWithUsers, isPublic } = ctx.request.body;
|
|
194
|
+
if (!name || name.trim().length === 0) {
|
|
195
|
+
return ctx.throw(400, "Name is required");
|
|
196
|
+
}
|
|
197
|
+
if (!path || path.trim().length === 0) {
|
|
198
|
+
return ctx.throw(400, "Path is required");
|
|
199
|
+
}
|
|
200
|
+
const service = strapi.plugin("magic-mark").service("bookmarks");
|
|
201
|
+
const bookmark = await service.create(name, path, query, emoji, description, user.id, sharedWithRoles, sharedWithUsers, isPublic);
|
|
202
|
+
ctx.body = {
|
|
203
|
+
data: bookmark
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
ctx.throw(500, `Error creating bookmark: ${error}`);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
async update(ctx) {
|
|
210
|
+
try {
|
|
211
|
+
const user = ctx.state.user;
|
|
212
|
+
if (!user) {
|
|
213
|
+
return ctx.throw(401, "User not authenticated");
|
|
214
|
+
}
|
|
215
|
+
const { id } = ctx.params;
|
|
216
|
+
const { name, path, query, emoji, description, isPinned, order, sharedWithRoles, sharedWithUsers, isPublic } = ctx.request.body;
|
|
217
|
+
if (!name || name.trim().length === 0) {
|
|
218
|
+
return ctx.throw(400, "Name is required");
|
|
219
|
+
}
|
|
220
|
+
if (!path || path.trim().length === 0) {
|
|
221
|
+
return ctx.throw(400, "Path is required");
|
|
222
|
+
}
|
|
223
|
+
const bookmark = await strapi.plugin("magic-mark").service("bookmarks").update(id, { name, path, query, emoji, description, isPinned, order, sharedWithRoles, sharedWithUsers, isPublic }, user.id);
|
|
224
|
+
ctx.body = {
|
|
225
|
+
data: bookmark
|
|
226
|
+
};
|
|
227
|
+
} catch (error) {
|
|
228
|
+
ctx.throw(500, `Error updating bookmark: ${error}`);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
async delete(ctx) {
|
|
232
|
+
try {
|
|
233
|
+
const user = ctx.state.user;
|
|
234
|
+
if (!user) {
|
|
235
|
+
return ctx.throw(401, "User not authenticated");
|
|
236
|
+
}
|
|
237
|
+
const { id } = ctx.params;
|
|
238
|
+
await strapi.plugin("magic-mark").service("bookmarks").delete(id);
|
|
239
|
+
ctx.body = {
|
|
240
|
+
data: { success: true }
|
|
241
|
+
};
|
|
242
|
+
} catch (error) {
|
|
243
|
+
ctx.throw(500, `Error deleting bookmark: ${error}`);
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
async pin(ctx) {
|
|
247
|
+
try {
|
|
248
|
+
const user = ctx.state.user;
|
|
249
|
+
if (!user) {
|
|
250
|
+
return ctx.throw(401, "User not authenticated");
|
|
251
|
+
}
|
|
252
|
+
const { id } = ctx.params;
|
|
253
|
+
const { isPinned } = ctx.request.body;
|
|
254
|
+
const bookmark = await strapi.plugin("magic-mark").service("bookmarks").pin(id, isPinned, user.id);
|
|
255
|
+
ctx.body = {
|
|
256
|
+
data: bookmark
|
|
257
|
+
};
|
|
258
|
+
} catch (error) {
|
|
259
|
+
ctx.throw(500, `Error pinning bookmark: ${error}`);
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
async reorder(ctx) {
|
|
263
|
+
try {
|
|
264
|
+
const user = ctx.state.user;
|
|
265
|
+
if (!user) {
|
|
266
|
+
return ctx.throw(401, "User not authenticated");
|
|
267
|
+
}
|
|
268
|
+
const { bookmarkIds } = ctx.request.body;
|
|
269
|
+
if (!bookmarkIds || !Array.isArray(bookmarkIds)) {
|
|
270
|
+
return ctx.throw(400, "Bookmark IDs array is required");
|
|
271
|
+
}
|
|
272
|
+
const bookmarks = await strapi.plugin("magic-mark").service("bookmarks").reorder(bookmarkIds, user.id);
|
|
273
|
+
ctx.body = {
|
|
274
|
+
data: bookmarks
|
|
275
|
+
};
|
|
276
|
+
} catch (error) {
|
|
277
|
+
ctx.throw(500, `Error reordering bookmarks: ${error}`);
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
async getRoles(ctx) {
|
|
281
|
+
try {
|
|
282
|
+
const user = ctx.state.user;
|
|
283
|
+
if (!user) {
|
|
284
|
+
return ctx.throw(401, "User not authenticated");
|
|
285
|
+
}
|
|
286
|
+
const roles = await strapi.entityService.findMany(
|
|
287
|
+
"admin::role",
|
|
288
|
+
{
|
|
289
|
+
fields: ["id", "name", "code", "description", "createdAt", "updatedAt"],
|
|
290
|
+
filters: {},
|
|
291
|
+
sort: { name: "asc" },
|
|
292
|
+
populate: ["users"]
|
|
293
|
+
// Also get user count for each role
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
const rolesWithDetails = roles?.map((role) => ({
|
|
297
|
+
id: role.id,
|
|
298
|
+
name: role.name,
|
|
299
|
+
code: role.code,
|
|
300
|
+
description: role.description,
|
|
301
|
+
userCount: role.users?.length || 0,
|
|
302
|
+
isCustom: !["super_admin", "editor", "author"].includes(role.code)
|
|
303
|
+
// Mark custom roles
|
|
304
|
+
})) || [];
|
|
305
|
+
ctx.body = {
|
|
306
|
+
data: {
|
|
307
|
+
data: rolesWithDetails
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
} catch (error) {
|
|
311
|
+
ctx.throw(500, `Error fetching roles: ${error}`);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
async getUsers(ctx) {
|
|
315
|
+
try {
|
|
316
|
+
const user = ctx.state.user;
|
|
317
|
+
if (!user) {
|
|
318
|
+
return ctx.throw(401, "User not authenticated");
|
|
319
|
+
}
|
|
320
|
+
let users = [];
|
|
321
|
+
try {
|
|
322
|
+
const adminUserService = strapi.admin?.services?.user || strapi.service("admin::user");
|
|
323
|
+
if (adminUserService?.findPage) {
|
|
324
|
+
const results = await adminUserService.findPage({
|
|
325
|
+
pagination: {
|
|
326
|
+
page: 1,
|
|
327
|
+
pageSize: 100
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
users = results.results || [];
|
|
331
|
+
} else if (adminUserService?.find) {
|
|
332
|
+
users = await adminUserService.find();
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.log("[Magic-Mark] Admin service error, trying entity service:", err);
|
|
336
|
+
}
|
|
337
|
+
if (!users || users.length === 0) {
|
|
338
|
+
try {
|
|
339
|
+
users = await strapi.entityService.findMany("admin::user", {
|
|
340
|
+
limit: 100
|
|
341
|
+
});
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.log("[Magic-Mark] Entity service error:", err);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (!users || users.length === 0) {
|
|
347
|
+
try {
|
|
348
|
+
users = await strapi.db.query("admin::user").findMany({
|
|
349
|
+
limit: 100
|
|
350
|
+
});
|
|
351
|
+
} catch (err) {
|
|
352
|
+
console.log("[Magic-Mark] DB query error:", err);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const filteredUsers = users.filter((u) => u.id !== user.id);
|
|
356
|
+
console.log("[Magic-Mark] Total admin users found:", users.length);
|
|
357
|
+
console.log("[Magic-Mark] Filtered users (excluding current):", filteredUsers.map((u) => ({
|
|
358
|
+
id: u.id,
|
|
359
|
+
name: `${u.firstname} ${u.lastname}`,
|
|
360
|
+
email: u.email
|
|
361
|
+
})));
|
|
362
|
+
ctx.body = {
|
|
363
|
+
data: {
|
|
364
|
+
data: filteredUsers.map((u) => ({
|
|
365
|
+
id: u.id,
|
|
366
|
+
firstname: u.firstname || "",
|
|
367
|
+
lastname: u.lastname || "",
|
|
368
|
+
email: u.email,
|
|
369
|
+
username: u.username || u.email
|
|
370
|
+
}))
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
} catch (error) {
|
|
374
|
+
ctx.throw(500, `Error fetching users: ${error}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
const license = ({ strapi }) => ({
|
|
379
|
+
/**
|
|
380
|
+
* Auto-create license with logged-in admin user data
|
|
381
|
+
*/
|
|
382
|
+
async autoCreate(ctx) {
|
|
383
|
+
try {
|
|
384
|
+
const adminUser = ctx.state.user;
|
|
385
|
+
if (!adminUser) {
|
|
386
|
+
return ctx.unauthorized("No admin user logged in");
|
|
387
|
+
}
|
|
388
|
+
const licenseGuard = strapi.plugin("magic-mark").service("license-guard");
|
|
389
|
+
const license2 = await licenseGuard.createLicense({
|
|
390
|
+
email: adminUser.email,
|
|
391
|
+
firstName: adminUser.firstname || "Admin",
|
|
392
|
+
lastName: adminUser.lastname || "User"
|
|
393
|
+
});
|
|
394
|
+
if (!license2) {
|
|
395
|
+
return ctx.badRequest("Failed to create license");
|
|
396
|
+
}
|
|
397
|
+
await licenseGuard.storeLicenseKey(license2.licenseKey);
|
|
398
|
+
const pingInterval = licenseGuard.startPinging(license2.licenseKey, 15);
|
|
399
|
+
strapi.licenseGuard = {
|
|
400
|
+
licenseKey: license2.licenseKey,
|
|
401
|
+
pingInterval,
|
|
402
|
+
data: license2
|
|
403
|
+
};
|
|
404
|
+
return ctx.send({
|
|
405
|
+
success: true,
|
|
406
|
+
message: "License automatically created and activated",
|
|
407
|
+
data: license2
|
|
408
|
+
});
|
|
409
|
+
} catch (error) {
|
|
410
|
+
strapi.log.error("Error auto-creating license:", error);
|
|
411
|
+
return ctx.badRequest("Error creating license");
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
/**
|
|
415
|
+
* Get current license status
|
|
416
|
+
*/
|
|
417
|
+
async getStatus(ctx) {
|
|
418
|
+
try {
|
|
419
|
+
const licenseGuard = strapi.plugin("magic-mark").service("license-guard");
|
|
420
|
+
const pluginStore = strapi.store({
|
|
421
|
+
type: "plugin",
|
|
422
|
+
name: "magic-mark"
|
|
423
|
+
});
|
|
424
|
+
const licenseKey = await pluginStore.get({ key: "licenseKey" });
|
|
425
|
+
if (!licenseKey) {
|
|
426
|
+
return ctx.send({
|
|
427
|
+
success: false,
|
|
428
|
+
demo: true,
|
|
429
|
+
valid: false,
|
|
430
|
+
message: "No license found. Running in demo mode."
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const verification = await licenseGuard.verifyLicense(licenseKey);
|
|
434
|
+
const license2 = await licenseGuard.getLicenseByKey(licenseKey);
|
|
435
|
+
return ctx.send({
|
|
436
|
+
success: true,
|
|
437
|
+
valid: verification.valid,
|
|
438
|
+
demo: false,
|
|
439
|
+
data: {
|
|
440
|
+
licenseKey,
|
|
441
|
+
email: license2?.email || null,
|
|
442
|
+
firstName: license2?.firstName || null,
|
|
443
|
+
lastName: license2?.lastName || null,
|
|
444
|
+
isActive: license2?.isActive || false,
|
|
445
|
+
isExpired: license2?.isExpired || false,
|
|
446
|
+
isOnline: license2?.isOnline || false,
|
|
447
|
+
expiresAt: license2?.expiresAt,
|
|
448
|
+
lastPingAt: license2?.lastPingAt,
|
|
449
|
+
deviceName: license2?.deviceName,
|
|
450
|
+
deviceId: license2?.deviceId,
|
|
451
|
+
ipAddress: license2?.ipAddress,
|
|
452
|
+
features: {
|
|
453
|
+
premium: license2?.featurePremium || false,
|
|
454
|
+
advanced: license2?.featureAdvanced || false,
|
|
455
|
+
enterprise: license2?.featureEnterprise || false,
|
|
456
|
+
custom: license2?.featureCustom || false
|
|
457
|
+
},
|
|
458
|
+
maxDevices: license2?.maxDevices || 1,
|
|
459
|
+
currentDevices: license2?.currentDevices || 0
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
} catch (error) {
|
|
463
|
+
strapi.log.error("Error getting license status:", error);
|
|
464
|
+
return ctx.badRequest("Error getting license status");
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
/**
|
|
468
|
+
* Create and activate a new license
|
|
469
|
+
*/
|
|
470
|
+
async createAndActivate(ctx) {
|
|
471
|
+
try {
|
|
472
|
+
const { email, firstName, lastName } = ctx.request.body;
|
|
473
|
+
if (!email || !firstName || !lastName) {
|
|
474
|
+
return ctx.badRequest("Email, firstName, and lastName are required");
|
|
475
|
+
}
|
|
476
|
+
const licenseGuard = strapi.plugin("magic-mark").service("license-guard");
|
|
477
|
+
const license2 = await licenseGuard.createLicense({ email, firstName, lastName });
|
|
478
|
+
if (!license2) {
|
|
479
|
+
return ctx.badRequest("Failed to create license");
|
|
480
|
+
}
|
|
481
|
+
await licenseGuard.storeLicenseKey(license2.licenseKey);
|
|
482
|
+
const pingInterval = licenseGuard.startPinging(license2.licenseKey, 15);
|
|
483
|
+
strapi.licenseGuard = {
|
|
484
|
+
licenseKey: license2.licenseKey,
|
|
485
|
+
pingInterval,
|
|
486
|
+
data: license2
|
|
487
|
+
};
|
|
488
|
+
return ctx.send({
|
|
489
|
+
success: true,
|
|
490
|
+
message: "License created and activated successfully",
|
|
491
|
+
data: license2
|
|
492
|
+
});
|
|
493
|
+
} catch (error) {
|
|
494
|
+
strapi.log.error("Error creating license:", error);
|
|
495
|
+
return ctx.badRequest("Error creating license");
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
/**
|
|
499
|
+
* Manually ping the current license
|
|
500
|
+
*/
|
|
501
|
+
async ping(ctx) {
|
|
502
|
+
try {
|
|
503
|
+
const pluginStore = strapi.store({
|
|
504
|
+
type: "plugin",
|
|
505
|
+
name: "magic-mark"
|
|
506
|
+
});
|
|
507
|
+
const licenseKey = await pluginStore.get({ key: "licenseKey" });
|
|
508
|
+
if (!licenseKey) {
|
|
509
|
+
return ctx.badRequest("No license key found");
|
|
510
|
+
}
|
|
511
|
+
const licenseGuard = strapi.plugin("magic-mark").service("license-guard");
|
|
512
|
+
const pingResult = await licenseGuard.pingLicense(licenseKey);
|
|
513
|
+
if (!pingResult) {
|
|
514
|
+
return ctx.badRequest("Ping failed");
|
|
515
|
+
}
|
|
516
|
+
return ctx.send({
|
|
517
|
+
success: true,
|
|
518
|
+
message: "License pinged successfully",
|
|
519
|
+
data: pingResult
|
|
520
|
+
});
|
|
521
|
+
} catch (error) {
|
|
522
|
+
strapi.log.error("Error pinging license:", error);
|
|
523
|
+
return ctx.badRequest("Error pinging license");
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
/**
|
|
527
|
+
* Store and validate an existing license key
|
|
528
|
+
*/
|
|
529
|
+
async storeKey(ctx) {
|
|
530
|
+
try {
|
|
531
|
+
const { licenseKey, email } = ctx.request.body;
|
|
532
|
+
if (!licenseKey || !licenseKey.trim()) {
|
|
533
|
+
return ctx.badRequest("License key is required");
|
|
534
|
+
}
|
|
535
|
+
if (!email || !email.trim()) {
|
|
536
|
+
return ctx.badRequest("Email address is required");
|
|
537
|
+
}
|
|
538
|
+
const trimmedKey = licenseKey.trim();
|
|
539
|
+
const trimmedEmail = email.trim().toLowerCase();
|
|
540
|
+
const licenseGuard = strapi.plugin("magic-mark").service("license-guard");
|
|
541
|
+
const verification = await licenseGuard.verifyLicense(trimmedKey);
|
|
542
|
+
if (!verification.valid) {
|
|
543
|
+
strapi.log.warn(`⚠️ Invalid license key attempted: ${trimmedKey.substring(0, 8)}...`);
|
|
544
|
+
return ctx.badRequest("Invalid or expired license key");
|
|
545
|
+
}
|
|
546
|
+
const license2 = await licenseGuard.getLicenseByKey(trimmedKey);
|
|
547
|
+
if (!license2) {
|
|
548
|
+
strapi.log.warn(`⚠️ License not found in database: ${trimmedKey.substring(0, 8)}...`);
|
|
549
|
+
return ctx.badRequest("License not found");
|
|
550
|
+
}
|
|
551
|
+
if (license2.email.toLowerCase() !== trimmedEmail) {
|
|
552
|
+
strapi.log.warn(`⚠️ Email mismatch for license key: ${trimmedKey.substring(0, 8)}... (Attempted: ${trimmedEmail})`);
|
|
553
|
+
return ctx.badRequest("Email address does not match this license key");
|
|
554
|
+
}
|
|
555
|
+
await licenseGuard.storeLicenseKey(trimmedKey);
|
|
556
|
+
const pingInterval = licenseGuard.startPinging(trimmedKey, 15);
|
|
557
|
+
strapi.licenseGuard = {
|
|
558
|
+
licenseKey: trimmedKey,
|
|
559
|
+
pingInterval,
|
|
560
|
+
data: verification.data
|
|
561
|
+
};
|
|
562
|
+
strapi.log.info(`✅ Existing license key validated and stored: ${trimmedKey.substring(0, 8)}... (Email: ${trimmedEmail})`);
|
|
563
|
+
return ctx.send({
|
|
564
|
+
success: true,
|
|
565
|
+
message: "License key validated and activated successfully",
|
|
566
|
+
data: verification.data
|
|
567
|
+
});
|
|
568
|
+
} catch (error) {
|
|
569
|
+
strapi.log.error("Error storing license key:", error);
|
|
570
|
+
return ctx.badRequest("Error validating license key");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
const controllers = {
|
|
575
|
+
bookmarks: bookmarkController,
|
|
576
|
+
license
|
|
577
|
+
};
|
|
578
|
+
const admin = {
|
|
579
|
+
type: "admin",
|
|
580
|
+
routes: [
|
|
581
|
+
// Get available roles for sharing
|
|
582
|
+
{
|
|
583
|
+
method: "GET",
|
|
584
|
+
path: "/roles",
|
|
585
|
+
handler: "bookmarks.getRoles",
|
|
586
|
+
config: {
|
|
587
|
+
policies: []
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
// Get available users for sharing
|
|
591
|
+
{
|
|
592
|
+
method: "GET",
|
|
593
|
+
path: "/users",
|
|
594
|
+
handler: "bookmarks.getUsers",
|
|
595
|
+
config: {
|
|
596
|
+
policies: []
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
// Get all bookmarks
|
|
600
|
+
{
|
|
601
|
+
method: "GET",
|
|
602
|
+
path: "/bookmarks",
|
|
603
|
+
handler: "bookmarks.getAll",
|
|
604
|
+
config: {
|
|
605
|
+
policies: []
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
// Reorder bookmarks (SPECIFIC - must come before :id routes)
|
|
609
|
+
{
|
|
610
|
+
method: "POST",
|
|
611
|
+
path: "/bookmarks/reorder",
|
|
612
|
+
handler: "bookmarks.reorder",
|
|
613
|
+
config: {
|
|
614
|
+
policies: []
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
// Pin/Unpin bookmark (SPECIFIC - must come before generic :id)
|
|
618
|
+
{
|
|
619
|
+
method: "POST",
|
|
620
|
+
path: "/bookmarks/:id/pin",
|
|
621
|
+
handler: "bookmarks.pin",
|
|
622
|
+
config: {
|
|
623
|
+
policies: []
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
// Create bookmark
|
|
627
|
+
{
|
|
628
|
+
method: "POST",
|
|
629
|
+
path: "/bookmarks",
|
|
630
|
+
handler: "bookmarks.create",
|
|
631
|
+
config: {
|
|
632
|
+
policies: []
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
// Update bookmark
|
|
636
|
+
{
|
|
637
|
+
method: "PUT",
|
|
638
|
+
path: "/bookmarks/:id",
|
|
639
|
+
handler: "bookmarks.update",
|
|
640
|
+
config: {
|
|
641
|
+
policies: []
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
// Delete bookmark
|
|
645
|
+
{
|
|
646
|
+
method: "DELETE",
|
|
647
|
+
path: "/bookmarks/:id",
|
|
648
|
+
handler: "bookmarks.delete",
|
|
649
|
+
config: {
|
|
650
|
+
policies: []
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
// License Management
|
|
654
|
+
{
|
|
655
|
+
method: "GET",
|
|
656
|
+
path: "/license/status",
|
|
657
|
+
handler: "license.getStatus",
|
|
658
|
+
config: {
|
|
659
|
+
policies: []
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
method: "POST",
|
|
664
|
+
path: "/license/auto-create",
|
|
665
|
+
handler: "license.autoCreate",
|
|
666
|
+
config: {
|
|
667
|
+
policies: []
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
method: "POST",
|
|
672
|
+
path: "/license/create",
|
|
673
|
+
handler: "license.createAndActivate",
|
|
674
|
+
config: {
|
|
675
|
+
policies: []
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
method: "POST",
|
|
680
|
+
path: "/license/ping",
|
|
681
|
+
handler: "license.ping",
|
|
682
|
+
config: {
|
|
683
|
+
policies: []
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
method: "POST",
|
|
688
|
+
path: "/license/store-key",
|
|
689
|
+
handler: "license.storeKey",
|
|
690
|
+
config: {
|
|
691
|
+
policies: []
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
]
|
|
695
|
+
};
|
|
696
|
+
const contentApi = {
|
|
697
|
+
type: "content-api",
|
|
698
|
+
routes: []
|
|
699
|
+
};
|
|
700
|
+
const routes = {
|
|
701
|
+
admin,
|
|
702
|
+
"content-api": contentApi
|
|
703
|
+
};
|
|
704
|
+
const licenseCheck$1 = (config2, { strapi }) => {
|
|
705
|
+
return async (ctx, next) => {
|
|
706
|
+
if (ctx.request.url.includes("/magic-mark/license/")) {
|
|
707
|
+
return await next();
|
|
708
|
+
}
|
|
709
|
+
if (ctx.request.url.includes("/admin/login")) {
|
|
710
|
+
return await next();
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const licenseGuard = strapi.plugin("magic-mark")?.service("license-guard");
|
|
714
|
+
if (!licenseGuard) {
|
|
715
|
+
return await next();
|
|
716
|
+
}
|
|
717
|
+
const pluginStore = strapi.store({
|
|
718
|
+
type: "plugin",
|
|
719
|
+
name: "magic-mark"
|
|
720
|
+
});
|
|
721
|
+
const licenseKey = await pluginStore.get({ key: "licenseKey" });
|
|
722
|
+
if (!licenseKey) {
|
|
723
|
+
strapi.log.warn("⚠️ MagicMark: No license key found");
|
|
724
|
+
return await next();
|
|
725
|
+
}
|
|
726
|
+
const verification = await licenseGuard.verifyLicense(licenseKey, true);
|
|
727
|
+
if (!verification.valid && !verification.gracePeriod) {
|
|
728
|
+
strapi.log.warn("⚠️ MagicMark: Invalid license detected");
|
|
729
|
+
}
|
|
730
|
+
return await next();
|
|
731
|
+
} catch (error) {
|
|
732
|
+
strapi.log.error("Error in license check middleware:", error);
|
|
733
|
+
return await next();
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
};
|
|
737
|
+
const middlewares = {
|
|
738
|
+
"license-check": licenseCheck$1
|
|
739
|
+
};
|
|
740
|
+
const licenseCheck = async (policyContext, config2, { strapi }) => {
|
|
741
|
+
try {
|
|
742
|
+
const licenseGuard = strapi.plugin("magic-mark").service("license-guard");
|
|
743
|
+
const pluginStore = strapi.store({ type: "plugin", name: "magic-mark" });
|
|
744
|
+
const licenseKey = await pluginStore.get({ key: "licenseKey" });
|
|
745
|
+
if (!licenseKey) {
|
|
746
|
+
strapi.log.warn("⚠️ API access denied: No license key found");
|
|
747
|
+
return policyContext.unauthorized("No license found. Please activate the plugin first.");
|
|
748
|
+
}
|
|
749
|
+
const verification = await licenseGuard.verifyLicense(licenseKey);
|
|
750
|
+
if (!verification.valid) {
|
|
751
|
+
strapi.log.warn("⚠️ API access denied: Invalid license");
|
|
752
|
+
return policyContext.unauthorized("Invalid or expired license. Please check your license status.");
|
|
753
|
+
}
|
|
754
|
+
const license2 = await licenseGuard.getLicenseByKey(licenseKey);
|
|
755
|
+
if (!license2) {
|
|
756
|
+
strapi.log.warn("⚠️ API access denied: License not found in database");
|
|
757
|
+
return policyContext.unauthorized("License not found. Please contact support.");
|
|
758
|
+
}
|
|
759
|
+
if (!license2.isActive) {
|
|
760
|
+
strapi.log.warn("⚠️ API access denied: License is inactive");
|
|
761
|
+
return policyContext.unauthorized("License is inactive. Please activate your license.");
|
|
762
|
+
}
|
|
763
|
+
if (license2.isExpired) {
|
|
764
|
+
strapi.log.warn("⚠️ API access denied: License has expired");
|
|
765
|
+
return policyContext.unauthorized("License has expired. Please renew your license.");
|
|
766
|
+
}
|
|
767
|
+
return true;
|
|
768
|
+
} catch (error) {
|
|
769
|
+
strapi.log.error("Error checking license:", error);
|
|
770
|
+
return policyContext.unauthorized("Error verifying license. Please try again.");
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
const policies = {
|
|
774
|
+
"license-check": licenseCheck
|
|
775
|
+
};
|
|
776
|
+
const bookmarkService = ({ strapi }) => ({
|
|
777
|
+
async findAll(userId) {
|
|
778
|
+
try {
|
|
779
|
+
const user = await strapi.entityService.findOne(
|
|
780
|
+
"admin::user",
|
|
781
|
+
userId,
|
|
782
|
+
{
|
|
783
|
+
populate: ["roles"]
|
|
784
|
+
}
|
|
785
|
+
);
|
|
786
|
+
const userRoleIds = user?.roles?.map((role) => role.id) || [];
|
|
787
|
+
const allBookmarks = await strapi.entityService.findMany(
|
|
788
|
+
"plugin::magic-mark.bookmark",
|
|
789
|
+
{
|
|
790
|
+
sort: [
|
|
791
|
+
{ isPinned: "desc" },
|
|
792
|
+
{ order: "asc" }
|
|
793
|
+
],
|
|
794
|
+
populate: ["createdBy"]
|
|
795
|
+
}
|
|
796
|
+
);
|
|
797
|
+
const accessibleBookmarks = allBookmarks?.filter((bookmark) => {
|
|
798
|
+
if (bookmark.createdBy?.id === userId) {
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
if (bookmark.isPublic) {
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
if (bookmark.sharedWithUsers && Array.isArray(bookmark.sharedWithUsers)) {
|
|
805
|
+
if (bookmark.sharedWithUsers.includes(userId)) {
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (bookmark.sharedWithRoles && Array.isArray(bookmark.sharedWithRoles)) {
|
|
810
|
+
return bookmark.sharedWithRoles.some(
|
|
811
|
+
(roleId) => userRoleIds.includes(roleId)
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
return false;
|
|
815
|
+
}) || [];
|
|
816
|
+
return accessibleBookmarks;
|
|
817
|
+
} catch (error) {
|
|
818
|
+
strapi.log.error("[magic-mark] Error finding bookmarks:", error);
|
|
819
|
+
throw error;
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
async create(name, path, query, emoji, description, userId, sharedWithRoles, sharedWithUsers, isPublic) {
|
|
823
|
+
try {
|
|
824
|
+
const bookmarks = await strapi.entityService.findMany(
|
|
825
|
+
"plugin::magic-mark.bookmark",
|
|
826
|
+
{
|
|
827
|
+
sort: [{ order: "desc" }],
|
|
828
|
+
limit: 1
|
|
829
|
+
}
|
|
830
|
+
);
|
|
831
|
+
const maxOrder = bookmarks && bookmarks.length > 0 ? bookmarks[0].order : 0;
|
|
832
|
+
const bookmark = await strapi.entityService.create(
|
|
833
|
+
"plugin::magic-mark.bookmark",
|
|
834
|
+
{
|
|
835
|
+
data: {
|
|
836
|
+
name,
|
|
837
|
+
path,
|
|
838
|
+
query: query || "",
|
|
839
|
+
emoji: emoji || "🔖",
|
|
840
|
+
description,
|
|
841
|
+
isPinned: false,
|
|
842
|
+
order: (maxOrder || 0) + 1,
|
|
843
|
+
createdBy: userId,
|
|
844
|
+
sharedWithRoles: sharedWithRoles || [],
|
|
845
|
+
sharedWithUsers: sharedWithUsers || [],
|
|
846
|
+
isPublic: isPublic || false
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
);
|
|
850
|
+
return bookmark;
|
|
851
|
+
} catch (error) {
|
|
852
|
+
strapi.log.error("[magic-mark] Error creating bookmark:", error);
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
},
|
|
856
|
+
async update(id, data, userId) {
|
|
857
|
+
try {
|
|
858
|
+
const existingBookmark = await strapi.entityService.findOne(
|
|
859
|
+
"plugin::magic-mark.bookmark",
|
|
860
|
+
id,
|
|
861
|
+
{
|
|
862
|
+
populate: ["createdBy"]
|
|
863
|
+
}
|
|
864
|
+
);
|
|
865
|
+
if (!existingBookmark || existingBookmark.createdBy?.id !== userId) {
|
|
866
|
+
throw new Error("Unauthorized: You can only edit your own bookmarks");
|
|
867
|
+
}
|
|
868
|
+
const bookmark = await strapi.entityService.update(
|
|
869
|
+
"plugin::magic-mark.bookmark",
|
|
870
|
+
id,
|
|
871
|
+
{
|
|
872
|
+
data: {
|
|
873
|
+
name: data.name,
|
|
874
|
+
path: data.path,
|
|
875
|
+
query: data.query || "",
|
|
876
|
+
emoji: data.emoji || "🔖",
|
|
877
|
+
description: data.description,
|
|
878
|
+
isPinned: data.isPinned,
|
|
879
|
+
order: data.order,
|
|
880
|
+
sharedWithRoles: data.sharedWithRoles,
|
|
881
|
+
sharedWithUsers: data.sharedWithUsers,
|
|
882
|
+
isPublic: data.isPublic,
|
|
883
|
+
updatedBy: userId
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
);
|
|
887
|
+
return bookmark;
|
|
888
|
+
} catch (error) {
|
|
889
|
+
strapi.log.error("[magic-mark] Error updating bookmark:", error);
|
|
890
|
+
throw error;
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
async delete(id) {
|
|
894
|
+
try {
|
|
895
|
+
return await strapi.entityService.delete(
|
|
896
|
+
"plugin::magic-mark.bookmark",
|
|
897
|
+
id
|
|
898
|
+
);
|
|
899
|
+
} catch (error) {
|
|
900
|
+
strapi.log.error("[magic-mark] Error deleting bookmark:", error);
|
|
901
|
+
throw error;
|
|
902
|
+
}
|
|
903
|
+
},
|
|
904
|
+
async pin(id, isPinned, userId) {
|
|
905
|
+
try {
|
|
906
|
+
const bookmark = await strapi.entityService.update(
|
|
907
|
+
"plugin::magic-mark.bookmark",
|
|
908
|
+
id,
|
|
909
|
+
{
|
|
910
|
+
data: {
|
|
911
|
+
isPinned,
|
|
912
|
+
updatedBy: userId
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
);
|
|
916
|
+
return bookmark;
|
|
917
|
+
} catch (error) {
|
|
918
|
+
strapi.log.error("[magic-mark] Error pinning bookmark:", error);
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
},
|
|
922
|
+
async reorder(bookmarkIds, userId) {
|
|
923
|
+
try {
|
|
924
|
+
const updates = bookmarkIds.map(
|
|
925
|
+
(id, index2) => strapi.entityService.update(
|
|
926
|
+
"plugin::magic-mark.bookmark",
|
|
927
|
+
id,
|
|
928
|
+
{
|
|
929
|
+
data: {
|
|
930
|
+
order: index2,
|
|
931
|
+
updatedBy: userId
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
)
|
|
935
|
+
);
|
|
936
|
+
return Promise.all(updates);
|
|
937
|
+
} catch (error) {
|
|
938
|
+
strapi.log.error("[magic-mark] Error reordering bookmarks:", error);
|
|
939
|
+
throw error;
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
validateUrl(url) {
|
|
943
|
+
try {
|
|
944
|
+
new URL(url);
|
|
945
|
+
return true;
|
|
946
|
+
} catch {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
const LICENSE_SERVER_URL = "https://magicapi.fitlex.me";
|
|
952
|
+
const licenseGuardService = ({ strapi }) => ({
|
|
953
|
+
/**
|
|
954
|
+
* Get license server URL (hardcoded and immutable for security)
|
|
955
|
+
* @returns The fixed license server URL - cannot be overridden
|
|
956
|
+
*/
|
|
957
|
+
getLicenseServerUrl() {
|
|
958
|
+
return LICENSE_SERVER_URL;
|
|
959
|
+
},
|
|
960
|
+
/**
|
|
961
|
+
* Generate a unique device ID based on machine identifiers
|
|
962
|
+
*/
|
|
963
|
+
generateDeviceId() {
|
|
964
|
+
try {
|
|
965
|
+
const networkInterfaces = os.networkInterfaces();
|
|
966
|
+
const macAddresses = [];
|
|
967
|
+
Object.values(networkInterfaces).forEach((interfaces) => {
|
|
968
|
+
interfaces?.forEach((iface) => {
|
|
969
|
+
if (iface.mac && iface.mac !== "00:00:00:00:00:00") {
|
|
970
|
+
macAddresses.push(iface.mac);
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
const identifier = `${macAddresses.join("-")}-${os.hostname()}`;
|
|
975
|
+
return crypto.createHash("sha256").update(identifier).digest("hex").substring(0, 32);
|
|
976
|
+
} catch (error) {
|
|
977
|
+
strapi.log.error("Error generating device ID:", error);
|
|
978
|
+
return crypto.randomBytes(16).toString("hex");
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
/**
|
|
982
|
+
* Get device name
|
|
983
|
+
*/
|
|
984
|
+
getDeviceName() {
|
|
985
|
+
try {
|
|
986
|
+
return os.hostname() || "Unknown Device";
|
|
987
|
+
} catch (error) {
|
|
988
|
+
return "Unknown Device";
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
/**
|
|
992
|
+
* Get server IP address
|
|
993
|
+
*/
|
|
994
|
+
getIpAddress() {
|
|
995
|
+
try {
|
|
996
|
+
const networkInterfaces = os.networkInterfaces();
|
|
997
|
+
for (const name of Object.keys(networkInterfaces)) {
|
|
998
|
+
const interfaces = networkInterfaces[name];
|
|
999
|
+
if (interfaces) {
|
|
1000
|
+
for (const iface of interfaces) {
|
|
1001
|
+
if (iface.family === "IPv4" && !iface.internal) {
|
|
1002
|
+
return iface.address;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return "127.0.0.1";
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
return "127.0.0.1";
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
1012
|
+
/**
|
|
1013
|
+
* Get user agent (server context)
|
|
1014
|
+
*/
|
|
1015
|
+
getUserAgent() {
|
|
1016
|
+
return `Strapi/${strapi.config.get("info.strapi") || "5.0.0"} Node/${process.version} ${os.platform()}/${os.release()}`;
|
|
1017
|
+
},
|
|
1018
|
+
/**
|
|
1019
|
+
* Create a license
|
|
1020
|
+
*/
|
|
1021
|
+
async createLicense({ email, firstName, lastName }) {
|
|
1022
|
+
try {
|
|
1023
|
+
const deviceId = this.generateDeviceId();
|
|
1024
|
+
const deviceName = this.getDeviceName();
|
|
1025
|
+
const ipAddress = this.getIpAddress();
|
|
1026
|
+
const userAgent = this.getUserAgent();
|
|
1027
|
+
const licenseServerUrl = this.getLicenseServerUrl();
|
|
1028
|
+
const response = await fetch(`${licenseServerUrl}/api/licenses/create`, {
|
|
1029
|
+
method: "POST",
|
|
1030
|
+
headers: {
|
|
1031
|
+
"Content-Type": "application/json"
|
|
1032
|
+
},
|
|
1033
|
+
body: JSON.stringify({
|
|
1034
|
+
email,
|
|
1035
|
+
firstName,
|
|
1036
|
+
lastName,
|
|
1037
|
+
deviceName,
|
|
1038
|
+
deviceId,
|
|
1039
|
+
ipAddress,
|
|
1040
|
+
userAgent,
|
|
1041
|
+
pluginName: "magic-mark",
|
|
1042
|
+
productName: "MagicMark - Advanced Query Builder"
|
|
1043
|
+
})
|
|
1044
|
+
});
|
|
1045
|
+
const data = await response.json();
|
|
1046
|
+
if (data.success) {
|
|
1047
|
+
strapi.log.info("✅ License created successfully:", data.data.licenseKey);
|
|
1048
|
+
return data.data;
|
|
1049
|
+
} else {
|
|
1050
|
+
strapi.log.error("❌ License creation failed:", data);
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
strapi.log.error("❌ Error creating license:", error);
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
/**
|
|
1059
|
+
* Verify a license (with grace period support)
|
|
1060
|
+
*/
|
|
1061
|
+
async verifyLicense(licenseKey, allowGracePeriod = false) {
|
|
1062
|
+
try {
|
|
1063
|
+
const controller = new AbortController();
|
|
1064
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
1065
|
+
const licenseServerUrl = this.getLicenseServerUrl();
|
|
1066
|
+
const response = await fetch(`${licenseServerUrl}/api/licenses/verify`, {
|
|
1067
|
+
method: "POST",
|
|
1068
|
+
headers: {
|
|
1069
|
+
"Content-Type": "application/json"
|
|
1070
|
+
},
|
|
1071
|
+
body: JSON.stringify({
|
|
1072
|
+
licenseKey,
|
|
1073
|
+
pluginName: "magic-mark",
|
|
1074
|
+
productName: "MagicMark - Advanced Query Builder"
|
|
1075
|
+
}),
|
|
1076
|
+
signal: controller.signal
|
|
1077
|
+
});
|
|
1078
|
+
clearTimeout(timeoutId);
|
|
1079
|
+
const data = await response.json();
|
|
1080
|
+
if (data.success) {
|
|
1081
|
+
const isValid = data.data.isActive && !data.data.isExpired;
|
|
1082
|
+
const statusInfo = data.data.isExpired ? "EXPIRED" : data.data.isActive ? "ACTIVE" : "INACTIVE";
|
|
1083
|
+
strapi.log.info(`✅ License verified online: ${statusInfo} (Key: ${licenseKey?.substring(0, 8)}...)`);
|
|
1084
|
+
if (isValid) {
|
|
1085
|
+
const pluginStore = strapi.store({
|
|
1086
|
+
type: "plugin",
|
|
1087
|
+
name: "magic-mark"
|
|
1088
|
+
});
|
|
1089
|
+
await pluginStore.set({ key: "lastValidated", value: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1090
|
+
}
|
|
1091
|
+
return {
|
|
1092
|
+
valid: isValid,
|
|
1093
|
+
data: data.data
|
|
1094
|
+
};
|
|
1095
|
+
} else {
|
|
1096
|
+
strapi.log.warn(`⚠️ License verification failed: ${data.message || "Unknown error"} (Key: ${licenseKey?.substring(0, 8)}...)`);
|
|
1097
|
+
return { valid: false, data: null };
|
|
1098
|
+
}
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
if (allowGracePeriod) {
|
|
1101
|
+
strapi.log.warn(`⚠️ Cannot verify license online: ${error.message} (Key: ${licenseKey?.substring(0, 8)}...)`);
|
|
1102
|
+
strapi.log.info(`🕐 Grace period active - accepting stored license key`);
|
|
1103
|
+
return { valid: true, data: null, gracePeriod: true };
|
|
1104
|
+
}
|
|
1105
|
+
strapi.log.error(`❌ Error verifying license: ${error.message} (Key: ${licenseKey?.substring(0, 8)}...)`);
|
|
1106
|
+
return { valid: false, data: null };
|
|
1107
|
+
}
|
|
1108
|
+
},
|
|
1109
|
+
/**
|
|
1110
|
+
* Ping a license (lightweight check)
|
|
1111
|
+
*/
|
|
1112
|
+
async pingLicense(licenseKey) {
|
|
1113
|
+
try {
|
|
1114
|
+
const licenseServerUrl = this.getLicenseServerUrl();
|
|
1115
|
+
const response = await fetch(`${licenseServerUrl}/api/licenses/ping`, {
|
|
1116
|
+
method: "POST",
|
|
1117
|
+
headers: {
|
|
1118
|
+
"Content-Type": "application/json"
|
|
1119
|
+
},
|
|
1120
|
+
body: JSON.stringify({
|
|
1121
|
+
licenseKey,
|
|
1122
|
+
pluginName: "magic-mark",
|
|
1123
|
+
productName: "MagicMark - Advanced Query Builder"
|
|
1124
|
+
})
|
|
1125
|
+
});
|
|
1126
|
+
const data = await response.json();
|
|
1127
|
+
if (data.success) {
|
|
1128
|
+
strapi.log.debug(`📡 License ping successful: ${data.data?.isActive ? "ACTIVE" : "INACTIVE"} (Key: ${licenseKey?.substring(0, 8)}...)`);
|
|
1129
|
+
return data.data;
|
|
1130
|
+
} else {
|
|
1131
|
+
strapi.log.debug(`⚠️ License ping failed: ${data.message || "Unknown error"} (Key: ${licenseKey?.substring(0, 8)}...)`);
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
strapi.log.debug(`License ping error: ${error.message} (Key: ${licenseKey?.substring(0, 8)}...)`);
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
},
|
|
1139
|
+
/**
|
|
1140
|
+
* Get license by key
|
|
1141
|
+
*/
|
|
1142
|
+
async getLicenseByKey(licenseKey) {
|
|
1143
|
+
try {
|
|
1144
|
+
const licenseServerUrl = this.getLicenseServerUrl();
|
|
1145
|
+
const response = await fetch(`${licenseServerUrl}/api/licenses/key/${licenseKey}`);
|
|
1146
|
+
const data = await response.json();
|
|
1147
|
+
if (data.success) {
|
|
1148
|
+
return data.data;
|
|
1149
|
+
}
|
|
1150
|
+
return null;
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
strapi.log.error("❌ Error fetching license by key:", error);
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
/**
|
|
1157
|
+
* Get online statistics
|
|
1158
|
+
*/
|
|
1159
|
+
async getOnlineStats() {
|
|
1160
|
+
try {
|
|
1161
|
+
const licenseServerUrl = this.getLicenseServerUrl();
|
|
1162
|
+
const response = await fetch(`${licenseServerUrl}/api/licenses/stats/online`);
|
|
1163
|
+
const data = await response.json();
|
|
1164
|
+
if (data.success) {
|
|
1165
|
+
return data.data;
|
|
1166
|
+
}
|
|
1167
|
+
return null;
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
strapi.log.error("❌ Error fetching online stats:", error);
|
|
1170
|
+
return null;
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
/**
|
|
1174
|
+
* Start periodic pinging for a license
|
|
1175
|
+
*/
|
|
1176
|
+
startPinging(licenseKey, intervalMinutes = 15) {
|
|
1177
|
+
const intervalMs = intervalMinutes * 60 * 1e3;
|
|
1178
|
+
this.pingLicense(licenseKey);
|
|
1179
|
+
const pingInterval = setInterval(async () => {
|
|
1180
|
+
await this.pingLicense(licenseKey);
|
|
1181
|
+
}, intervalMs);
|
|
1182
|
+
strapi.log.info(`📡 Started pinging license every ${intervalMinutes} minutes`);
|
|
1183
|
+
return pingInterval;
|
|
1184
|
+
},
|
|
1185
|
+
/**
|
|
1186
|
+
* Initialize license guard
|
|
1187
|
+
* Checks for existing license or prompts for creation
|
|
1188
|
+
*/
|
|
1189
|
+
async initialize() {
|
|
1190
|
+
try {
|
|
1191
|
+
strapi.log.info("🔐 Initializing License Guard...");
|
|
1192
|
+
const pluginStore = strapi.store({
|
|
1193
|
+
type: "plugin",
|
|
1194
|
+
name: "magic-mark"
|
|
1195
|
+
});
|
|
1196
|
+
const licenseKey = await pluginStore.get({ key: "licenseKey" });
|
|
1197
|
+
const lastValidated = await pluginStore.get({ key: "lastValidated" });
|
|
1198
|
+
const now = /* @__PURE__ */ new Date();
|
|
1199
|
+
const gracePeriodHours = 24;
|
|
1200
|
+
let withinGracePeriod = false;
|
|
1201
|
+
if (lastValidated) {
|
|
1202
|
+
const lastValidatedDate = new Date(lastValidated);
|
|
1203
|
+
const hoursSinceValidation = (now.getTime() - lastValidatedDate.getTime()) / (1e3 * 60 * 60);
|
|
1204
|
+
withinGracePeriod = hoursSinceValidation < gracePeriodHours;
|
|
1205
|
+
}
|
|
1206
|
+
strapi.log.info("──────────────────────────────────────────────────────────");
|
|
1207
|
+
strapi.log.info(`📦 Plugin Store Check:`);
|
|
1208
|
+
if (licenseKey) {
|
|
1209
|
+
strapi.log.info(` ✅ License Key found: ${licenseKey}`);
|
|
1210
|
+
strapi.log.info(` 🔑 Key (short): ${licenseKey.substring(0, 8)}...`);
|
|
1211
|
+
if (lastValidated) {
|
|
1212
|
+
const lastValidatedDate = new Date(lastValidated);
|
|
1213
|
+
const hoursAgo = Math.floor((now.getTime() - lastValidatedDate.getTime()) / (1e3 * 60 * 60));
|
|
1214
|
+
strapi.log.info(` 🕐 Last validated: ${hoursAgo}h ago (Grace: ${withinGracePeriod ? "ACTIVE" : "EXPIRED"})`);
|
|
1215
|
+
} else {
|
|
1216
|
+
strapi.log.info(` 🕐 Last validated: Never (Grace: ACTIVE for first ${gracePeriodHours}h)`);
|
|
1217
|
+
}
|
|
1218
|
+
} else {
|
|
1219
|
+
strapi.log.info(` ❌ No license key stored`);
|
|
1220
|
+
}
|
|
1221
|
+
strapi.log.info("──────────────────────────────────────────────────────────");
|
|
1222
|
+
if (licenseKey) {
|
|
1223
|
+
strapi.log.info("📄 Verifying stored license key...");
|
|
1224
|
+
const verification = await this.verifyLicense(licenseKey, true);
|
|
1225
|
+
if (verification.valid) {
|
|
1226
|
+
if (verification.gracePeriod) {
|
|
1227
|
+
strapi.log.info("✅ License accepted (offline mode / grace period)");
|
|
1228
|
+
} else {
|
|
1229
|
+
strapi.log.info("✅ License is valid and active");
|
|
1230
|
+
}
|
|
1231
|
+
const pingInterval = this.startPinging(licenseKey, 15);
|
|
1232
|
+
strapi.licenseGuard = {
|
|
1233
|
+
licenseKey,
|
|
1234
|
+
pingInterval,
|
|
1235
|
+
data: verification.data
|
|
1236
|
+
};
|
|
1237
|
+
return { valid: true, data: verification.data };
|
|
1238
|
+
} else {
|
|
1239
|
+
strapi.log.warn("⚠️ Stored license is invalid or expired");
|
|
1240
|
+
if (!withinGracePeriod) {
|
|
1241
|
+
await pluginStore.delete({ key: "licenseKey" });
|
|
1242
|
+
await pluginStore.delete({ key: "lastValidated" });
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
strapi.log.warn("⚠️ No valid license found. Plugin will run with limited functionality.");
|
|
1247
|
+
return { valid: false, demo: true };
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
strapi.log.error("❌ Error initializing license guard:", error);
|
|
1250
|
+
return { valid: false, error: error.message };
|
|
1251
|
+
}
|
|
1252
|
+
},
|
|
1253
|
+
/**
|
|
1254
|
+
* Store license key after creation
|
|
1255
|
+
*/
|
|
1256
|
+
async storeLicenseKey(licenseKey) {
|
|
1257
|
+
try {
|
|
1258
|
+
strapi.log.info(`🔐 Storing license key: ${licenseKey}`);
|
|
1259
|
+
const pluginStore = strapi.store({
|
|
1260
|
+
type: "plugin",
|
|
1261
|
+
name: "magic-mark"
|
|
1262
|
+
});
|
|
1263
|
+
await pluginStore.set({ key: "licenseKey", value: licenseKey });
|
|
1264
|
+
await pluginStore.set({ key: "lastValidated", value: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1265
|
+
const stored = await pluginStore.get({ key: "licenseKey" });
|
|
1266
|
+
if (stored === licenseKey) {
|
|
1267
|
+
strapi.log.info("✅ License key stored and verified successfully");
|
|
1268
|
+
return true;
|
|
1269
|
+
} else {
|
|
1270
|
+
strapi.log.error("❌ License key storage verification failed");
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
strapi.log.error("❌ Error storing license key:", error);
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
},
|
|
1278
|
+
/**
|
|
1279
|
+
* Cleanup on plugin destroy
|
|
1280
|
+
*/
|
|
1281
|
+
cleanup() {
|
|
1282
|
+
const licenseGuard = strapi.licenseGuard;
|
|
1283
|
+
if (licenseGuard && licenseGuard.pingInterval) {
|
|
1284
|
+
clearInterval(licenseGuard.pingInterval);
|
|
1285
|
+
strapi.log.info("🛑 License pinging stopped");
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
const services = {
|
|
1290
|
+
bookmarks: bookmarkService,
|
|
1291
|
+
"license-guard": licenseGuardService
|
|
1292
|
+
};
|
|
1293
|
+
const index = {
|
|
1294
|
+
register,
|
|
1295
|
+
bootstrap,
|
|
1296
|
+
destroy,
|
|
1297
|
+
config,
|
|
1298
|
+
controllers,
|
|
1299
|
+
routes,
|
|
1300
|
+
services,
|
|
1301
|
+
contentTypes,
|
|
1302
|
+
policies,
|
|
1303
|
+
middlewares
|
|
1304
|
+
};
|
|
1305
|
+
export {
|
|
1306
|
+
index as default
|
|
1307
|
+
};
|