magic-editor-x 1.0.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/LICENSE +21 -0
- package/README.md +890 -0
- package/dist/_chunks/App-B1FgOsWa.mjs +2143 -0
- package/dist/_chunks/App-mtrlABtd.js +2146 -0
- package/dist/_chunks/LicensePage-BnyWSrWs.js +375 -0
- package/dist/_chunks/LicensePage-CWH-AFR-.mjs +373 -0
- package/dist/_chunks/LiveCollaborationPanel-DbDHwr2C.js +222 -0
- package/dist/_chunks/LiveCollaborationPanel-ryjcDAA7.mjs +220 -0
- package/dist/_chunks/Settings-Bk9bxJTy.js +440 -0
- package/dist/_chunks/Settings-D-V2MLVm.mjs +438 -0
- package/dist/_chunks/de-CSrHZWEb.mjs +295 -0
- package/dist/_chunks/de-CzSo1oD2.js +295 -0
- package/dist/_chunks/en-DuQun2v4.mjs +295 -0
- package/dist/_chunks/en-DxIkVPUh.js +295 -0
- package/dist/_chunks/es-DAQ_97zx.js +273 -0
- package/dist/_chunks/es-DEB0CA8S.mjs +273 -0
- package/dist/_chunks/fr-Bqkhvdx2.mjs +273 -0
- package/dist/_chunks/fr-ChPabvNP.js +273 -0
- package/dist/_chunks/getTranslation-C4uWR0DB.mjs +50985 -0
- package/dist/_chunks/getTranslation-D35vbDap.js +51001 -0
- package/dist/_chunks/index-B5MzUyo0.mjs +2541 -0
- package/dist/_chunks/index-BRVqbnOb.mjs +4450 -0
- package/dist/_chunks/index-BiLy_f7C.js +2540 -0
- package/dist/_chunks/index-CQx7-dFP.js +4472 -0
- package/dist/_chunks/pt-BMoYltav.mjs +273 -0
- package/dist/_chunks/pt-Cm74LpyZ.js +273 -0
- package/dist/_chunks/tools-CjnQJ9w2.mjs +2155 -0
- package/dist/_chunks/tools-DNt2tioN.js +2186 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/server/index.js +2554 -0
- package/dist/server/index.mjs +2544 -0
- package/dist/style.css +164 -0
- package/package.json +122 -0
- package/pics/collab-magiceditorX.png +0 -0
- package/pics/editorX.png +0 -0
- package/pics/liveCollabwidget1.png +0 -0
|
@@ -0,0 +1,2554 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const require$$0 = require("open-graph-scraper");
|
|
3
|
+
const require$$1 = require("fs");
|
|
4
|
+
const require$$2$1 = require("path");
|
|
5
|
+
const require$$3 = require("https");
|
|
6
|
+
const require$$4 = require("http");
|
|
7
|
+
const require$$5 = require("url");
|
|
8
|
+
const require$$0$1 = require("crypto");
|
|
9
|
+
const require$$1$1 = require("socket.io");
|
|
10
|
+
const require$$2$2 = require("yjs");
|
|
11
|
+
const require$$1$2 = require("os");
|
|
12
|
+
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
13
|
+
const require$$0__default = /* @__PURE__ */ _interopDefault(require$$0);
|
|
14
|
+
const require$$1__default = /* @__PURE__ */ _interopDefault(require$$1);
|
|
15
|
+
const require$$2__default = /* @__PURE__ */ _interopDefault(require$$2$1);
|
|
16
|
+
const require$$3__default = /* @__PURE__ */ _interopDefault(require$$3);
|
|
17
|
+
const require$$4__default = /* @__PURE__ */ _interopDefault(require$$4);
|
|
18
|
+
const require$$5__default = /* @__PURE__ */ _interopDefault(require$$5);
|
|
19
|
+
const require$$0__default$1 = /* @__PURE__ */ _interopDefault(require$$0$1);
|
|
20
|
+
const require$$1__default$1 = /* @__PURE__ */ _interopDefault(require$$1$1);
|
|
21
|
+
const require$$2__default$1 = /* @__PURE__ */ _interopDefault(require$$2$2);
|
|
22
|
+
const require$$1__default$2 = /* @__PURE__ */ _interopDefault(require$$1$2);
|
|
23
|
+
function getDefaultExportFromCjs(x) {
|
|
24
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
|
|
25
|
+
}
|
|
26
|
+
var bootstrap$1 = async ({ strapi: strapi2 }) => {
|
|
27
|
+
try {
|
|
28
|
+
const contentTypes2 = [
|
|
29
|
+
"plugin::magic-editor-x.collab-session",
|
|
30
|
+
"plugin::magic-editor-x.document-snapshot",
|
|
31
|
+
"plugin::magic-editor-x.collab-permission"
|
|
32
|
+
];
|
|
33
|
+
for (const contentType of contentTypes2) {
|
|
34
|
+
const exists = strapi2.contentType(contentType);
|
|
35
|
+
if (exists) {
|
|
36
|
+
strapi2.log.info(`[Magic Editor X] [SUCCESS] Content type registered: ${contentType}`);
|
|
37
|
+
} else {
|
|
38
|
+
strapi2.log.warn(`[Magic Editor X] [WARNING] Content type NOT found: ${contentType}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const staleSessions = await strapi2.documents("plugin::magic-editor-x.collab-session").findMany({
|
|
43
|
+
limit: 1e3
|
|
44
|
+
});
|
|
45
|
+
if (staleSessions && staleSessions.length > 0) {
|
|
46
|
+
for (const session of staleSessions) {
|
|
47
|
+
await strapi2.documents("plugin::magic-editor-x.collab-session").delete({
|
|
48
|
+
documentId: session.documentId
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
strapi2.log.info(`[Magic Editor X] [CLEANUP] Cleaned up ${staleSessions.length} stale sessions`);
|
|
52
|
+
}
|
|
53
|
+
} catch (cleanupError) {
|
|
54
|
+
strapi2.log.debug("[Magic Editor X] Session cleanup skipped:", cleanupError.message);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const now = /* @__PURE__ */ new Date();
|
|
58
|
+
const expiredPerms = await strapi2.documents("plugin::magic-editor-x.collab-permission").findMany({
|
|
59
|
+
filters: {
|
|
60
|
+
expiresAt: { $lt: now, $ne: null }
|
|
61
|
+
},
|
|
62
|
+
limit: 1e3
|
|
63
|
+
});
|
|
64
|
+
if (expiredPerms && expiredPerms.length > 0) {
|
|
65
|
+
for (const perm of expiredPerms) {
|
|
66
|
+
await strapi2.documents("plugin::magic-editor-x.collab-permission").delete({
|
|
67
|
+
documentId: perm.documentId
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
strapi2.log.info(`[Magic Editor X] [CLEANUP] Cleaned up ${expiredPerms.length} expired permissions`);
|
|
71
|
+
}
|
|
72
|
+
} catch (cleanupError) {
|
|
73
|
+
strapi2.log.debug("[Magic Editor X] Permission cleanup skipped:", cleanupError.message);
|
|
74
|
+
}
|
|
75
|
+
if (strapi2.$io) {
|
|
76
|
+
strapi2.log.info("[Magic Editor X] [INFO] strapi-plugin-io detected - running in compatibility mode");
|
|
77
|
+
}
|
|
78
|
+
await strapi2.plugin("magic-editor-x").service("realtimeService").initSocketServer();
|
|
79
|
+
strapi2.log.info("[Magic Editor X] [SUCCESS] Realtime server started");
|
|
80
|
+
} catch (error) {
|
|
81
|
+
strapi2.log.error("[Magic Editor X] [ERROR] Bootstrap failed:", error);
|
|
82
|
+
}
|
|
83
|
+
strapi2.log.info("[Magic Editor X] Plugin bootstrapped");
|
|
84
|
+
};
|
|
85
|
+
var destroy$1 = async ({ strapi: strapi2 }) => {
|
|
86
|
+
try {
|
|
87
|
+
await strapi2.plugin("magic-editor-x").service("realtimeService").close();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
strapi2.log.error("[Magic Editor X] Failed to gracefully shutdown realtime server", error);
|
|
90
|
+
}
|
|
91
|
+
strapi2.log.info("[Magic Editor X] Plugin destroyed");
|
|
92
|
+
};
|
|
93
|
+
var register$1 = ({ strapi: strapi2 }) => {
|
|
94
|
+
strapi2.customFields.register({
|
|
95
|
+
name: "richtext",
|
|
96
|
+
plugin: "magic-editor-x",
|
|
97
|
+
type: "text",
|
|
98
|
+
// Stores JSON content as text
|
|
99
|
+
inputSize: {
|
|
100
|
+
default: 12,
|
|
101
|
+
// Full width
|
|
102
|
+
isResizable: true
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
strapi2.log.info("[Magic Editor X] Custom field registered");
|
|
106
|
+
};
|
|
107
|
+
var config$1 = {
|
|
108
|
+
default: {
|
|
109
|
+
// Default configuration options
|
|
110
|
+
enabledTools: [
|
|
111
|
+
"header",
|
|
112
|
+
"paragraph",
|
|
113
|
+
"list",
|
|
114
|
+
"checklist",
|
|
115
|
+
"quote",
|
|
116
|
+
"warning",
|
|
117
|
+
"code",
|
|
118
|
+
"delimiter",
|
|
119
|
+
"table",
|
|
120
|
+
"embed",
|
|
121
|
+
"raw",
|
|
122
|
+
"image",
|
|
123
|
+
"mediaLib",
|
|
124
|
+
"linkTool",
|
|
125
|
+
"marker",
|
|
126
|
+
"inlineCode",
|
|
127
|
+
"underline"
|
|
128
|
+
],
|
|
129
|
+
// Link preview timeout (ms)
|
|
130
|
+
linkPreviewTimeout: 1e4,
|
|
131
|
+
// Max image upload size (bytes)
|
|
132
|
+
maxImageSize: 10 * 1024 * 1024,
|
|
133
|
+
// 10MB
|
|
134
|
+
// Allowed image types
|
|
135
|
+
allowedImageTypes: ["image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"],
|
|
136
|
+
// Realtime collaboration defaults
|
|
137
|
+
collaboration: {
|
|
138
|
+
enabled: true,
|
|
139
|
+
sessionTTL: 2 * 60 * 1e3,
|
|
140
|
+
// 2 minutes
|
|
141
|
+
wsPath: "/magic-editor-x/realtime",
|
|
142
|
+
wsUrl: null,
|
|
143
|
+
allowedOrigins: [],
|
|
144
|
+
allowedAdminRoles: ["strapi-super-admin"],
|
|
145
|
+
allowedAdminUserIds: []
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
validator: (config2) => {
|
|
149
|
+
if (config2.linkPreviewTimeout && typeof config2.linkPreviewTimeout !== "number") {
|
|
150
|
+
throw new Error("[Magic Editor X] linkPreviewTimeout must be a number");
|
|
151
|
+
}
|
|
152
|
+
if (config2.maxImageSize && typeof config2.maxImageSize !== "number") {
|
|
153
|
+
throw new Error("[Magic Editor X] maxImageSize must be a number");
|
|
154
|
+
}
|
|
155
|
+
if (config2.collaboration) {
|
|
156
|
+
if (typeof config2.collaboration.enabled !== "boolean") {
|
|
157
|
+
throw new Error("[Magic Editor X] collaboration.enabled must be a boolean");
|
|
158
|
+
}
|
|
159
|
+
if (config2.collaboration.sessionTTL && typeof config2.collaboration.sessionTTL !== "number") {
|
|
160
|
+
throw new Error("[Magic Editor X] collaboration.sessionTTL must be a number");
|
|
161
|
+
}
|
|
162
|
+
["allowedOrigins", "allowedAdminRoles", "allowedAdminUserIds"].forEach((key) => {
|
|
163
|
+
if (config2.collaboration[key] && !Array.isArray(config2.collaboration[key])) {
|
|
164
|
+
throw new Error(`[Magic Editor X] collaboration.${key} must be an array`);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
var contentTypes$1 = {
|
|
171
|
+
/**
|
|
172
|
+
* Collaboration Sessions
|
|
173
|
+
* Tracks active realtime editing sessions
|
|
174
|
+
*
|
|
175
|
+
* NOTE: Sessions are transient and should NOT be transferred.
|
|
176
|
+
* They are cleared on server restart anyway.
|
|
177
|
+
*/
|
|
178
|
+
"collab-session": {
|
|
179
|
+
schema: {
|
|
180
|
+
kind: "collectionType",
|
|
181
|
+
collectionName: "magic_editor_collab_sessions",
|
|
182
|
+
info: {
|
|
183
|
+
singularName: "collab-session",
|
|
184
|
+
pluralName: "collab-sessions",
|
|
185
|
+
displayName: "Collaboration Session",
|
|
186
|
+
description: "Active realtime collaboration sessions"
|
|
187
|
+
},
|
|
188
|
+
options: {
|
|
189
|
+
draftAndPublish: false
|
|
190
|
+
},
|
|
191
|
+
pluginOptions: {
|
|
192
|
+
"content-manager": {
|
|
193
|
+
visible: false
|
|
194
|
+
},
|
|
195
|
+
"content-type-builder": {
|
|
196
|
+
visible: false
|
|
197
|
+
},
|
|
198
|
+
// Exclude from Strapi Transfer - sessions are transient
|
|
199
|
+
"import-export-entries": {
|
|
200
|
+
idField: "roomId"
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
attributes: {
|
|
204
|
+
roomId: {
|
|
205
|
+
type: "string",
|
|
206
|
+
required: true,
|
|
207
|
+
unique: true
|
|
208
|
+
},
|
|
209
|
+
contentType: {
|
|
210
|
+
type: "string",
|
|
211
|
+
required: true
|
|
212
|
+
},
|
|
213
|
+
entryId: {
|
|
214
|
+
type: "string",
|
|
215
|
+
required: true
|
|
216
|
+
},
|
|
217
|
+
fieldName: {
|
|
218
|
+
type: "string",
|
|
219
|
+
required: true
|
|
220
|
+
},
|
|
221
|
+
activeUsers: {
|
|
222
|
+
type: "json",
|
|
223
|
+
default: []
|
|
224
|
+
},
|
|
225
|
+
lastActivity: {
|
|
226
|
+
type: "datetime"
|
|
227
|
+
},
|
|
228
|
+
yjsState: {
|
|
229
|
+
type: "text",
|
|
230
|
+
default: null
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
/**
|
|
236
|
+
* Document Snapshots
|
|
237
|
+
* Periodic snapshots for version history and recovery
|
|
238
|
+
*
|
|
239
|
+
* NOTE: Snapshots contain Yjs binary data and admin::user references.
|
|
240
|
+
* Transfer is possible but user relations may break.
|
|
241
|
+
*/
|
|
242
|
+
"document-snapshot": {
|
|
243
|
+
schema: {
|
|
244
|
+
kind: "collectionType",
|
|
245
|
+
collectionName: "magic_editor_doc_snapshots",
|
|
246
|
+
info: {
|
|
247
|
+
singularName: "document-snapshot",
|
|
248
|
+
pluralName: "document-snapshots",
|
|
249
|
+
displayName: "Document Snapshot",
|
|
250
|
+
description: "Document version snapshots"
|
|
251
|
+
},
|
|
252
|
+
options: {
|
|
253
|
+
draftAndPublish: false
|
|
254
|
+
},
|
|
255
|
+
pluginOptions: {
|
|
256
|
+
"content-manager": {
|
|
257
|
+
visible: false
|
|
258
|
+
},
|
|
259
|
+
"content-type-builder": {
|
|
260
|
+
visible: false
|
|
261
|
+
},
|
|
262
|
+
// Transfer config - use roomId+version as unique identifier
|
|
263
|
+
"import-export-entries": {
|
|
264
|
+
idField: "roomId"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
attributes: {
|
|
268
|
+
roomId: {
|
|
269
|
+
type: "string",
|
|
270
|
+
required: true
|
|
271
|
+
},
|
|
272
|
+
contentType: {
|
|
273
|
+
type: "string",
|
|
274
|
+
required: true
|
|
275
|
+
},
|
|
276
|
+
entryId: {
|
|
277
|
+
type: "string",
|
|
278
|
+
required: true
|
|
279
|
+
},
|
|
280
|
+
fieldName: {
|
|
281
|
+
type: "string",
|
|
282
|
+
required: true
|
|
283
|
+
},
|
|
284
|
+
version: {
|
|
285
|
+
type: "integer",
|
|
286
|
+
required: true
|
|
287
|
+
},
|
|
288
|
+
yjsSnapshot: {
|
|
289
|
+
type: "text",
|
|
290
|
+
required: true
|
|
291
|
+
},
|
|
292
|
+
jsonContent: {
|
|
293
|
+
type: "json"
|
|
294
|
+
},
|
|
295
|
+
createdBy: {
|
|
296
|
+
type: "relation",
|
|
297
|
+
relation: "oneToOne",
|
|
298
|
+
target: "admin::user"
|
|
299
|
+
},
|
|
300
|
+
createdAt: {
|
|
301
|
+
type: "datetime"
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
/**
|
|
307
|
+
* Collaboration Permissions
|
|
308
|
+
* Access control for realtime editing
|
|
309
|
+
*
|
|
310
|
+
* NOTE: Permissions reference admin::user which are environment-specific.
|
|
311
|
+
* On transfer, user relations will need to be re-created manually.
|
|
312
|
+
* The contentType field can be used to match permissions.
|
|
313
|
+
*/
|
|
314
|
+
"collab-permission": {
|
|
315
|
+
schema: {
|
|
316
|
+
kind: "collectionType",
|
|
317
|
+
collectionName: "magic_editor_collab_permissions",
|
|
318
|
+
info: {
|
|
319
|
+
singularName: "collab-permission",
|
|
320
|
+
pluralName: "collab-permissions",
|
|
321
|
+
displayName: "Collaboration Permission",
|
|
322
|
+
description: "User permissions for realtime editing"
|
|
323
|
+
},
|
|
324
|
+
options: {
|
|
325
|
+
draftAndPublish: false
|
|
326
|
+
},
|
|
327
|
+
pluginOptions: {
|
|
328
|
+
"content-manager": {
|
|
329
|
+
visible: false
|
|
330
|
+
// Hidden - use plugin settings page instead
|
|
331
|
+
},
|
|
332
|
+
"content-type-builder": {
|
|
333
|
+
visible: false
|
|
334
|
+
},
|
|
335
|
+
// Transfer note: admin::user relations won't transfer
|
|
336
|
+
// Permissions should be re-created in target environment
|
|
337
|
+
"import-export-entries": {
|
|
338
|
+
idField: "id"
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
attributes: {
|
|
342
|
+
contentType: {
|
|
343
|
+
type: "string",
|
|
344
|
+
required: false,
|
|
345
|
+
// null = all content types
|
|
346
|
+
default: "*"
|
|
347
|
+
},
|
|
348
|
+
entryId: {
|
|
349
|
+
type: "string"
|
|
350
|
+
},
|
|
351
|
+
fieldName: {
|
|
352
|
+
type: "string"
|
|
353
|
+
},
|
|
354
|
+
user: {
|
|
355
|
+
type: "relation",
|
|
356
|
+
relation: "oneToOne",
|
|
357
|
+
target: "admin::user",
|
|
358
|
+
required: true
|
|
359
|
+
},
|
|
360
|
+
role: {
|
|
361
|
+
type: "enumeration",
|
|
362
|
+
enum: ["viewer", "editor", "owner"],
|
|
363
|
+
default: "editor"
|
|
364
|
+
},
|
|
365
|
+
expiresAt: {
|
|
366
|
+
type: "datetime"
|
|
367
|
+
},
|
|
368
|
+
grantedBy: {
|
|
369
|
+
type: "relation",
|
|
370
|
+
relation: "oneToOne",
|
|
371
|
+
target: "admin::user"
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
var editorController = ({ strapi: strapi2 }) => ({
|
|
378
|
+
/**
|
|
379
|
+
* Fetch link metadata (OpenGraph) for URL preview
|
|
380
|
+
* GET /api/magic-editor-x/link?url=https://example.com
|
|
381
|
+
*/
|
|
382
|
+
async fetchLinkMeta(ctx) {
|
|
383
|
+
try {
|
|
384
|
+
const { url } = ctx.query;
|
|
385
|
+
if (!url) {
|
|
386
|
+
return ctx.send({
|
|
387
|
+
success: 0,
|
|
388
|
+
message: "URL parameter is required"
|
|
389
|
+
}, 400);
|
|
390
|
+
}
|
|
391
|
+
const result = await strapi2.plugin("magic-editor-x").service("editorService").fetchLinkMeta(url);
|
|
392
|
+
ctx.send(result);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
strapi2.log.error("[Magic Editor X] Link fetch error:", error);
|
|
395
|
+
ctx.send({
|
|
396
|
+
success: 0,
|
|
397
|
+
message: error.message || "Failed to fetch link metadata"
|
|
398
|
+
}, 500);
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
/**
|
|
402
|
+
* Upload image by file
|
|
403
|
+
* POST /api/magic-editor-x/image/byFile
|
|
404
|
+
* Multipart form data with files.image
|
|
405
|
+
*/
|
|
406
|
+
async uploadByFile(ctx) {
|
|
407
|
+
try {
|
|
408
|
+
const result = await strapi2.plugin("magic-editor-x").service("editorService").uploadByFile(ctx);
|
|
409
|
+
ctx.send(result);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
strapi2.log.error("[Magic Editor X] File upload error:", error);
|
|
412
|
+
ctx.send({
|
|
413
|
+
success: 0,
|
|
414
|
+
message: error.message || "Failed to upload file"
|
|
415
|
+
}, 500);
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
/**
|
|
419
|
+
* Upload image by URL
|
|
420
|
+
* POST /api/magic-editor-x/image/byUrl
|
|
421
|
+
* JSON body with url field
|
|
422
|
+
*/
|
|
423
|
+
async uploadByUrl(ctx) {
|
|
424
|
+
try {
|
|
425
|
+
const { url } = ctx.request.body;
|
|
426
|
+
if (!url) {
|
|
427
|
+
return ctx.send({
|
|
428
|
+
success: 0,
|
|
429
|
+
message: "URL is required"
|
|
430
|
+
}, 400);
|
|
431
|
+
}
|
|
432
|
+
const result = await strapi2.plugin("magic-editor-x").service("editorService").uploadByUrl(url);
|
|
433
|
+
ctx.send(result);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
strapi2.log.error("[Magic Editor X] URL upload error:", error);
|
|
436
|
+
ctx.send({
|
|
437
|
+
success: 0,
|
|
438
|
+
message: error.message || "Failed to upload from URL"
|
|
439
|
+
}, 500);
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
/**
|
|
443
|
+
* Upload file (for Attaches Tool)
|
|
444
|
+
* POST /api/magic-editor-x/file/upload
|
|
445
|
+
* Multipart form data with file
|
|
446
|
+
*/
|
|
447
|
+
async uploadFile(ctx) {
|
|
448
|
+
try {
|
|
449
|
+
const result = await strapi2.plugin("magic-editor-x").service("editorService").uploadAttachment(ctx);
|
|
450
|
+
ctx.send(result);
|
|
451
|
+
} catch (error) {
|
|
452
|
+
strapi2.log.error("[Magic Editor X] Attachment upload error:", error);
|
|
453
|
+
ctx.send({
|
|
454
|
+
success: 0,
|
|
455
|
+
message: error.message || "Failed to upload attachment"
|
|
456
|
+
}, 500);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
const pluginId$2 = "magic-editor-x";
|
|
461
|
+
const sanitizeInitialValue = (value) => {
|
|
462
|
+
if (!value) {
|
|
463
|
+
return "";
|
|
464
|
+
}
|
|
465
|
+
if (typeof value === "string") {
|
|
466
|
+
return value;
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
return JSON.stringify(value);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
return "";
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
const verifyAdminToken = async (strapi2, ctx) => {
|
|
475
|
+
const authHeader = ctx.request.header.authorization;
|
|
476
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
const token = authHeader.substring(7);
|
|
480
|
+
try {
|
|
481
|
+
const decoded = await strapi2.admin.services.token.decodeJwtToken(token);
|
|
482
|
+
if (!decoded || !decoded.id) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const adminUser = await strapi2.query("admin::user").findOne({
|
|
486
|
+
where: { id: decoded.id },
|
|
487
|
+
select: ["id", "firstname", "lastname", "email", "isActive"]
|
|
488
|
+
});
|
|
489
|
+
if (!adminUser || !adminUser.isActive) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
return adminUser;
|
|
493
|
+
} catch (error) {
|
|
494
|
+
strapi2.log.warn("[Realtime] Admin token verification failed:", error.message);
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
var realtimeController = ({ strapi: strapi2 }) => ({
|
|
499
|
+
/**
|
|
500
|
+
* POST /magic-editor-x/realtime/session
|
|
501
|
+
* Issues a short-lived collaboration token for the socket handshake.
|
|
502
|
+
*/
|
|
503
|
+
async createSession(ctx) {
|
|
504
|
+
const { roomId, fieldName, meta = {}, initialValue } = ctx.request.body || {};
|
|
505
|
+
strapi2.log.info("[Realtime] createSession called with:", { roomId, fieldName });
|
|
506
|
+
if (!roomId || !fieldName) {
|
|
507
|
+
strapi2.log.warn("[Realtime] Missing roomId or fieldName");
|
|
508
|
+
return ctx.badRequest("roomId and fieldName are required");
|
|
509
|
+
}
|
|
510
|
+
let adminUser = ctx.state?.user;
|
|
511
|
+
if (!adminUser || !adminUser.email) {
|
|
512
|
+
adminUser = await verifyAdminToken(strapi2, ctx);
|
|
513
|
+
}
|
|
514
|
+
if (!adminUser) {
|
|
515
|
+
strapi2.log.warn("[Realtime] No admin user in context or invalid token");
|
|
516
|
+
return ctx.unauthorized("Admin authentication required");
|
|
517
|
+
}
|
|
518
|
+
strapi2.log.info("[Realtime] Admin user:", {
|
|
519
|
+
id: adminUser.id,
|
|
520
|
+
email: adminUser.email,
|
|
521
|
+
firstname: adminUser.firstname,
|
|
522
|
+
lastname: adminUser.lastname
|
|
523
|
+
});
|
|
524
|
+
const accessService2 = strapi2.plugin(pluginId$2).service("accessService");
|
|
525
|
+
let extractedContentType = null;
|
|
526
|
+
let extractedDocumentId = null;
|
|
527
|
+
if (roomId) {
|
|
528
|
+
const parts = roomId.split("|");
|
|
529
|
+
if (parts.length >= 1) {
|
|
530
|
+
const contentType = parts[0];
|
|
531
|
+
if (contentType && contentType.includes("::")) {
|
|
532
|
+
extractedContentType = contentType;
|
|
533
|
+
}
|
|
534
|
+
if (parts[1]) {
|
|
535
|
+
extractedDocumentId = parts[1];
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
strapi2.log.info("[Realtime] Parsed roomId:", {
|
|
539
|
+
contentType: extractedContentType,
|
|
540
|
+
documentId: extractedDocumentId,
|
|
541
|
+
roomId
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
const access = await accessService2.canUseCollaboration(adminUser, extractedContentType);
|
|
545
|
+
strapi2.log.info("[Realtime] Access check result:", {
|
|
546
|
+
allowed: access.allowed,
|
|
547
|
+
reason: access.reason || "none",
|
|
548
|
+
role: access.role || "none",
|
|
549
|
+
userId: adminUser.id,
|
|
550
|
+
userEmail: adminUser.email,
|
|
551
|
+
contentType: extractedContentType
|
|
552
|
+
});
|
|
553
|
+
if (!access.allowed) {
|
|
554
|
+
if (access.reason === "permission-required") {
|
|
555
|
+
return ctx.forbidden(
|
|
556
|
+
"Du benötigst eine Freigabe für die Echtzeit-Bearbeitung. Bitte kontaktiere einen Super Admin, um Zugriff zu erhalten."
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
return ctx.forbidden(access.reason || "Realtime collaboration is not enabled for your role");
|
|
560
|
+
}
|
|
561
|
+
const realtimeService2 = strapi2.plugin(pluginId$2).service("realtimeService");
|
|
562
|
+
try {
|
|
563
|
+
const session = realtimeService2.issueSession({
|
|
564
|
+
roomId,
|
|
565
|
+
fieldName,
|
|
566
|
+
meta,
|
|
567
|
+
user: adminUser,
|
|
568
|
+
initialValue: sanitizeInitialValue(initialValue)
|
|
569
|
+
});
|
|
570
|
+
strapi2.log.info("[Realtime] [SUCCESS] Session created successfully with role:", access.role);
|
|
571
|
+
ctx.body = {
|
|
572
|
+
...session,
|
|
573
|
+
role: access.role || "viewer",
|
|
574
|
+
// Default to viewer if no role
|
|
575
|
+
canEdit: ["editor", "owner"].includes(access.role)
|
|
576
|
+
};
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if (error.message === "collaboration-disabled") {
|
|
579
|
+
return ctx.forbidden("Realtime collaboration is disabled");
|
|
580
|
+
}
|
|
581
|
+
strapi2.log.error("[Magic Editor X] Failed to create realtime session", error);
|
|
582
|
+
ctx.internalServerError("Unable to create realtime session");
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
var collaborationController = ({ strapi: strapi2 }) => ({
|
|
587
|
+
/**
|
|
588
|
+
* List admin users for collaboration
|
|
589
|
+
*/
|
|
590
|
+
async listAdminUsers(ctx) {
|
|
591
|
+
try {
|
|
592
|
+
const users = await strapi2.query("admin::user").findMany({
|
|
593
|
+
where: { isActive: true },
|
|
594
|
+
select: ["id", "firstname", "lastname", "email", "username"],
|
|
595
|
+
limit: 100
|
|
596
|
+
});
|
|
597
|
+
ctx.body = { data: users };
|
|
598
|
+
} catch (error) {
|
|
599
|
+
strapi2.log.error("[Collab] Error listing admin users:", error);
|
|
600
|
+
ctx.throw(500, error);
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
/**
|
|
604
|
+
* List all collaboration permissions
|
|
605
|
+
* Using Document Service API (strapi.documents) for Strapi v5
|
|
606
|
+
*/
|
|
607
|
+
async listPermissions(ctx) {
|
|
608
|
+
try {
|
|
609
|
+
const permissions = await strapi2.documents("plugin::magic-editor-x.collab-permission").findMany({
|
|
610
|
+
populate: ["user", "grantedBy"],
|
|
611
|
+
sort: [{ createdAt: "desc" }]
|
|
612
|
+
});
|
|
613
|
+
ctx.body = { data: permissions };
|
|
614
|
+
} catch (error) {
|
|
615
|
+
ctx.throw(500, error);
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
/**
|
|
619
|
+
* Create collaboration permission
|
|
620
|
+
* Checks license limits before creating
|
|
621
|
+
*/
|
|
622
|
+
async createPermission(ctx) {
|
|
623
|
+
try {
|
|
624
|
+
const { userId, role, contentType, entryId, fieldName, expiresAt } = ctx.request.body;
|
|
625
|
+
strapi2.log.info("[Collab] Creating permission with data:", { userId, role, contentType });
|
|
626
|
+
if (!userId || !role) {
|
|
627
|
+
strapi2.log.warn("[Collab] Missing userId or role");
|
|
628
|
+
return ctx.badRequest("userId and role are required");
|
|
629
|
+
}
|
|
630
|
+
const accessService2 = strapi2.plugin("magic-editor-x").service("accessService");
|
|
631
|
+
const limitCheck = await accessService2.checkCollaboratorLimit();
|
|
632
|
+
if (!limitCheck.canAdd) {
|
|
633
|
+
strapi2.log.warn("[Collab] Collaborator limit reached:", limitCheck);
|
|
634
|
+
return ctx.forbidden({
|
|
635
|
+
error: "Collaborator limit reached",
|
|
636
|
+
message: `You have reached the maximum of ${limitCheck.max} collaborators for your plan. Upgrade to add more.`,
|
|
637
|
+
current: limitCheck.current,
|
|
638
|
+
max: limitCheck.max,
|
|
639
|
+
upgradeRequired: true
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
const user = await strapi2.query("admin::user").findOne({
|
|
643
|
+
where: { id: userId }
|
|
644
|
+
});
|
|
645
|
+
if (!user) {
|
|
646
|
+
strapi2.log.warn("[Collab] User not found:", userId);
|
|
647
|
+
return ctx.badRequest("User not found");
|
|
648
|
+
}
|
|
649
|
+
strapi2.log.info("[Collab] Creating permission for user:", user.email);
|
|
650
|
+
const permission = await strapi2.documents("plugin::magic-editor-x.collab-permission").create({
|
|
651
|
+
data: {
|
|
652
|
+
user: userId,
|
|
653
|
+
role,
|
|
654
|
+
// null = all content types, store null not '*'
|
|
655
|
+
contentType: contentType === "*" || !contentType ? null : contentType,
|
|
656
|
+
entryId: entryId || null,
|
|
657
|
+
fieldName: fieldName || null,
|
|
658
|
+
expiresAt: expiresAt || null,
|
|
659
|
+
grantedBy: ctx.state.user.id
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
strapi2.log.info("[Collab] Permission created successfully:", permission.documentId);
|
|
663
|
+
ctx.body = { data: permission };
|
|
664
|
+
} catch (error) {
|
|
665
|
+
strapi2.log.error("[Collab] Error creating permission:", error);
|
|
666
|
+
ctx.throw(500, error.message || "Failed to create permission");
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
/**
|
|
670
|
+
* Update collaboration permission
|
|
671
|
+
* Using Document Service API (strapi.documents) for Strapi v5
|
|
672
|
+
* Note: Uses documentId instead of numeric id
|
|
673
|
+
*/
|
|
674
|
+
async updatePermission(ctx) {
|
|
675
|
+
try {
|
|
676
|
+
const { id } = ctx.params;
|
|
677
|
+
const { role, contentType, entryId, fieldName, expiresAt } = ctx.request.body;
|
|
678
|
+
const updateData = {};
|
|
679
|
+
if (role !== void 0) {
|
|
680
|
+
updateData.role = role;
|
|
681
|
+
}
|
|
682
|
+
if (contentType !== void 0) {
|
|
683
|
+
updateData.contentType = contentType === "*" || contentType === null ? null : contentType;
|
|
684
|
+
}
|
|
685
|
+
if (entryId !== void 0) {
|
|
686
|
+
updateData.entryId = entryId;
|
|
687
|
+
}
|
|
688
|
+
if (fieldName !== void 0) {
|
|
689
|
+
updateData.fieldName = fieldName;
|
|
690
|
+
}
|
|
691
|
+
if (expiresAt !== void 0) {
|
|
692
|
+
updateData.expiresAt = expiresAt;
|
|
693
|
+
}
|
|
694
|
+
strapi2.log.info("[Collab] Updating permission:", { documentId: id, updateData });
|
|
695
|
+
const permission = await strapi2.documents("plugin::magic-editor-x.collab-permission").update({
|
|
696
|
+
documentId: id,
|
|
697
|
+
data: updateData
|
|
698
|
+
});
|
|
699
|
+
ctx.body = { data: permission };
|
|
700
|
+
} catch (error) {
|
|
701
|
+
strapi2.log.error("[Collab] Error updating permission:", error);
|
|
702
|
+
ctx.throw(500, error);
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
/**
|
|
706
|
+
* Delete collaboration permission
|
|
707
|
+
* Using Document Service API (strapi.documents) for Strapi v5
|
|
708
|
+
* Note: Uses documentId instead of numeric id
|
|
709
|
+
*/
|
|
710
|
+
async deletePermission(ctx) {
|
|
711
|
+
try {
|
|
712
|
+
const { id } = ctx.params;
|
|
713
|
+
await strapi2.documents("plugin::magic-editor-x.collab-permission").delete({
|
|
714
|
+
documentId: id
|
|
715
|
+
});
|
|
716
|
+
ctx.body = { data: { documentId: id } };
|
|
717
|
+
} catch (error) {
|
|
718
|
+
ctx.throw(500, error);
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
/**
|
|
722
|
+
* Check if user can access a specific document
|
|
723
|
+
*/
|
|
724
|
+
async checkAccess(ctx) {
|
|
725
|
+
try {
|
|
726
|
+
const { roomId, action } = ctx.query;
|
|
727
|
+
const userId = ctx.state.user.id;
|
|
728
|
+
if (!roomId) {
|
|
729
|
+
return ctx.badRequest("roomId is required");
|
|
730
|
+
}
|
|
731
|
+
const canAccess = await strapi2.plugin("magic-editor-x").service("accessService").canAccessRoom(userId, roomId, action || "view");
|
|
732
|
+
ctx.body = { data: { canAccess } };
|
|
733
|
+
} catch (error) {
|
|
734
|
+
ctx.throw(500, error);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
var licenseController = ({ strapi: strapi2 }) => ({
|
|
739
|
+
/**
|
|
740
|
+
* Auto-create a FREE license with logged-in admin user data
|
|
741
|
+
*/
|
|
742
|
+
async autoCreate(ctx) {
|
|
743
|
+
try {
|
|
744
|
+
const adminUser = ctx.state.user;
|
|
745
|
+
if (!adminUser) {
|
|
746
|
+
return ctx.unauthorized("No admin user logged in");
|
|
747
|
+
}
|
|
748
|
+
const licenseService2 = strapi2.plugin("magic-editor-x").service("licenseService");
|
|
749
|
+
const license2 = await licenseService2.createLicense({
|
|
750
|
+
email: adminUser.email,
|
|
751
|
+
firstName: adminUser.firstname || "Admin",
|
|
752
|
+
lastName: adminUser.lastname || "User"
|
|
753
|
+
});
|
|
754
|
+
if (!license2) {
|
|
755
|
+
return ctx.badRequest("Failed to create license");
|
|
756
|
+
}
|
|
757
|
+
await licenseService2.storeLicenseKey(license2.licenseKey);
|
|
758
|
+
const pingInterval = licenseService2.startPinging(license2.licenseKey, 15);
|
|
759
|
+
strapi2.licenseGuardEditorX = {
|
|
760
|
+
licenseKey: license2.licenseKey,
|
|
761
|
+
pingInterval,
|
|
762
|
+
data: license2,
|
|
763
|
+
tier: "free"
|
|
764
|
+
};
|
|
765
|
+
return ctx.send({
|
|
766
|
+
success: true,
|
|
767
|
+
message: "License automatically created and activated",
|
|
768
|
+
data: license2
|
|
769
|
+
});
|
|
770
|
+
} catch (error) {
|
|
771
|
+
strapi2.log.error("[Magic Editor X] Error auto-creating license:", error);
|
|
772
|
+
return ctx.badRequest("Error creating license");
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
/**
|
|
776
|
+
* Get current license status
|
|
777
|
+
*/
|
|
778
|
+
async getStatus(ctx) {
|
|
779
|
+
try {
|
|
780
|
+
const licenseService2 = strapi2.plugin("magic-editor-x").service("licenseService");
|
|
781
|
+
const licenseKey = await licenseService2.getStoredLicenseKey();
|
|
782
|
+
if (!licenseKey) {
|
|
783
|
+
return ctx.send({
|
|
784
|
+
success: false,
|
|
785
|
+
demo: true,
|
|
786
|
+
valid: false,
|
|
787
|
+
tier: "free",
|
|
788
|
+
message: "No license found. Running in FREE mode."
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
const verification = await licenseService2.verifyLicense(licenseKey);
|
|
792
|
+
const license2 = await licenseService2.getLicenseByKey(licenseKey);
|
|
793
|
+
const tier = await licenseService2.getCurrentTier();
|
|
794
|
+
return ctx.send({
|
|
795
|
+
success: true,
|
|
796
|
+
valid: verification.valid,
|
|
797
|
+
demo: false,
|
|
798
|
+
tier,
|
|
799
|
+
data: {
|
|
800
|
+
licenseKey,
|
|
801
|
+
email: license2?.email || null,
|
|
802
|
+
firstName: license2?.firstName || null,
|
|
803
|
+
lastName: license2?.lastName || null,
|
|
804
|
+
isActive: license2?.isActive || false,
|
|
805
|
+
isExpired: license2?.isExpired || false,
|
|
806
|
+
isOnline: license2?.isOnline || false,
|
|
807
|
+
expiresAt: license2?.expiresAt,
|
|
808
|
+
lastPingAt: license2?.lastPingAt,
|
|
809
|
+
deviceName: license2?.deviceName,
|
|
810
|
+
features: {
|
|
811
|
+
premium: license2?.featurePremium || false,
|
|
812
|
+
advanced: license2?.featureAdvanced || false,
|
|
813
|
+
enterprise: license2?.featureEnterprise || false
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
} catch (error) {
|
|
818
|
+
strapi2.log.error("[Magic Editor X] Error getting license status:", error);
|
|
819
|
+
return ctx.badRequest("Error getting license status");
|
|
820
|
+
}
|
|
821
|
+
},
|
|
822
|
+
/**
|
|
823
|
+
* Store and validate an existing license key
|
|
824
|
+
*/
|
|
825
|
+
async storeKey(ctx) {
|
|
826
|
+
try {
|
|
827
|
+
const { licenseKey, email } = ctx.request.body;
|
|
828
|
+
if (!licenseKey || !licenseKey.trim()) {
|
|
829
|
+
return ctx.badRequest("License key is required");
|
|
830
|
+
}
|
|
831
|
+
if (!email || !email.trim()) {
|
|
832
|
+
return ctx.badRequest("Email address is required");
|
|
833
|
+
}
|
|
834
|
+
const trimmedKey = licenseKey.trim();
|
|
835
|
+
const trimmedEmail = email.trim().toLowerCase();
|
|
836
|
+
const licenseService2 = strapi2.plugin("magic-editor-x").service("licenseService");
|
|
837
|
+
const verification = await licenseService2.verifyLicense(trimmedKey);
|
|
838
|
+
if (!verification.valid) {
|
|
839
|
+
strapi2.log.warn(`[Magic Editor X] [WARNING] Invalid license key attempted: ${trimmedKey.substring(0, 8)}...`);
|
|
840
|
+
return ctx.badRequest("Invalid or expired license key");
|
|
841
|
+
}
|
|
842
|
+
const license2 = await licenseService2.getLicenseByKey(trimmedKey);
|
|
843
|
+
if (!license2) {
|
|
844
|
+
return ctx.badRequest("License not found");
|
|
845
|
+
}
|
|
846
|
+
if (license2.email.toLowerCase() !== trimmedEmail) {
|
|
847
|
+
strapi2.log.warn(`[Magic Editor X] [WARNING] Email mismatch for license key`);
|
|
848
|
+
return ctx.badRequest("Email address does not match this license key");
|
|
849
|
+
}
|
|
850
|
+
await licenseService2.storeLicenseKey(trimmedKey);
|
|
851
|
+
const pingInterval = licenseService2.startPinging(trimmedKey, 15);
|
|
852
|
+
const tier = await licenseService2.getCurrentTier();
|
|
853
|
+
strapi2.licenseGuardEditorX = {
|
|
854
|
+
licenseKey: trimmedKey,
|
|
855
|
+
pingInterval,
|
|
856
|
+
data: verification.data,
|
|
857
|
+
tier
|
|
858
|
+
};
|
|
859
|
+
strapi2.log.info(`[Magic Editor X] [SUCCESS] License validated and stored`);
|
|
860
|
+
return ctx.send({
|
|
861
|
+
success: true,
|
|
862
|
+
message: "License activated successfully",
|
|
863
|
+
tier,
|
|
864
|
+
data: verification.data
|
|
865
|
+
});
|
|
866
|
+
} catch (error) {
|
|
867
|
+
strapi2.log.error("[Magic Editor X] Error storing license key:", error);
|
|
868
|
+
return ctx.badRequest("Error storing license key");
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
/**
|
|
872
|
+
* Get license limits and available features
|
|
873
|
+
*/
|
|
874
|
+
async getLimits(ctx) {
|
|
875
|
+
try {
|
|
876
|
+
const licenseService2 = strapi2.plugin("magic-editor-x").service("licenseService");
|
|
877
|
+
const tier = await licenseService2.getCurrentTier();
|
|
878
|
+
const tierConfig = licenseService2.getTierConfig(tier);
|
|
879
|
+
const collaboratorCheck = await licenseService2.canAddCollaborator();
|
|
880
|
+
ctx.body = {
|
|
881
|
+
success: true,
|
|
882
|
+
tier,
|
|
883
|
+
tierName: tierConfig.name,
|
|
884
|
+
limits: {
|
|
885
|
+
collaborators: {
|
|
886
|
+
current: collaboratorCheck.current,
|
|
887
|
+
max: collaboratorCheck.max,
|
|
888
|
+
unlimited: collaboratorCheck.unlimited,
|
|
889
|
+
canAdd: collaboratorCheck.canAdd
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
features: tierConfig.features
|
|
893
|
+
};
|
|
894
|
+
} catch (error) {
|
|
895
|
+
strapi2.log.error("[Magic Editor X] Error getting license limits:", error);
|
|
896
|
+
ctx.throw(500, "Error getting license limits");
|
|
897
|
+
}
|
|
898
|
+
},
|
|
899
|
+
/**
|
|
900
|
+
* Check if user can add a collaborator (used by frontend)
|
|
901
|
+
*/
|
|
902
|
+
async canAddCollaborator(ctx) {
|
|
903
|
+
try {
|
|
904
|
+
const licenseService2 = strapi2.plugin("magic-editor-x").service("licenseService");
|
|
905
|
+
const result = await licenseService2.canAddCollaborator();
|
|
906
|
+
ctx.body = {
|
|
907
|
+
success: true,
|
|
908
|
+
...result
|
|
909
|
+
};
|
|
910
|
+
} catch (error) {
|
|
911
|
+
strapi2.log.error("[Magic Editor X] Error checking collaborator limit:", error);
|
|
912
|
+
ctx.throw(500, "Error checking collaborator limit");
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
const editor = editorController;
|
|
917
|
+
const realtime = realtimeController;
|
|
918
|
+
const collaboration = collaborationController;
|
|
919
|
+
const license$1 = licenseController;
|
|
920
|
+
var controllers$1 = {
|
|
921
|
+
editor,
|
|
922
|
+
realtime,
|
|
923
|
+
collaboration,
|
|
924
|
+
license: license$1
|
|
925
|
+
};
|
|
926
|
+
var middlewares$1 = {};
|
|
927
|
+
var policies$1 = {};
|
|
928
|
+
var admin$1 = {
|
|
929
|
+
type: "admin",
|
|
930
|
+
routes: [
|
|
931
|
+
// Collaboration Session
|
|
932
|
+
{
|
|
933
|
+
method: "POST",
|
|
934
|
+
path: "/collab/session",
|
|
935
|
+
handler: "realtime.createSession",
|
|
936
|
+
config: {
|
|
937
|
+
policies: []
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
// Collaboration Users & Permissions
|
|
941
|
+
{
|
|
942
|
+
method: "GET",
|
|
943
|
+
path: "/collaboration/users",
|
|
944
|
+
handler: "collaboration.listAdminUsers",
|
|
945
|
+
config: {
|
|
946
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
method: "GET",
|
|
951
|
+
path: "/collaboration/permissions",
|
|
952
|
+
handler: "collaboration.listPermissions",
|
|
953
|
+
config: {
|
|
954
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
955
|
+
}
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
method: "POST",
|
|
959
|
+
path: "/collaboration/permissions",
|
|
960
|
+
handler: "collaboration.createPermission",
|
|
961
|
+
config: {
|
|
962
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
method: "PUT",
|
|
967
|
+
path: "/collaboration/permissions/:id",
|
|
968
|
+
handler: "collaboration.updatePermission",
|
|
969
|
+
config: {
|
|
970
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
method: "DELETE",
|
|
975
|
+
path: "/collaboration/permissions/:id",
|
|
976
|
+
handler: "collaboration.deletePermission",
|
|
977
|
+
config: {
|
|
978
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
method: "GET",
|
|
983
|
+
path: "/collaboration/check-access",
|
|
984
|
+
handler: "collaboration.checkAccess",
|
|
985
|
+
config: {
|
|
986
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
// License Management
|
|
990
|
+
{
|
|
991
|
+
method: "GET",
|
|
992
|
+
path: "/license/status",
|
|
993
|
+
handler: "license.getStatus",
|
|
994
|
+
config: {
|
|
995
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
996
|
+
}
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
method: "POST",
|
|
1000
|
+
path: "/license/auto-create",
|
|
1001
|
+
handler: "license.autoCreate",
|
|
1002
|
+
config: {
|
|
1003
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
method: "POST",
|
|
1008
|
+
path: "/license/store-key",
|
|
1009
|
+
handler: "license.storeKey",
|
|
1010
|
+
config: {
|
|
1011
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
method: "GET",
|
|
1016
|
+
path: "/license/limits",
|
|
1017
|
+
handler: "license.getLimits",
|
|
1018
|
+
config: {
|
|
1019
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1020
|
+
}
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
method: "GET",
|
|
1024
|
+
path: "/license/can-add-collaborator",
|
|
1025
|
+
handler: "license.canAddCollaborator",
|
|
1026
|
+
config: {
|
|
1027
|
+
policies: ["admin::isAuthenticatedAdmin"]
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
]
|
|
1031
|
+
};
|
|
1032
|
+
var contentApi$1 = {
|
|
1033
|
+
type: "content-api",
|
|
1034
|
+
routes: [
|
|
1035
|
+
/**
|
|
1036
|
+
* Link Preview Endpoint
|
|
1037
|
+
* GET /api/magic-editor-x/link?url=https://example.com
|
|
1038
|
+
* Returns OpenGraph metadata for URL
|
|
1039
|
+
*/
|
|
1040
|
+
{
|
|
1041
|
+
method: "GET",
|
|
1042
|
+
path: "/link",
|
|
1043
|
+
handler: "editor.fetchLinkMeta",
|
|
1044
|
+
config: {
|
|
1045
|
+
description: "Fetch link metadata (OpenGraph) for URL preview",
|
|
1046
|
+
auth: false,
|
|
1047
|
+
policies: []
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
/**
|
|
1051
|
+
* Upload Image by File
|
|
1052
|
+
* POST /api/magic-editor-x/image/byFile
|
|
1053
|
+
* Multipart form data with files.image
|
|
1054
|
+
*/
|
|
1055
|
+
{
|
|
1056
|
+
method: "POST",
|
|
1057
|
+
path: "/image/byFile",
|
|
1058
|
+
handler: "editor.uploadByFile",
|
|
1059
|
+
config: {
|
|
1060
|
+
description: "Upload image by file to Strapi Media Library",
|
|
1061
|
+
auth: false,
|
|
1062
|
+
policies: []
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
/**
|
|
1066
|
+
* Upload Image by URL
|
|
1067
|
+
* POST /api/magic-editor-x/image/byUrl
|
|
1068
|
+
* JSON body with url field
|
|
1069
|
+
*/
|
|
1070
|
+
{
|
|
1071
|
+
method: "POST",
|
|
1072
|
+
path: "/image/byUrl",
|
|
1073
|
+
handler: "editor.uploadByUrl",
|
|
1074
|
+
config: {
|
|
1075
|
+
description: "Upload image by URL to Strapi Media Library",
|
|
1076
|
+
auth: false,
|
|
1077
|
+
policies: []
|
|
1078
|
+
}
|
|
1079
|
+
},
|
|
1080
|
+
/**
|
|
1081
|
+
* Upload File (for Attaches Tool)
|
|
1082
|
+
* POST /api/magic-editor-x/file/upload
|
|
1083
|
+
* Multipart form data with file
|
|
1084
|
+
*/
|
|
1085
|
+
{
|
|
1086
|
+
method: "POST",
|
|
1087
|
+
path: "/file/upload",
|
|
1088
|
+
handler: "editor.uploadFile",
|
|
1089
|
+
config: {
|
|
1090
|
+
description: "Upload file to Strapi Media Library (for Attaches)",
|
|
1091
|
+
auth: false,
|
|
1092
|
+
policies: []
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
]
|
|
1096
|
+
};
|
|
1097
|
+
const admin = admin$1;
|
|
1098
|
+
const contentApi = contentApi$1;
|
|
1099
|
+
var routes$1 = {
|
|
1100
|
+
admin,
|
|
1101
|
+
"content-api": contentApi
|
|
1102
|
+
};
|
|
1103
|
+
const ogs = require$$0__default.default;
|
|
1104
|
+
const fs = require$$1__default.default;
|
|
1105
|
+
const path = require$$2__default.default;
|
|
1106
|
+
const https = require$$3__default.default;
|
|
1107
|
+
const http = require$$4__default.default;
|
|
1108
|
+
const { URL } = require$$5__default.default;
|
|
1109
|
+
var editorService$1 = ({ strapi: strapi2 }) => ({
|
|
1110
|
+
/**
|
|
1111
|
+
* Fetch OpenGraph metadata for a URL
|
|
1112
|
+
* @param {string} url - URL to fetch metadata from
|
|
1113
|
+
* @returns {object} EditorJS compatible link data
|
|
1114
|
+
*/
|
|
1115
|
+
async fetchLinkMeta(url) {
|
|
1116
|
+
try {
|
|
1117
|
+
const options = {
|
|
1118
|
+
url,
|
|
1119
|
+
timeout: 1e4,
|
|
1120
|
+
fetchOptions: {
|
|
1121
|
+
headers: {
|
|
1122
|
+
"User-Agent": "Mozilla/5.0 (compatible; MagicEditorX/1.0)"
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
const { result, error } = await ogs(options);
|
|
1127
|
+
if (error) {
|
|
1128
|
+
strapi2.log.warn("[Magic Editor X] OGS error:", error);
|
|
1129
|
+
return {
|
|
1130
|
+
success: 1,
|
|
1131
|
+
meta: {
|
|
1132
|
+
title: url,
|
|
1133
|
+
description: "",
|
|
1134
|
+
image: void 0
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
let imageUrl = void 0;
|
|
1139
|
+
if (result.ogImage) {
|
|
1140
|
+
if (Array.isArray(result.ogImage) && result.ogImage.length > 0) {
|
|
1141
|
+
imageUrl = { url: result.ogImage[0].url };
|
|
1142
|
+
} else if (result.ogImage.url) {
|
|
1143
|
+
imageUrl = { url: result.ogImage.url };
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return {
|
|
1147
|
+
success: 1,
|
|
1148
|
+
meta: {
|
|
1149
|
+
title: result.ogTitle || result.dcTitle || url,
|
|
1150
|
+
description: result.ogDescription || result.dcDescription || "",
|
|
1151
|
+
image: imageUrl,
|
|
1152
|
+
siteName: result.ogSiteName || "",
|
|
1153
|
+
url: result.ogUrl || url
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
strapi2.log.error("[Magic Editor X] Link meta fetch error:", error);
|
|
1158
|
+
return {
|
|
1159
|
+
success: 1,
|
|
1160
|
+
meta: {
|
|
1161
|
+
title: url,
|
|
1162
|
+
description: "",
|
|
1163
|
+
image: void 0
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
},
|
|
1168
|
+
/**
|
|
1169
|
+
* Upload image from multipart form data
|
|
1170
|
+
* @param {object} ctx - Koa context
|
|
1171
|
+
* @returns {object} EditorJS compatible upload result
|
|
1172
|
+
*/
|
|
1173
|
+
async uploadByFile(ctx) {
|
|
1174
|
+
try {
|
|
1175
|
+
const { files: files2 } = ctx.request;
|
|
1176
|
+
if (!files2 || !files2["files.image"]) {
|
|
1177
|
+
throw new Error("No file provided");
|
|
1178
|
+
}
|
|
1179
|
+
const file = files2["files.image"];
|
|
1180
|
+
const uploadService = strapi2.plugin("upload").service("upload");
|
|
1181
|
+
const uploadedFiles = await uploadService.upload({
|
|
1182
|
+
data: {},
|
|
1183
|
+
files: Array.isArray(file) ? file : [file]
|
|
1184
|
+
});
|
|
1185
|
+
const uploadedFile = uploadedFiles[0];
|
|
1186
|
+
return {
|
|
1187
|
+
success: 1,
|
|
1188
|
+
file: {
|
|
1189
|
+
url: uploadedFile.url,
|
|
1190
|
+
name: uploadedFile.name,
|
|
1191
|
+
size: uploadedFile.size,
|
|
1192
|
+
width: uploadedFile.width,
|
|
1193
|
+
height: uploadedFile.height,
|
|
1194
|
+
mime: uploadedFile.mime,
|
|
1195
|
+
formats: uploadedFile.formats
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
strapi2.log.error("[Magic Editor X] File upload error:", error);
|
|
1200
|
+
throw error;
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
/**
|
|
1204
|
+
* Download and upload image from URL
|
|
1205
|
+
* @param {string} imageUrl - URL of image to download
|
|
1206
|
+
* @returns {object} EditorJS compatible upload result
|
|
1207
|
+
*/
|
|
1208
|
+
async uploadByUrl(imageUrl) {
|
|
1209
|
+
try {
|
|
1210
|
+
const parsedUrl = new URL(imageUrl);
|
|
1211
|
+
const pathname = parsedUrl.pathname;
|
|
1212
|
+
const ext = path.extname(pathname) || ".jpg";
|
|
1213
|
+
const name2 = path.basename(pathname, ext) || "image";
|
|
1214
|
+
const filename = `${name2}${ext}`;
|
|
1215
|
+
const tempDir = path.join(strapi2.dirs.static.public, "uploads", "temp");
|
|
1216
|
+
if (!fs.existsSync(tempDir)) {
|
|
1217
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1218
|
+
}
|
|
1219
|
+
const tempFilePath = path.join(tempDir, `${Date.now()}-${filename}`);
|
|
1220
|
+
const buffer = await this.downloadFile(imageUrl);
|
|
1221
|
+
await fs.promises.writeFile(tempFilePath, buffer);
|
|
1222
|
+
const stats = await fs.promises.stat(tempFilePath);
|
|
1223
|
+
const fileData = {
|
|
1224
|
+
path: tempFilePath,
|
|
1225
|
+
name: filename,
|
|
1226
|
+
type: this.getMimeType(ext),
|
|
1227
|
+
size: stats.size
|
|
1228
|
+
};
|
|
1229
|
+
const uploadService = strapi2.plugin("upload").service("upload");
|
|
1230
|
+
const uploadedFiles = await uploadService.upload({
|
|
1231
|
+
data: {},
|
|
1232
|
+
files: fileData
|
|
1233
|
+
});
|
|
1234
|
+
try {
|
|
1235
|
+
await fs.promises.unlink(tempFilePath);
|
|
1236
|
+
} catch (unlinkError) {
|
|
1237
|
+
strapi2.log.warn("[Magic Editor X] Could not delete temp file:", unlinkError);
|
|
1238
|
+
}
|
|
1239
|
+
const uploadedFile = uploadedFiles[0];
|
|
1240
|
+
return {
|
|
1241
|
+
success: 1,
|
|
1242
|
+
file: {
|
|
1243
|
+
url: uploadedFile.url,
|
|
1244
|
+
name: uploadedFile.name,
|
|
1245
|
+
size: uploadedFile.size,
|
|
1246
|
+
width: uploadedFile.width,
|
|
1247
|
+
height: uploadedFile.height,
|
|
1248
|
+
mime: uploadedFile.mime,
|
|
1249
|
+
formats: uploadedFile.formats
|
|
1250
|
+
}
|
|
1251
|
+
};
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
strapi2.log.error("[Magic Editor X] URL upload error:", error);
|
|
1254
|
+
throw error;
|
|
1255
|
+
}
|
|
1256
|
+
},
|
|
1257
|
+
/**
|
|
1258
|
+
* Download file from URL
|
|
1259
|
+
* @param {string} url - URL to download from
|
|
1260
|
+
* @returns {Promise<Buffer>} File buffer
|
|
1261
|
+
*/
|
|
1262
|
+
downloadFile(url) {
|
|
1263
|
+
return new Promise((resolve, reject) => {
|
|
1264
|
+
const protocol = url.startsWith("https") ? https : http;
|
|
1265
|
+
const request = protocol.get(url, {
|
|
1266
|
+
headers: {
|
|
1267
|
+
"User-Agent": "Mozilla/5.0 (compatible; MagicEditorX/1.0)"
|
|
1268
|
+
}
|
|
1269
|
+
}, (response) => {
|
|
1270
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
1271
|
+
return this.downloadFile(response.headers.location).then(resolve).catch(reject);
|
|
1272
|
+
}
|
|
1273
|
+
if (response.statusCode !== 200) {
|
|
1274
|
+
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
const chunks = [];
|
|
1278
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
1279
|
+
response.on("end", () => resolve(Buffer.concat(chunks)));
|
|
1280
|
+
response.on("error", reject);
|
|
1281
|
+
});
|
|
1282
|
+
request.on("error", reject);
|
|
1283
|
+
request.setTimeout(3e4, () => {
|
|
1284
|
+
request.destroy();
|
|
1285
|
+
reject(new Error("Download timeout"));
|
|
1286
|
+
});
|
|
1287
|
+
});
|
|
1288
|
+
},
|
|
1289
|
+
/**
|
|
1290
|
+
* Get MIME type from file extension
|
|
1291
|
+
* @param {string} ext - File extension
|
|
1292
|
+
* @returns {string} MIME type
|
|
1293
|
+
*/
|
|
1294
|
+
getMimeType(ext) {
|
|
1295
|
+
const mimeTypes = {
|
|
1296
|
+
".jpg": "image/jpeg",
|
|
1297
|
+
".jpeg": "image/jpeg",
|
|
1298
|
+
".png": "image/png",
|
|
1299
|
+
".gif": "image/gif",
|
|
1300
|
+
".webp": "image/webp",
|
|
1301
|
+
".svg": "image/svg+xml",
|
|
1302
|
+
".ico": "image/x-icon",
|
|
1303
|
+
".bmp": "image/bmp",
|
|
1304
|
+
".tiff": "image/tiff",
|
|
1305
|
+
".tif": "image/tiff",
|
|
1306
|
+
// Documents
|
|
1307
|
+
".pdf": "application/pdf",
|
|
1308
|
+
".doc": "application/msword",
|
|
1309
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1310
|
+
".xls": "application/vnd.ms-excel",
|
|
1311
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1312
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
1313
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1314
|
+
// Archives
|
|
1315
|
+
".zip": "application/zip",
|
|
1316
|
+
".rar": "application/vnd.rar",
|
|
1317
|
+
".7z": "application/x-7z-compressed",
|
|
1318
|
+
".tar": "application/x-tar",
|
|
1319
|
+
".gz": "application/gzip",
|
|
1320
|
+
// Text
|
|
1321
|
+
".txt": "text/plain",
|
|
1322
|
+
".csv": "text/csv",
|
|
1323
|
+
".json": "application/json",
|
|
1324
|
+
".xml": "application/xml",
|
|
1325
|
+
// Audio
|
|
1326
|
+
".mp3": "audio/mpeg",
|
|
1327
|
+
".wav": "audio/wav",
|
|
1328
|
+
".ogg": "audio/ogg",
|
|
1329
|
+
// Video
|
|
1330
|
+
".mp4": "video/mp4",
|
|
1331
|
+
".webm": "video/webm",
|
|
1332
|
+
".avi": "video/x-msvideo"
|
|
1333
|
+
};
|
|
1334
|
+
return mimeTypes[ext.toLowerCase()] || "application/octet-stream";
|
|
1335
|
+
},
|
|
1336
|
+
/**
|
|
1337
|
+
* Upload attachment file (for Attaches Tool)
|
|
1338
|
+
* @param {object} ctx - Koa context
|
|
1339
|
+
* @returns {object} EditorJS compatible attachment result
|
|
1340
|
+
*/
|
|
1341
|
+
async uploadAttachment(ctx) {
|
|
1342
|
+
try {
|
|
1343
|
+
const { files: files2 } = ctx.request;
|
|
1344
|
+
const file = files2?.file || files2?.["files.file"] || Object.values(files2 || {})[0];
|
|
1345
|
+
if (!file) {
|
|
1346
|
+
throw new Error("No file provided");
|
|
1347
|
+
}
|
|
1348
|
+
const uploadService = strapi2.plugin("upload").service("upload");
|
|
1349
|
+
const uploadedFiles = await uploadService.upload({
|
|
1350
|
+
data: {},
|
|
1351
|
+
files: Array.isArray(file) ? file : [file]
|
|
1352
|
+
});
|
|
1353
|
+
const uploadedFile = uploadedFiles[0];
|
|
1354
|
+
const ext = path.extname(uploadedFile.name);
|
|
1355
|
+
return {
|
|
1356
|
+
success: 1,
|
|
1357
|
+
file: {
|
|
1358
|
+
url: uploadedFile.url,
|
|
1359
|
+
name: uploadedFile.name,
|
|
1360
|
+
title: uploadedFile.name,
|
|
1361
|
+
size: uploadedFile.size,
|
|
1362
|
+
extension: ext.replace(".", "")
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
strapi2.log.error("[Magic Editor X] Attachment upload error:", error);
|
|
1367
|
+
throw error;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1371
|
+
const { randomUUID } = require$$0__default$1.default;
|
|
1372
|
+
const { Server } = require$$1__default$1.default;
|
|
1373
|
+
const Y = require$$2__default$1.default;
|
|
1374
|
+
const pluginId$1 = "magic-editor-x";
|
|
1375
|
+
const DEFAULT_SOCKET_PATH = "/magic-editor-x/realtime";
|
|
1376
|
+
const DEFAULT_CORS_CONFIG = {
|
|
1377
|
+
origin: "*",
|
|
1378
|
+
methods: ["GET", "POST"],
|
|
1379
|
+
credentials: true
|
|
1380
|
+
};
|
|
1381
|
+
const isPluginIoActive = (strapi2) => {
|
|
1382
|
+
try {
|
|
1383
|
+
return !!strapi2.$io;
|
|
1384
|
+
} catch {
|
|
1385
|
+
return false;
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
var realtimeService$1 = ({ strapi: strapi2 }) => {
|
|
1389
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
1390
|
+
const sessionTokens = /* @__PURE__ */ new Map();
|
|
1391
|
+
let io;
|
|
1392
|
+
let cleanupInterval;
|
|
1393
|
+
const getConfig = () => strapi2.config.get(`plugin::${pluginId$1}`, {});
|
|
1394
|
+
const cleanupStaleRooms = () => {
|
|
1395
|
+
if (!io) return;
|
|
1396
|
+
const now = Date.now();
|
|
1397
|
+
const STALE_THRESHOLD = 60 * 60 * 1e3;
|
|
1398
|
+
let removedCount = 0;
|
|
1399
|
+
rooms.forEach((room, roomId) => {
|
|
1400
|
+
const socketsInRoom = io.sockets.adapter.rooms.get(roomId);
|
|
1401
|
+
const connectionCount = socketsInRoom ? socketsInRoom.size : 0;
|
|
1402
|
+
if (connectionCount === 0) {
|
|
1403
|
+
if (now - room.updatedAt > STALE_THRESHOLD) {
|
|
1404
|
+
room.doc.destroy();
|
|
1405
|
+
rooms.delete(roomId);
|
|
1406
|
+
removedCount++;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
if (removedCount > 0) {
|
|
1411
|
+
strapi2.log.info(`[Magic Editor X] [CLEANUP] Removed ${removedCount} stale rooms`);
|
|
1412
|
+
}
|
|
1413
|
+
};
|
|
1414
|
+
if (!cleanupInterval) {
|
|
1415
|
+
cleanupInterval = setInterval(cleanupStaleRooms, 15 * 60 * 1e3);
|
|
1416
|
+
}
|
|
1417
|
+
const ensureRoom = (roomId) => {
|
|
1418
|
+
if (!rooms.has(roomId)) {
|
|
1419
|
+
const doc = new Y.Doc();
|
|
1420
|
+
rooms.set(roomId, {
|
|
1421
|
+
roomId,
|
|
1422
|
+
doc,
|
|
1423
|
+
initialized: false,
|
|
1424
|
+
createdAt: Date.now(),
|
|
1425
|
+
updatedAt: Date.now(),
|
|
1426
|
+
meta: {}
|
|
1427
|
+
});
|
|
1428
|
+
doc.on("update", () => {
|
|
1429
|
+
const room = rooms.get(roomId);
|
|
1430
|
+
if (room) {
|
|
1431
|
+
room.initialized = true;
|
|
1432
|
+
room.updatedAt = Date.now();
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
return rooms.get(roomId);
|
|
1437
|
+
};
|
|
1438
|
+
const initializeDoc = (roomId, initialValue) => {
|
|
1439
|
+
const room = ensureRoom(roomId);
|
|
1440
|
+
const blocksMap = room.doc.getMap("blocks");
|
|
1441
|
+
const isDocEmpty = blocksMap.size === 0;
|
|
1442
|
+
if (!initialValue || !isDocEmpty && room.initialized) {
|
|
1443
|
+
return room;
|
|
1444
|
+
}
|
|
1445
|
+
try {
|
|
1446
|
+
const data = JSON.parse(initialValue);
|
|
1447
|
+
const blocks = data?.blocks || [];
|
|
1448
|
+
const time = data?.time || Date.now();
|
|
1449
|
+
room.doc.transact(() => {
|
|
1450
|
+
const metaMap = room.doc.getMap("meta");
|
|
1451
|
+
for (const block of blocks) {
|
|
1452
|
+
if (block.id) {
|
|
1453
|
+
blocksMap.set(block.id, JSON.stringify(block));
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
metaMap.set("time", time);
|
|
1457
|
+
metaMap.set("blockOrder", JSON.stringify(blocks.map((b) => b.id)));
|
|
1458
|
+
}, "bootstrap");
|
|
1459
|
+
room.initialized = true;
|
|
1460
|
+
strapi2.log.info(`[Magic Editor X] [INIT] Initialized room ${roomId} with ${blocks.length} blocks`);
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
strapi2.log.error(`[Magic Editor X] Failed to initialize Y.Doc for room ${roomId}`, error);
|
|
1463
|
+
}
|
|
1464
|
+
return room;
|
|
1465
|
+
};
|
|
1466
|
+
const getStateUpdate = (roomId) => {
|
|
1467
|
+
const room = ensureRoom(roomId);
|
|
1468
|
+
try {
|
|
1469
|
+
return Y.encodeStateAsUpdate(room.doc);
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
strapi2.log.error(`[Magic Editor X] Failed to encode state for room ${roomId}`, error);
|
|
1472
|
+
return null;
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
const applyUpdate2 = (roomId, update, origin = "remote") => {
|
|
1476
|
+
if (!update) {
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
const room = ensureRoom(roomId);
|
|
1480
|
+
try {
|
|
1481
|
+
Y.applyUpdate(room.doc, update, origin);
|
|
1482
|
+
} catch (error) {
|
|
1483
|
+
strapi2.log.error(`[Magic Editor X] Failed to apply update for room ${roomId}`, error);
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
const issueSession = ({ roomId, fieldName, meta = {}, user, initialValue = "" }) => {
|
|
1487
|
+
const pluginConfig = getConfig();
|
|
1488
|
+
const collabConfig = pluginConfig.collaboration || {};
|
|
1489
|
+
if (collabConfig.enabled === false) {
|
|
1490
|
+
throw new Error("collaboration-disabled");
|
|
1491
|
+
}
|
|
1492
|
+
initializeDoc(roomId, initialValue);
|
|
1493
|
+
const token = randomUUID();
|
|
1494
|
+
const expiresAt = Date.now() + (collabConfig.sessionTTL || 2 * 60 * 1e3);
|
|
1495
|
+
sessionTokens.set(token, {
|
|
1496
|
+
token,
|
|
1497
|
+
roomId,
|
|
1498
|
+
fieldName,
|
|
1499
|
+
meta,
|
|
1500
|
+
user: {
|
|
1501
|
+
id: user.id,
|
|
1502
|
+
firstname: user.firstname,
|
|
1503
|
+
lastname: user.lastname,
|
|
1504
|
+
email: user.email,
|
|
1505
|
+
roles: user.roles?.map((role) => ({
|
|
1506
|
+
id: role.id,
|
|
1507
|
+
code: role.code,
|
|
1508
|
+
name: role.name
|
|
1509
|
+
})) || []
|
|
1510
|
+
},
|
|
1511
|
+
expiresAt
|
|
1512
|
+
});
|
|
1513
|
+
const actualPath = io?._magicEditorPath || collabConfig.wsPath || DEFAULT_SOCKET_PATH;
|
|
1514
|
+
return {
|
|
1515
|
+
token,
|
|
1516
|
+
roomId,
|
|
1517
|
+
expiresAt,
|
|
1518
|
+
wsPath: actualPath,
|
|
1519
|
+
wsUrl: collabConfig.wsUrl || void 0,
|
|
1520
|
+
approvals: {
|
|
1521
|
+
roleApproved: true
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
};
|
|
1525
|
+
const consumeSessionToken = (token) => {
|
|
1526
|
+
if (!token) {
|
|
1527
|
+
return null;
|
|
1528
|
+
}
|
|
1529
|
+
const session = sessionTokens.get(token);
|
|
1530
|
+
if (!session) {
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
if (session.expiresAt < Date.now()) {
|
|
1534
|
+
sessionTokens.delete(token);
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
sessionTokens.delete(token);
|
|
1538
|
+
return session;
|
|
1539
|
+
};
|
|
1540
|
+
const initSocketServer = () => {
|
|
1541
|
+
const pluginConfig = getConfig();
|
|
1542
|
+
const collabConfig = pluginConfig.collaboration || {};
|
|
1543
|
+
if (collabConfig.enabled === false) {
|
|
1544
|
+
strapi2.log.info("[Magic Editor X] Realtime server disabled (collaboration.enabled=false)");
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
if (io) {
|
|
1548
|
+
return io;
|
|
1549
|
+
}
|
|
1550
|
+
const httpServer = strapi2.server.httpServer;
|
|
1551
|
+
if (!httpServer) {
|
|
1552
|
+
strapi2.log.warn("[Magic Editor X] HTTP server not ready. Realtime collaboration skipped.");
|
|
1553
|
+
return null;
|
|
1554
|
+
}
|
|
1555
|
+
if (isPluginIoActive(strapi2)) {
|
|
1556
|
+
strapi2.log.info("[Magic Editor X] [INFO] strapi-plugin-io detected - using separate namespace");
|
|
1557
|
+
}
|
|
1558
|
+
const wsPath = collabConfig.wsPath || DEFAULT_SOCKET_PATH;
|
|
1559
|
+
if (wsPath === "/socket.io") {
|
|
1560
|
+
strapi2.log.warn('[Magic Editor X] [WARNING] wsPath "/socket.io" conflicts with strapi-plugin-io!');
|
|
1561
|
+
strapi2.log.warn("[Magic Editor X] Using default path instead: " + DEFAULT_SOCKET_PATH);
|
|
1562
|
+
}
|
|
1563
|
+
const finalPath = wsPath === "/socket.io" ? DEFAULT_SOCKET_PATH : wsPath;
|
|
1564
|
+
strapi2.log.info(`[Magic Editor X] [SOCKET] Starting Socket.io server on path: ${finalPath}`);
|
|
1565
|
+
io = new Server(httpServer, {
|
|
1566
|
+
path: finalPath,
|
|
1567
|
+
cors: collabConfig.cors || DEFAULT_CORS_CONFIG,
|
|
1568
|
+
transports: ["websocket", "polling"],
|
|
1569
|
+
allowEIO3: true,
|
|
1570
|
+
// Backward compatibility
|
|
1571
|
+
// Avoid conflicts with other Socket.io instances
|
|
1572
|
+
serveClient: false,
|
|
1573
|
+
// Don't serve socket.io client files
|
|
1574
|
+
connectTimeout: 45e3
|
|
1575
|
+
});
|
|
1576
|
+
io._magicEditorPath = finalPath;
|
|
1577
|
+
io.on("connection", (socket) => {
|
|
1578
|
+
const token = socket.handshake.auth?.token;
|
|
1579
|
+
strapi2.log.info(`[Magic Editor X] [SOCKET] Client connecting with token: ${token ? "valid" : "missing"}`);
|
|
1580
|
+
const session = consumeSessionToken(token);
|
|
1581
|
+
if (!session) {
|
|
1582
|
+
strapi2.log.warn("[Magic Editor X] [WARNING] Invalid or expired token");
|
|
1583
|
+
socket.emit("collab:error", { code: "INVALID_TOKEN", message: "Invalid or expired session token" });
|
|
1584
|
+
socket.disconnect(true);
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
const { roomId, user } = session;
|
|
1588
|
+
socket.data.user = user;
|
|
1589
|
+
socket.data.roomId = roomId;
|
|
1590
|
+
socket.join(roomId);
|
|
1591
|
+
strapi2.log.info(`[Magic Editor X] [SUCCESS] User ${user.email} joined room: ${roomId}`);
|
|
1592
|
+
const initialState = getStateUpdate(roomId);
|
|
1593
|
+
if (initialState) {
|
|
1594
|
+
const stateArray = Array.from(initialState);
|
|
1595
|
+
socket.emit("collab:sync", stateArray);
|
|
1596
|
+
strapi2.log.info(`[Magic Editor X] [SYNC] Sent initial state (${stateArray.length} bytes)`);
|
|
1597
|
+
}
|
|
1598
|
+
const socketsInRoom = io.sockets.adapter.rooms.get(roomId);
|
|
1599
|
+
if (socketsInRoom) {
|
|
1600
|
+
const existingPeers = [];
|
|
1601
|
+
for (const socketId of socketsInRoom) {
|
|
1602
|
+
const peerSocket = io.sockets.sockets.get(socketId);
|
|
1603
|
+
if (peerSocket && peerSocket.data.user && peerSocket.id !== socket.id) {
|
|
1604
|
+
existingPeers.push(peerSocket.data.user);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
existingPeers.forEach((peerUser) => {
|
|
1608
|
+
socket.emit("collab:presence", { type: "join", user: peerUser });
|
|
1609
|
+
});
|
|
1610
|
+
strapi2.log.info(`[Magic Editor X] [PEERS] Sent ${existingPeers.length} existing peers to new user`);
|
|
1611
|
+
}
|
|
1612
|
+
socket.to(roomId).emit("collab:presence", { type: "join", user });
|
|
1613
|
+
socket.on("collab:update", (update) => {
|
|
1614
|
+
try {
|
|
1615
|
+
const updateBuffer = new Uint8Array(update);
|
|
1616
|
+
applyUpdate2(roomId, updateBuffer, "remote");
|
|
1617
|
+
socket.to(roomId).emit("collab:update", update);
|
|
1618
|
+
strapi2.log.debug(`[Magic Editor X] [BROADCAST] Update broadcast to room ${roomId}`);
|
|
1619
|
+
} catch (error) {
|
|
1620
|
+
strapi2.log.error("[Magic Editor X] Failed to process update:", error);
|
|
1621
|
+
socket.emit("collab:error", { code: "UPDATE_FAILED", message: "Failed to process update" });
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
socket.on("collab:awareness", (payload) => {
|
|
1625
|
+
socket.to(roomId).emit("collab:awareness", { user, payload });
|
|
1626
|
+
});
|
|
1627
|
+
socket.on("disconnect", (reason) => {
|
|
1628
|
+
strapi2.log.info(`[Magic Editor X] [DISCONNECT] User ${user.email} left room ${roomId} (${reason})`);
|
|
1629
|
+
socket.to(roomId).emit("collab:presence", { type: "leave", user });
|
|
1630
|
+
});
|
|
1631
|
+
socket.on("error", (error) => {
|
|
1632
|
+
strapi2.log.error("[Magic Editor X] Socket error:", error);
|
|
1633
|
+
});
|
|
1634
|
+
});
|
|
1635
|
+
strapi2.log.info("[Magic Editor X] [SUCCESS] Realtime collaboration server ready");
|
|
1636
|
+
return io;
|
|
1637
|
+
};
|
|
1638
|
+
const close = async () => {
|
|
1639
|
+
if (cleanupInterval) {
|
|
1640
|
+
clearInterval(cleanupInterval);
|
|
1641
|
+
cleanupInterval = null;
|
|
1642
|
+
}
|
|
1643
|
+
if (io) {
|
|
1644
|
+
await io.close();
|
|
1645
|
+
io = null;
|
|
1646
|
+
}
|
|
1647
|
+
sessionTokens.clear();
|
|
1648
|
+
rooms.forEach((room) => room.doc.destroy());
|
|
1649
|
+
rooms.clear();
|
|
1650
|
+
};
|
|
1651
|
+
return {
|
|
1652
|
+
issueSession,
|
|
1653
|
+
consumeSessionToken,
|
|
1654
|
+
applyUpdate: applyUpdate2,
|
|
1655
|
+
initSocketServer,
|
|
1656
|
+
close
|
|
1657
|
+
};
|
|
1658
|
+
};
|
|
1659
|
+
const pluginId = "magic-editor-x";
|
|
1660
|
+
const getRoleCodes = (user) => {
|
|
1661
|
+
if (!user?.roles) {
|
|
1662
|
+
return [];
|
|
1663
|
+
}
|
|
1664
|
+
return user.roles.map((role) => role?.code || role?.name).filter(Boolean);
|
|
1665
|
+
};
|
|
1666
|
+
var accessService$1 = ({ strapi: strapi2 }) => {
|
|
1667
|
+
const getConfig = () => strapi2.config.get(`plugin::${pluginId}`, {});
|
|
1668
|
+
return {
|
|
1669
|
+
/**
|
|
1670
|
+
* Prüft ob ein User Collaboration nutzen darf
|
|
1671
|
+
* Standard: Nur Super Admins haben automatisch Zugriff
|
|
1672
|
+
* Alle anderen brauchen explizite Freigabe über collab-permission
|
|
1673
|
+
*
|
|
1674
|
+
* @param {Object} user - Der Admin User
|
|
1675
|
+
* @param {string} contentType - Optional: Der spezifische Content Type (z.B. 'api::article.article')
|
|
1676
|
+
*/
|
|
1677
|
+
async canUseCollaboration(user, contentType = null) {
|
|
1678
|
+
if (!user) {
|
|
1679
|
+
strapi2.log.warn("[Access Service] No user provided");
|
|
1680
|
+
return { allowed: false, reason: "not-authenticated", role: null };
|
|
1681
|
+
}
|
|
1682
|
+
strapi2.log.info("[Access Service] Checking access for user:", user.email, "contentType:", contentType);
|
|
1683
|
+
const config2 = getConfig();
|
|
1684
|
+
const collab = config2.collaboration || {};
|
|
1685
|
+
if (collab.enabled === false) {
|
|
1686
|
+
strapi2.log.info("[Access Service] Collaboration is disabled");
|
|
1687
|
+
return { allowed: false, reason: "collaboration-disabled", role: null };
|
|
1688
|
+
}
|
|
1689
|
+
const userRoleCodes = getRoleCodes(user);
|
|
1690
|
+
const isSuperAdmin = userRoleCodes.includes("strapi-super-admin");
|
|
1691
|
+
strapi2.log.info("[Access Service] User roles:", userRoleCodes, "isSuperAdmin:", isSuperAdmin);
|
|
1692
|
+
if (isSuperAdmin) {
|
|
1693
|
+
strapi2.log.info("[Access Service] [SUCCESS] Super Admin access granted");
|
|
1694
|
+
return { allowed: true, reason: "super-admin", role: "owner" };
|
|
1695
|
+
}
|
|
1696
|
+
try {
|
|
1697
|
+
const permissions = await strapi2.documents("plugin::magic-editor-x.collab-permission").findMany({
|
|
1698
|
+
filters: {
|
|
1699
|
+
user: { id: user.id }
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
strapi2.log.info("[Access Service] Found permissions:", permissions?.length || 0);
|
|
1703
|
+
if (permissions && permissions.length > 0) {
|
|
1704
|
+
let bestPermission = null;
|
|
1705
|
+
for (const perm of permissions) {
|
|
1706
|
+
strapi2.log.info("[Access Service] Checking permission:", {
|
|
1707
|
+
permContentType: perm.contentType,
|
|
1708
|
+
requestedContentType: contentType,
|
|
1709
|
+
role: perm.role,
|
|
1710
|
+
expiresAt: perm.expiresAt
|
|
1711
|
+
});
|
|
1712
|
+
if (perm.expiresAt && new Date(perm.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
1713
|
+
strapi2.log.info("[Access Service] Permission expired, skipping");
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
if (!perm.contentType || perm.contentType === "*" || perm.contentType === "") {
|
|
1717
|
+
strapi2.log.info("[Access Service] ✅ Global permission found");
|
|
1718
|
+
if (!bestPermission || this.getRoleLevel(perm.role) > this.getRoleLevel(bestPermission.role)) {
|
|
1719
|
+
bestPermission = perm;
|
|
1720
|
+
}
|
|
1721
|
+
} else if (contentType && perm.contentType === contentType) {
|
|
1722
|
+
strapi2.log.info("[Access Service] ✅ Specific content type match");
|
|
1723
|
+
bestPermission = perm;
|
|
1724
|
+
break;
|
|
1725
|
+
} else if (!contentType || contentType === "unknown") {
|
|
1726
|
+
strapi2.log.info("[Access Service] ✅ Unknown/null contentType - accepting any permission");
|
|
1727
|
+
if (!bestPermission || this.getRoleLevel(perm.role) > this.getRoleLevel(bestPermission.role)) {
|
|
1728
|
+
bestPermission = perm;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
if (bestPermission) {
|
|
1733
|
+
strapi2.log.info("[Access Service] ✅ Permission granted via collab-permission, role:", bestPermission.role);
|
|
1734
|
+
return {
|
|
1735
|
+
allowed: true,
|
|
1736
|
+
reason: "explicit-permission",
|
|
1737
|
+
role: bestPermission.role,
|
|
1738
|
+
permission: bestPermission
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
strapi2.log.error("[Access Service] Error checking permissions:", error);
|
|
1744
|
+
}
|
|
1745
|
+
strapi2.log.info("[Access Service] [DENIED] No permission found for user");
|
|
1746
|
+
return { allowed: false, reason: "permission-required", role: null };
|
|
1747
|
+
},
|
|
1748
|
+
/**
|
|
1749
|
+
* Hilfsfunktion: Gibt Rollen-Level zurück (höher = mehr Rechte)
|
|
1750
|
+
*/
|
|
1751
|
+
getRoleLevel(role) {
|
|
1752
|
+
const levels = { viewer: 1, editor: 2, owner: 3 };
|
|
1753
|
+
return levels[role] || 0;
|
|
1754
|
+
},
|
|
1755
|
+
/**
|
|
1756
|
+
* Checks if a new collaborator can be added based on license limits
|
|
1757
|
+
* @returns {Promise<object>} Result with canAdd, current, max, and unlimited flags
|
|
1758
|
+
*/
|
|
1759
|
+
async checkCollaboratorLimit() {
|
|
1760
|
+
try {
|
|
1761
|
+
const licenseService2 = strapi2.plugin("magic-editor-x").service("licenseService");
|
|
1762
|
+
return await licenseService2.canAddCollaborator();
|
|
1763
|
+
} catch (error) {
|
|
1764
|
+
strapi2.log.error("[Access Service] Error checking collaborator limit:", error);
|
|
1765
|
+
return {
|
|
1766
|
+
canAdd: true,
|
|
1767
|
+
current: 0,
|
|
1768
|
+
max: 2,
|
|
1769
|
+
unlimited: false,
|
|
1770
|
+
error: true
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
},
|
|
1774
|
+
/**
|
|
1775
|
+
* Prüft ob User Zugriff auf einen bestimmten Room hat
|
|
1776
|
+
*/
|
|
1777
|
+
async canAccessRoom(userId, roomId, action = "view") {
|
|
1778
|
+
try {
|
|
1779
|
+
const user = await strapi2.query("admin::user").findOne({
|
|
1780
|
+
where: { id: userId },
|
|
1781
|
+
populate: ["roles"]
|
|
1782
|
+
});
|
|
1783
|
+
if (!user) {
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
const userRoleCodes = getRoleCodes(user);
|
|
1787
|
+
const isSuperAdmin = userRoleCodes.includes("strapi-super-admin");
|
|
1788
|
+
if (isSuperAdmin) {
|
|
1789
|
+
return true;
|
|
1790
|
+
}
|
|
1791
|
+
let contentTypeFromRoom = null;
|
|
1792
|
+
if (roomId) {
|
|
1793
|
+
const parts = roomId.split("|");
|
|
1794
|
+
if (parts.length >= 1 && parts[0]?.includes("::")) {
|
|
1795
|
+
contentTypeFromRoom = parts[0];
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
const permissions = await strapi2.documents("plugin::magic-editor-x.collab-permission").findMany({
|
|
1799
|
+
filters: {
|
|
1800
|
+
user: { id: userId }
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
if (!permissions || permissions.length === 0) {
|
|
1804
|
+
return false;
|
|
1805
|
+
}
|
|
1806
|
+
const hasValidPermission = permissions.some((perm) => {
|
|
1807
|
+
if (!perm.contentType || perm.contentType === "*") {
|
|
1808
|
+
return true;
|
|
1809
|
+
}
|
|
1810
|
+
if (contentTypeFromRoom && perm.contentType === contentTypeFromRoom) {
|
|
1811
|
+
return true;
|
|
1812
|
+
}
|
|
1813
|
+
return false;
|
|
1814
|
+
});
|
|
1815
|
+
if (!hasValidPermission) {
|
|
1816
|
+
return false;
|
|
1817
|
+
}
|
|
1818
|
+
const permission = permissions[0];
|
|
1819
|
+
if (action === "view") {
|
|
1820
|
+
return ["viewer", "editor", "owner"].includes(permission.role);
|
|
1821
|
+
}
|
|
1822
|
+
if (action === "edit") {
|
|
1823
|
+
return ["editor", "owner"].includes(permission.role);
|
|
1824
|
+
}
|
|
1825
|
+
if (action === "manage") {
|
|
1826
|
+
return permission.role === "owner";
|
|
1827
|
+
}
|
|
1828
|
+
return false;
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
strapi2.log.error("[Access Service] Error checking room access:", error);
|
|
1831
|
+
return false;
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
};
|
|
1836
|
+
const { encodeStateAsUpdate, encodeStateVector, applyUpdate } = require$$2__default$1.default;
|
|
1837
|
+
var snapshotService$1 = ({ strapi: strapi2 }) => ({
|
|
1838
|
+
/**
|
|
1839
|
+
* Create snapshot from Y.Doc
|
|
1840
|
+
*/
|
|
1841
|
+
async createSnapshot(roomId, contentType, entryId, fieldName, ydoc, userId) {
|
|
1842
|
+
try {
|
|
1843
|
+
const latestSnapshots = await strapi2.documents("plugin::magic-editor-x.document-snapshot").findMany({
|
|
1844
|
+
filters: { roomId },
|
|
1845
|
+
sort: [{ version: "desc" }],
|
|
1846
|
+
limit: 1
|
|
1847
|
+
});
|
|
1848
|
+
const nextVersion = latestSnapshots?.[0]?.version ? latestSnapshots[0].version + 1 : 1;
|
|
1849
|
+
const yjsState = encodeStateAsUpdate(ydoc);
|
|
1850
|
+
const yjsSnapshot = Buffer.from(yjsState).toString("base64");
|
|
1851
|
+
let jsonContent = null;
|
|
1852
|
+
try {
|
|
1853
|
+
const text = ydoc.getText("content");
|
|
1854
|
+
jsonContent = text.toString();
|
|
1855
|
+
} catch (e) {
|
|
1856
|
+
strapi2.log.warn("[Snapshot] Could not extract JSON content:", e);
|
|
1857
|
+
}
|
|
1858
|
+
const snapshot = await strapi2.documents("plugin::magic-editor-x.document-snapshot").create({
|
|
1859
|
+
data: {
|
|
1860
|
+
roomId,
|
|
1861
|
+
contentType,
|
|
1862
|
+
entryId,
|
|
1863
|
+
fieldName,
|
|
1864
|
+
version: nextVersion,
|
|
1865
|
+
yjsSnapshot,
|
|
1866
|
+
jsonContent: jsonContent ? JSON.parse(jsonContent) : null,
|
|
1867
|
+
createdBy: userId,
|
|
1868
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
strapi2.log.info(`[Snapshot] Created v${nextVersion} for ${roomId}`);
|
|
1872
|
+
return snapshot;
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
strapi2.log.error("[Snapshot] Error creating snapshot:", error);
|
|
1875
|
+
throw error;
|
|
1876
|
+
}
|
|
1877
|
+
},
|
|
1878
|
+
/**
|
|
1879
|
+
* List snapshots for a room
|
|
1880
|
+
*/
|
|
1881
|
+
async listSnapshots(roomId, limit = 50) {
|
|
1882
|
+
return await strapi2.documents("plugin::magic-editor-x.document-snapshot").findMany({
|
|
1883
|
+
filters: { roomId },
|
|
1884
|
+
sort: [{ version: "desc" }],
|
|
1885
|
+
limit,
|
|
1886
|
+
populate: ["createdBy"]
|
|
1887
|
+
});
|
|
1888
|
+
},
|
|
1889
|
+
/**
|
|
1890
|
+
* Restore snapshot to Y.Doc
|
|
1891
|
+
* Note: Uses documentId instead of numeric id
|
|
1892
|
+
*/
|
|
1893
|
+
async restoreSnapshot(snapshotDocumentId, ydoc) {
|
|
1894
|
+
try {
|
|
1895
|
+
const snapshot = await strapi2.documents("plugin::magic-editor-x.document-snapshot").findOne({
|
|
1896
|
+
documentId: snapshotDocumentId
|
|
1897
|
+
});
|
|
1898
|
+
if (!snapshot) {
|
|
1899
|
+
throw new Error("Snapshot not found");
|
|
1900
|
+
}
|
|
1901
|
+
const yjsState = Buffer.from(snapshot.yjsSnapshot, "base64");
|
|
1902
|
+
applyUpdate(ydoc, yjsState);
|
|
1903
|
+
strapi2.log.info(`[Snapshot] Restored v${snapshot.version} for ${snapshot.roomId}`);
|
|
1904
|
+
return snapshot;
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
strapi2.log.error("[Snapshot] Error restoring snapshot:", error);
|
|
1907
|
+
throw error;
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
/**
|
|
1911
|
+
* Auto-cleanup old snapshots (keep last N versions)
|
|
1912
|
+
*/
|
|
1913
|
+
async cleanupSnapshots(roomId, keepLast = 10) {
|
|
1914
|
+
try {
|
|
1915
|
+
const snapshots = await this.listSnapshots(roomId, 1e3);
|
|
1916
|
+
if (snapshots.length <= keepLast) {
|
|
1917
|
+
return { deleted: 0 };
|
|
1918
|
+
}
|
|
1919
|
+
const toDelete = snapshots.slice(keepLast);
|
|
1920
|
+
for (const snapshot of toDelete) {
|
|
1921
|
+
await strapi2.documents("plugin::magic-editor-x.document-snapshot").delete({
|
|
1922
|
+
documentId: snapshot.documentId
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
strapi2.log.info(`[Snapshot] Cleaned up ${toDelete.length} old snapshots for ${roomId}`);
|
|
1926
|
+
return { deleted: toDelete.length };
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
strapi2.log.error("[Snapshot] Error cleaning up snapshots:", error);
|
|
1929
|
+
throw error;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
});
|
|
1933
|
+
const name = "magic-editor-x";
|
|
1934
|
+
const version = "1.0.1";
|
|
1935
|
+
const description = "Advanced block-based editor for Strapi v5 with Editor.js, Media Library integration, and real-time collaboration support";
|
|
1936
|
+
const keywords = [
|
|
1937
|
+
"strapi",
|
|
1938
|
+
"plugin",
|
|
1939
|
+
"editor-js",
|
|
1940
|
+
"wysiwyg",
|
|
1941
|
+
"block-editor",
|
|
1942
|
+
"strapi-v5"
|
|
1943
|
+
];
|
|
1944
|
+
const type = "commonjs";
|
|
1945
|
+
const exports$1 = {
|
|
1946
|
+
"./package.json": "./package.json",
|
|
1947
|
+
"./strapi-admin": {
|
|
1948
|
+
source: "./admin/src/index.js",
|
|
1949
|
+
"import": "./dist/admin/index.mjs",
|
|
1950
|
+
require: "./dist/admin/index.js",
|
|
1951
|
+
"default": "./dist/admin/index.js"
|
|
1952
|
+
},
|
|
1953
|
+
"./strapi-server": {
|
|
1954
|
+
source: "./server/src/index.js",
|
|
1955
|
+
"import": "./dist/server/index.mjs",
|
|
1956
|
+
require: "./dist/server/index.js",
|
|
1957
|
+
"default": "./dist/server/index.js"
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
const files = [
|
|
1961
|
+
"dist",
|
|
1962
|
+
"README.md",
|
|
1963
|
+
"LICENSE",
|
|
1964
|
+
"pics"
|
|
1965
|
+
];
|
|
1966
|
+
const scripts = {
|
|
1967
|
+
build: "strapi-plugin build",
|
|
1968
|
+
watch: "strapi-plugin watch",
|
|
1969
|
+
"watch:link": "strapi-plugin watch:link",
|
|
1970
|
+
verify: "strapi-plugin verify"
|
|
1971
|
+
};
|
|
1972
|
+
const dependencies = {
|
|
1973
|
+
"@calumk/editorjs-codeflask": "^1.0.10",
|
|
1974
|
+
"@editorjs/attaches": "^1.3.0",
|
|
1975
|
+
"@editorjs/checklist": "^1.6.0",
|
|
1976
|
+
"@editorjs/code": "^2.9.3",
|
|
1977
|
+
"@editorjs/delimiter": "^1.4.2",
|
|
1978
|
+
"@editorjs/editorjs": "^2.31.0",
|
|
1979
|
+
"@editorjs/embed": "^2.7.6",
|
|
1980
|
+
"@editorjs/header": "^2.8.8",
|
|
1981
|
+
"@editorjs/image": "^2.10.1",
|
|
1982
|
+
"@editorjs/inline-code": "^1.5.1",
|
|
1983
|
+
"@editorjs/link": "^2.6.2",
|
|
1984
|
+
"@editorjs/marker": "^1.4.0",
|
|
1985
|
+
"@editorjs/nested-list": "^1.4.3",
|
|
1986
|
+
"@editorjs/paragraph": "^2.11.6",
|
|
1987
|
+
"@editorjs/personality": "^2.0.2",
|
|
1988
|
+
"@editorjs/quote": "^2.7.2",
|
|
1989
|
+
"@editorjs/raw": "^2.5.0",
|
|
1990
|
+
"@editorjs/simple-image": "^1.6.0",
|
|
1991
|
+
"@editorjs/table": "^2.4.2",
|
|
1992
|
+
"@editorjs/text-variant-tune": "^1.0.2",
|
|
1993
|
+
"@editorjs/underline": "^1.1.0",
|
|
1994
|
+
"@editorjs/warning": "^1.4.0",
|
|
1995
|
+
"@heroicons/react": "^2.2.0",
|
|
1996
|
+
"@sotaproject/strikethrough": "^1.0.1",
|
|
1997
|
+
"editorjs-alert": "^1.1.4",
|
|
1998
|
+
"editorjs-drag-drop": "^1.1.16",
|
|
1999
|
+
"editorjs-indent-tune": "^1.4.3",
|
|
2000
|
+
"editorjs-text-alignment-blocktune": "^1.0.3",
|
|
2001
|
+
"editorjs-toggle-block": "^0.3.16",
|
|
2002
|
+
"editorjs-tooltip": "^1.2.2",
|
|
2003
|
+
"editorjs-undo": "^2.0.28",
|
|
2004
|
+
"open-graph-scraper": "^6.8.3",
|
|
2005
|
+
prismjs: "^1.30.0",
|
|
2006
|
+
"socket.io": "^4.8.1",
|
|
2007
|
+
"socket.io-client": "^4.8.1",
|
|
2008
|
+
"y-indexeddb": "^9.0.12",
|
|
2009
|
+
"y-socket.io": "^1.1.3",
|
|
2010
|
+
yjs: "^13.6.15"
|
|
2011
|
+
};
|
|
2012
|
+
const devDependencies = {
|
|
2013
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
2014
|
+
"@semantic-release/commit-analyzer": "^13.0.0",
|
|
2015
|
+
"@semantic-release/git": "^10.0.1",
|
|
2016
|
+
"@semantic-release/github": "^11.0.1",
|
|
2017
|
+
"@semantic-release/npm": "^12.0.1",
|
|
2018
|
+
"@semantic-release/release-notes-generator": "^14.0.1",
|
|
2019
|
+
"@strapi/design-system": "^2.0.0-rc.30",
|
|
2020
|
+
"@strapi/icons": "^2.0.0-rc.30",
|
|
2021
|
+
"@strapi/sdk-plugin": "^5.3.2",
|
|
2022
|
+
"@strapi/strapi": "^5.31.2",
|
|
2023
|
+
prettier: "^3.7.3",
|
|
2024
|
+
react: "^18.3.1",
|
|
2025
|
+
"react-dom": "^18.3.1",
|
|
2026
|
+
"react-router-dom": "^6.30.2",
|
|
2027
|
+
"semantic-release": "^25.0.2",
|
|
2028
|
+
"styled-components": "^6.1.19"
|
|
2029
|
+
};
|
|
2030
|
+
const peerDependencies = {
|
|
2031
|
+
"@strapi/sdk-plugin": "^5.3.2",
|
|
2032
|
+
"@strapi/strapi": "^5.31.2",
|
|
2033
|
+
react: "^18.3.1",
|
|
2034
|
+
"react-dom": "^18.3.1",
|
|
2035
|
+
"react-router-dom": "^6.30.2",
|
|
2036
|
+
"styled-components": "^6.1.19"
|
|
2037
|
+
};
|
|
2038
|
+
const overrides = {
|
|
2039
|
+
prismjs: "^1.30.0"
|
|
2040
|
+
};
|
|
2041
|
+
const strapi = {
|
|
2042
|
+
kind: "plugin",
|
|
2043
|
+
name: "magic-editor-x",
|
|
2044
|
+
displayName: "Magic Editor X",
|
|
2045
|
+
description: "Advanced block-based editor with Editor.js for Strapi v5"
|
|
2046
|
+
};
|
|
2047
|
+
const license = "MIT";
|
|
2048
|
+
const author = "Schero D. <schero1894@gmail.com>";
|
|
2049
|
+
const repository = {
|
|
2050
|
+
type: "git",
|
|
2051
|
+
url: "https://github.com/Schero94/magic-editor-x.git"
|
|
2052
|
+
};
|
|
2053
|
+
const require$$2 = {
|
|
2054
|
+
name,
|
|
2055
|
+
version,
|
|
2056
|
+
description,
|
|
2057
|
+
keywords,
|
|
2058
|
+
type,
|
|
2059
|
+
exports: exports$1,
|
|
2060
|
+
files,
|
|
2061
|
+
scripts,
|
|
2062
|
+
dependencies,
|
|
2063
|
+
devDependencies,
|
|
2064
|
+
peerDependencies,
|
|
2065
|
+
overrides,
|
|
2066
|
+
strapi,
|
|
2067
|
+
license,
|
|
2068
|
+
author,
|
|
2069
|
+
repository
|
|
2070
|
+
};
|
|
2071
|
+
const crypto = require$$0__default$1.default;
|
|
2072
|
+
const os = require$$1__default$2.default;
|
|
2073
|
+
const LICENSE_SERVER_URL = "https://magicapi.fitlex.me";
|
|
2074
|
+
const PLUGIN_NAME = "magic-editor-x";
|
|
2075
|
+
const PRODUCT_NAME = "Magic Editor X - Collaborative Editor";
|
|
2076
|
+
const TIERS = {
|
|
2077
|
+
free: {
|
|
2078
|
+
name: "FREE",
|
|
2079
|
+
maxCollaborators: 2,
|
|
2080
|
+
features: {
|
|
2081
|
+
editor: true,
|
|
2082
|
+
allTools: true,
|
|
2083
|
+
collaboration: true,
|
|
2084
|
+
ai: false
|
|
2085
|
+
}
|
|
2086
|
+
},
|
|
2087
|
+
premium: {
|
|
2088
|
+
name: "PREMIUM",
|
|
2089
|
+
maxCollaborators: 10,
|
|
2090
|
+
features: {
|
|
2091
|
+
editor: true,
|
|
2092
|
+
allTools: true,
|
|
2093
|
+
collaboration: true,
|
|
2094
|
+
ai: true
|
|
2095
|
+
}
|
|
2096
|
+
},
|
|
2097
|
+
advanced: {
|
|
2098
|
+
name: "ADVANCED",
|
|
2099
|
+
maxCollaborators: -1,
|
|
2100
|
+
// Unlimited
|
|
2101
|
+
features: {
|
|
2102
|
+
editor: true,
|
|
2103
|
+
allTools: true,
|
|
2104
|
+
collaboration: true,
|
|
2105
|
+
ai: true
|
|
2106
|
+
}
|
|
2107
|
+
},
|
|
2108
|
+
enterprise: {
|
|
2109
|
+
name: "ENTERPRISE",
|
|
2110
|
+
maxCollaborators: -1,
|
|
2111
|
+
// Unlimited
|
|
2112
|
+
features: {
|
|
2113
|
+
editor: true,
|
|
2114
|
+
allTools: true,
|
|
2115
|
+
collaboration: true,
|
|
2116
|
+
ai: true,
|
|
2117
|
+
prioritySupport: true
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
var licenseService$1 = ({ strapi: strapi2 }) => ({
|
|
2122
|
+
/**
|
|
2123
|
+
* Get license server URL
|
|
2124
|
+
* @returns {string} License server URL
|
|
2125
|
+
*/
|
|
2126
|
+
getLicenseServerUrl() {
|
|
2127
|
+
return LICENSE_SERVER_URL;
|
|
2128
|
+
},
|
|
2129
|
+
/**
|
|
2130
|
+
* Generate unique device ID based on hardware
|
|
2131
|
+
* @returns {string} 32-character device ID
|
|
2132
|
+
*/
|
|
2133
|
+
generateDeviceId() {
|
|
2134
|
+
try {
|
|
2135
|
+
const networkInterfaces = os.networkInterfaces();
|
|
2136
|
+
const macAddresses = [];
|
|
2137
|
+
Object.values(networkInterfaces).forEach((interfaces) => {
|
|
2138
|
+
interfaces?.forEach((iface) => {
|
|
2139
|
+
if (iface.mac && iface.mac !== "00:00:00:00:00:00") {
|
|
2140
|
+
macAddresses.push(iface.mac);
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
});
|
|
2144
|
+
const identifier = `${macAddresses.join("-")}-${os.hostname()}`;
|
|
2145
|
+
return crypto.createHash("sha256").update(identifier).digest("hex").substring(0, 32);
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
return crypto.randomBytes(16).toString("hex");
|
|
2148
|
+
}
|
|
2149
|
+
},
|
|
2150
|
+
/**
|
|
2151
|
+
* Get device hostname
|
|
2152
|
+
* @returns {string} Device name
|
|
2153
|
+
*/
|
|
2154
|
+
getDeviceName() {
|
|
2155
|
+
try {
|
|
2156
|
+
return os.hostname() || "Unknown Device";
|
|
2157
|
+
} catch (error) {
|
|
2158
|
+
return "Unknown Device";
|
|
2159
|
+
}
|
|
2160
|
+
},
|
|
2161
|
+
/**
|
|
2162
|
+
* Get external IP address
|
|
2163
|
+
* @returns {string} IP address
|
|
2164
|
+
*/
|
|
2165
|
+
getIpAddress() {
|
|
2166
|
+
try {
|
|
2167
|
+
const networkInterfaces = os.networkInterfaces();
|
|
2168
|
+
for (const name2 of Object.keys(networkInterfaces)) {
|
|
2169
|
+
const interfaces = networkInterfaces[name2];
|
|
2170
|
+
if (interfaces) {
|
|
2171
|
+
for (const iface of interfaces) {
|
|
2172
|
+
if (iface.family === "IPv4" && !iface.internal) {
|
|
2173
|
+
return iface.address;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
return "127.0.0.1";
|
|
2179
|
+
} catch (error) {
|
|
2180
|
+
return "127.0.0.1";
|
|
2181
|
+
}
|
|
2182
|
+
},
|
|
2183
|
+
/**
|
|
2184
|
+
* Get user agent string for license requests
|
|
2185
|
+
* @returns {string} User agent
|
|
2186
|
+
*/
|
|
2187
|
+
getUserAgent() {
|
|
2188
|
+
try {
|
|
2189
|
+
const pluginPkg = require$$2;
|
|
2190
|
+
const pluginVersion = pluginPkg.version || "1.0.0";
|
|
2191
|
+
const strapiVersion = strapi2.config.get("info.strapi") || "5.0.0";
|
|
2192
|
+
return `MagicEditorX/${pluginVersion} Strapi/${strapiVersion} Node/${process.version} ${os.platform()}/${os.release()}`;
|
|
2193
|
+
} catch (error) {
|
|
2194
|
+
return `MagicEditorX/1.0.0 Node/${process.version}`;
|
|
2195
|
+
}
|
|
2196
|
+
},
|
|
2197
|
+
/**
|
|
2198
|
+
* Create a new license
|
|
2199
|
+
* @param {object} params - License parameters
|
|
2200
|
+
* @param {string} params.email - User email
|
|
2201
|
+
* @param {string} params.firstName - User first name
|
|
2202
|
+
* @param {string} params.lastName - User last name
|
|
2203
|
+
* @returns {Promise<object|null>} Created license or null
|
|
2204
|
+
*/
|
|
2205
|
+
async createLicense({ email, firstName, lastName }) {
|
|
2206
|
+
try {
|
|
2207
|
+
const deviceId = this.generateDeviceId();
|
|
2208
|
+
const deviceName = this.getDeviceName();
|
|
2209
|
+
const ipAddress = this.getIpAddress();
|
|
2210
|
+
const userAgent = this.getUserAgent();
|
|
2211
|
+
const response = await fetch(`${LICENSE_SERVER_URL}/api/licenses/create`, {
|
|
2212
|
+
method: "POST",
|
|
2213
|
+
headers: { "Content-Type": "application/json" },
|
|
2214
|
+
body: JSON.stringify({
|
|
2215
|
+
email,
|
|
2216
|
+
firstName,
|
|
2217
|
+
lastName,
|
|
2218
|
+
deviceName,
|
|
2219
|
+
deviceId,
|
|
2220
|
+
ipAddress,
|
|
2221
|
+
userAgent,
|
|
2222
|
+
pluginName: PLUGIN_NAME,
|
|
2223
|
+
productName: PRODUCT_NAME
|
|
2224
|
+
})
|
|
2225
|
+
});
|
|
2226
|
+
const data = await response.json();
|
|
2227
|
+
if (data.success) {
|
|
2228
|
+
strapi2.log.info(`[Magic Editor X] [SUCCESS] License created: ${data.data.licenseKey}`);
|
|
2229
|
+
return data.data;
|
|
2230
|
+
} else {
|
|
2231
|
+
strapi2.log.error("[Magic Editor X] [ERROR] License creation failed:", data);
|
|
2232
|
+
return null;
|
|
2233
|
+
}
|
|
2234
|
+
} catch (error) {
|
|
2235
|
+
strapi2.log.error("[Magic Editor X] [ERROR] Error creating license:", error);
|
|
2236
|
+
return null;
|
|
2237
|
+
}
|
|
2238
|
+
},
|
|
2239
|
+
/**
|
|
2240
|
+
* Verify a license key
|
|
2241
|
+
* @param {string} licenseKey - License key to verify
|
|
2242
|
+
* @param {boolean} allowGracePeriod - Allow offline grace period
|
|
2243
|
+
* @returns {Promise<object>} Verification result
|
|
2244
|
+
*/
|
|
2245
|
+
async verifyLicense(licenseKey, allowGracePeriod = false) {
|
|
2246
|
+
try {
|
|
2247
|
+
const controller = new AbortController();
|
|
2248
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
2249
|
+
const response = await fetch(`${LICENSE_SERVER_URL}/api/licenses/verify`, {
|
|
2250
|
+
method: "POST",
|
|
2251
|
+
headers: { "Content-Type": "application/json" },
|
|
2252
|
+
body: JSON.stringify({
|
|
2253
|
+
licenseKey,
|
|
2254
|
+
pluginName: PLUGIN_NAME,
|
|
2255
|
+
productName: PRODUCT_NAME
|
|
2256
|
+
}),
|
|
2257
|
+
signal: controller.signal
|
|
2258
|
+
});
|
|
2259
|
+
clearTimeout(timeoutId);
|
|
2260
|
+
const data = await response.json();
|
|
2261
|
+
if (data.success && data.data) {
|
|
2262
|
+
return { valid: true, data: data.data, gracePeriod: false };
|
|
2263
|
+
} else {
|
|
2264
|
+
return { valid: false, data: null };
|
|
2265
|
+
}
|
|
2266
|
+
} catch (error) {
|
|
2267
|
+
if (allowGracePeriod) {
|
|
2268
|
+
strapi2.log.warn("[Magic Editor X] [WARNING] License verification timeout - grace period active");
|
|
2269
|
+
return { valid: true, data: null, gracePeriod: true };
|
|
2270
|
+
}
|
|
2271
|
+
strapi2.log.error("[Magic Editor X] [ERROR] License verification error:", error.message);
|
|
2272
|
+
return { valid: false, data: null };
|
|
2273
|
+
}
|
|
2274
|
+
},
|
|
2275
|
+
/**
|
|
2276
|
+
* Get license details by key
|
|
2277
|
+
* @param {string} licenseKey - License key
|
|
2278
|
+
* @returns {Promise<object|null>} License data or null
|
|
2279
|
+
*/
|
|
2280
|
+
async getLicenseByKey(licenseKey) {
|
|
2281
|
+
try {
|
|
2282
|
+
const response = await fetch(`${LICENSE_SERVER_URL}/api/licenses/key/${licenseKey}`, {
|
|
2283
|
+
method: "GET",
|
|
2284
|
+
headers: { "Content-Type": "application/json" }
|
|
2285
|
+
});
|
|
2286
|
+
const data = await response.json();
|
|
2287
|
+
if (data.success && data.data) {
|
|
2288
|
+
return data.data;
|
|
2289
|
+
}
|
|
2290
|
+
return null;
|
|
2291
|
+
} catch (error) {
|
|
2292
|
+
strapi2.log.error("[Magic Editor X] Error fetching license by key:", error);
|
|
2293
|
+
return null;
|
|
2294
|
+
}
|
|
2295
|
+
},
|
|
2296
|
+
/**
|
|
2297
|
+
* Ping license server to update online status
|
|
2298
|
+
* @param {string} licenseKey - License key
|
|
2299
|
+
* @returns {Promise<object|null>} Ping result or null
|
|
2300
|
+
*/
|
|
2301
|
+
async pingLicense(licenseKey) {
|
|
2302
|
+
try {
|
|
2303
|
+
const deviceId = this.generateDeviceId();
|
|
2304
|
+
const deviceName = this.getDeviceName();
|
|
2305
|
+
const ipAddress = this.getIpAddress();
|
|
2306
|
+
const userAgent = this.getUserAgent();
|
|
2307
|
+
const response = await fetch(`${LICENSE_SERVER_URL}/api/licenses/ping`, {
|
|
2308
|
+
method: "POST",
|
|
2309
|
+
headers: { "Content-Type": "application/json" },
|
|
2310
|
+
body: JSON.stringify({
|
|
2311
|
+
licenseKey,
|
|
2312
|
+
deviceId,
|
|
2313
|
+
deviceName,
|
|
2314
|
+
ipAddress,
|
|
2315
|
+
userAgent,
|
|
2316
|
+
pluginName: PLUGIN_NAME
|
|
2317
|
+
})
|
|
2318
|
+
});
|
|
2319
|
+
const data = await response.json();
|
|
2320
|
+
return data.success ? data.data : null;
|
|
2321
|
+
} catch (error) {
|
|
2322
|
+
return null;
|
|
2323
|
+
}
|
|
2324
|
+
},
|
|
2325
|
+
/**
|
|
2326
|
+
* Store license key in plugin store
|
|
2327
|
+
* @param {string} licenseKey - License key to store
|
|
2328
|
+
*/
|
|
2329
|
+
async storeLicenseKey(licenseKey) {
|
|
2330
|
+
const pluginStore = strapi2.store({
|
|
2331
|
+
type: "plugin",
|
|
2332
|
+
name: "magic-editor-x"
|
|
2333
|
+
});
|
|
2334
|
+
await pluginStore.set({ key: "licenseKey", value: licenseKey });
|
|
2335
|
+
strapi2.log.info(`[Magic Editor X] [SUCCESS] License key stored: ${licenseKey.substring(0, 8)}...`);
|
|
2336
|
+
},
|
|
2337
|
+
/**
|
|
2338
|
+
* Get stored license key
|
|
2339
|
+
* @returns {Promise<string|null>} License key or null
|
|
2340
|
+
*/
|
|
2341
|
+
async getStoredLicenseKey() {
|
|
2342
|
+
const pluginStore = strapi2.store({
|
|
2343
|
+
type: "plugin",
|
|
2344
|
+
name: "magic-editor-x"
|
|
2345
|
+
});
|
|
2346
|
+
return await pluginStore.get({ key: "licenseKey" });
|
|
2347
|
+
},
|
|
2348
|
+
/**
|
|
2349
|
+
* Start automatic license pinging
|
|
2350
|
+
* @param {string} licenseKey - License key
|
|
2351
|
+
* @param {number} intervalMinutes - Ping interval in minutes
|
|
2352
|
+
* @returns {NodeJS.Timeout} Interval handle
|
|
2353
|
+
*/
|
|
2354
|
+
startPinging(licenseKey, intervalMinutes = 15) {
|
|
2355
|
+
this.pingLicense(licenseKey);
|
|
2356
|
+
const interval = setInterval(async () => {
|
|
2357
|
+
try {
|
|
2358
|
+
await this.pingLicense(licenseKey);
|
|
2359
|
+
} catch (error) {
|
|
2360
|
+
}
|
|
2361
|
+
}, intervalMinutes * 60 * 1e3);
|
|
2362
|
+
return interval;
|
|
2363
|
+
},
|
|
2364
|
+
/**
|
|
2365
|
+
* Get current license data from store and server
|
|
2366
|
+
* @returns {Promise<object|null>} License data or null
|
|
2367
|
+
*/
|
|
2368
|
+
async getCurrentLicense() {
|
|
2369
|
+
try {
|
|
2370
|
+
const licenseKey = await this.getStoredLicenseKey();
|
|
2371
|
+
if (!licenseKey) {
|
|
2372
|
+
return null;
|
|
2373
|
+
}
|
|
2374
|
+
const license2 = await this.getLicenseByKey(licenseKey);
|
|
2375
|
+
return license2;
|
|
2376
|
+
} catch (error) {
|
|
2377
|
+
strapi2.log.error(`[Magic Editor X] [ERROR] Error loading license:`, error);
|
|
2378
|
+
return null;
|
|
2379
|
+
}
|
|
2380
|
+
},
|
|
2381
|
+
/**
|
|
2382
|
+
* Get current tier based on license
|
|
2383
|
+
* @returns {Promise<string>} Tier name (free, premium, advanced, enterprise)
|
|
2384
|
+
*/
|
|
2385
|
+
async getCurrentTier() {
|
|
2386
|
+
const license2 = await this.getCurrentLicense();
|
|
2387
|
+
if (!license2) {
|
|
2388
|
+
return "free";
|
|
2389
|
+
}
|
|
2390
|
+
if (license2.featureEnterprise === true) return "enterprise";
|
|
2391
|
+
if (license2.featureAdvanced === true) return "advanced";
|
|
2392
|
+
if (license2.featurePremium === true) return "premium";
|
|
2393
|
+
return "free";
|
|
2394
|
+
},
|
|
2395
|
+
/**
|
|
2396
|
+
* Get tier configuration
|
|
2397
|
+
* @param {string} tierName - Tier name
|
|
2398
|
+
* @returns {object} Tier configuration
|
|
2399
|
+
*/
|
|
2400
|
+
getTierConfig(tierName) {
|
|
2401
|
+
return TIERS[tierName] || TIERS.free;
|
|
2402
|
+
},
|
|
2403
|
+
/**
|
|
2404
|
+
* Get maximum allowed collaborators for current license
|
|
2405
|
+
* @returns {Promise<number>} Max collaborators (-1 for unlimited)
|
|
2406
|
+
*/
|
|
2407
|
+
async getMaxCollaborators() {
|
|
2408
|
+
const tier = await this.getCurrentTier();
|
|
2409
|
+
const config2 = this.getTierConfig(tier);
|
|
2410
|
+
return config2.maxCollaborators;
|
|
2411
|
+
},
|
|
2412
|
+
/**
|
|
2413
|
+
* Check if a specific feature is available
|
|
2414
|
+
* @param {string} featureName - Feature name
|
|
2415
|
+
* @returns {Promise<boolean>} Feature availability
|
|
2416
|
+
*/
|
|
2417
|
+
async hasFeature(featureName) {
|
|
2418
|
+
const tier = await this.getCurrentTier();
|
|
2419
|
+
const config2 = this.getTierConfig(tier);
|
|
2420
|
+
return config2.features[featureName] === true;
|
|
2421
|
+
},
|
|
2422
|
+
/**
|
|
2423
|
+
* Check if user can add more collaborators
|
|
2424
|
+
* @returns {Promise<object>} Check result with canAdd and current/max counts
|
|
2425
|
+
*/
|
|
2426
|
+
async canAddCollaborator() {
|
|
2427
|
+
const maxCollaborators = await this.getMaxCollaborators();
|
|
2428
|
+
const currentCount = await strapi2.documents("plugin::magic-editor-x.collab-permission").count();
|
|
2429
|
+
const canAdd = maxCollaborators === -1 || currentCount < maxCollaborators;
|
|
2430
|
+
return {
|
|
2431
|
+
canAdd,
|
|
2432
|
+
current: currentCount,
|
|
2433
|
+
max: maxCollaborators,
|
|
2434
|
+
unlimited: maxCollaborators === -1
|
|
2435
|
+
};
|
|
2436
|
+
},
|
|
2437
|
+
/**
|
|
2438
|
+
* Initialize license service on plugin startup
|
|
2439
|
+
* @returns {Promise<object>} Initialization result
|
|
2440
|
+
*/
|
|
2441
|
+
async initialize() {
|
|
2442
|
+
try {
|
|
2443
|
+
strapi2.log.info("[Magic Editor X] [INIT] Initializing License Service...");
|
|
2444
|
+
const licenseKey = await this.getStoredLicenseKey();
|
|
2445
|
+
if (!licenseKey) {
|
|
2446
|
+
strapi2.log.info("[Magic Editor X] [FREE] No license found - Running in FREE mode (2 Collaborators)");
|
|
2447
|
+
return {
|
|
2448
|
+
valid: false,
|
|
2449
|
+
demo: true,
|
|
2450
|
+
tier: "free",
|
|
2451
|
+
data: null
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
const pluginStore = strapi2.store({
|
|
2455
|
+
type: "plugin",
|
|
2456
|
+
name: "magic-editor-x"
|
|
2457
|
+
});
|
|
2458
|
+
const lastValidated = await pluginStore.get({ key: "lastValidated" });
|
|
2459
|
+
const now = /* @__PURE__ */ new Date();
|
|
2460
|
+
const gracePeriodHours = 24;
|
|
2461
|
+
let withinGracePeriod = false;
|
|
2462
|
+
if (lastValidated) {
|
|
2463
|
+
const lastValidatedDate = new Date(lastValidated);
|
|
2464
|
+
const hoursSinceValidation = (now.getTime() - lastValidatedDate.getTime()) / (1e3 * 60 * 60);
|
|
2465
|
+
withinGracePeriod = hoursSinceValidation < gracePeriodHours;
|
|
2466
|
+
}
|
|
2467
|
+
const verification = await this.verifyLicense(licenseKey, withinGracePeriod);
|
|
2468
|
+
if (verification.valid) {
|
|
2469
|
+
const license2 = await this.getLicenseByKey(licenseKey);
|
|
2470
|
+
const tier = await this.getCurrentTier();
|
|
2471
|
+
const tierConfig = this.getTierConfig(tier);
|
|
2472
|
+
await pluginStore.set({
|
|
2473
|
+
key: "lastValidated",
|
|
2474
|
+
value: now.toISOString()
|
|
2475
|
+
});
|
|
2476
|
+
const pingInterval = this.startPinging(licenseKey, 15);
|
|
2477
|
+
strapi2.licenseGuardEditorX = {
|
|
2478
|
+
licenseKey,
|
|
2479
|
+
pingInterval,
|
|
2480
|
+
data: verification.data,
|
|
2481
|
+
tier
|
|
2482
|
+
};
|
|
2483
|
+
strapi2.log.info("==================================================================");
|
|
2484
|
+
strapi2.log.info("[SUCCESS] MAGIC EDITOR X LICENSE ACTIVE");
|
|
2485
|
+
strapi2.log.info(` License: ${licenseKey.substring(0, 15)}...`);
|
|
2486
|
+
strapi2.log.info(` Tier: ${tierConfig.name}`);
|
|
2487
|
+
strapi2.log.info(` Collaborators: ${tierConfig.maxCollaborators === -1 ? "Unlimited" : tierConfig.maxCollaborators}`);
|
|
2488
|
+
strapi2.log.info(` User: ${license2?.firstName} ${license2?.lastName}`);
|
|
2489
|
+
strapi2.log.info("==================================================================");
|
|
2490
|
+
return {
|
|
2491
|
+
valid: true,
|
|
2492
|
+
demo: false,
|
|
2493
|
+
tier,
|
|
2494
|
+
data: verification.data,
|
|
2495
|
+
gracePeriod: verification.gracePeriod || false
|
|
2496
|
+
};
|
|
2497
|
+
} else {
|
|
2498
|
+
strapi2.log.warn("[Magic Editor X] [WARNING] License validation failed - Running in FREE mode");
|
|
2499
|
+
return {
|
|
2500
|
+
valid: false,
|
|
2501
|
+
demo: true,
|
|
2502
|
+
tier: "free",
|
|
2503
|
+
error: "Invalid or expired license",
|
|
2504
|
+
data: null
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
} catch (error) {
|
|
2508
|
+
strapi2.log.error("[Magic Editor X] [ERROR] Error initializing License Service:", error);
|
|
2509
|
+
return {
|
|
2510
|
+
valid: false,
|
|
2511
|
+
demo: true,
|
|
2512
|
+
tier: "free",
|
|
2513
|
+
error: error.message,
|
|
2514
|
+
data: null
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
});
|
|
2519
|
+
const editorService = editorService$1;
|
|
2520
|
+
const realtimeService = realtimeService$1;
|
|
2521
|
+
const accessService = accessService$1;
|
|
2522
|
+
const snapshotService = snapshotService$1;
|
|
2523
|
+
const licenseService = licenseService$1;
|
|
2524
|
+
var services$1 = {
|
|
2525
|
+
editorService,
|
|
2526
|
+
realtimeService,
|
|
2527
|
+
accessService,
|
|
2528
|
+
snapshotService,
|
|
2529
|
+
licenseService
|
|
2530
|
+
};
|
|
2531
|
+
const bootstrap = bootstrap$1;
|
|
2532
|
+
const destroy = destroy$1;
|
|
2533
|
+
const register = register$1;
|
|
2534
|
+
const config = config$1;
|
|
2535
|
+
const contentTypes = contentTypes$1;
|
|
2536
|
+
const controllers = controllers$1;
|
|
2537
|
+
const middlewares = middlewares$1;
|
|
2538
|
+
const policies = policies$1;
|
|
2539
|
+
const routes = routes$1;
|
|
2540
|
+
const services = services$1;
|
|
2541
|
+
var src = {
|
|
2542
|
+
bootstrap,
|
|
2543
|
+
destroy,
|
|
2544
|
+
register,
|
|
2545
|
+
config,
|
|
2546
|
+
controllers,
|
|
2547
|
+
contentTypes,
|
|
2548
|
+
middlewares,
|
|
2549
|
+
policies,
|
|
2550
|
+
routes,
|
|
2551
|
+
services
|
|
2552
|
+
};
|
|
2553
|
+
const index = /* @__PURE__ */ getDefaultExportFromCjs(src);
|
|
2554
|
+
module.exports = index;
|