mongodb-backup-service 1.0.2 → 1.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mongodb-backup-service",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Reusable Node.js service for automated MongoDB backups with scheduling, startup recovery, Excel export, retention, email notifications, and backup locking.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,3 +1,4 @@
1
+ // src/notifications/emailNotifier.js
1
2
  const nodemailer = require('nodemailer');
2
3
  const logger = require('../utils/logger');
3
4
  const EmailTemplates = require('./emailTemplates');
@@ -6,6 +7,7 @@ class EmailNotifier {
6
7
  constructor(config) {
7
8
  this.config = config.email;
8
9
  this.transporter = null;
10
+ this.recipients = [];
9
11
 
10
12
  if (!this.config.enabled) {
11
13
  logger.info("Email notifications are disabled (email.enabled = false).");
@@ -19,6 +21,22 @@ class EmailNotifier {
19
21
  return;
20
22
  }
21
23
 
24
+ // Normalize recipient(s) into an array of trimmed, non‑empty email addresses
25
+ const normalizeRecipients = (to) => {
26
+ if (Array.isArray(to)) {
27
+ return to.map(r => String(r).trim()).filter(Boolean);
28
+ }
29
+ if (typeof to === 'string') {
30
+ return to.split(',').map(r => r.trim()).filter(Boolean);
31
+ }
32
+ return [];
33
+ };
34
+ this.recipients = normalizeRecipients(this.config.to);
35
+ if (this.recipients.length === 0) {
36
+ logger.warn('Email notifications enabled but recipient list is empty. Notifications will be skipped.');
37
+ return;
38
+ }
39
+
22
40
  this.transporter = nodemailer.createTransport({
23
41
  host: this.config.host,
24
42
  port: this.config.port,
@@ -29,7 +47,7 @@ class EmailNotifier {
29
47
  }
30
48
  });
31
49
 
32
- logger.info(`Email notifier initialised. SMTP host: ${this.config.host}, port: ${this.config.port}, secure: ${this.config.secure}, recipient: ${this.config.to}`);
50
+ logger.info(`Email notifier initialised. SMTP host: ${this.config.host}, port: ${this.config.port}, secure: ${this.config.secure}, recipients: ${this.recipients.join(', ')}`);
33
51
  }
34
52
 
35
53
  /**
@@ -61,16 +79,16 @@ class EmailNotifier {
61
79
  return;
62
80
  }
63
81
 
64
- const { subject, text } = EmailTemplates.getBackupReport(metadata);
82
+ const { subject, text, html } = EmailTemplates.getBackupReport(metadata);
65
83
 
66
84
  const mailOptions = {
67
85
  from: this.config.from || `"Database Backup Service" <${this.config.user}>`,
68
- to: this.config.to,
86
+ to: this.recipients,
69
87
  subject,
70
- text
88
+ text,
89
+ html
71
90
  };
72
-
73
- logger.info(`Sending email notification to: ${this.config.to} | Subject: ${subject}`);
91
+ logger.info(`Sending email notification to: ${this.recipients.join(', ')} | Subject: ${subject}`);
74
92
 
75
93
  try {
76
94
  const info = await this.transporter.sendMail(mailOptions);
@@ -1,33 +1,596 @@
1
+ // src/notifications/emailTemplates.js
2
+ /**
3
+ * EmailTemplates – Generates email subject, plain-text and HTML bodies for backup reports.
4
+ */
1
5
  class EmailTemplates {
2
- static getBackupReport(metadata) {
6
+ /** Escape HTML special characters to avoid injection. */
7
+ static _escapeHtml(str) {
8
+ if (typeof str !== 'string') return '';
9
+ return str
10
+ .replace(/&/g, '&amp;')
11
+ .replace(/</g, '&lt;')
12
+ .replace(/>/g, '&gt;')
13
+ .replace(/"/g, '&quot;')
14
+ .replace(/'/g, '&#39;');
15
+ }
16
+
17
+ /** Build the email subject line. */
18
+ static _buildSubject(metadata) {
19
+ const statusWord = metadata.status === 'success' ? 'Successful' : 'Failed';
20
+ return `Database Backup ${statusWord} — ${metadata.projectName} | ${metadata.backupDate}`;
21
+ }
22
+
23
+ /** Format duration from milliseconds into a human-readable string. */
24
+ static _formatDuration(ms) {
25
+ const mins = Math.floor(ms / 60000);
26
+ const secs = Math.floor((ms % 60000) / 1000);
27
+
28
+ if (mins === 0) {
29
+ return `${secs} seconds`;
30
+ }
31
+
32
+ return `${mins} minutes ${secs} seconds`;
33
+ }
34
+
35
+ /** Render a component status badge. */
36
+ static _renderStatusBadge(status) {
37
+ const normalized = (status || 'skipped').toLowerCase();
38
+
39
+ const map = {
40
+ success: {
41
+ icon: '✓',
42
+ label: 'Completed',
43
+ bg: '#ecfdf3',
44
+ color: '#067647'
45
+ },
46
+ failed: {
47
+ icon: '✕',
48
+ label: 'Failed',
49
+ bg: '#fef3f2',
50
+ color: '#b42318'
51
+ },
52
+ skipped: {
53
+ icon: '—',
54
+ label: 'Skipped',
55
+ bg: '#f2f4f7',
56
+ color: '#667085'
57
+ }
58
+ };
59
+
60
+ const item = map[normalized] || map.skipped;
61
+
62
+ return `
63
+ <span style="
64
+ display:inline-block;
65
+ background:${item.bg};
66
+ color:${item.color};
67
+ padding:5px 10px;
68
+ border-radius:999px;
69
+ font-size:12px;
70
+ font-weight:700;
71
+ white-space:nowrap;
72
+ ">
73
+ ${item.icon}&nbsp; ${item.label}
74
+ </span>
75
+ `;
76
+ }
77
+
78
+ /** Build the full HTML body. */
79
+ static _buildHtml(metadata) {
3
80
  const isSuccess = metadata.status === 'success';
4
- const subjectStatus = isSuccess ? 'Successful' : 'Failed';
5
-
6
- const durationMins = Math.floor(metadata.durationMs / 60000);
7
- const durationSecs = Math.floor((metadata.durationMs % 60000) / 1000);
8
- const durationStr = `${durationMins} minutes ${durationSecs} seconds`;
9
-
10
- const subject = `Database Backup ${subjectStatus} - ${metadata.projectName}`;
11
-
12
- let text = `Subject:\nDatabase Backup ${subjectStatus} - ${metadata.projectName}\n\n`;
13
- text += `Project:\n${metadata.projectName}\n\n`;
14
- text += `Backup Date:\n${metadata.backupDate}\n\n`;
15
- text += `Overall Status:\n${metadata.status.toUpperCase()}\n\n`;
16
-
17
- text += `MongoDB Dump:\n${metadata.mongoDump.status.toUpperCase()}\n`;
18
- if (metadata.mongoDump.file) text += `File: ${metadata.mongoDump.file}\n`;
19
- if (metadata.mongoDump.error) text += `Error: ${metadata.mongoDump.error}\n`;
20
- text += `\n`;
21
-
22
- text += `Excel Export:\n${metadata.excel.status.toUpperCase()}\n`;
23
- if (metadata.excel.file) text += `File: ${metadata.excel.file}\n`;
24
- if (metadata.excel.error) text += `Error: ${metadata.excel.error}\n`;
25
- text += `\n`;
26
-
27
- text += `Duration:\n${durationStr}\n`;
28
-
29
- return { subject, text };
81
+ const esc = EmailTemplates._escapeHtml;
82
+
83
+ const projectName = esc(metadata.projectName || 'Database');
84
+ const backupDate = esc(metadata.backupDate || 'N/A');
85
+ const startTime = esc(metadata.startTime || metadata.startedAt || 'N/A');
86
+ const endTime = esc(metadata.endTime || metadata.completedAt || 'N/A');
87
+ const duration = EmailTemplates._formatDuration(metadata.durationMs || 0);
88
+
89
+ const scheduleType = esc(
90
+ (metadata.schedule && metadata.schedule.type) || 'N/A'
91
+ );
92
+
93
+ const timezone = esc(
94
+ (metadata.schedule && metadata.schedule.timezone) || 'N/A'
95
+ );
96
+
97
+ const statusText = isSuccess ? 'SUCCESS' : 'FAILED';
98
+
99
+ const theme = isSuccess
100
+ ? {
101
+ accent: '#16a34a',
102
+ accentDark: '#15803d',
103
+ light: '#f0fdf4',
104
+ border: '#bbf7d0',
105
+ icon: '✓',
106
+ title: 'Backup Completed Successfully',
107
+ description: 'Your scheduled database backup completed successfully.'
108
+ }
109
+ : {
110
+ accent: '#dc2626',
111
+ accentDark: '#b91c1c',
112
+ light: '#fef2f2',
113
+ border: '#fecaca',
114
+ icon: '✕',
115
+ title: 'Backup Failed',
116
+ description: 'The database backup could not be completed successfully.'
117
+ };
118
+
119
+ const components = [
120
+ { name: 'MongoDB Dump', data: metadata.mongoDump },
121
+ { name: 'Excel Export', data: metadata.excel },
122
+ { name: 'Metadata Generation', data: metadata.metadata },
123
+ { name: 'Retention Cleanup', data: metadata.retention }
124
+ ];
125
+
126
+ const files = [
127
+ metadata.mongoDump && metadata.mongoDump.file,
128
+ metadata.excel && metadata.excel.file,
129
+ metadata.metadata && metadata.metadata.file
130
+ ].filter(Boolean);
131
+
132
+ const componentRows = components.map(component => {
133
+ const status =
134
+ component.data && component.data.status
135
+ ? component.data.status.toLowerCase()
136
+ : 'skipped';
137
+
138
+ return `
139
+ <tr>
140
+ <td style="
141
+ padding:13px 0;
142
+ border-bottom:1px solid #eaecf0;
143
+ font-size:14px;
144
+ color:#344054;
145
+ ">
146
+ ${esc(component.name)}
147
+ </td>
148
+
149
+ <td style="
150
+ padding:13px 0;
151
+ border-bottom:1px solid #eaecf0;
152
+ text-align:right;
153
+ ">
154
+ ${EmailTemplates._renderStatusBadge(status)}
155
+ </td>
156
+ </tr>
157
+ `;
158
+ }).join('');
159
+
160
+ const filesHtml = files.length
161
+ ? files.map(file => `
162
+ <div style="
163
+ padding:10px 12px;
164
+ margin-bottom:8px;
165
+ background:#f8fafc;
166
+ border:1px solid #eaecf0;
167
+ border-radius:8px;
168
+ font-size:13px;
169
+ color:#475467;
170
+ word-break:break-word;
171
+ ">
172
+ ${esc(file)}
173
+ </div>
174
+ `).join('')
175
+ : `
176
+ <div style="color:#667085;font-size:13px;">
177
+ No backup files were generated.
178
+ </div>
179
+ `;
180
+
181
+ let errorSection = '';
182
+
183
+ if (!isSuccess) {
184
+ const errors = components
185
+ .filter(component => component.data && component.data.error)
186
+ .map(component => `
187
+ <div style="
188
+ margin-bottom:10px;
189
+ padding:12px;
190
+ background:#fffafa;
191
+ border:1px solid #fecdca;
192
+ border-radius:8px;
193
+ font-size:13px;
194
+ line-height:1.5;
195
+ color:#475467;
196
+ ">
197
+ <strong style="color:#b42318;">
198
+ ${esc(component.name)}
199
+ </strong>
200
+ <br>
201
+ ${esc(component.data.error)}
202
+ </div>
203
+ `)
204
+ .join('');
205
+
206
+ errorSection = `
207
+ <tr>
208
+ <td style="padding-top:20px;">
209
+
210
+ <div style="
211
+ font-size:15px;
212
+ font-weight:700;
213
+ color:#101828;
214
+ margin-bottom:12px;
215
+ ">
216
+ Error Details
217
+ </div>
218
+
219
+ ${errors || `
220
+ <div style="
221
+ padding:12px;
222
+ background:#f8fafc;
223
+ border-radius:8px;
224
+ font-size:13px;
225
+ color:#667085;
226
+ ">
227
+ No detailed error information was provided.
228
+ </div>
229
+ `}
230
+
231
+ <div style="
232
+ margin-top:18px;
233
+ padding:14px;
234
+ background:#fffbeb;
235
+ border:1px solid #fedf89;
236
+ border-radius:8px;
237
+ font-size:13px;
238
+ line-height:1.5;
239
+ color:#475467;
240
+ ">
241
+ <strong style="color:#92400e;">
242
+ Action Required
243
+ </strong>
244
+ <br>
245
+ Please review the application logs and investigate
246
+ the failed backup component before the next scheduled run.
247
+ </div>
248
+
249
+ </td>
250
+ </tr>
251
+ `;
252
+ }
253
+
254
+ return `<!DOCTYPE html>
255
+ <html lang="en">
256
+ <head>
257
+ <meta charset="UTF-8">
258
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
259
+ <title>Database Backup Report</title>
260
+ </head>
261
+
262
+ <body style="
263
+ margin:0;
264
+ padding:0;
265
+ background:#f2f4f7;
266
+ font-family:Arial,Helvetica,sans-serif;
267
+ color:#101828;
268
+ ">
269
+
270
+ <table width="100%" cellpadding="0" cellspacing="0" border="0">
271
+ <tr>
272
+ <td align="center" style="padding:32px 16px;">
273
+
274
+ <table width="100%" cellpadding="0" cellspacing="0" border="0"
275
+ style="
276
+ max-width:620px;
277
+ background:#ffffff;
278
+ border:1px solid #eaecf0;
279
+ border-radius:14px;
280
+ overflow:hidden;
281
+ ">
282
+
283
+ <!-- HEADER -->
284
+ <tr>
285
+ <td style="
286
+ padding:26px 28px;
287
+ border-bottom:1px solid #eaecf0;
288
+ ">
289
+
290
+ <div style="
291
+ font-size:12px;
292
+ font-weight:700;
293
+ letter-spacing:1px;
294
+ color:#667085;
295
+ text-transform:uppercase;
296
+ margin-bottom:7px;
297
+ ">
298
+ Automated Database Backup
299
+ </div>
300
+
301
+ <div style="
302
+ font-size:24px;
303
+ font-weight:700;
304
+ color:#101828;
305
+ ">
306
+ Backup Report
307
+ </div>
308
+
309
+ <div style="
310
+ margin-top:6px;
311
+ font-size:14px;
312
+ color:#667085;
313
+ ">
314
+ ${projectName}
315
+ </div>
316
+
317
+ </td>
318
+ </tr>
319
+
320
+ <!-- STATUS -->
321
+ <tr>
322
+ <td style="padding:24px 28px 10px;">
323
+
324
+ <div style="
325
+ background:${theme.light};
326
+ border:1px solid ${theme.border};
327
+ border-radius:12px;
328
+ padding:18px;
329
+ ">
330
+
331
+ <table width="100%" cellpadding="0" cellspacing="0">
332
+ <tr>
333
+
334
+ <td width="48" valign="top">
335
+ <div style="
336
+ width:38px;
337
+ height:38px;
338
+ line-height:38px;
339
+ text-align:center;
340
+ background:${theme.accent};
341
+ color:#ffffff;
342
+ border-radius:50%;
343
+ font-size:20px;
344
+ font-weight:bold;
345
+ ">
346
+ ${theme.icon}
347
+ </div>
348
+ </td>
349
+
350
+ <td valign="middle">
351
+
352
+ <div style="
353
+ font-size:16px;
354
+ font-weight:700;
355
+ color:${theme.accentDark};
356
+ ">
357
+ ${theme.title}
358
+ </div>
359
+
360
+ <div style="
361
+ margin-top:4px;
362
+ font-size:13px;
363
+ line-height:1.5;
364
+ color:#475467;
365
+ ">
366
+ ${theme.description}
367
+ </div>
368
+
369
+ </td>
370
+
371
+ </tr>
372
+ </table>
373
+
374
+ </div>
375
+
376
+ </td>
377
+ </tr>
378
+
379
+ <!-- SUMMARY -->
380
+ <tr>
381
+ <td style="padding:14px 28px;">
382
+
383
+ <div style="
384
+ font-size:15px;
385
+ font-weight:700;
386
+ color:#101828;
387
+ margin-bottom:12px;
388
+ ">
389
+ Backup Summary
390
+ </div>
391
+
392
+ <table width="100%" cellpadding="0" cellspacing="0">
393
+
394
+ <tr>
395
+ <td style="padding:8px 0;color:#667085;font-size:13px;">
396
+ Backup Date
397
+ </td>
398
+ <td align="right" style="padding:8px 0;font-size:13px;font-weight:600;">
399
+ ${backupDate}
400
+ </td>
401
+ </tr>
402
+
403
+ <tr>
404
+ <td style="padding:8px 0;color:#667085;font-size:13px;">
405
+ Start Time
406
+ </td>
407
+ <td align="right" style="padding:8px 0;font-size:13px;">
408
+ ${startTime}
409
+ </td>
410
+ </tr>
411
+
412
+ <tr>
413
+ <td style="padding:8px 0;color:#667085;font-size:13px;">
414
+ End Time
415
+ </td>
416
+ <td align="right" style="padding:8px 0;font-size:13px;">
417
+ ${endTime}
418
+ </td>
419
+ </tr>
420
+
421
+ <tr>
422
+ <td style="padding:8px 0;color:#667085;font-size:13px;">
423
+ Duration
424
+ </td>
425
+ <td align="right" style="padding:8px 0;font-size:13px;font-weight:600;">
426
+ ${duration}
427
+ </td>
428
+ </tr>
429
+
430
+ <tr>
431
+ <td style="padding:8px 0;color:#667085;font-size:13px;">
432
+ Schedule
433
+ </td>
434
+ <td align="right" style="padding:8px 0;font-size:13px;">
435
+ ${scheduleType}
436
+ </td>
437
+ </tr>
438
+
439
+ <tr>
440
+ <td style="padding:8px 0;color:#667085;font-size:13px;">
441
+ Timezone
442
+ </td>
443
+ <td align="right" style="padding:8px 0;font-size:13px;">
444
+ ${timezone}
445
+ </td>
446
+ </tr>
447
+
448
+ <tr>
449
+ <td style="padding:8px 0;color:#667085;font-size:13px;">
450
+ Overall Status
451
+ </td>
452
+ <td align="right" style="
453
+ padding:8px 0;
454
+ font-size:13px;
455
+ font-weight:700;
456
+ color:${theme.accentDark};
457
+ ">
458
+ ${statusText}
459
+ </td>
460
+ </tr>
461
+
462
+ </table>
463
+
464
+ </td>
465
+ </tr>
466
+
467
+ <!-- COMPONENTS -->
468
+ <tr>
469
+ <td style="padding:14px 28px;">
470
+
471
+ <div style="
472
+ font-size:15px;
473
+ font-weight:700;
474
+ color:#101828;
475
+ margin-bottom:10px;
476
+ ">
477
+ Backup Components
478
+ </div>
479
+
480
+ <table width="100%" cellpadding="0" cellspacing="0">
481
+ <tbody>
482
+ ${componentRows}
483
+ </tbody>
484
+ </table>
485
+
486
+ </td>
487
+ </tr>
488
+
489
+ <!-- FILES -->
490
+ <tr>
491
+ <td style="padding:14px 28px;">
492
+
493
+ <div style="
494
+ font-size:15px;
495
+ font-weight:700;
496
+ color:#101828;
497
+ margin-bottom:12px;
498
+ ">
499
+ Generated Files
500
+ </div>
501
+
502
+ ${filesHtml}
503
+
504
+ </td>
505
+ </tr>
506
+
507
+ <!-- ERRORS -->
508
+ ${errorSection}
509
+
510
+ <!-- FOOTER -->
511
+ <tr>
512
+ <td style="
513
+ padding:24px 28px;
514
+ background:#f8fafc;
515
+ border-top:1px solid #eaecf0;
516
+ ">
517
+
518
+ <div style="
519
+ font-size:12px;
520
+ line-height:1.6;
521
+ color:#667085;
522
+ text-align:center;
523
+ ">
524
+ This is an automated notification from
525
+ <strong>MongoDB Backup Service</strong>.
526
+ <br>
527
+ Please do not reply to this email.
528
+ </div>
529
+
530
+ </td>
531
+ </tr>
532
+
533
+ </table>
534
+
535
+ </td>
536
+ </tr>
537
+ </table>
538
+
539
+ </body>
540
+ </html>`;
541
+ }
542
+
543
+ /** Generate subject, plain-text and HTML payloads. */
544
+ static getBackupReport(metadata) {
545
+ const subject = EmailTemplates._buildSubject(metadata);
546
+
547
+ let text =
548
+ `Database Backup Report\n` +
549
+ `======================\n\n` +
550
+ `Project: ${metadata.projectName || ''}\n` +
551
+ `Backup Date: ${metadata.backupDate || ''}\n` +
552
+ `Start Time: ${metadata.startTime || metadata.startedAt || ''}\n` +
553
+ `End Time: ${metadata.endTime || metadata.completedAt || ''}\n` +
554
+ `Duration: ${EmailTemplates._formatDuration(metadata.durationMs || 0)}\n` +
555
+ `Overall Status: ${(metadata.status || '').toUpperCase()}\n\n` +
556
+
557
+ `Backup Components\n` +
558
+ `-----------------\n` +
559
+ `MongoDB Dump: ${(metadata.mongoDump?.status || 'skipped').toUpperCase()}\n` +
560
+ `Excel Export: ${(metadata.excel?.status || 'skipped').toUpperCase()}\n` +
561
+ `Metadata Generation: ${(metadata.metadata?.status || 'skipped').toUpperCase()}\n` +
562
+ `Retention Cleanup: ${(metadata.retention?.status || 'skipped').toUpperCase()}\n\n` +
563
+
564
+ `Backup Files\n` +
565
+ `------------\n` +
566
+ `${[
567
+ metadata.mongoDump?.file,
568
+ metadata.excel?.file,
569
+ metadata.metadata?.file
570
+ ].filter(Boolean).join('\n') || 'None'}\n`;
571
+
572
+ if (metadata.status !== 'success') {
573
+ text +=
574
+ `\nError Details\n` +
575
+ `-------------\n` +
576
+ `${[
577
+ metadata.mongoDump,
578
+ metadata.excel,
579
+ metadata.metadata,
580
+ metadata.retention
581
+ ]
582
+ .filter(item => item?.error)
583
+ .map(item => item.error)
584
+ .join('\n') || 'No detailed error information provided.'}\n\n` +
585
+ `Action Required\n` +
586
+ `---------------\n` +
587
+ `Please review the application logs and investigate the failed backup component.\n`;
588
+ }
589
+
590
+ const html = EmailTemplates._buildHtml(metadata);
591
+
592
+ return { subject, text, html };
30
593
  }
31
594
  }
32
595
 
33
- module.exports = EmailTemplates;
596
+ module.exports = EmailTemplates;