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,182 @@
1
+ 'use strict';
2
+
3
+ const PLUGIN_ID = 'strapi-content-sync-pro';
4
+
5
+ module.exports = ({ strapi }) => ({
6
+ /**
7
+ * GET /sync-profiles
8
+ * List all sync profiles
9
+ */
10
+ async find(ctx) {
11
+ try {
12
+ const profiles = await strapi.plugin(PLUGIN_ID).service('syncProfiles').getProfiles();
13
+ ctx.body = { data: profiles };
14
+ } catch (err) {
15
+ ctx.throw(500, err.message);
16
+ }
17
+ },
18
+
19
+ /**
20
+ * GET /sync-profiles/:id
21
+ * Get a single sync profile
22
+ */
23
+ async findOne(ctx) {
24
+ const { id } = ctx.params;
25
+ try {
26
+ const profile = await strapi.plugin(PLUGIN_ID).service('syncProfiles').getProfile(id);
27
+ if (!profile) {
28
+ return ctx.throw(404, `Profile with id "${id}" not found`);
29
+ }
30
+ ctx.body = { data: profile };
31
+ } catch (err) {
32
+ ctx.throw(500, err.message);
33
+ }
34
+ },
35
+
36
+ /**
37
+ * GET /sync-profiles/content-type/:uid
38
+ * Get all profiles for a content type
39
+ */
40
+ async findByContentType(ctx) {
41
+ const { uid } = ctx.params;
42
+ try {
43
+ const profiles = await strapi.plugin(PLUGIN_ID).service('syncProfiles').getProfilesForContentType(uid);
44
+ ctx.body = { data: profiles };
45
+ } catch (err) {
46
+ ctx.throw(500, err.message);
47
+ }
48
+ },
49
+
50
+ /**
51
+ * GET /sync-profiles/content-type/:uid/active
52
+ * Get active profile for a content type
53
+ */
54
+ async findActiveByContentType(ctx) {
55
+ const { uid } = ctx.params;
56
+ try {
57
+ const profile = await strapi.plugin(PLUGIN_ID).service('syncProfiles').getActiveProfileForContentType(uid);
58
+ ctx.body = { data: profile };
59
+ } catch (err) {
60
+ ctx.throw(500, err.message);
61
+ }
62
+ },
63
+
64
+ /**
65
+ * POST /sync-profiles
66
+ * Create a new sync profile
67
+ */
68
+ async create(ctx) {
69
+ const body = ctx.request.body;
70
+ try {
71
+ const profile = await strapi.plugin(PLUGIN_ID).service('syncProfiles').createProfile(body);
72
+ ctx.body = { data: profile };
73
+ } catch (err) {
74
+ ctx.throw(400, err.message);
75
+ }
76
+ },
77
+
78
+ /**
79
+ * POST /sync-profiles/auto-generate
80
+ * Auto-generate default profiles for a content type
81
+ */
82
+ async autoGenerate(ctx) {
83
+ const { contentType } = ctx.request.body;
84
+ if (!contentType) {
85
+ return ctx.throw(400, 'contentType is required');
86
+ }
87
+ try {
88
+ const profiles = await strapi.plugin(PLUGIN_ID).service('syncProfiles').autoGenerateProfiles(contentType);
89
+ ctx.body = { data: profiles };
90
+ } catch (err) {
91
+ ctx.throw(400, err.message);
92
+ }
93
+ },
94
+
95
+ /**
96
+ * POST /sync-profiles/simple
97
+ * Create a simple preset profile
98
+ */
99
+ async createSimple(ctx) {
100
+ const { contentType, preset } = ctx.request.body;
101
+ if (!contentType) {
102
+ return ctx.throw(400, 'contentType is required');
103
+ }
104
+ if (!preset) {
105
+ return ctx.throw(400, 'preset is required');
106
+ }
107
+ try {
108
+ const profile = await strapi.plugin(PLUGIN_ID).service('syncProfiles').createSimpleProfile(contentType, preset);
109
+ ctx.body = { data: profile };
110
+ } catch (err) {
111
+ ctx.throw(400, err.message);
112
+ }
113
+ },
114
+
115
+ /**
116
+ * PUT /sync-profiles/:id
117
+ * Update an existing sync profile
118
+ */
119
+ async update(ctx) {
120
+ const { id } = ctx.params;
121
+ const body = ctx.request.body;
122
+ try {
123
+ const profile = await strapi.plugin(PLUGIN_ID).service('syncProfiles').updateProfile(id, body);
124
+ ctx.body = { data: profile };
125
+ } catch (err) {
126
+ if (err.message.includes('not found')) {
127
+ return ctx.throw(404, err.message);
128
+ }
129
+ ctx.throw(400, err.message);
130
+ }
131
+ },
132
+
133
+ /**
134
+ * DELETE /sync-profiles/:id
135
+ * Delete a sync profile
136
+ */
137
+ async delete(ctx) {
138
+ const { id } = ctx.params;
139
+ try {
140
+ const result = await strapi.plugin(PLUGIN_ID).service('syncProfiles').deleteProfile(id);
141
+ ctx.body = { data: result };
142
+ } catch (err) {
143
+ if (err.message.includes('not found')) {
144
+ return ctx.throw(404, err.message);
145
+ }
146
+ ctx.throw(500, err.message);
147
+ }
148
+ },
149
+
150
+ /**
151
+ * GET /content-type-schema/:uid
152
+ * Get schema/fields for a content type (for UI to display available fields)
153
+ */
154
+ async getContentTypeSchema(ctx) {
155
+ const { uid } = ctx.params;
156
+ try {
157
+ const contentType = strapi.contentTypes[uid];
158
+ if (!contentType) {
159
+ return ctx.throw(404, `Content type "${uid}" not found`);
160
+ }
161
+
162
+ const attributes = contentType.attributes || {};
163
+ const fields = Object.entries(attributes).map(([name, attr]) => ({
164
+ name,
165
+ type: attr.type,
166
+ required: attr.required || false,
167
+ relation: attr.type === 'relation' ? attr.relation : null,
168
+ target: attr.target || null,
169
+ }));
170
+
171
+ ctx.body = {
172
+ data: {
173
+ uid,
174
+ displayName: contentType.info?.displayName || uid,
175
+ fields,
176
+ },
177
+ };
178
+ } catch (err) {
179
+ ctx.throw(500, err.message);
180
+ }
181
+ },
182
+ });
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ async syncNow(ctx) {
5
+ const syncService = strapi.plugin('strapi-content-sync-pro').service('sync');
6
+
7
+ try {
8
+ const result = await syncService.syncNow();
9
+ ctx.body = { data: result };
10
+ } catch (err) {
11
+ return ctx.badRequest(err.message);
12
+ }
13
+ },
14
+
15
+ async receive(ctx) {
16
+ const { body } = ctx.request;
17
+
18
+ if (!body || !body.uid || !body.syncId) {
19
+ return ctx.badRequest('Missing uid, data, or syncId');
20
+ }
21
+
22
+ const syncService = strapi.plugin('strapi-content-sync-pro').service('sync');
23
+
24
+ try {
25
+ const result = await syncService.receiveRecord(body.uid, body.data || {}, body.syncId);
26
+ ctx.body = { data: result };
27
+ } catch (err) {
28
+ return ctx.badRequest(err.message);
29
+ }
30
+ },
31
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const destroy = ({ strapi }) => {
4
+ // destroy phase
5
+ };
6
+
7
+ module.exports = destroy;
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ const register = require('./register');
4
+ const bootstrap = require('./bootstrap');
5
+ const destroy = require('./destroy');
6
+ const config = require('./config');
7
+ const contentTypes = require('./content-types');
8
+ const controllers = require('./controllers');
9
+ const routes = require('./routes');
10
+ const services = require('./services');
11
+
12
+ module.exports = {
13
+ register,
14
+ bootstrap,
15
+ destroy,
16
+ config,
17
+ routes,
18
+ controllers,
19
+ services,
20
+ contentTypes,
21
+ };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const { verifySignature } = require('../utils/hmac');
4
+
5
+ /**
6
+ * Koa middleware that validates the HMAC signature sent by a remote
7
+ * Strapi instance. Used on the /receive endpoint.
8
+ */
9
+ module.exports = async (ctx, next) => {
10
+ const signature = ctx.request.headers['x-sync-signature'];
11
+ const timestamp = ctx.request.headers['x-sync-timestamp'];
12
+
13
+ if (!signature || !timestamp) {
14
+ return ctx.unauthorized('Missing x-sync-signature or x-sync-timestamp header');
15
+ }
16
+
17
+ const configService = strapi.plugin('strapi-content-sync-pro').service('config');
18
+ const serverConfig = await configService.getConfig({ safe: false });
19
+
20
+ if (!serverConfig || !serverConfig.sharedSecret) {
21
+ return ctx.unauthorized('Server not configured for sync');
22
+ }
23
+
24
+ const body = ctx.request.body || {};
25
+ const isValid = verifySignature(body, serverConfig.sharedSecret, signature, timestamp);
26
+
27
+ if (!isValid) {
28
+ return ctx.unauthorized('Invalid sync signature');
29
+ }
30
+
31
+ await next();
32
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const register = ({ strapi }) => {
4
+ // register phase
5
+ };
6
+
7
+ module.exports = register;
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ const verifySignature = require('../middlewares/verify-signature');
4
+
5
+ /**
6
+ * Strapi v5 plugin routes MUST be split between:
7
+ * - content-api: exposed under /api/<plugin>/... (remote server calls)
8
+ * - admin: exposed under /<plugin>/... (local admin UI calls)
9
+ */
10
+
11
+ const contentApiRoutes = [
12
+ { method: 'GET', path: '/ping', handler: 'ping.index', config: { policies: [], auth: false } },
13
+ { method: 'GET', path: '/enforcement/local-info', handler: 'syncEnforcement.getLocalInfo', config: { policies: [] } },
14
+ { method: 'GET', path: '/enforcement/schema/:uid', handler: 'syncEnforcement.getLocalSchema', config: { policies: [] } },
15
+ { method: 'POST', path: '/receive', handler: 'sync.receive', config: { policies: [], auth: false, middlewares: [verifySignature] } },
16
+ ];
17
+
18
+ const adminRoutes = [
19
+ // Config
20
+ { method: 'GET', path: '/config', handler: 'config.get', config: { policies: [] } },
21
+ { method: 'POST', path: '/config', handler: 'config.set', config: { policies: [] } },
22
+ { method: 'POST', path: '/config/remote-login', handler: 'config.remoteLogin', config: { policies: [] } },
23
+ { method: 'GET', path: '/config/test', handler: 'config.test', config: { policies: [] } },
24
+
25
+ // Content-type discovery
26
+ { method: 'GET', path: '/content-types', handler: 'contentTypeDiscovery.find', config: { policies: [] } },
27
+
28
+ // Sync configuration
29
+ { method: 'GET', path: '/sync-config', handler: 'syncConfig.get', config: { policies: [] } },
30
+ { method: 'POST', path: '/sync-config', handler: 'syncConfig.set', config: { policies: [] } },
31
+
32
+ // Manual sync trigger
33
+ { method: 'POST', path: '/sync-now', handler: 'sync.syncNow', config: { policies: [] } },
34
+
35
+ // Logs
36
+ { method: 'GET', path: '/logs', handler: 'syncLog.find', config: { policies: [] } },
37
+
38
+ // Sync Profiles
39
+ { method: 'GET', path: '/sync-profiles', handler: 'syncProfiles.find', config: { policies: [] } },
40
+ { method: 'GET', path: '/sync-profiles/:id', handler: 'syncProfiles.findOne', config: { policies: [] } },
41
+ { method: 'GET', path: '/sync-profiles/content-type/:uid', handler: 'syncProfiles.findByContentType', config: { policies: [] } },
42
+ { method: 'GET', path: '/sync-profiles/content-type/:uid/active', handler: 'syncProfiles.findActiveByContentType', config: { policies: [] } },
43
+ { method: 'POST', path: '/sync-profiles', handler: 'syncProfiles.create', config: { policies: [] } },
44
+ { method: 'POST', path: '/sync-profiles/auto-generate', handler: 'syncProfiles.autoGenerate', config: { policies: [] } },
45
+ { method: 'POST', path: '/sync-profiles/simple', handler: 'syncProfiles.createSimple', config: { policies: [] } },
46
+ { method: 'PUT', path: '/sync-profiles/:id', handler: 'syncProfiles.update', config: { policies: [] } },
47
+ { method: 'DELETE', path: '/sync-profiles/:id', handler: 'syncProfiles.delete', config: { policies: [] } },
48
+ { method: 'GET', path: '/content-type-schema/:uid', handler: 'syncProfiles.getContentTypeSchema', config: { policies: [] } },
49
+
50
+ // Sync Execution
51
+ { method: 'GET', path: '/sync-execution/settings', handler: 'syncExecution.getSettings', config: { policies: [] } },
52
+ { method: 'GET', path: '/sync-execution/settings/:profileId', handler: 'syncExecution.getProfileSettings', config: { policies: [] } },
53
+ { method: 'PUT', path: '/sync-execution/settings/:profileId', handler: 'syncExecution.updateProfileSettings', config: { policies: [] } },
54
+ { method: 'GET', path: '/sync-execution/global-settings', handler: 'syncExecution.getGlobalSettings', config: { policies: [] } },
55
+ { method: 'PUT', path: '/sync-execution/global-settings', handler: 'syncExecution.updateGlobalSettings', config: { policies: [] } },
56
+ { method: 'POST', path: '/sync-execution/execute/:profileId', handler: 'syncExecution.executeProfile', config: { policies: [] } },
57
+ { method: 'POST', path: '/sync-execution/execute-batch', handler: 'syncExecution.executeProfiles', config: { policies: [] } },
58
+ { method: 'POST', path: '/sync-execution/execute-content-type/:uid', handler: 'syncExecution.executeContentType', config: { policies: [] } },
59
+ { method: 'GET', path: '/sync-execution/status', handler: 'syncExecution.getStatus', config: { policies: [] } },
60
+
61
+ // Enforcement (admin-side)
62
+ { method: 'GET', path: '/enforcement/settings', handler: 'syncEnforcement.getSettings', config: { policies: [] } },
63
+ { method: 'PUT', path: '/enforcement/settings', handler: 'syncEnforcement.updateSettings', config: { policies: [] } },
64
+ { method: 'GET', path: '/enforcement/remote-info', handler: 'syncEnforcement.getRemoteInfo', config: { policies: [] } },
65
+ { method: 'GET', path: '/enforcement/check/:type', handler: 'syncEnforcement.runDiagnosticCheck', config: { policies: [] } },
66
+ { method: 'POST', path: '/enforcement/check', handler: 'syncEnforcement.runChecks', config: { policies: [] } },
67
+ { method: 'GET', path: '/enforcement/summary', handler: 'syncEnforcement.getSummary', config: { policies: [] } },
68
+
69
+ // Alerts
70
+ { method: 'GET', path: '/alerts/settings', handler: 'alerts.getSettings', config: { policies: [] } },
71
+ { method: 'PUT', path: '/alerts/settings', handler: 'alerts.updateSettings', config: { policies: [] } },
72
+ { method: 'POST', path: '/alerts/test/:channel', handler: 'alerts.testChannel', config: { policies: [] } },
73
+ { method: 'GET', path: '/alerts/stats', handler: 'alerts.getStats', config: { policies: [] } },
74
+
75
+ // Media sync
76
+ { method: 'GET', path: '/media-sync/profiles', handler: 'syncMedia.getProfiles', config: { policies: [] } },
77
+ { method: 'GET', path: '/media-sync/profiles/:id', handler: 'syncMedia.getProfile', config: { policies: [] } },
78
+ { method: 'POST', path: '/media-sync/profiles', handler: 'syncMedia.createProfile', config: { policies: [] } },
79
+ { method: 'PUT', path: '/media-sync/profiles/:id', handler: 'syncMedia.updateProfile', config: { policies: [] } },
80
+ { method: 'DELETE', path: '/media-sync/profiles/:id', handler: 'syncMedia.deleteProfile', config: { policies: [] } },
81
+ { method: 'POST', path: '/media-sync/profiles/:id/activate', handler: 'syncMedia.activateProfile', config: { policies: [] } },
82
+ { method: 'POST', path: '/media-sync/profiles/:id/run', handler: 'syncMedia.runProfile', config: { policies: [] } },
83
+ { method: 'POST', path: '/media-sync/run-active', handler: 'syncMedia.runActiveProfiles', config: { policies: [] } },
84
+ { method: 'GET', path: '/media-sync/global-settings', handler: 'syncMedia.getGlobalSettings', config: { policies: [] } },
85
+ { method: 'PUT', path: '/media-sync/global-settings', handler: 'syncMedia.updateGlobalSettings', config: { policies: [] } },
86
+ { method: 'GET', path: '/media-sync/defaults', handler: 'syncMedia.getDefaults', config: { policies: [] } },
87
+ { method: 'GET', path: '/media-sync/settings', handler: 'syncMedia.getSettings', config: { policies: [] } },
88
+ { method: 'PUT', path: '/media-sync/settings', handler: 'syncMedia.updateSettings', config: { policies: [] } },
89
+ { method: 'GET', path: '/media-sync/status', handler: 'syncMedia.getStatus', config: { policies: [] } },
90
+ { method: 'POST', path: '/media-sync/test', handler: 'syncMedia.test', config: { policies: [] } },
91
+ { method: 'POST', path: '/media-sync/run', handler: 'syncMedia.run', config: { policies: [] } },
92
+
93
+ // Dependencies
94
+ { method: 'GET', path: '/dependencies/all', handler: 'dependencies.analyzeAll', config: { policies: [] } },
95
+ { method: 'GET', path: '/dependencies/:uid', handler: 'dependencies.analyze', config: { policies: [] } },
96
+ { method: 'GET', path: '/dependencies/:uid/graph', handler: 'dependencies.getGraph', config: { policies: [] } },
97
+ { method: 'GET', path: '/dependencies/:uid/sync-order', handler: 'dependencies.getSyncOrder', config: { policies: [] } },
98
+ { method: 'GET', path: '/dependencies/:uid/summary', handler: 'dependencies.getSummary', config: { policies: [] } },
99
+ { method: 'POST', path: '/dependencies/clear-cache', handler: 'dependencies.clearCache', config: { policies: [] } },
100
+ ];
101
+
102
+ module.exports = {
103
+ 'content-api': {
104
+ type: 'content-api',
105
+ routes: contentApiRoutes,
106
+ },
107
+ admin: {
108
+ type: 'admin',
109
+ routes: adminRoutes,
110
+ },
111
+ };