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.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/admin/src/components/ConfigTab.jsx +1038 -0
- package/admin/src/components/ContentTypesTab.jsx +160 -0
- package/admin/src/components/HelpTab.jsx +945 -0
- package/admin/src/components/LogsTab.jsx +136 -0
- package/admin/src/components/MediaTab.jsx +557 -0
- package/admin/src/components/SyncProfilesTab.jsx +715 -0
- package/admin/src/components/SyncTab.jsx +988 -0
- package/admin/src/index.js +31 -0
- package/admin/src/pages/App/index.jsx +129 -0
- package/admin/src/pluginId.js +3 -0
- package/package.json +84 -0
- package/server/src/bootstrap.js +151 -0
- package/server/src/config/index.js +5 -0
- package/server/src/content-types/index.js +7 -0
- package/server/src/content-types/sync-log/schema.json +24 -0
- package/server/src/controllers/alerts.js +59 -0
- package/server/src/controllers/config.js +292 -0
- package/server/src/controllers/content-type-discovery.js +9 -0
- package/server/src/controllers/dependencies.js +109 -0
- package/server/src/controllers/index.js +29 -0
- package/server/src/controllers/ping.js +7 -0
- package/server/src/controllers/sync-config.js +26 -0
- package/server/src/controllers/sync-enforcement.js +323 -0
- package/server/src/controllers/sync-execution.js +134 -0
- package/server/src/controllers/sync-log.js +18 -0
- package/server/src/controllers/sync-media.js +158 -0
- package/server/src/controllers/sync-profiles.js +182 -0
- package/server/src/controllers/sync.js +31 -0
- package/server/src/destroy.js +7 -0
- package/server/src/index.js +21 -0
- package/server/src/middlewares/verify-signature.js +32 -0
- package/server/src/register.js +7 -0
- package/server/src/routes/index.js +111 -0
- package/server/src/services/alerts.js +437 -0
- package/server/src/services/config.js +68 -0
- package/server/src/services/content-type-discovery.js +41 -0
- package/server/src/services/dependency-resolver.js +284 -0
- package/server/src/services/index.js +30 -0
- package/server/src/services/ping.js +7 -0
- package/server/src/services/sync-config.js +45 -0
- package/server/src/services/sync-enforcement.js +362 -0
- package/server/src/services/sync-execution.js +541 -0
- package/server/src/services/sync-log.js +56 -0
- package/server/src/services/sync-media.js +963 -0
- package/server/src/services/sync-profiles.js +380 -0
- package/server/src/services/sync.js +248 -0
- package/server/src/utils/applier.js +89 -0
- package/server/src/utils/comparator.js +83 -0
- package/server/src/utils/fetcher.js +142 -0
- package/server/src/utils/hmac.js +37 -0
- package/server/src/utils/pagination.js +51 -0
- package/server/src/utils/sync-guard.js +29 -0
- package/server/src/utils/sync-id.js +16 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const STORE_KEY = 'sync-alerts-settings';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sync Alerts Service
|
|
7
|
+
*
|
|
8
|
+
* Manages notifications for sync success/failure events.
|
|
9
|
+
* Supports:
|
|
10
|
+
* - Strapi built-in notifications (sync log)
|
|
11
|
+
* - Email notifications (using Strapi's email plugin - requires configuration)
|
|
12
|
+
* - Custom webhook notifications
|
|
13
|
+
*
|
|
14
|
+
* For email to work, you need to configure Strapi's email plugin:
|
|
15
|
+
* - @strapi/provider-email-sendgrid
|
|
16
|
+
* - @strapi/provider-email-mailgun
|
|
17
|
+
* - @strapi/provider-email-amazon-ses
|
|
18
|
+
* - @strapi/provider-email-nodemailer
|
|
19
|
+
*
|
|
20
|
+
* See: https://docs.strapi.io/dev-docs/providers#configuring-providers
|
|
21
|
+
*/
|
|
22
|
+
module.exports = ({ strapi }) => {
|
|
23
|
+
function getStore() {
|
|
24
|
+
return strapi.store({ type: 'plugin', name: 'strapi-content-sync-pro' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function plugin() {
|
|
28
|
+
return strapi.plugin('strapi-content-sync-pro');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEFAULT_ALERT_SETTINGS = {
|
|
32
|
+
enabled: true,
|
|
33
|
+
channels: {
|
|
34
|
+
strapiNotification: {
|
|
35
|
+
enabled: true,
|
|
36
|
+
onSuccess: false,
|
|
37
|
+
onFailure: true,
|
|
38
|
+
},
|
|
39
|
+
email: {
|
|
40
|
+
enabled: false,
|
|
41
|
+
onSuccess: false,
|
|
42
|
+
onFailure: true,
|
|
43
|
+
recipients: [],
|
|
44
|
+
// Optional: custom from address (uses Strapi email plugin default if not set)
|
|
45
|
+
from: '',
|
|
46
|
+
},
|
|
47
|
+
webhook: {
|
|
48
|
+
enabled: false,
|
|
49
|
+
onSuccess: true,
|
|
50
|
+
onFailure: true,
|
|
51
|
+
url: '',
|
|
52
|
+
headers: {},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
throttle: {
|
|
56
|
+
enabled: true,
|
|
57
|
+
maxAlertsPerHour: 10,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// In-memory alert tracking for throttling
|
|
62
|
+
let alertHistory = [];
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
/**
|
|
66
|
+
* Get alert settings
|
|
67
|
+
*/
|
|
68
|
+
async getSettings() {
|
|
69
|
+
const store = getStore();
|
|
70
|
+
const data = await store.get({ key: STORE_KEY });
|
|
71
|
+
const settings = { ...DEFAULT_ALERT_SETTINGS };
|
|
72
|
+
|
|
73
|
+
if (data) {
|
|
74
|
+
Object.assign(settings, data);
|
|
75
|
+
// Deep merge channels
|
|
76
|
+
if (data.channels) {
|
|
77
|
+
settings.channels = {
|
|
78
|
+
...DEFAULT_ALERT_SETTINGS.channels,
|
|
79
|
+
...data.channels,
|
|
80
|
+
};
|
|
81
|
+
for (const channel of ['strapiNotification', 'email', 'webhook']) {
|
|
82
|
+
if (data.channels[channel]) {
|
|
83
|
+
settings.channels[channel] = {
|
|
84
|
+
...DEFAULT_ALERT_SETTINGS.channels[channel],
|
|
85
|
+
...data.channels[channel],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check if Strapi email plugin is configured
|
|
93
|
+
const emailPluginConfigured = this.isEmailPluginConfigured();
|
|
94
|
+
settings.emailPluginConfigured = emailPluginConfigured;
|
|
95
|
+
|
|
96
|
+
return settings;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if Strapi's email plugin is configured
|
|
101
|
+
*/
|
|
102
|
+
isEmailPluginConfigured() {
|
|
103
|
+
try {
|
|
104
|
+
// Check if email service exists and has send method
|
|
105
|
+
return !!(strapi.plugin('email')?.service('email')?.send);
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Update alert settings
|
|
113
|
+
*/
|
|
114
|
+
async updateSettings(updates) {
|
|
115
|
+
const store = getStore();
|
|
116
|
+
const storedData = await store.get({ key: STORE_KEY }) || {};
|
|
117
|
+
|
|
118
|
+
// Deep merge for nested channel settings
|
|
119
|
+
const newSettings = {
|
|
120
|
+
...storedData,
|
|
121
|
+
...updates,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (updates.channels) {
|
|
125
|
+
newSettings.channels = {
|
|
126
|
+
...(storedData.channels || {}),
|
|
127
|
+
...updates.channels,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
for (const channel of ['strapiNotification', 'email', 'webhook']) {
|
|
131
|
+
if (updates.channels[channel]) {
|
|
132
|
+
newSettings.channels[channel] = {
|
|
133
|
+
...(storedData.channels?.[channel] || {}),
|
|
134
|
+
...updates.channels[channel],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Validate email settings if enabled
|
|
141
|
+
if (newSettings.channels?.email?.enabled) {
|
|
142
|
+
if (!newSettings.channels.email.recipients || newSettings.channels.email.recipients.length === 0) {
|
|
143
|
+
throw new Error('Email channel enabled but no recipients configured');
|
|
144
|
+
}
|
|
145
|
+
if (!this.isEmailPluginConfigured()) {
|
|
146
|
+
throw new Error('Email channel enabled but Strapi email plugin is not configured. Please install and configure an email provider (e.g., @strapi/provider-email-sendgrid, @strapi/provider-email-nodemailer)');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Validate webhook URL
|
|
151
|
+
if (newSettings.channels?.webhook?.enabled && !newSettings.channels.webhook.url) {
|
|
152
|
+
throw new Error('Webhook channel enabled but no URL configured');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await store.set({ key: STORE_KEY, value: newSettings });
|
|
156
|
+
|
|
157
|
+
return newSettings;
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if alerts are being throttled
|
|
162
|
+
*/
|
|
163
|
+
isThrottled() {
|
|
164
|
+
const oneHourAgo = Date.now() - 3600000;
|
|
165
|
+
alertHistory = alertHistory.filter(ts => ts > oneHourAgo);
|
|
166
|
+
return alertHistory.length >= 10; // Default max
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Record an alert for throttling
|
|
171
|
+
*/
|
|
172
|
+
recordAlert() {
|
|
173
|
+
alertHistory.push(Date.now());
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Send an alert through configured channels
|
|
178
|
+
*/
|
|
179
|
+
async sendAlert(eventType, data) {
|
|
180
|
+
const store = getStore();
|
|
181
|
+
const settings = await store.get({ key: STORE_KEY }) || DEFAULT_ALERT_SETTINGS;
|
|
182
|
+
|
|
183
|
+
if (!settings.enabled) {
|
|
184
|
+
return { sent: false, reason: 'Alerts disabled' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check throttling
|
|
188
|
+
if (settings.throttle?.enabled && this.isThrottled()) {
|
|
189
|
+
strapi.log.warn('Alert throttled - too many alerts in the past hour');
|
|
190
|
+
return { sent: false, reason: 'Throttled' };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const isSuccess = eventType === 'sync_success';
|
|
194
|
+
const isFailure = eventType === 'sync_failure';
|
|
195
|
+
const results = [];
|
|
196
|
+
|
|
197
|
+
// Strapi notification channel
|
|
198
|
+
if (settings.channels?.strapiNotification?.enabled) {
|
|
199
|
+
const shouldSend = (isSuccess && settings.channels.strapiNotification.onSuccess) ||
|
|
200
|
+
(isFailure && settings.channels.strapiNotification.onFailure);
|
|
201
|
+
if (shouldSend) {
|
|
202
|
+
try {
|
|
203
|
+
await this.sendStrapiNotification(eventType, data);
|
|
204
|
+
results.push({ channel: 'strapiNotification', success: true });
|
|
205
|
+
} catch (error) {
|
|
206
|
+
results.push({ channel: 'strapiNotification', success: false, error: error.message });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Email channel
|
|
212
|
+
if (settings.channels?.email?.enabled) {
|
|
213
|
+
const shouldSend = (isSuccess && settings.channels.email.onSuccess) ||
|
|
214
|
+
(isFailure && settings.channels.email.onFailure);
|
|
215
|
+
if (shouldSend) {
|
|
216
|
+
try {
|
|
217
|
+
await this.sendEmailNotification(eventType, data, settings.channels.email);
|
|
218
|
+
results.push({ channel: 'email', success: true });
|
|
219
|
+
} catch (error) {
|
|
220
|
+
results.push({ channel: 'email', success: false, error: error.message });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Webhook channel
|
|
226
|
+
if (settings.channels?.webhook?.enabled) {
|
|
227
|
+
const shouldSend = (isSuccess && settings.channels.webhook.onSuccess) ||
|
|
228
|
+
(isFailure && settings.channels.webhook.onFailure);
|
|
229
|
+
if (shouldSend) {
|
|
230
|
+
try {
|
|
231
|
+
await this.sendWebhookNotification(eventType, data, settings.channels.webhook);
|
|
232
|
+
results.push({ channel: 'webhook', success: true });
|
|
233
|
+
} catch (error) {
|
|
234
|
+
results.push({ channel: 'webhook', success: false, error: error.message });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (results.length > 0) {
|
|
240
|
+
this.recordAlert();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { sent: results.length > 0, results };
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Send Strapi admin notification
|
|
248
|
+
*/
|
|
249
|
+
async sendStrapiNotification(eventType, data) {
|
|
250
|
+
const logService = plugin().service('syncLog');
|
|
251
|
+
const isFailure = eventType === 'sync_failure';
|
|
252
|
+
|
|
253
|
+
// Log to sync log (visible in admin)
|
|
254
|
+
await logService.log({
|
|
255
|
+
action: isFailure ? 'sync_error' : 'sync_complete',
|
|
256
|
+
contentType: data.contentType || 'unknown',
|
|
257
|
+
direction: 'system',
|
|
258
|
+
status: isFailure ? 'error' : 'success',
|
|
259
|
+
message: this.formatMessage(eventType, data),
|
|
260
|
+
details: data,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
strapi.log.info(`[Sync Alert] ${eventType}: ${this.formatMessage(eventType, data)}`);
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Send email notification using Strapi's email plugin
|
|
268
|
+
*/
|
|
269
|
+
async sendEmailNotification(eventType, data, emailConfig) {
|
|
270
|
+
if (!this.isEmailPluginConfigured()) {
|
|
271
|
+
throw new Error('Strapi email plugin is not configured');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const emailService = strapi.plugin('email').service('email');
|
|
275
|
+
|
|
276
|
+
const subject = eventType === 'sync_failure'
|
|
277
|
+
? `[Sync Alert] Sync Failed - ${data.profile || data.contentType}`
|
|
278
|
+
: `[Sync Alert] Sync Completed - ${data.profile || data.contentType}`;
|
|
279
|
+
|
|
280
|
+
const html = this.formatEmailBody(eventType, data);
|
|
281
|
+
const text = this.formatMessage(eventType, data);
|
|
282
|
+
|
|
283
|
+
for (const recipient of emailConfig.recipients) {
|
|
284
|
+
const emailOptions = {
|
|
285
|
+
to: recipient,
|
|
286
|
+
subject,
|
|
287
|
+
html,
|
|
288
|
+
text,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Only set from if explicitly configured
|
|
292
|
+
if (emailConfig.from) {
|
|
293
|
+
emailOptions.from = emailConfig.from;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await emailService.send(emailOptions);
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Send webhook notification
|
|
302
|
+
*/
|
|
303
|
+
async sendWebhookNotification(eventType, data, webhookConfig) {
|
|
304
|
+
const payload = {
|
|
305
|
+
event: eventType,
|
|
306
|
+
timestamp: new Date().toISOString(),
|
|
307
|
+
data,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const response = await fetch(webhookConfig.url, {
|
|
311
|
+
method: 'POST',
|
|
312
|
+
headers: {
|
|
313
|
+
'Content-Type': 'application/json',
|
|
314
|
+
...webhookConfig.headers,
|
|
315
|
+
},
|
|
316
|
+
body: JSON.stringify(payload),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
throw new Error(`Webhook failed with status ${response.status}`);
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Format alert message
|
|
326
|
+
*/
|
|
327
|
+
formatMessage(eventType, data) {
|
|
328
|
+
if (eventType === 'sync_failure') {
|
|
329
|
+
return `Sync failed for ${data.profile || data.contentType}: ${data.error || 'Unknown error'}`;
|
|
330
|
+
}
|
|
331
|
+
if (eventType === 'sync_success') {
|
|
332
|
+
const duration = data.duration ? ` (${Math.round(data.duration / 1000)}s)` : '';
|
|
333
|
+
return `Sync completed for ${data.profile || data.contentType}${duration}`;
|
|
334
|
+
}
|
|
335
|
+
return `Sync event: ${eventType}`;
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Format email body
|
|
340
|
+
*/
|
|
341
|
+
formatEmailBody(eventType, data) {
|
|
342
|
+
const isFailure = eventType === 'sync_failure';
|
|
343
|
+
const statusColor = isFailure ? '#dc3545' : '#28a745';
|
|
344
|
+
const statusText = isFailure ? 'Failed' : 'Completed';
|
|
345
|
+
|
|
346
|
+
return `
|
|
347
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
348
|
+
<h2 style="color: ${statusColor};">Sync ${statusText}</h2>
|
|
349
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
350
|
+
<tr>
|
|
351
|
+
<td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>Profile:</strong></td>
|
|
352
|
+
<td style="padding: 8px; border-bottom: 1px solid #ddd;">${data.profile || 'N/A'}</td>
|
|
353
|
+
</tr>
|
|
354
|
+
<tr>
|
|
355
|
+
<td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>Content Type:</strong></td>
|
|
356
|
+
<td style="padding: 8px; border-bottom: 1px solid #ddd;">${data.contentType || 'N/A'}</td>
|
|
357
|
+
</tr>
|
|
358
|
+
<tr>
|
|
359
|
+
<td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>Time:</strong></td>
|
|
360
|
+
<td style="padding: 8px; border-bottom: 1px solid #ddd;">${new Date().toISOString()}</td>
|
|
361
|
+
</tr>
|
|
362
|
+
${data.duration ? `
|
|
363
|
+
<tr>
|
|
364
|
+
<td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>Duration:</strong></td>
|
|
365
|
+
<td style="padding: 8px; border-bottom: 1px solid #ddd;">${Math.round(data.duration / 1000)}s</td>
|
|
366
|
+
</tr>
|
|
367
|
+
` : ''}
|
|
368
|
+
${isFailure ? `
|
|
369
|
+
<tr>
|
|
370
|
+
<td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>Error:</strong></td>
|
|
371
|
+
<td style="padding: 8px; border-bottom: 1px solid #ddd; color: #dc3545;">${data.error || 'Unknown error'}</td>
|
|
372
|
+
</tr>
|
|
373
|
+
` : ''}
|
|
374
|
+
</table>
|
|
375
|
+
<p style="margin-top: 20px; color: #666; font-size: 12px;">
|
|
376
|
+
This is an automated notification from Strapi-to-Strapi Data Sync Plugin.
|
|
377
|
+
</p>
|
|
378
|
+
</div>
|
|
379
|
+
`;
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Test alert channels
|
|
384
|
+
*/
|
|
385
|
+
async testChannel(channel) {
|
|
386
|
+
const testData = {
|
|
387
|
+
profile: 'Test Profile',
|
|
388
|
+
contentType: 'api::test.test',
|
|
389
|
+
duration: 5000,
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const store = getStore();
|
|
393
|
+
const settings = await store.get({ key: STORE_KEY }) || DEFAULT_ALERT_SETTINGS;
|
|
394
|
+
|
|
395
|
+
switch (channel) {
|
|
396
|
+
case 'strapiNotification':
|
|
397
|
+
await this.sendStrapiNotification('sync_success', testData);
|
|
398
|
+
return { success: true, message: 'Strapi notification sent - check sync logs' };
|
|
399
|
+
|
|
400
|
+
case 'email':
|
|
401
|
+
if (!this.isEmailPluginConfigured()) {
|
|
402
|
+
throw new Error('Strapi email plugin is not configured. Install and configure an email provider first.');
|
|
403
|
+
}
|
|
404
|
+
if (!settings.channels?.email?.recipients?.length) {
|
|
405
|
+
throw new Error('No email recipients configured');
|
|
406
|
+
}
|
|
407
|
+
await this.sendEmailNotification('sync_success', testData, settings.channels.email);
|
|
408
|
+
return { success: true, message: `Test email sent to: ${settings.channels.email.recipients.join(', ')}` };
|
|
409
|
+
|
|
410
|
+
case 'webhook':
|
|
411
|
+
if (!settings.channels?.webhook?.url) {
|
|
412
|
+
throw new Error('No webhook URL configured');
|
|
413
|
+
}
|
|
414
|
+
await this.sendWebhookNotification('sync_success', testData, settings.channels.webhook);
|
|
415
|
+
return { success: true, message: 'Webhook notification sent' };
|
|
416
|
+
|
|
417
|
+
default:
|
|
418
|
+
throw new Error(`Unknown channel: ${channel}`);
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Get alert history summary
|
|
424
|
+
*/
|
|
425
|
+
getAlertStats() {
|
|
426
|
+
const oneHourAgo = Date.now() - 3600000;
|
|
427
|
+
const recentAlerts = alertHistory.filter(ts => ts > oneHourAgo);
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
alertsLastHour: recentAlerts.length,
|
|
431
|
+
throttleLimit: 10,
|
|
432
|
+
isThrottled: this.isThrottled(),
|
|
433
|
+
emailPluginConfigured: this.isEmailPluginConfigured(),
|
|
434
|
+
};
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const STORE_KEY = 'remote-server-config';
|
|
4
|
+
|
|
5
|
+
const SENSITIVE_FIELDS = ['apiToken', 'sharedSecret'];
|
|
6
|
+
|
|
7
|
+
module.exports = ({ strapi }) => {
|
|
8
|
+
function getStore() {
|
|
9
|
+
return strapi.store({
|
|
10
|
+
type: 'plugin',
|
|
11
|
+
name: 'strapi-content-sync-pro',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
async getConfig({ safe = true } = {}) {
|
|
17
|
+
const store = getStore();
|
|
18
|
+
const data = await store.get({ key: STORE_KEY });
|
|
19
|
+
|
|
20
|
+
if (!data) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!safe) {
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const sanitized = { ...data };
|
|
29
|
+
for (const field of SENSITIVE_FIELDS) {
|
|
30
|
+
if (sanitized[field]) {
|
|
31
|
+
sanitized[field] = '••••••••';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return sanitized;
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async setConfig(config) {
|
|
38
|
+
const store = getStore();
|
|
39
|
+
|
|
40
|
+
const existing = await store.get({ key: STORE_KEY }) || {};
|
|
41
|
+
|
|
42
|
+
const merged = { ...existing };
|
|
43
|
+
|
|
44
|
+
if (config.baseUrl !== undefined) {
|
|
45
|
+
merged.baseUrl = config.baseUrl;
|
|
46
|
+
}
|
|
47
|
+
if (config.apiToken !== undefined) {
|
|
48
|
+
merged.apiToken = config.apiToken;
|
|
49
|
+
}
|
|
50
|
+
if (config.syncDirection !== undefined) {
|
|
51
|
+
if (!['push', 'pull', 'bidirectional'].includes(config.syncDirection)) {
|
|
52
|
+
throw new Error('syncDirection must be "push", "pull", or "bidirectional"');
|
|
53
|
+
}
|
|
54
|
+
merged.syncDirection = config.syncDirection;
|
|
55
|
+
}
|
|
56
|
+
if (config.instanceId !== undefined) {
|
|
57
|
+
merged.instanceId = config.instanceId;
|
|
58
|
+
}
|
|
59
|
+
if (config.sharedSecret !== undefined) {
|
|
60
|
+
merged.sharedSecret = config.sharedSecret;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await store.set({ key: STORE_KEY, value: merged });
|
|
64
|
+
|
|
65
|
+
return merged;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PRIMITIVE_TYPES = [
|
|
4
|
+
'string', 'text', 'richtext', 'integer', 'biginteger',
|
|
5
|
+
'float', 'decimal', 'boolean', 'date', 'datetime',
|
|
6
|
+
'time', 'email', 'password', 'enumeration', 'uid', 'json',
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
module.exports = ({ strapi }) => ({
|
|
10
|
+
/**
|
|
11
|
+
* Return every user-defined collection type that is eligible for sync.
|
|
12
|
+
* Excludes admin, upload, and users-permissions content types.
|
|
13
|
+
*/
|
|
14
|
+
getSyncableContentTypes() {
|
|
15
|
+
const result = [];
|
|
16
|
+
|
|
17
|
+
for (const [uid, ct] of Object.entries(strapi.contentTypes)) {
|
|
18
|
+
if (!uid.startsWith('api::')) continue;
|
|
19
|
+
if (ct.kind !== 'collectionType') continue;
|
|
20
|
+
|
|
21
|
+
const primitiveAttributes = {};
|
|
22
|
+
for (const [name, attr] of Object.entries(ct.attributes || {})) {
|
|
23
|
+
if (PRIMITIVE_TYPES.includes(attr.type)) {
|
|
24
|
+
primitiveAttributes[name] = {
|
|
25
|
+
type: attr.type,
|
|
26
|
+
required: attr.required || false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
result.push({
|
|
32
|
+
uid,
|
|
33
|
+
kind: ct.kind,
|
|
34
|
+
displayName: ct.info?.displayName || uid,
|
|
35
|
+
attributes: primitiveAttributes,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
},
|
|
41
|
+
});
|