@jhits/plugin-dep 0.0.1

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/src/actions.ts ADDED
@@ -0,0 +1,765 @@
1
+ /**
2
+ * Plugin Deprecated - Actions
3
+ * Framework-agnostic business logic for deprecated client API routes
4
+ */
5
+
6
+ import { ObjectId } from 'mongodb';
7
+ import { DepApiConfig } from './types';
8
+ import bcrypt from 'bcryptjs';
9
+ import jwt from 'jsonwebtoken';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { exec, execSync } from 'child_process';
13
+ import nodemailer from 'nodemailer';
14
+
15
+ // ============================================================================
16
+ // AUTH ACTIONS
17
+ // ============================================================================
18
+
19
+ export async function getMe(config: DepApiConfig, session?: any) {
20
+ if (!session || !session.user) {
21
+ return { loggedIn: false };
22
+ }
23
+
24
+ return {
25
+ loggedIn: true,
26
+ user: {
27
+ id: (session.user as any).id,
28
+ email: session.user.email,
29
+ name: session.user.name || '',
30
+ role: (session.user as any).role || '',
31
+ },
32
+ };
33
+ }
34
+
35
+ // ============================================================================
36
+ // FEEDBACK ACTIONS
37
+ // ============================================================================
38
+
39
+ export async function createFeedback(
40
+ config: DepApiConfig,
41
+ data: { type: string; message: string; image?: string },
42
+ userId?: string
43
+ ) {
44
+ const client = await config.mongoClient;
45
+ const db = client.db();
46
+
47
+ let senderInfo = { name: 'Onbekend', email: 'Onbekend' };
48
+ if (userId) {
49
+ const user = await db.collection('users').findOne({ _id: new ObjectId(userId) });
50
+ if (user) {
51
+ senderInfo = { name: user.name, email: user.email };
52
+ }
53
+ }
54
+
55
+ const result = await db.collection('feedback').insertOne({
56
+ type: data.type,
57
+ message: data.message,
58
+ image: data.image,
59
+ sender: senderInfo,
60
+ status: 'Open',
61
+ createdAt: new Date(),
62
+ });
63
+
64
+ await db.collection('notifications').insertOne({
65
+ category: data.type === 'Foutmelding' ? 'alert' : 'feedback',
66
+ title: `Nieuwe ${data.type}`,
67
+ description: `${senderInfo.name}: ${data.message.substring(0, 60)}${data.message.length > 60 ? '...' : ''}`,
68
+ link: '/dashboard/feedback',
69
+ forRole: 'dev',
70
+ feedbackId: result.insertedId,
71
+ senderId: userId,
72
+ createdAt: new Date(),
73
+ });
74
+
75
+ return { success: true, id: result.insertedId };
76
+ }
77
+
78
+ export async function listFeedback(config: DepApiConfig) {
79
+ const client = await config.mongoClient;
80
+ const db = client.db();
81
+ const reports = await db.collection('feedback')
82
+ .find({})
83
+ .sort({ createdAt: -1 })
84
+ .toArray();
85
+ return reports;
86
+ }
87
+
88
+ export async function updateFeedback(
89
+ config: DepApiConfig,
90
+ id: string,
91
+ data: { status: string }
92
+ ) {
93
+ const client = await config.mongoClient;
94
+ const db = client.db();
95
+ await db.collection('feedback').updateOne(
96
+ { _id: new ObjectId(id) },
97
+ { $set: { status: data.status, updatedAt: new Date() } }
98
+ );
99
+ return { success: true };
100
+ }
101
+
102
+ export async function deleteFeedback(config: DepApiConfig, id: string) {
103
+ const client = await config.mongoClient;
104
+ const db = client.db();
105
+ await db.collection('feedback').deleteOne({ _id: new ObjectId(id) });
106
+ return { success: true };
107
+ }
108
+
109
+ // ============================================================================
110
+ // SETTINGS ACTIONS
111
+ // ============================================================================
112
+
113
+ export async function getSettings(config: DepApiConfig) {
114
+ const client = await config.mongoClient;
115
+ const db = client.db();
116
+ const settings = await db.collection('settings').findOne({ identifier: 'site_config' });
117
+ return settings || {};
118
+ }
119
+
120
+ export async function updateSettings(config: DepApiConfig, data: any) {
121
+ const client = await config.mongoClient;
122
+ const db = client.db();
123
+ const { _id, ...updateData } = data;
124
+ await db.collection('settings').updateOne(
125
+ { identifier: 'site_config' },
126
+ {
127
+ $set: {
128
+ ...updateData,
129
+ updatedAt: new Date(),
130
+ },
131
+ },
132
+ { upsert: true }
133
+ );
134
+ return { success: true };
135
+ }
136
+
137
+ export async function getMaintenanceMode(config: DepApiConfig) {
138
+ const client = await config.mongoClient;
139
+ const db = client.db();
140
+ const setting = await db.collection('settings').findOne({ key: 'maintenance_mode' });
141
+ return { active: setting?.value ?? true };
142
+ }
143
+
144
+ export async function setMaintenanceMode(config: DepApiConfig, active: boolean) {
145
+ const client = await config.mongoClient;
146
+ const db = client.db();
147
+ await db.collection('settings').updateOne(
148
+ { key: 'maintenance_mode' },
149
+ { $set: { value: active, updatedAt: new Date() } },
150
+ { upsert: true }
151
+ );
152
+ return { success: true, active };
153
+ }
154
+
155
+ // ============================================================================
156
+ // USER ACTIONS
157
+ // ============================================================================
158
+
159
+ export async function listUsers(config: DepApiConfig) {
160
+ const client = await config.mongoClient;
161
+ const db = client.db();
162
+ const users = await db.collection('users')
163
+ .find({}, { projection: { password: 0 } })
164
+ .toArray();
165
+ return users;
166
+ }
167
+
168
+ export async function createUser(
169
+ config: DepApiConfig,
170
+ data: { email: string; name: string; role: string; password: string }
171
+ ) {
172
+ if (!data.password || data.password.length < 6) {
173
+ throw new Error('Password too short');
174
+ }
175
+
176
+ const client = await config.mongoClient;
177
+ const db = client.db();
178
+
179
+ const exists = await db.collection('users').findOne({ email: data.email });
180
+ if (exists) {
181
+ throw new Error('User already exists');
182
+ }
183
+
184
+ const hashedPassword = await bcrypt.hash(data.password, 12);
185
+ const result = await db.collection('users').insertOne({
186
+ email: data.email,
187
+ name: data.name,
188
+ role: data.role || 'editor',
189
+ password: hashedPassword,
190
+ createdAt: new Date(),
191
+ });
192
+
193
+ return {
194
+ _id: result.insertedId,
195
+ email: data.email,
196
+ name: data.name,
197
+ role: data.role,
198
+ createdAt: new Date(),
199
+ };
200
+ }
201
+
202
+ export async function updateUser(
203
+ config: DepApiConfig,
204
+ id: string,
205
+ data: { role?: string; name?: string; password?: string; currentPassword?: string }
206
+ ) {
207
+ const client = await config.mongoClient;
208
+ const db = client.db();
209
+
210
+ const updateData: any = { updatedAt: new Date() };
211
+ if (data.role) updateData.role = data.role;
212
+ if (data.name) updateData.name = data.name;
213
+
214
+ if (data.password) {
215
+ const user = await db.collection('users').findOne({ _id: new ObjectId(id) });
216
+ if (!user) {
217
+ throw new Error('User not found');
218
+ }
219
+
220
+ if (!data.currentPassword) {
221
+ throw new Error('Current password is required');
222
+ }
223
+
224
+ const isCorrect = await bcrypt.compare(data.currentPassword, user.password);
225
+ if (!isCorrect) {
226
+ throw new Error('Current password is incorrect');
227
+ }
228
+
229
+ if (data.password.length < 6) {
230
+ throw new Error('New password too short');
231
+ }
232
+
233
+ updateData.password = await bcrypt.hash(data.password, 12);
234
+ }
235
+
236
+ const result = await db.collection('users').updateOne(
237
+ { _id: new ObjectId(id) },
238
+ { $set: updateData }
239
+ );
240
+
241
+ if (result.matchedCount === 0) {
242
+ throw new Error('User not found');
243
+ }
244
+
245
+ return { message: 'Update successful' };
246
+ }
247
+
248
+ export async function deleteUser(config: DepApiConfig, id: string) {
249
+ const client = await config.mongoClient;
250
+ const db = client.db();
251
+
252
+ const user = await db.collection('users').findOne({ _id: new ObjectId(id) });
253
+ if (user?.role === 'dev') {
254
+ throw new Error('Cannot delete developer account');
255
+ }
256
+
257
+ await db.collection('users').deleteOne({ _id: new ObjectId(id) });
258
+ return { message: 'User deleted' };
259
+ }
260
+
261
+ // ============================================================================
262
+ // NEWSLETTER ACTIONS
263
+ // ============================================================================
264
+
265
+ export async function subscribeNewsletter(
266
+ config: DepApiConfig,
267
+ data: { email: string; language: string },
268
+ host?: string
269
+ ) {
270
+ if (!data.email || !data.email.includes('@')) {
271
+ throw new Error('Invalid email address.');
272
+ }
273
+
274
+ const client = await config.mongoClient;
275
+ const db = client.db();
276
+ const collection = db.collection('subscribers');
277
+
278
+ const existing = await collection.findOne({ email: data.email.toLowerCase() });
279
+ if (existing) {
280
+ throw new Error('You are already subscribed!');
281
+ }
282
+
283
+ await collection.insertOne({
284
+ email: data.email.toLowerCase(),
285
+ language: data.language || 'en',
286
+ subscribedAt: new Date(),
287
+ });
288
+
289
+ if (config.emailConfig) {
290
+ await sendNewsletterWelcomeEmail(config, data.email, data.language, host);
291
+ }
292
+
293
+ return { message: 'Success' };
294
+ }
295
+
296
+ export async function listSubscribers(config: DepApiConfig) {
297
+ const client = await config.mongoClient;
298
+ const db = client.db();
299
+ const subscribers = await db.collection('subscribers')
300
+ .find({})
301
+ .sort({ subscribedAt: -1 })
302
+ .toArray();
303
+ return subscribers;
304
+ }
305
+
306
+ export async function getSubscriber(config: DepApiConfig, email: string) {
307
+ const client = await config.mongoClient;
308
+ const db = client.db();
309
+ const subscriber = await db.collection('subscribers').findOne({ email: email.toLowerCase() });
310
+ if (!subscriber) {
311
+ throw new Error('Subscriber not found');
312
+ }
313
+ return subscriber;
314
+ }
315
+
316
+ export async function deleteSubscriber(config: DepApiConfig, email: string) {
317
+ const client = await config.mongoClient;
318
+ const db = client.db();
319
+ const result = await db.collection('subscribers').deleteOne({ email: email.toLowerCase() });
320
+ if (result.deletedCount === 0) {
321
+ throw new Error('Subscriber not found');
322
+ }
323
+ return { message: 'Subscriber successfully removed' };
324
+ }
325
+
326
+ export async function getNewsletterSettings(config: DepApiConfig) {
327
+ const client = await config.mongoClient;
328
+ const db = client.db();
329
+ const newsletter = await db.collection('newsletters').findOne({ id: 'welcome_automation' });
330
+ return newsletter || {};
331
+ }
332
+
333
+ export async function updateNewsletterSettings(config: DepApiConfig, data: { languages: any }) {
334
+ const client = await config.mongoClient;
335
+ const db = client.db();
336
+ await db.collection('newsletters').updateOne(
337
+ { id: 'welcome_automation' },
338
+ {
339
+ $set: {
340
+ languages: data.languages,
341
+ updatedAt: new Date(),
342
+ },
343
+ },
344
+ { upsert: true }
345
+ );
346
+ return { success: true };
347
+ }
348
+
349
+ // ============================================================================
350
+ // NOTIFICATIONS ACTIONS
351
+ // ============================================================================
352
+
353
+ export async function getNotificationStream(
354
+ config: DepApiConfig,
355
+ userRole: string,
356
+ userId: string | null
357
+ ) {
358
+ const client = await config.mongoClient;
359
+ const db = client.db();
360
+ const collection = db.collection('notifications');
361
+
362
+ const history = await collection
363
+ .find({
364
+ removedBy: { $ne: userId },
365
+ $and: [
366
+ {
367
+ $or: [
368
+ { forRole: { $in: [userRole] } },
369
+ { forRole: 'all' },
370
+ { forRole: { $exists: false } },
371
+ ],
372
+ },
373
+ { senderId: { $ne: userId } },
374
+ ],
375
+ })
376
+ .sort({ createdAt: -1 })
377
+ .limit(20)
378
+ .toArray();
379
+
380
+ return {
381
+ history: history.reverse().map((notif) => ({
382
+ ...notif,
383
+ id: notif._id,
384
+ isRead: notif.readBy?.includes(userId) || false,
385
+ })),
386
+ pipeline: [
387
+ {
388
+ $match: {
389
+ $and: [
390
+ { operationType: 'insert' },
391
+ {
392
+ $or: [
393
+ { 'fullDocument.forRole': { $in: [userRole] } },
394
+ { 'fullDocument.forRole': 'all' },
395
+ { 'fullDocument.forRole': { $exists: false } },
396
+ ],
397
+ },
398
+ { 'fullDocument.senderId': { $ne: userId } },
399
+ { 'fullDocument.category': { $in: ['alert', 'feedback', 'system', 'success'] } },
400
+ ],
401
+ },
402
+ },
403
+ ],
404
+ };
405
+ }
406
+
407
+ export async function updateNotification(
408
+ config: DepApiConfig,
409
+ data: { notificationId?: string; action: 'read' | 'remove'; all?: boolean },
410
+ userId: string
411
+ ) {
412
+ const client = await config.mongoClient;
413
+ const db = client.db();
414
+
415
+ const updateOperation = data.action === 'remove'
416
+ ? { $addToSet: { removedBy: userId, readBy: userId } }
417
+ : { $addToSet: { readBy: userId } };
418
+
419
+ if (data.all) {
420
+ await db.collection('notifications').updateMany(
421
+ {
422
+ $or: [
423
+ { forRole: 'all' },
424
+ { forRole: { $exists: false } },
425
+ ],
426
+ removedBy: { $ne: userId },
427
+ },
428
+ {
429
+ $addToSet: {
430
+ removedBy: userId,
431
+ readBy: userId,
432
+ },
433
+ }
434
+ );
435
+ } else if (data.notificationId) {
436
+ await db.collection('notifications').updateOne(
437
+ { _id: new ObjectId(String(data.notificationId)) },
438
+ updateOperation
439
+ );
440
+ }
441
+
442
+ // Cleanup old notifications
443
+ await db.collection('notifications').deleteMany({
444
+ $or: [
445
+ { createdAt: { $lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } },
446
+ {
447
+ $and: [
448
+ { 'readBy.0': { $exists: true } },
449
+ { createdAt: { $lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) } },
450
+ ],
451
+ },
452
+ ],
453
+ });
454
+
455
+ return { success: true };
456
+ }
457
+
458
+ // ============================================================================
459
+ // TRANSLATION ACTIONS
460
+ // ============================================================================
461
+
462
+ export async function saveTranslation(
463
+ config: DepApiConfig,
464
+ data: { locale: string; messages: Record<string, any> }
465
+ ) {
466
+ if (!data.locale || !data.messages) {
467
+ throw new Error('Missing locale or messages');
468
+ }
469
+
470
+ const localesDir = config.localesDir || path.join(process.cwd(), 'data/locales');
471
+ const currentLocaleDir = path.join(localesDir, data.locale);
472
+
473
+ if (!fs.existsSync(currentLocaleDir)) {
474
+ fs.mkdirSync(currentLocaleDir, { recursive: true });
475
+ }
476
+
477
+ for (const [namespace, content] of Object.entries(data.messages)) {
478
+ const filePath = path.join(currentLocaleDir, `${namespace}.json`);
479
+ await fs.promises.writeFile(filePath, JSON.stringify(content, null, 2), 'utf8');
480
+
481
+ const otherLocales = fs.readdirSync(localesDir).filter((d) => d !== data.locale);
482
+ for (const other of otherLocales) {
483
+ const otherFilePath = path.join(localesDir, other, `${namespace}.json`);
484
+ if (!fs.existsSync(otherFilePath)) {
485
+ await fs.promises.writeFile(otherFilePath, JSON.stringify({}, null, 2), 'utf8');
486
+ }
487
+ }
488
+ }
489
+
490
+ return { success: true };
491
+ }
492
+
493
+ // ============================================================================
494
+ // STATS ACTIONS
495
+ // ============================================================================
496
+
497
+ export async function getStorageStats(config: DepApiConfig) {
498
+ try {
499
+ const stats = execSync('df -m .').toString().split('\n')[1].split(/\s+/);
500
+ const totalMB = parseInt(stats[1]);
501
+ const usedMB = parseInt(stats[2]);
502
+ const availableMB = parseInt(stats[3]);
503
+ const percent = (usedMB / totalMB) * 100;
504
+
505
+ return {
506
+ usedMB,
507
+ totalMB,
508
+ availableMB,
509
+ percent: parseFloat(percent.toFixed(1)),
510
+ unit: 'MB',
511
+ };
512
+ } catch (error) {
513
+ return {
514
+ usedMB: 0,
515
+ totalMB: 25000,
516
+ percent: 0,
517
+ error: 'Could not read disk',
518
+ };
519
+ }
520
+ }
521
+
522
+ export async function getMediaStats(config: DepApiConfig) {
523
+ const dirPath = config.uploadsDir || path.join(process.cwd(), 'data/uploads');
524
+
525
+ if (!fs.existsSync(dirPath)) {
526
+ return { totalSizeMB: 0, fileCount: 0 };
527
+ }
528
+
529
+ const files = fs.readdirSync(dirPath);
530
+ let totalBytes = 0;
531
+
532
+ files.forEach((file) => {
533
+ const stats = fs.statSync(path.join(dirPath, file));
534
+ totalBytes += stats.size;
535
+ });
536
+
537
+ return {
538
+ totalSizeMB: (totalBytes / (1024 * 1024)).toFixed(2),
539
+ fileCount: files.length,
540
+ };
541
+ }
542
+
543
+ // ============================================================================
544
+ // VERSION ACTIONS
545
+ // ============================================================================
546
+
547
+ export async function getVersion() {
548
+ return { version: process.env.NEXT_PUBLIC_BUILD_ID || Date.now().toString() };
549
+ }
550
+
551
+ // ============================================================================
552
+ // GITHUB DEPLOYMENT ACTIONS
553
+ // ============================================================================
554
+
555
+ export async function getDeploymentStatus(config: DepApiConfig) {
556
+ const flagPath = config.deploymentPaths?.flagPath || '/home/pi/botanics/update-pending.json';
557
+ if (fs.existsSync(flagPath)) {
558
+ const data = JSON.parse(fs.readFileSync(flagPath, 'utf8'));
559
+ return { pending: true, ...data };
560
+ }
561
+ return { pending: false };
562
+ }
563
+
564
+ export async function triggerDeployment(
565
+ config: DepApiConfig,
566
+ signature: string,
567
+ payload?: any
568
+ ) {
569
+ const flagPath = config.deploymentPaths?.flagPath || '/home/pi/botanics/update-pending.json';
570
+ const scriptPath = config.deploymentPaths?.scriptPath || '/home/pi/botanics/scripts/update.sh';
571
+
572
+ if (signature === 'manual') {
573
+ if (fs.existsSync(flagPath)) {
574
+ const current = JSON.parse(fs.readFileSync(flagPath, 'utf8'));
575
+ fs.writeFileSync(flagPath, JSON.stringify({ ...current, status: 'building' }));
576
+ }
577
+
578
+ await createNotification(config, 'Update Gestart', 'De installatie is handmatig gestart.', 'system');
579
+ executeDeploymentScript(config, scriptPath, flagPath);
580
+ return { message: 'Deployment started' };
581
+ }
582
+
583
+ if (!config.githubWebhookSecret) {
584
+ throw new Error('Server configuration error');
585
+ }
586
+
587
+ if (payload?.ref === 'refs/heads/main') {
588
+ const commitId = payload.after?.substring(0, 7) || '???';
589
+ const commitMsg = payload.head_commit?.message || 'Nieuwe wijzigingen';
590
+
591
+ fs.writeFileSync(flagPath, JSON.stringify({
592
+ pending: true,
593
+ at: new Date().toISOString(),
594
+ commit: commitId,
595
+ }));
596
+
597
+ await createNotification(
598
+ config,
599
+ 'Update Beschikbaar',
600
+ `📦 Nieuwe versie: "${commitMsg}" (${commitId}). Klik hier om te installeren.`,
601
+ 'system'
602
+ );
603
+
604
+ return { message: 'Update flagged' };
605
+ }
606
+
607
+ return { message: 'Ignored branch' };
608
+ }
609
+
610
+ // ============================================================================
611
+ // HELPER FUNCTIONS
612
+ // ============================================================================
613
+
614
+ async function sendNewsletterWelcomeEmail(
615
+ config: DepApiConfig,
616
+ email: string,
617
+ language: string,
618
+ host?: string
619
+ ) {
620
+ if (!config.emailConfig) return;
621
+
622
+ const transporter = nodemailer.createTransport({
623
+ host: config.emailConfig.host,
624
+ port: config.emailConfig.port,
625
+ secure: true,
626
+ auth: {
627
+ user: config.emailConfig.user,
628
+ pass: config.emailConfig.password,
629
+ },
630
+ connectionTimeout: 10000,
631
+ });
632
+
633
+ const isDutch = language === 'nl';
634
+ const baseUrl = host
635
+ ? (host.includes('localhost') ? 'http' : 'https') + '://' + host
636
+ : config.baseUrl || 'https://bya.jorishummel.com';
637
+
638
+ const slugs: Record<string, string> = {
639
+ sv: '/avmälla',
640
+ nl: '/afmelden',
641
+ en: '/unsubscribe',
642
+ };
643
+ const slug = slugs[language] || slugs.en;
644
+ const unsubscribeUrl = `${baseUrl}${slug}?email=${encodeURIComponent(email)}`;
645
+
646
+ const message = isDutch
647
+ ? `Bedankt dat je deel uitmaakt van de **Botanics & You** community.\n\n` +
648
+ `Wij geloven dat de natuur alles biedt wat we werkelijk nodig hebben. Terwijl we achter de schermen hard werken aan de lancering, nemen we je graag mee op reis door de wereld van kruiden en natuurlijke vitaliteit.\n\n` +
649
+ `**Wat kun je van ons verwachten:**\n` +
650
+ `• Exclusieve updates over onze lancering\n` +
651
+ `• Inzichten in de kracht van lokale kruiden\n` +
652
+ `• Tips voor een diepere verbinding met de natuur\n\n` +
653
+ `Bedankt voor je geduld en interesse in een natuurlijke manier van leven. We spreken je snel!`
654
+ : `Thank you for joining the **Botanics & You** community.\n\n` +
655
+ `We believe that nature provides everything we truly need. While we work hard behind the scenes for our launch, we look forward to taking you on a journey through the world of herbs and natural vitality.\n\n` +
656
+ `**What to expect from us:**\n` +
657
+ `• Exclusive updates on our upcoming launch\n` +
658
+ `• Insights into the healing properties of local herbs\n` +
659
+ `• Tips for restoring your connection with nature\n\n` +
660
+ `Thank you for your patience and your interest in a more natural way of living. We'll be in touch soon!`;
661
+
662
+ const formattedMessage = message
663
+ .replace(/\n/g, '<br/>')
664
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
665
+ .replace(/• (.*?)(<br\/>|$)/g, '<div style="margin-left: 10px; margin-bottom: 5px;">• $1</div>');
666
+
667
+ const html = `
668
+ <!DOCTYPE html>
669
+ <html>
670
+ <head>
671
+ <meta charset="utf-8">
672
+ <style>
673
+ body { background-color: #faf9f6; margin: 0; padding: 0; font-family: 'Georgia', serif; }
674
+ .container { max-width: 600px; margin: 20px auto; background-color: #ffffff; border-radius: 40px; border: 1px solid #1a2e260d; overflow: hidden; }
675
+ .header { padding: 40px 0 20px 0; text-align: center; }
676
+ .logo { width: 180px; height: auto; }
677
+ .content { padding: 0 50px 40px 50px; color: #1a2e26; line-height: 1.8; font-size: 15px; }
678
+ .footer { padding: 40px 50px; text-align: center; font-family: sans-serif; font-size: 10px; color: #a1a1aa; letter-spacing: 1px; border-top: 1px solid #faf9f6; }
679
+ h1 { font-weight: normal; font-style: italic; font-size: 30px; margin-bottom: 30px; color: #1a2e26; text-align: center; }
680
+ .divider { height: 1px; width: 40px; background-color: #1a2e2620; margin: 30px auto; }
681
+ </style>
682
+ </head>
683
+ <body>
684
+ <div class="container">
685
+ <div class="header">
686
+ <img src="cid:botanics-logo" alt="Botanics & You" class="logo">
687
+ </div>
688
+ <div class="content">
689
+ <h1>${isDutch ? 'Fijn dat je er bent' : "We're glad you're here"}</h1>
690
+ <div class="divider"></div>
691
+ <div>${formattedMessage}</div>
692
+ </div>
693
+ <div class="footer">
694
+ <strong>Botanics & You</strong> &copy; ${new Date().getFullYear()}<br/>
695
+ ${isDutch ? 'Natuurlijk verbonden' : 'Naturally connected'}<br/><br/>
696
+ <a href="${unsubscribeUrl}" style="color: #a1a1aa; text-decoration: underline;">
697
+ ${isDutch ? 'Afmelden' : 'Unsubscribe'}
698
+ </a>
699
+ </div>
700
+ </div>
701
+ </body>
702
+ </html>
703
+ `;
704
+
705
+ await transporter.sendMail({
706
+ from: config.emailConfig.from,
707
+ to: email,
708
+ subject: isDutch ? 'Welkom bij Botanics & You' : 'Welcome to Botanics & You',
709
+ html,
710
+ attachments: [
711
+ {
712
+ filename: 'logo.png',
713
+ path: path.join(process.cwd(), 'public/BotanicsYouFull.png'),
714
+ cid: 'botanics-logo',
715
+ },
716
+ ],
717
+ });
718
+ }
719
+
720
+ async function createNotification(
721
+ config: DepApiConfig,
722
+ title: string,
723
+ description: string,
724
+ category: string
725
+ ) {
726
+ try {
727
+ const client = await config.mongoClient;
728
+ const db = client.db();
729
+ await db.collection('notifications').insertOne({
730
+ category,
731
+ title,
732
+ description,
733
+ forRole: ['admin', 'dev'],
734
+ link: '/dashboard/settings',
735
+ createdAt: new Date().toISOString(),
736
+ read: false,
737
+ });
738
+ } catch (e) {
739
+ console.error('Failed to create notification document', e);
740
+ }
741
+ }
742
+
743
+ function executeDeploymentScript(
744
+ config: DepApiConfig,
745
+ scriptPath: string,
746
+ flagPath: string
747
+ ) {
748
+ const cmd = `sudo -u pi bash ${scriptPath}`;
749
+
750
+ exec(cmd, async (error, stdout, stderr) => {
751
+ if (!error) {
752
+ console.log(`Deployment Success: ${stdout}`);
753
+ if (fs.existsSync(flagPath)) fs.unlinkSync(flagPath);
754
+ await createNotification(config, 'Update Voltooid', 'De website is succesvol bijgewerkt!', 'success');
755
+ } else {
756
+ console.error(`Deployment Error: ${stderr || error.message}`);
757
+ if (fs.existsSync(flagPath)) {
758
+ const current = JSON.parse(fs.readFileSync(flagPath, 'utf8'));
759
+ fs.writeFileSync(flagPath, JSON.stringify({ ...current, status: 'failed' }));
760
+ }
761
+ await createNotification(config, 'Update Mislukt', 'De build is mislukt. Probeer het opnieuw vanuit instellingen.', 'alert');
762
+ }
763
+ });
764
+ }
765
+