strapi-content-sync-pro 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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +206 -0
  3. package/admin/src/components/ConfigTab.jsx +1038 -0
  4. package/admin/src/components/ContentTypesTab.jsx +160 -0
  5. package/admin/src/components/HelpTab.jsx +945 -0
  6. package/admin/src/components/LogsTab.jsx +136 -0
  7. package/admin/src/components/MediaTab.jsx +557 -0
  8. package/admin/src/components/SyncProfilesTab.jsx +715 -0
  9. package/admin/src/components/SyncTab.jsx +988 -0
  10. package/admin/src/index.js +31 -0
  11. package/admin/src/pages/App/index.jsx +129 -0
  12. package/admin/src/pluginId.js +3 -0
  13. package/package.json +84 -0
  14. package/server/src/bootstrap.js +151 -0
  15. package/server/src/config/index.js +5 -0
  16. package/server/src/content-types/index.js +7 -0
  17. package/server/src/content-types/sync-log/schema.json +24 -0
  18. package/server/src/controllers/alerts.js +59 -0
  19. package/server/src/controllers/config.js +292 -0
  20. package/server/src/controllers/content-type-discovery.js +9 -0
  21. package/server/src/controllers/dependencies.js +109 -0
  22. package/server/src/controllers/index.js +29 -0
  23. package/server/src/controllers/ping.js +7 -0
  24. package/server/src/controllers/sync-config.js +26 -0
  25. package/server/src/controllers/sync-enforcement.js +323 -0
  26. package/server/src/controllers/sync-execution.js +134 -0
  27. package/server/src/controllers/sync-log.js +18 -0
  28. package/server/src/controllers/sync-media.js +158 -0
  29. package/server/src/controllers/sync-profiles.js +182 -0
  30. package/server/src/controllers/sync.js +31 -0
  31. package/server/src/destroy.js +7 -0
  32. package/server/src/index.js +21 -0
  33. package/server/src/middlewares/verify-signature.js +32 -0
  34. package/server/src/register.js +7 -0
  35. package/server/src/routes/index.js +111 -0
  36. package/server/src/services/alerts.js +437 -0
  37. package/server/src/services/config.js +68 -0
  38. package/server/src/services/content-type-discovery.js +41 -0
  39. package/server/src/services/dependency-resolver.js +284 -0
  40. package/server/src/services/index.js +30 -0
  41. package/server/src/services/ping.js +7 -0
  42. package/server/src/services/sync-config.js +45 -0
  43. package/server/src/services/sync-enforcement.js +362 -0
  44. package/server/src/services/sync-execution.js +541 -0
  45. package/server/src/services/sync-log.js +56 -0
  46. package/server/src/services/sync-media.js +963 -0
  47. package/server/src/services/sync-profiles.js +380 -0
  48. package/server/src/services/sync.js +248 -0
  49. package/server/src/utils/applier.js +89 -0
  50. package/server/src/utils/comparator.js +83 -0
  51. package/server/src/utils/fetcher.js +142 -0
  52. package/server/src/utils/hmac.js +37 -0
  53. package/server/src/utils/pagination.js +51 -0
  54. package/server/src/utils/sync-guard.js +29 -0
  55. package/server/src/utils/sync-id.js +16 -0
@@ -0,0 +1,323 @@
1
+ 'use strict';
2
+
3
+ const PLUGIN_ID = 'strapi-content-sync-pro';
4
+
5
+ module.exports = ({ strapi }) => ({
6
+ /**
7
+ * GET /enforcement/settings
8
+ * Get enforcement settings
9
+ */
10
+ async getSettings(ctx) {
11
+ try {
12
+ const settings = await strapi.plugin(PLUGIN_ID).service('syncEnforcement').getSettings();
13
+ ctx.body = { data: settings };
14
+ } catch (err) {
15
+ ctx.throw(500, err.message);
16
+ }
17
+ },
18
+
19
+ /**
20
+ * PUT /enforcement/settings
21
+ * Update enforcement settings
22
+ */
23
+ async updateSettings(ctx) {
24
+ const body = ctx.request.body;
25
+ try {
26
+ const settings = await strapi.plugin(PLUGIN_ID).service('syncEnforcement').updateSettings(body);
27
+ ctx.body = { data: settings };
28
+ } catch (err) {
29
+ ctx.throw(400, err.message);
30
+ }
31
+ },
32
+
33
+ /**
34
+ * GET /enforcement/local-info
35
+ * Get local version and schema info
36
+ */
37
+ async getLocalInfo(ctx) {
38
+ try {
39
+ const service = strapi.plugin(PLUGIN_ID).service('syncEnforcement');
40
+ const versionInfo = service.getLocalVersionInfo();
41
+ ctx.body = { data: versionInfo };
42
+ } catch (err) {
43
+ ctx.throw(500, err.message);
44
+ }
45
+ },
46
+
47
+ /**
48
+ * GET /enforcement/remote-info
49
+ * Get remote server info for connection test
50
+ */
51
+ async getRemoteInfo(ctx) {
52
+ try {
53
+ const configService = strapi.plugin(PLUGIN_ID).service('config');
54
+ const config = await configService.getConfig({ safe: false });
55
+
56
+ if (!config || !config.baseUrl) {
57
+ return ctx.throw(400, 'Remote server not configured');
58
+ }
59
+
60
+ const url = `${config.baseUrl}/api/${PLUGIN_ID}/enforcement/local-info`;
61
+ const response = await fetch(url, {
62
+ headers: {
63
+ Authorization: `Bearer ${config.apiToken}`,
64
+ },
65
+ });
66
+
67
+ if (!response.ok) {
68
+ let errorDetail = '';
69
+ try {
70
+ const errorBody = await response.json();
71
+ errorDetail = errorBody?.error?.message || JSON.stringify(errorBody);
72
+ } catch {
73
+ errorDetail = await response.text();
74
+ }
75
+ return ctx.throw(response.status, `Remote server returned ${response.status}: ${errorDetail}`);
76
+ }
77
+
78
+ const data = await response.json();
79
+ ctx.body = { data: data.data };
80
+ } catch (err) {
81
+ if (err.status) {
82
+ throw err; // Re-throw Koa errors
83
+ }
84
+ ctx.throw(500, err.message || 'Failed to get remote info');
85
+ }
86
+ },
87
+
88
+ /**
89
+ * GET /enforcement/check/:type
90
+ * Run individual diagnostic check (schema, version, time)
91
+ */
92
+ async runDiagnosticCheck(ctx) {
93
+ const { type } = ctx.params;
94
+ const validTypes = ['schema', 'version', 'time'];
95
+
96
+ if (!validTypes.includes(type)) {
97
+ return ctx.throw(400, `Invalid check type. Must be one of: ${validTypes.join(', ')}`);
98
+ }
99
+
100
+ try {
101
+ const configService = strapi.plugin(PLUGIN_ID).service('config');
102
+ const enforcementService = strapi.plugin(PLUGIN_ID).service('syncEnforcement');
103
+ const syncConfigService = strapi.plugin(PLUGIN_ID).service('syncConfig');
104
+
105
+ const config = await configService.getConfig({ safe: false });
106
+
107
+ if (!config || !config.baseUrl) {
108
+ return ctx.body = {
109
+ data: {
110
+ passed: false,
111
+ error: 'Remote server not configured',
112
+ },
113
+ };
114
+ }
115
+
116
+ // Get remote info
117
+ let remoteInfo;
118
+ try {
119
+ const response = await fetch(`${config.baseUrl}/api/${PLUGIN_ID}/enforcement/local-info`, {
120
+ headers: {
121
+ Authorization: `Bearer ${config.apiToken}`,
122
+ },
123
+ });
124
+
125
+ if (!response.ok) {
126
+ return ctx.body = {
127
+ data: {
128
+ passed: false,
129
+ error: `Cannot reach remote server (${response.status})`,
130
+ },
131
+ };
132
+ }
133
+
134
+ remoteInfo = (await response.json()).data;
135
+ } catch (err) {
136
+ return ctx.body = {
137
+ data: {
138
+ passed: false,
139
+ error: `Connection failed: ${err.message}`,
140
+ },
141
+ };
142
+ }
143
+
144
+ const localInfo = enforcementService.getLocalVersionInfo();
145
+ const settings = await enforcementService.getSettings();
146
+
147
+ let result = { passed: true, details: {} };
148
+
149
+ switch (type) {
150
+ case 'version': {
151
+ const versionResult = enforcementService.compareVersions(
152
+ localInfo.strapiVersion,
153
+ remoteInfo.strapiVersion,
154
+ settings.allowedVersionDrift
155
+ );
156
+ result = {
157
+ passed: versionResult.compatible,
158
+ details: {
159
+ localVersion: localInfo.strapiVersion,
160
+ remoteVersion: remoteInfo.strapiVersion,
161
+ driftLevel: versionResult.driftLevel,
162
+ message: versionResult.message,
163
+ },
164
+ };
165
+ break;
166
+ }
167
+
168
+ case 'time': {
169
+ const localTime = new Date();
170
+ const remoteTime = new Date(remoteInfo.serverTime);
171
+ const driftMs = Math.abs(localTime - remoteTime);
172
+ const passed = driftMs <= settings.maxTimeDriftMs;
173
+
174
+ result = {
175
+ passed,
176
+ details: {
177
+ localTime: localTime.toISOString(),
178
+ remoteTime: remoteInfo.serverTime,
179
+ driftMs,
180
+ maxAllowed: settings.maxTimeDriftMs,
181
+ message: passed
182
+ ? `Time drift of ${driftMs}ms is within allowed limit`
183
+ : `Time drift of ${driftMs}ms exceeds limit of ${settings.maxTimeDriftMs}ms`,
184
+ },
185
+ };
186
+ break;
187
+ }
188
+
189
+ case 'schema': {
190
+ const syncConfig = await syncConfigService.getSyncConfig();
191
+ const enabledTypes = (syncConfig.contentTypes || [])
192
+ .filter((ct) => ct.enabled !== false)
193
+ .map((ct) => ct.uid);
194
+ const mismatches = [];
195
+
196
+ if (enabledTypes.length === 0) {
197
+ result = {
198
+ passed: true,
199
+ details: {
200
+ checkedTypes: [],
201
+ mismatches: [],
202
+ matchMode: settings.schemaMatchMode,
203
+ message: 'No content types enabled for sync',
204
+ },
205
+ };
206
+ break;
207
+ }
208
+
209
+ for (const uid of enabledTypes) {
210
+ try {
211
+ // Get remote schema
212
+ const schemaResponse = await fetch(
213
+ `${config.baseUrl}/api/${PLUGIN_ID}/enforcement/schema/${encodeURIComponent(uid)}`,
214
+ {
215
+ headers: {
216
+ Authorization: `Bearer ${config.apiToken}`,
217
+ },
218
+ }
219
+ );
220
+
221
+ if (!schemaResponse.ok) {
222
+ mismatches.push({
223
+ type: uid,
224
+ reason: `Not found on remote (${schemaResponse.status})`,
225
+ });
226
+ continue;
227
+ }
228
+
229
+ const remoteSchema = (await schemaResponse.json()).data?.schema;
230
+ const localSchema = enforcementService.getLocalSchema(uid);
231
+
232
+ if (!localSchema) {
233
+ mismatches.push({
234
+ type: uid,
235
+ reason: 'Not found locally',
236
+ });
237
+ continue;
238
+ }
239
+
240
+ // Compare schemas
241
+ const comparison = enforcementService.compareSchemas(localSchema, remoteSchema, settings.schemaMatchMode);
242
+ if (!comparison.compatible) {
243
+ mismatches.push({
244
+ type: uid,
245
+ reason: comparison.differences?.join(', ') || 'Schema mismatch',
246
+ });
247
+ }
248
+ } catch (err) {
249
+ mismatches.push({
250
+ type: uid,
251
+ reason: err.message,
252
+ });
253
+ }
254
+ }
255
+
256
+ result = {
257
+ passed: mismatches.length === 0,
258
+ details: {
259
+ checkedTypes: enabledTypes,
260
+ mismatches,
261
+ matchMode: settings.schemaMatchMode,
262
+ },
263
+ };
264
+ break;
265
+ }
266
+ }
267
+
268
+ ctx.body = { data: result };
269
+ } catch (err) {
270
+ ctx.throw(500, err.message);
271
+ }
272
+ },
273
+
274
+ /**
275
+ * GET /enforcement/schema/:uid
276
+ * Get local schema for a content type
277
+ */
278
+ async getLocalSchema(ctx) {
279
+ const { uid } = ctx.params;
280
+ try {
281
+ const schema = strapi.plugin(PLUGIN_ID).service('syncEnforcement').getLocalSchema(uid);
282
+ if (!schema) {
283
+ return ctx.throw(404, `Content type "${uid}" not found`);
284
+ }
285
+ ctx.body = { data: { uid, schema } };
286
+ } catch (err) {
287
+ ctx.throw(500, err.message);
288
+ }
289
+ },
290
+
291
+ /**
292
+ * POST /enforcement/check
293
+ * Run pre-sync enforcement checks
294
+ */
295
+ async runChecks(ctx) {
296
+ const { contentType, remoteInfo } = ctx.request.body;
297
+ if (!contentType) {
298
+ return ctx.throw(400, 'contentType is required');
299
+ }
300
+ if (!remoteInfo) {
301
+ return ctx.throw(400, 'remoteInfo is required');
302
+ }
303
+ try {
304
+ const results = await strapi.plugin(PLUGIN_ID).service('syncEnforcement').runPreSyncChecks(contentType, remoteInfo);
305
+ ctx.body = { data: results };
306
+ } catch (err) {
307
+ ctx.throw(400, err.message);
308
+ }
309
+ },
310
+
311
+ /**
312
+ * GET /enforcement/summary
313
+ * Get enforcement summary for UI
314
+ */
315
+ async getSummary(ctx) {
316
+ try {
317
+ const summary = await strapi.plugin(PLUGIN_ID).service('syncEnforcement').getEnforcementSummary();
318
+ ctx.body = { data: summary };
319
+ } catch (err) {
320
+ ctx.throw(500, err.message);
321
+ }
322
+ },
323
+ });
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ const PLUGIN_ID = 'strapi-content-sync-pro';
4
+
5
+ module.exports = ({ strapi }) => ({
6
+ /**
7
+ * GET /sync-execution/settings
8
+ * Get all execution settings
9
+ */
10
+ async getSettings(ctx) {
11
+ try {
12
+ const settings = await strapi.plugin(PLUGIN_ID).service('syncExecution').getExecutionSettings();
13
+ ctx.body = { data: settings };
14
+ } catch (err) {
15
+ ctx.throw(500, err.message);
16
+ }
17
+ },
18
+
19
+ /**
20
+ * GET /sync-execution/settings/:profileId
21
+ * Get execution settings for a profile
22
+ */
23
+ async getProfileSettings(ctx) {
24
+ const { profileId } = ctx.params;
25
+ try {
26
+ const settings = await strapi.plugin(PLUGIN_ID).service('syncExecution').getProfileExecutionSettings(profileId);
27
+ ctx.body = { data: settings };
28
+ } catch (err) {
29
+ ctx.throw(500, err.message);
30
+ }
31
+ },
32
+
33
+ /**
34
+ * PUT /sync-execution/settings/:profileId
35
+ * Update execution settings for a profile
36
+ */
37
+ async updateProfileSettings(ctx) {
38
+ const { profileId } = ctx.params;
39
+ const body = ctx.request.body;
40
+ try {
41
+ const settings = await strapi.plugin(PLUGIN_ID).service('syncExecution').setProfileExecutionSettings(profileId, body);
42
+ ctx.body = { data: settings };
43
+ } catch (err) {
44
+ ctx.throw(400, err.message);
45
+ }
46
+ },
47
+
48
+ /**
49
+ * GET /sync-execution/global-settings
50
+ * Get global execution settings
51
+ */
52
+ async getGlobalSettings(ctx) {
53
+ try {
54
+ const settings = await strapi.plugin(PLUGIN_ID).service('syncExecution').getGlobalSettings();
55
+ ctx.body = { data: settings };
56
+ } catch (err) {
57
+ ctx.throw(500, err.message);
58
+ }
59
+ },
60
+
61
+ /**
62
+ * PUT /sync-execution/global-settings
63
+ * Update global execution settings
64
+ */
65
+ async updateGlobalSettings(ctx) {
66
+ const body = ctx.request.body;
67
+ try {
68
+ const settings = await strapi.plugin(PLUGIN_ID).service('syncExecution').setGlobalSettings(body);
69
+ ctx.body = { data: settings };
70
+ } catch (err) {
71
+ ctx.throw(400, err.message);
72
+ }
73
+ },
74
+
75
+ /**
76
+ * POST /sync-execution/execute/:profileId
77
+ * Execute a profile on-demand
78
+ */
79
+ async executeProfile(ctx) {
80
+ const { profileId } = ctx.params;
81
+ const options = ctx.request.body || {};
82
+ try {
83
+ const result = await strapi.plugin(PLUGIN_ID).service('syncExecution').executeProfile(profileId, options);
84
+ ctx.body = { data: result };
85
+ } catch (err) {
86
+ ctx.throw(400, err.message);
87
+ }
88
+ },
89
+
90
+ /**
91
+ * POST /sync-execution/execute-batch
92
+ * Execute multiple profiles
93
+ */
94
+ async executeProfiles(ctx) {
95
+ const { profileIds, options } = ctx.request.body;
96
+ if (!profileIds || !Array.isArray(profileIds)) {
97
+ return ctx.throw(400, 'profileIds array is required');
98
+ }
99
+ try {
100
+ const result = await strapi.plugin(PLUGIN_ID).service('syncExecution').executeProfiles(profileIds, options || {});
101
+ ctx.body = { data: result };
102
+ } catch (err) {
103
+ ctx.throw(400, err.message);
104
+ }
105
+ },
106
+
107
+ /**
108
+ * POST /sync-execution/execute-content-type/:uid
109
+ * Execute active profile for a content type
110
+ */
111
+ async executeContentType(ctx) {
112
+ const { uid } = ctx.params;
113
+ const options = ctx.request.body || {};
114
+ try {
115
+ const result = await strapi.plugin(PLUGIN_ID).service('syncExecution').executeContentType(uid, options);
116
+ ctx.body = { data: result };
117
+ } catch (err) {
118
+ ctx.throw(400, err.message);
119
+ }
120
+ },
121
+
122
+ /**
123
+ * GET /sync-execution/status
124
+ * Get execution status for all profiles
125
+ */
126
+ async getStatus(ctx) {
127
+ try {
128
+ const status = await strapi.plugin(PLUGIN_ID).service('syncExecution').getExecutionStatus();
129
+ ctx.body = { data: status };
130
+ } catch (err) {
131
+ ctx.throw(500, err.message);
132
+ }
133
+ },
134
+ });
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ async find(ctx) {
5
+ const { page, pageSize, status, contentType, action } = ctx.query;
6
+
7
+ const service = strapi.plugin('strapi-content-sync-pro').service('syncLog');
8
+ const result = await service.getLogs({
9
+ page: page ? parseInt(page, 10) : 1,
10
+ pageSize: pageSize ? parseInt(pageSize, 10) : 25,
11
+ status,
12
+ contentType,
13
+ action,
14
+ });
15
+
16
+ ctx.body = result;
17
+ },
18
+ };
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const PLUGIN_ID = 'strapi-content-sync-pro';
4
+
5
+ function service(strapi) {
6
+ return strapi.plugin(PLUGIN_ID).service('syncMedia');
7
+ }
8
+
9
+ module.exports = ({ strapi }) => ({
10
+ // ── Profile CRUD ──────────────────────────────────────────────────────────
11
+
12
+ async getProfiles(ctx) {
13
+ try {
14
+ ctx.body = { data: await service(strapi).getProfiles() };
15
+ } catch (err) {
16
+ ctx.throw(500, err.message);
17
+ }
18
+ },
19
+
20
+ async getProfile(ctx) {
21
+ try {
22
+ const profile = await service(strapi).getProfile(ctx.params.id);
23
+ if (!profile) return ctx.throw(404, 'Media profile not found');
24
+ ctx.body = { data: profile };
25
+ } catch (err) {
26
+ ctx.throw(500, err.message);
27
+ }
28
+ },
29
+
30
+ async createProfile(ctx) {
31
+ try {
32
+ const body = ctx.request.body || {};
33
+ ctx.body = { data: await service(strapi).createProfile(body) };
34
+ } catch (err) {
35
+ ctx.throw(400, err.message);
36
+ }
37
+ },
38
+
39
+ async updateProfile(ctx) {
40
+ try {
41
+ const body = ctx.request.body || {};
42
+ ctx.body = { data: await service(strapi).updateProfile(ctx.params.id, body) };
43
+ } catch (err) {
44
+ ctx.throw(400, err.message);
45
+ }
46
+ },
47
+
48
+ async deleteProfile(ctx) {
49
+ try {
50
+ ctx.body = { data: await service(strapi).deleteProfile(ctx.params.id) };
51
+ } catch (err) {
52
+ ctx.throw(400, err.message);
53
+ }
54
+ },
55
+
56
+ async activateProfile(ctx) {
57
+ try {
58
+ ctx.body = { data: await service(strapi).activateProfile(ctx.params.id) };
59
+ } catch (err) {
60
+ ctx.throw(400, err.message);
61
+ }
62
+ },
63
+
64
+ // ── Global settings ───────────────────────────────────────────────────────
65
+
66
+ async getGlobalSettings(ctx) {
67
+ try {
68
+ ctx.body = { data: await service(strapi).getGlobalSettings() };
69
+ } catch (err) {
70
+ ctx.throw(500, err.message);
71
+ }
72
+ },
73
+
74
+ async updateGlobalSettings(ctx) {
75
+ try {
76
+ const body = ctx.request.body || {};
77
+ ctx.body = { data: await service(strapi).setGlobalSettings(body) };
78
+ } catch (err) {
79
+ ctx.throw(400, err.message);
80
+ }
81
+ },
82
+
83
+ // ── Defaults / constants ──────────────────────────────────────────────────
84
+
85
+ async getDefaults(ctx) {
86
+ try {
87
+ ctx.body = { data: service(strapi).getDefaults() };
88
+ } catch (err) {
89
+ ctx.throw(500, err.message);
90
+ }
91
+ },
92
+
93
+ // ── Execution ─────────────────────────────────────────────────────────────
94
+
95
+ async runProfile(ctx) {
96
+ try {
97
+ const options = ctx.request.body || {};
98
+ const result = await service(strapi).runProfile(ctx.params.id, options);
99
+ ctx.body = { data: result };
100
+ } catch (err) {
101
+ ctx.throw(400, err.message);
102
+ }
103
+ },
104
+
105
+ async runActiveProfiles(ctx) {
106
+ try {
107
+ const results = await service(strapi).runActiveProfiles();
108
+ ctx.body = { data: results };
109
+ } catch (err) {
110
+ ctx.throw(400, err.message);
111
+ }
112
+ },
113
+
114
+ // ── Back-compat (old flat endpoints) ──────────────────────────────────────
115
+
116
+ async getSettings(ctx) {
117
+ try {
118
+ ctx.body = { data: await service(strapi).getSettings() };
119
+ } catch (err) {
120
+ ctx.throw(500, err.message);
121
+ }
122
+ },
123
+
124
+ async updateSettings(ctx) {
125
+ try {
126
+ const body = ctx.request.body || {};
127
+ ctx.body = { data: await service(strapi).setSettings(body) };
128
+ } catch (err) {
129
+ ctx.throw(400, err.message);
130
+ }
131
+ },
132
+
133
+ async getStatus(ctx) {
134
+ try {
135
+ ctx.body = { data: await service(strapi).getStatus() };
136
+ } catch (err) {
137
+ ctx.throw(500, err.message);
138
+ }
139
+ },
140
+
141
+ async test(ctx) {
142
+ try {
143
+ ctx.body = { data: await service(strapi).testConnection() };
144
+ } catch (err) {
145
+ ctx.throw(500, err.message);
146
+ }
147
+ },
148
+
149
+ async run(ctx) {
150
+ try {
151
+ const options = ctx.request.body || {};
152
+ const result = await service(strapi).run(options);
153
+ ctx.body = { data: result };
154
+ } catch (err) {
155
+ ctx.throw(400, err.message);
156
+ }
157
+ },
158
+ });