strapi-content-sync-pro 1.0.3 → 1.0.5

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 (54) hide show
  1. package/README.md +33 -14
  2. package/admin/src/components/BulkTransferTab.jsx +880 -0
  3. package/admin/src/components/ConfigTab.jsx +81 -3
  4. package/admin/src/components/HelpTab.jsx +148 -5
  5. package/admin/src/components/MediaTab.jsx +141 -30
  6. package/admin/src/components/SyncTab.jsx +2 -0
  7. package/admin/src/pages/App/index.jsx +12 -1
  8. package/docs/Screenshot 2026-04-22 183540.png +0 -0
  9. package/docs/Screenshot 2026-04-22 183552.png +0 -0
  10. package/docs/Screenshot 2026-04-23 114332.png +0 -0
  11. package/docs/Screenshot 2026-04-23 114644.png +0 -0
  12. package/docs/Screenshot 2026-04-23 114651.png +0 -0
  13. package/docs/Screenshot 2026-04-23 114737.png +0 -0
  14. package/docs/Screenshot 2026-04-23 114904.png +0 -0
  15. package/docs/Screenshot 2026-04-23 114940.png +0 -0
  16. package/docs/Screenshot 2026-04-23 115003.png +0 -0
  17. package/docs/Screenshot 2026-04-23 115024.png +0 -0
  18. package/docs/Screenshot 2026-04-23 115116.png +0 -0
  19. package/docs/Screenshot 2026-04-23 115141.png +0 -0
  20. package/docs/Screenshot 2026-04-23 115252.png +0 -0
  21. package/docs/Screenshot 2026-04-23 115448.png +0 -0
  22. package/docs/Screenshot 2026-04-23 120534.png +0 -0
  23. package/docs/Screenshot 2026-04-23 122544.png +0 -0
  24. package/docs/Screenshot 2026-04-23 122712.png +0 -0
  25. package/docs/Screenshot 2026-04-23 122730.png +0 -0
  26. package/docs/Screenshot 2026-04-23 122858.png +0 -0
  27. package/docs/Screenshot 2026-04-23 122924.png +0 -0
  28. package/docs/Screenshot 2026-04-23 122937.png +0 -0
  29. package/package.json +13 -4
  30. package/server/src/controllers/bulk-transfer.js +141 -0
  31. package/server/src/controllers/config.js +76 -3
  32. package/server/src/controllers/index.js +2 -0
  33. package/server/src/controllers/sync-media.js +24 -0
  34. package/server/src/routes/index.js +18 -0
  35. package/server/src/services/bulk-transfer.js +837 -0
  36. package/server/src/services/index.js +2 -0
  37. package/server/src/services/sync-media.js +168 -32
  38. package/server/src/services/sync.js +137 -1
  39. package/docs/Screenshot 2026-04-20 160506.png +0 -0
  40. package/docs/Screenshot 2026-04-20 160558.png +0 -0
  41. package/docs/Screenshot 2026-04-20 175903.png +0 -0
  42. package/docs/Screenshot 2026-04-20 175931.png +0 -0
  43. package/docs/Screenshot 2026-04-20 180001.png +0 -0
  44. package/docs/Screenshot 2026-04-20 180041.png +0 -0
  45. package/docs/Screenshot 2026-04-20 180116.png +0 -0
  46. package/docs/Screenshot 2026-04-20 180135.png +0 -0
  47. package/docs/Screenshot 2026-04-20 180202.png +0 -0
  48. package/docs/Screenshot 2026-04-20 180228.png +0 -0
  49. package/docs/Screenshot 2026-04-20 180251.png +0 -0
  50. package/docs/Screenshot 2026-04-20 180301.png +0 -0
  51. package/docs/clipchamp-screen-recording-script.md +0 -0
  52. package/docs/production-readiness-status.md +0 -34
  53. package/docs/production-readiness-test-matrix.md +0 -151
  54. package/docs/test-environments-setup-legacy.txt +0 -60
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "strapi-content-sync-pro",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Strapi v5 plugin to copy, migrate, and live-sync content, media, and data between multiple Strapi environments with bi-directional sync, field-level policies, scheduling, and alerts.",
5
5
  "license": "MIT",
6
6
  "author": {
7
- "name": "Ejaz Husain Arain",
7
+ "name": "Ejaz Hussain Arain",
8
8
  "email": "eharain@yahoo.com",
9
- "url": "https://github.com/eharain"
9
+ "url": "https://www.linkedin.com/in/ejazarain"
10
10
  },
11
11
  "repository": {
12
12
  "type": "git",
@@ -24,7 +24,10 @@
24
24
  "strapi",
25
25
  "strapi-plugin",
26
26
  "strapi-v5",
27
+ "strapi pulgin to transfer content",
27
28
  "sync",
29
+ "hot sync",
30
+ "real-time sync",
28
31
  "live-sync",
29
32
  "data-sync",
30
33
  "content-sync",
@@ -39,7 +42,13 @@
39
42
  "cross-environment",
40
43
  "data-transfer",
41
44
  "headless-cms",
42
- "content-management"
45
+ "content-management",
46
+ "data transfer",
47
+ "bulk transfer",
48
+ "field-level policies",
49
+ "scheduling",
50
+ "alerts",
51
+ "bi-directional sync"
43
52
  ],
44
53
  "exports": {
45
54
  "./strapi-admin": {
@@ -0,0 +1,141 @@
1
+ 'use strict';
2
+
3
+ const PLUGIN_ID = 'strapi-content-sync-pro';
4
+
5
+ function svc(strapi) {
6
+ return strapi.plugin(PLUGIN_ID).service('bulkTransfer');
7
+ }
8
+
9
+ module.exports = ({ strapi }) => ({
10
+ async preview(ctx) {
11
+ try {
12
+ const body = ctx.request.body || {};
13
+ const data = await svc(strapi).preview({
14
+ direction: body.direction,
15
+ scopes: body.scopes,
16
+ });
17
+ ctx.body = { data };
18
+ } catch (err) {
19
+ ctx.throw(400, err.message);
20
+ }
21
+ },
22
+
23
+ async start(ctx) {
24
+ try {
25
+ const body = ctx.request.body || {};
26
+ const data = await svc(strapi).start(body);
27
+ ctx.body = { data };
28
+ } catch (err) {
29
+ ctx.throw(400, err.message);
30
+ }
31
+ },
32
+
33
+ async next(ctx) {
34
+ try {
35
+ const { jobId } = ctx.params;
36
+ const data = await svc(strapi).next(jobId);
37
+ ctx.body = { data };
38
+ } catch (err) {
39
+ ctx.throw(400, err.message);
40
+ }
41
+ },
42
+
43
+ async runAll(ctx) {
44
+ try {
45
+ const { jobId } = ctx.params;
46
+ const data = await svc(strapi).runToCompletion(jobId);
47
+ ctx.body = { data };
48
+ } catch (err) {
49
+ ctx.throw(400, err.message);
50
+ }
51
+ },
52
+
53
+ async status(ctx) {
54
+ try {
55
+ const { jobId } = ctx.params;
56
+ const data = svc(strapi).getStatus(jobId);
57
+ if (!data) return ctx.notFound('Job not found');
58
+ ctx.body = { data };
59
+ } catch (err) {
60
+ ctx.throw(500, err.message);
61
+ }
62
+ },
63
+
64
+ async cancel(ctx) {
65
+ try {
66
+ const { jobId } = ctx.params;
67
+ const data = await svc(strapi).cancel(jobId);
68
+ ctx.body = { data };
69
+ } catch (err) {
70
+ ctx.throw(400, err.message);
71
+ }
72
+ },
73
+
74
+ async pause(ctx) {
75
+ try {
76
+ const { jobId } = ctx.params;
77
+ const data = await svc(strapi).pause(jobId);
78
+ ctx.body = { data };
79
+ } catch (err) {
80
+ ctx.throw(400, err.message);
81
+ }
82
+ },
83
+
84
+ async resume(ctx) {
85
+ try {
86
+ const { jobId } = ctx.params;
87
+ const data = await svc(strapi).resume(jobId);
88
+ ctx.body = { data };
89
+ } catch (err) {
90
+ ctx.throw(400, err.message);
91
+ }
92
+ },
93
+
94
+ async list(ctx) {
95
+ try {
96
+ ctx.body = { data: svc(strapi).listJobs() };
97
+ } catch (err) {
98
+ ctx.throw(500, err.message);
99
+ }
100
+ },
101
+
102
+ async history(ctx) {
103
+ try {
104
+ const data = await svc(strapi).getHistory();
105
+ ctx.body = { data };
106
+ } catch (err) {
107
+ ctx.throw(500, err.message);
108
+ }
109
+ },
110
+
111
+ async clearHistory(ctx) {
112
+ try {
113
+ const data = await svc(strapi).clearHistory();
114
+ ctx.body = { data };
115
+ } catch (err) {
116
+ ctx.throw(500, err.message);
117
+ }
118
+ },
119
+
120
+ async restart(ctx) {
121
+ try {
122
+ const { historyId } = ctx.params;
123
+ const body = ctx.request.body || {};
124
+ const data = await svc(strapi).restart(historyId, body);
125
+ ctx.body = { data };
126
+ } catch (err) {
127
+ ctx.throw(400, err.message);
128
+ }
129
+ },
130
+
131
+ async resumeFromHistory(ctx) {
132
+ try {
133
+ const { historyId } = ctx.params;
134
+ const body = ctx.request.body || {};
135
+ const data = await svc(strapi).resumeFromHistory(historyId, body);
136
+ ctx.body = { data };
137
+ } catch (err) {
138
+ ctx.throw(400, err.message);
139
+ }
140
+ },
141
+ });
@@ -198,12 +198,69 @@ module.exports = {
198
198
  * Proxy login to remote Strapi and retrieve/create API token
199
199
  */
200
200
  async remoteLogin(ctx) {
201
- const { baseUrl, email, password } = ctx.request.body;
201
+ const { baseUrl: rawBaseUrl, email, password } = ctx.request.body;
202
202
 
203
- if (!baseUrl || !email || !password) {
203
+ if (!rawBaseUrl || !email || !password) {
204
204
  return ctx.badRequest('baseUrl, email, and password are required');
205
205
  }
206
206
 
207
+ // --- Normalize and validate the base URL so URL mistakes produce a clear message ---
208
+ let baseUrl = String(rawBaseUrl).trim();
209
+ if (!/^https?:\/\//i.test(baseUrl)) {
210
+ return ctx.badRequest(
211
+ `Invalid Server URL "${rawBaseUrl}": must start with http:// or https:// (e.g. http://localhost:4010)`
212
+ );
213
+ }
214
+ // Strip trailing slashes and accidental /admin suffix
215
+ baseUrl = baseUrl.replace(/\/+$/, '').replace(/\/admin$/i, '');
216
+ let parsedUrl;
217
+ try {
218
+ parsedUrl = new URL(baseUrl);
219
+ } catch {
220
+ return ctx.badRequest(
221
+ `Invalid Server URL "${rawBaseUrl}": not a valid URL. Expected format: http(s)://host[:port]`
222
+ );
223
+ }
224
+ if (!parsedUrl.hostname) {
225
+ return ctx.badRequest(`Invalid Server URL "${rawBaseUrl}": missing hostname`);
226
+ }
227
+
228
+ // --- Pre-flight reachability check against /admin/init so wrong URL != "invalid credentials" ---
229
+ try {
230
+ const initResp = await fetch(`${baseUrl}/admin/init`, { method: 'GET' });
231
+ if (!initResp.ok) {
232
+ return ctx.throw(
233
+ 502,
234
+ `Server URL "${baseUrl}" is reachable but did not respond as a Strapi admin (HTTP ${initResp.status} on /admin/init). Check that the URL points to the root of a Strapi v5 server (no /admin suffix) and that the admin panel is enabled.`
235
+ );
236
+ }
237
+ const initData = await initResp.json().catch(() => null);
238
+ if (!initData || typeof initData !== 'object' || !('data' in initData)) {
239
+ return ctx.throw(
240
+ 502,
241
+ `Server URL "${baseUrl}" did not return a valid Strapi /admin/init response. Please verify the URL points to a Strapi v5 instance.`
242
+ );
243
+ }
244
+ if (initData.data && initData.data.hasAdmin === false) {
245
+ return ctx.throw(
246
+ 400,
247
+ `Remote Strapi at "${baseUrl}" has no admin user yet. Create the first admin in the remote Strapi panel before generating a token.`
248
+ );
249
+ }
250
+ } catch (err) {
251
+ if (err && err.status) throw err;
252
+ const code = err && (err.cause?.code || err.code);
253
+ const hint =
254
+ code === 'ECONNREFUSED'
255
+ ? 'Connection refused — the server is not running or not listening on that port.'
256
+ : code === 'ENOTFOUND'
257
+ ? 'Host not found — check the hostname/IP spelling and that it resolves from this machine.'
258
+ : code === 'ETIMEDOUT'
259
+ ? 'Request timed out — check firewall, network, and that the port is reachable.'
260
+ : 'Network error while contacting the remote server.';
261
+ return ctx.throw(502, `Cannot reach "${baseUrl}": ${hint}${code ? ` (${code})` : ''}`);
262
+ }
263
+
207
264
  try {
208
265
  // Step 1: Login to remote Strapi admin
209
266
  const loginResponse = await fetch(`${baseUrl}/admin/login`, {
@@ -219,7 +276,23 @@ module.exports = {
219
276
 
220
277
  if (!loginResponse.ok) {
221
278
  const errorBody = await loginResponse.json().catch(() => ({}));
222
- const errorMessage = errorBody?.error?.message || `Login failed with status ${loginResponse.status}`;
279
+ const remoteMsg = errorBody?.error?.message;
280
+ // Distinguish URL-vs-credential failures so users aren't misled
281
+ if (loginResponse.status === 404) {
282
+ return ctx.throw(
283
+ 404,
284
+ `"${baseUrl}/admin/login" not found (HTTP 404). The Server URL likely points to the wrong path — use the Strapi root URL (e.g. http://localhost:4010), not an admin or API sub-path.`
285
+ );
286
+ }
287
+ if (loginResponse.status === 405) {
288
+ return ctx.throw(
289
+ 405,
290
+ `"${baseUrl}/admin/login" rejected the request method. The Server URL may be pointing to a proxy or non-Strapi endpoint.`
291
+ );
292
+ }
293
+ const errorMessage = remoteMsg
294
+ ? `Remote login failed: ${remoteMsg}. If you believe the credentials are correct, double-check the Server URL "${baseUrl}" is the right Strapi instance.`
295
+ : `Login failed with status ${loginResponse.status} at ${baseUrl}/admin/login`;
223
296
  return ctx.throw(loginResponse.status, errorMessage);
224
297
  }
225
298
 
@@ -13,6 +13,7 @@ const syncMedia = require('./sync-media');
13
13
  const alerts = require('./alerts');
14
14
  const dependencies = require('./dependencies');
15
15
  const syncStats = require('./sync-stats');
16
+ const bulkTransfer = require('./bulk-transfer');
16
17
 
17
18
  module.exports = {
18
19
  ping,
@@ -28,4 +29,5 @@ module.exports = {
28
29
  alerts,
29
30
  dependencies,
30
31
  syncStats,
32
+ bulkTransfer,
31
33
  };
@@ -111,6 +111,30 @@ module.exports = ({ strapi }) => ({
111
111
  }
112
112
  },
113
113
 
114
+ async pauseProfile(ctx) {
115
+ try {
116
+ ctx.body = { data: await service(strapi).pauseProfile(ctx.params.id) };
117
+ } catch (err) {
118
+ ctx.throw(400, err.message);
119
+ }
120
+ },
121
+
122
+ async resumeProfile(ctx) {
123
+ try {
124
+ ctx.body = { data: await service(strapi).resumeProfile(ctx.params.id) };
125
+ } catch (err) {
126
+ ctx.throw(400, err.message);
127
+ }
128
+ },
129
+
130
+ async cancelProfile(ctx) {
131
+ try {
132
+ ctx.body = { data: await service(strapi).cancelProfile(ctx.params.id) };
133
+ } catch (err) {
134
+ ctx.throw(400, err.message);
135
+ }
136
+ },
137
+
114
138
  // ── Morph link sync (documentId-based mapping) ───────────────────────────
115
139
 
116
140
  async getMorphLinks(ctx) {
@@ -91,6 +91,9 @@ const adminRoutes = [
91
91
  { method: 'DELETE', path: '/media-sync/profiles/:id', handler: 'syncMedia.deleteProfile', config: { policies: [] } },
92
92
  { method: 'POST', path: '/media-sync/profiles/:id/activate', handler: 'syncMedia.activateProfile', config: { policies: [] } },
93
93
  { method: 'POST', path: '/media-sync/profiles/:id/run', handler: 'syncMedia.runProfile', config: { policies: [] } },
94
+ { method: 'POST', path: '/media-sync/profiles/:id/pause', handler: 'syncMedia.pauseProfile', config: { policies: [] } },
95
+ { method: 'POST', path: '/media-sync/profiles/:id/resume', handler: 'syncMedia.resumeProfile', config: { policies: [] } },
96
+ { method: 'POST', path: '/media-sync/profiles/:id/cancel', handler: 'syncMedia.cancelProfile', config: { policies: [] } },
94
97
  { method: 'POST', path: '/media-sync/run-active', handler: 'syncMedia.runActiveProfiles', config: { policies: [] } },
95
98
  { method: 'GET', path: '/media-sync/morph-links', handler: 'syncMedia.getMorphLinks', config: { policies: [] } },
96
99
  { method: 'POST', path: '/media-sync/morph-links/apply', handler: 'syncMedia.applyMorphLinks', config: { policies: [] } },
@@ -110,6 +113,21 @@ const adminRoutes = [
110
113
  { method: 'GET', path: '/dependencies/:uid/sync-order', handler: 'dependencies.getSyncOrder', config: { policies: [] } },
111
114
  { method: 'GET', path: '/dependencies/:uid/summary', handler: 'dependencies.getSummary', config: { policies: [] } },
112
115
  { method: 'POST', path: '/dependencies/clear-cache', handler: 'dependencies.clearCache', config: { policies: [] } },
116
+
117
+ // Bulk Transfer (one-click full pull / full push)
118
+ { method: 'POST', path: '/bulk-transfer/preview', handler: 'bulkTransfer.preview', config: { policies: [] } },
119
+ { method: 'POST', path: '/bulk-transfer/start', handler: 'bulkTransfer.start', config: { policies: [] } },
120
+ { method: 'GET', path: '/bulk-transfer/jobs', handler: 'bulkTransfer.list', config: { policies: [] } },
121
+ { method: 'GET', path: '/bulk-transfer/jobs/:jobId', handler: 'bulkTransfer.status', config: { policies: [] } },
122
+ { method: 'POST', path: '/bulk-transfer/jobs/:jobId/next', handler: 'bulkTransfer.next', config: { policies: [] } },
123
+ { method: 'POST', path: '/bulk-transfer/jobs/:jobId/run-all', handler: 'bulkTransfer.runAll', config: { policies: [] } },
124
+ { method: 'POST', path: '/bulk-transfer/jobs/:jobId/pause', handler: 'bulkTransfer.pause', config: { policies: [] } },
125
+ { method: 'POST', path: '/bulk-transfer/jobs/:jobId/resume', handler: 'bulkTransfer.resume', config: { policies: [] } },
126
+ { method: 'POST', path: '/bulk-transfer/jobs/:jobId/cancel', handler: 'bulkTransfer.cancel', config: { policies: [] } },
127
+ { method: 'GET', path: '/bulk-transfer/history', handler: 'bulkTransfer.history', config: { policies: [] } },
128
+ { method: 'POST', path: '/bulk-transfer/history/clear', handler: 'bulkTransfer.clearHistory', config: { policies: [] } },
129
+ { method: 'POST', path: '/bulk-transfer/history/:historyId/restart', handler: 'bulkTransfer.restart', config: { policies: [] } },
130
+ { method: 'POST', path: '/bulk-transfer/history/:historyId/resume', handler: 'bulkTransfer.resumeFromHistory', config: { policies: [] } },
113
131
  ];
114
132
 
115
133
  module.exports = {