@jhits/plugin-dep 0.0.12 → 0.0.14
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 +3 -2
- package/src/actions.ts +659 -0
- package/src/config.ts +130 -0
- package/src/index.server.ts +20 -0
- package/src/router.ts +391 -0
- package/src/types.ts +50 -0
- package/src/utils/auth.ts +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jhits/plugin-dep",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "DEPRECATED: Legacy dependencies and shared logic for JHITS (Phase out in progress)",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@types/jsonwebtoken": "^9.0.10",
|
|
41
41
|
"@types/node": "^25.2.3",
|
|
42
42
|
"@types/nodemailer": "^7.0.10",
|
|
43
|
-
"@jhits/plugin-core": "0.0.
|
|
43
|
+
"@jhits/plugin-core": "0.0.10"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"next": ">=15.0.0"
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"typescript": "^5.9.3"
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
|
+
"src",
|
|
53
54
|
"dist",
|
|
54
55
|
"package.json"
|
|
55
56
|
],
|
package/src/actions.ts
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Deprecated - Actions
|
|
3
|
+
* Framework-agnostic business logic for deprecated client API routes
|
|
4
|
+
*
|
|
5
|
+
* SERVER-ONLY: This module must never be imported by client code
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import 'server-only';
|
|
9
|
+
|
|
10
|
+
import { ObjectId, MongoClient } from 'mongodb';
|
|
11
|
+
import { DepApiConfig } from './types';
|
|
12
|
+
import bcrypt from 'bcryptjs';
|
|
13
|
+
import jwt from 'jsonwebtoken';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { exec, execSync } from 'child_process';
|
|
17
|
+
import nodemailer from 'nodemailer';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// AUTH ACTIONS
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
export async function getMe(config: DepApiConfig, session?: any) {
|
|
24
|
+
if (!session || !session.user) {
|
|
25
|
+
return { loggedIn: false };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
loggedIn: true,
|
|
30
|
+
user: {
|
|
31
|
+
id: (session.user as any).id,
|
|
32
|
+
email: session.user.email,
|
|
33
|
+
name: session.user.name || '',
|
|
34
|
+
role: (session.user as any).role || '',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// FEEDBACK ACTIONS
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
export async function createFeedback(
|
|
44
|
+
config: DepApiConfig,
|
|
45
|
+
data: { type: string; message: string; image?: string },
|
|
46
|
+
userId?: string
|
|
47
|
+
) {
|
|
48
|
+
const client = await config.mongoClient;
|
|
49
|
+
const db = client.db();
|
|
50
|
+
|
|
51
|
+
let senderInfo = { name: 'Onbekend', email: 'Onbekend' };
|
|
52
|
+
if (userId) {
|
|
53
|
+
const user = await db.collection('users').findOne({ _id: new ObjectId(userId) });
|
|
54
|
+
if (user) {
|
|
55
|
+
senderInfo = { name: user.name, email: user.email };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const result = await db.collection('feedback').insertOne({
|
|
60
|
+
type: data.type,
|
|
61
|
+
message: data.message,
|
|
62
|
+
image: data.image,
|
|
63
|
+
sender: senderInfo,
|
|
64
|
+
status: 'Open',
|
|
65
|
+
createdAt: new Date(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await db.collection('notifications').insertOne({
|
|
69
|
+
category: data.type === 'Foutmelding' ? 'alert' : 'feedback',
|
|
70
|
+
title: `Nieuwe ${data.type}`,
|
|
71
|
+
description: `${senderInfo.name}: ${data.message.substring(0, 60)}${data.message.length > 60 ? '...' : ''}`,
|
|
72
|
+
link: '/dashboard/feedback',
|
|
73
|
+
forRole: 'dev',
|
|
74
|
+
feedbackId: result.insertedId,
|
|
75
|
+
senderId: userId,
|
|
76
|
+
createdAt: new Date(),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return { success: true, id: result.insertedId };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function listFeedback(config: DepApiConfig) {
|
|
83
|
+
const client = await config.mongoClient;
|
|
84
|
+
const db = client.db();
|
|
85
|
+
const reports = await db.collection('feedback')
|
|
86
|
+
.find({})
|
|
87
|
+
.sort({ createdAt: -1 })
|
|
88
|
+
.toArray();
|
|
89
|
+
return reports;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function updateFeedback(
|
|
93
|
+
config: DepApiConfig,
|
|
94
|
+
id: string,
|
|
95
|
+
data: { status: string }
|
|
96
|
+
) {
|
|
97
|
+
const client = await config.mongoClient;
|
|
98
|
+
const db = client.db();
|
|
99
|
+
await db.collection('feedback').updateOne(
|
|
100
|
+
{ _id: new ObjectId(id) },
|
|
101
|
+
{ $set: { status: data.status, updatedAt: new Date() } }
|
|
102
|
+
);
|
|
103
|
+
return { success: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function deleteFeedback(config: DepApiConfig, id: string) {
|
|
107
|
+
const client = await config.mongoClient;
|
|
108
|
+
const db = client.db();
|
|
109
|
+
await db.collection('feedback').deleteOne({ _id: new ObjectId(id) });
|
|
110
|
+
return { success: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// SETTINGS ACTIONS
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
export async function getSettings(config: DepApiConfig) {
|
|
118
|
+
const client = await config.mongoClient;
|
|
119
|
+
const db = client.db();
|
|
120
|
+
const settings = await db.collection('settings').findOne({ identifier: 'site_config' });
|
|
121
|
+
return settings || {};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function updateSettings(config: DepApiConfig, data: any) {
|
|
125
|
+
const client = await config.mongoClient;
|
|
126
|
+
const db = client.db();
|
|
127
|
+
const { _id, ...updateData } = data;
|
|
128
|
+
await db.collection('settings').updateOne(
|
|
129
|
+
{ identifier: 'site_config' },
|
|
130
|
+
{
|
|
131
|
+
$set: {
|
|
132
|
+
...updateData,
|
|
133
|
+
updatedAt: new Date(),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{ upsert: true }
|
|
137
|
+
);
|
|
138
|
+
return { success: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function getMaintenanceMode(config: DepApiConfig) {
|
|
142
|
+
const client = await config.mongoClient;
|
|
143
|
+
const db = client.db();
|
|
144
|
+
const setting = await db.collection('settings').findOne({ key: 'maintenance_mode' });
|
|
145
|
+
return { active: setting?.value ?? true };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function setMaintenanceMode(config: DepApiConfig, active: boolean) {
|
|
149
|
+
const client = await config.mongoClient;
|
|
150
|
+
const db = client.db();
|
|
151
|
+
await db.collection('settings').updateOne(
|
|
152
|
+
{ key: 'maintenance_mode' },
|
|
153
|
+
{ $set: { value: active, updatedAt: new Date() } },
|
|
154
|
+
{ upsert: true }
|
|
155
|
+
);
|
|
156
|
+
return { success: true, active };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// USER ACTIONS
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
163
|
+
export async function listUsers(config: DepApiConfig) {
|
|
164
|
+
const client = await config.mongoClient;
|
|
165
|
+
const db = client.db();
|
|
166
|
+
const users = await db.collection('users')
|
|
167
|
+
.find({}, { projection: { password: 0 } })
|
|
168
|
+
.toArray();
|
|
169
|
+
return users;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function createUser(
|
|
173
|
+
config: DepApiConfig,
|
|
174
|
+
data: { email: string; name: string; role: string; password: string }
|
|
175
|
+
) {
|
|
176
|
+
if (!data.password || data.password.length < 6) {
|
|
177
|
+
throw new Error('Password too short');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const client = await config.mongoClient;
|
|
181
|
+
const db = client.db();
|
|
182
|
+
|
|
183
|
+
const exists = await db.collection('users').findOne({ email: data.email });
|
|
184
|
+
if (exists) {
|
|
185
|
+
throw new Error('User already exists');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const hashedPassword = await bcrypt.hash(data.password, 12);
|
|
189
|
+
const result = await db.collection('users').insertOne({
|
|
190
|
+
email: data.email,
|
|
191
|
+
name: data.name,
|
|
192
|
+
role: data.role || 'editor',
|
|
193
|
+
password: hashedPassword,
|
|
194
|
+
createdAt: new Date(),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
_id: result.insertedId,
|
|
199
|
+
email: data.email,
|
|
200
|
+
name: data.name,
|
|
201
|
+
role: data.role,
|
|
202
|
+
createdAt: new Date(),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function updateUser(
|
|
207
|
+
config: DepApiConfig,
|
|
208
|
+
id: string,
|
|
209
|
+
data: { role?: string; name?: string; password?: string; currentPassword?: string }
|
|
210
|
+
) {
|
|
211
|
+
const client = await config.mongoClient;
|
|
212
|
+
const db = client.db();
|
|
213
|
+
|
|
214
|
+
const updateData: any = { updatedAt: new Date() };
|
|
215
|
+
if (data.role) updateData.role = data.role;
|
|
216
|
+
if (data.name) updateData.name = data.name;
|
|
217
|
+
|
|
218
|
+
if (data.password) {
|
|
219
|
+
const user = await db.collection('users').findOne({ _id: new ObjectId(id) });
|
|
220
|
+
if (!user) {
|
|
221
|
+
throw new Error('User not found');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!data.currentPassword) {
|
|
225
|
+
throw new Error('Current password is required');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const isCorrect = await bcrypt.compare(data.currentPassword, user.password);
|
|
229
|
+
if (!isCorrect) {
|
|
230
|
+
throw new Error('Current password is incorrect');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (data.password.length < 6) {
|
|
234
|
+
throw new Error('New password too short');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
updateData.password = await bcrypt.hash(data.password, 12);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const result = await db.collection('users').updateOne(
|
|
241
|
+
{ _id: new ObjectId(id) },
|
|
242
|
+
{ $set: updateData }
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (result.matchedCount === 0) {
|
|
246
|
+
throw new Error('User not found');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { message: 'Update successful' };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function deleteUser(config: DepApiConfig, id: string) {
|
|
253
|
+
const client = await config.mongoClient;
|
|
254
|
+
const db = client.db();
|
|
255
|
+
|
|
256
|
+
const user = await db.collection('users').findOne({ _id: new ObjectId(id) });
|
|
257
|
+
if (user?.role === 'dev') {
|
|
258
|
+
throw new Error('Cannot delete developer account');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await db.collection('users').deleteOne({ _id: new ObjectId(id) });
|
|
262
|
+
return { message: 'User deleted' };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// NEWSLETTER ACTIONS
|
|
267
|
+
// ============================================================================
|
|
268
|
+
|
|
269
|
+
export async function subscribeNewsletter(
|
|
270
|
+
config: DepApiConfig,
|
|
271
|
+
data: { email: string; language: string },
|
|
272
|
+
host?: string
|
|
273
|
+
) {
|
|
274
|
+
if (!data.email || !data.email.includes('@')) {
|
|
275
|
+
throw new Error('Invalid email address.');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const client = await config.mongoClient;
|
|
279
|
+
const db = client.db();
|
|
280
|
+
const collection = db.collection('subscribers');
|
|
281
|
+
|
|
282
|
+
const existing = await collection.findOne({ email: data.email.toLowerCase() });
|
|
283
|
+
if (existing) {
|
|
284
|
+
throw new Error('You are already subscribed!');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await collection.insertOne({
|
|
288
|
+
email: data.email.toLowerCase(),
|
|
289
|
+
language: data.language || 'en',
|
|
290
|
+
subscribedAt: new Date(),
|
|
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 createNotification(
|
|
615
|
+
config: DepApiConfig,
|
|
616
|
+
title: string,
|
|
617
|
+
description: string,
|
|
618
|
+
category: string
|
|
619
|
+
) {
|
|
620
|
+
try {
|
|
621
|
+
const client = await config.mongoClient;
|
|
622
|
+
const db = client.db();
|
|
623
|
+
await db.collection('notifications').insertOne({
|
|
624
|
+
category,
|
|
625
|
+
title,
|
|
626
|
+
description,
|
|
627
|
+
forRole: ['admin', 'dev'],
|
|
628
|
+
link: '/dashboard/settings',
|
|
629
|
+
createdAt: new Date().toISOString(),
|
|
630
|
+
read: false,
|
|
631
|
+
});
|
|
632
|
+
} catch (e) {
|
|
633
|
+
console.error('Failed to create notification document', e);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function executeDeploymentScript(
|
|
638
|
+
config: DepApiConfig,
|
|
639
|
+
scriptPath: string,
|
|
640
|
+
flagPath: string
|
|
641
|
+
) {
|
|
642
|
+
const cmd = `sudo -u pi bash ${scriptPath}`;
|
|
643
|
+
|
|
644
|
+
exec(cmd, async (error, stdout, stderr) => {
|
|
645
|
+
if (!error) {
|
|
646
|
+
console.log(`Deployment Success: ${stdout}`);
|
|
647
|
+
if (fs.existsSync(flagPath)) fs.unlinkSync(flagPath);
|
|
648
|
+
await createNotification(config, 'Update Voltooid', 'De website is succesvol bijgewerkt!', 'success');
|
|
649
|
+
} else {
|
|
650
|
+
console.error(`Deployment Error: ${stderr || error.message}`);
|
|
651
|
+
if (fs.existsSync(flagPath)) {
|
|
652
|
+
const current = JSON.parse(fs.readFileSync(flagPath, 'utf8'));
|
|
653
|
+
fs.writeFileSync(flagPath, JSON.stringify({ ...current, status: 'failed' }));
|
|
654
|
+
}
|
|
655
|
+
await createNotification(config, 'Update Mislukt', 'De build is mislukt. Probeer het opnieuw vanuit instellingen.', 'alert');
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Deprecated - Auto Setup
|
|
3
|
+
* Automatically creates API routes in the client app
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ensure the deprecated API route is set up in the client app
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated Routes are now handled by the unified /api/[pluginId]/[...path]/route.ts
|
|
14
|
+
* This function is kept for backwards compatibility but does nothing
|
|
15
|
+
*/
|
|
16
|
+
export function ensureDepRoutes() {
|
|
17
|
+
// Routes are now handled by the unified /api/[pluginId]/[...path]/route.ts
|
|
18
|
+
// No need to generate individual routes anymore
|
|
19
|
+
return;
|
|
20
|
+
try {
|
|
21
|
+
// Find the host app directory (where next.config.ts is)
|
|
22
|
+
let appDir = process.cwd();
|
|
23
|
+
const possiblePaths = [
|
|
24
|
+
appDir,
|
|
25
|
+
join(appDir, '..'),
|
|
26
|
+
join(appDir, '..', '..'),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
for (const basePath of possiblePaths) {
|
|
30
|
+
const configPath = join(basePath, 'next.config.ts');
|
|
31
|
+
if (existsSync(configPath)) {
|
|
32
|
+
appDir = basePath;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const apiDir = join(appDir, 'src', 'app', 'api');
|
|
38
|
+
const pluginDepApiDir = join(apiDir, 'plugin-dep', '[...path]');
|
|
39
|
+
const pluginDepApiPath = join(pluginDepApiDir, 'route.ts');
|
|
40
|
+
|
|
41
|
+
// Check if route already exists
|
|
42
|
+
if (existsSync(pluginDepApiPath)) {
|
|
43
|
+
const fs = require('fs');
|
|
44
|
+
const existingContent = fs.readFileSync(pluginDepApiPath, 'utf8');
|
|
45
|
+
if (existingContent.includes('@jhits/plugin-dep') || existingContent.includes('plugin-dep')) {
|
|
46
|
+
// Already set up, skip
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Create plugin-dep API catch-all route
|
|
52
|
+
mkdirSync(pluginDepApiDir, { recursive: true });
|
|
53
|
+
writeFileSync(pluginDepApiPath, `// Auto-generated by @jhits/plugin-dep - Deprecated API Plugin
|
|
54
|
+
// This route is automatically created for the deprecated API plugin
|
|
55
|
+
import { NextRequest } from 'next/server';
|
|
56
|
+
import { handleDepApi } from '@jhits/plugin-dep';
|
|
57
|
+
import clientPromise from '@/lib/mongodb';
|
|
58
|
+
import { authOptions } from '@jhits/dashboard/lib/auth';
|
|
59
|
+
import { join } from 'path';
|
|
60
|
+
|
|
61
|
+
export const dynamic = 'force-dynamic';
|
|
62
|
+
|
|
63
|
+
async function getConfig() {
|
|
64
|
+
return {
|
|
65
|
+
mongoClient: clientPromise,
|
|
66
|
+
jwtSecret: process.env.JWT_SECRET || 'secret',
|
|
67
|
+
authOptions,
|
|
68
|
+
baseUrl: process.env.NEXT_PUBLIC_BASE_URL,
|
|
69
|
+
githubWebhookSecret: process.env.GITHUB_WEBHOOK_SECRET,
|
|
70
|
+
deploymentPaths: {
|
|
71
|
+
flagPath: process.env.DEPLOYMENT_FLAG_PATH || '/home/pi/botanics/update-pending.json',
|
|
72
|
+
scriptPath: process.env.DEPLOYMENT_SCRIPT_PATH || '/home/pi/botanics/scripts/update.sh',
|
|
73
|
+
},
|
|
74
|
+
localesDir: process.env.LOCALES_DIR || join(process.cwd(), 'data/locales'),
|
|
75
|
+
uploadsDir: process.env.UPLOADS_DIR || join(process.cwd(), 'data/uploads'),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function GET(
|
|
80
|
+
req: NextRequest,
|
|
81
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
82
|
+
) {
|
|
83
|
+
const { path } = await params;
|
|
84
|
+
const config = await getConfig();
|
|
85
|
+
return handleDepApi(req, path, config);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function POST(
|
|
89
|
+
req: NextRequest,
|
|
90
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
91
|
+
) {
|
|
92
|
+
const { path } = await params;
|
|
93
|
+
const config = await getConfig();
|
|
94
|
+
return handleDepApi(req, path, config);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function PUT(
|
|
98
|
+
req: NextRequest,
|
|
99
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
100
|
+
) {
|
|
101
|
+
const { path } = await params;
|
|
102
|
+
const config = await getConfig();
|
|
103
|
+
return handleDepApi(req, path, config);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function PATCH(
|
|
107
|
+
req: NextRequest,
|
|
108
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
109
|
+
) {
|
|
110
|
+
const { path } = await params;
|
|
111
|
+
const config = await getConfig();
|
|
112
|
+
return handleDepApi(req, path, config);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function DELETE(
|
|
116
|
+
req: NextRequest,
|
|
117
|
+
{ params }: { params: Promise<{ path: string[] }> }
|
|
118
|
+
) {
|
|
119
|
+
const { path } = await params;
|
|
120
|
+
const config = await getConfig();
|
|
121
|
+
return handleDepApi(req, path, config);
|
|
122
|
+
}
|
|
123
|
+
`);
|
|
124
|
+
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Ignore errors - route might already exist or app structure is different
|
|
127
|
+
console.warn('[plugin-dep] Could not ensure deprecated routes:', error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin Deprecated - Server-Only Entry Point
|
|
5
|
+
* Re-exports server-side API handlers and business logic
|
|
6
|
+
*
|
|
7
|
+
* Note: This file is server-only (no 'use server' needed - that's only for Server Actions)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Export router (main API handler)
|
|
11
|
+
export { handleDepApi as handleApi } from './router';
|
|
12
|
+
|
|
13
|
+
// Export actions (business logic)
|
|
14
|
+
export * as actions from './actions';
|
|
15
|
+
|
|
16
|
+
// Export types
|
|
17
|
+
export * from './types';
|
|
18
|
+
|
|
19
|
+
// Note: Config functions are server-only and not exported to avoid client-side import errors
|
|
20
|
+
// They are deprecated anyway since routes are now handled by the unified route system
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin Deprecated - Router
|
|
5
|
+
* Framework-agnostic router that routes API requests to business logic actions
|
|
6
|
+
*
|
|
7
|
+
* SERVER-ONLY: This module must never be imported by client code
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import 'server-only';
|
|
11
|
+
|
|
12
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
13
|
+
import { DepApiConfig } from './types';
|
|
14
|
+
import * as actions from './actions';
|
|
15
|
+
import { verifyToken } from './utils/auth';
|
|
16
|
+
import crypto from 'crypto';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Handle deprecated API requests
|
|
20
|
+
* Routes requests to appropriate action handlers based on path and method
|
|
21
|
+
*/
|
|
22
|
+
export async function handleDepApi(
|
|
23
|
+
req: NextRequest,
|
|
24
|
+
path: string[],
|
|
25
|
+
config: DepApiConfig
|
|
26
|
+
): Promise<NextResponse> {
|
|
27
|
+
const method = req.method;
|
|
28
|
+
const route = path[0] || '';
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// ====================================================================
|
|
32
|
+
// AUTH ROUTES
|
|
33
|
+
// ====================================================================
|
|
34
|
+
if (route === 'me') {
|
|
35
|
+
if (method === 'GET') {
|
|
36
|
+
let session: any = null;
|
|
37
|
+
if (config.authOptions) {
|
|
38
|
+
const { getServerSession } = await import('next-auth');
|
|
39
|
+
session = await getServerSession(config.authOptions);
|
|
40
|
+
}
|
|
41
|
+
const result = await actions.getMe(config, session);
|
|
42
|
+
return NextResponse.json(result);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ====================================================================
|
|
47
|
+
// FEEDBACK ROUTES
|
|
48
|
+
// ====================================================================
|
|
49
|
+
else if (route === 'feedback') {
|
|
50
|
+
if (path[1]) {
|
|
51
|
+
// /api/plugin-dep/feedback/[id]
|
|
52
|
+
const id = path[1];
|
|
53
|
+
if (method === 'PATCH') {
|
|
54
|
+
const body = await req.json();
|
|
55
|
+
const result = await actions.updateFeedback(config, id, body);
|
|
56
|
+
return NextResponse.json(result);
|
|
57
|
+
} else if (method === 'DELETE') {
|
|
58
|
+
const result = await actions.deleteFeedback(config, id);
|
|
59
|
+
return NextResponse.json(result);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
// /api/plugin-dep/feedback
|
|
63
|
+
if (method === 'GET') {
|
|
64
|
+
const result = await actions.listFeedback(config);
|
|
65
|
+
return NextResponse.json(result);
|
|
66
|
+
} else if (method === 'POST') {
|
|
67
|
+
const body = await req.json();
|
|
68
|
+
const cookies = await req.cookies;
|
|
69
|
+
const token = cookies.get('auth_token')?.value;
|
|
70
|
+
let userId: string | undefined;
|
|
71
|
+
if (token) {
|
|
72
|
+
const payload = verifyToken(token, config.jwtSecret);
|
|
73
|
+
if (payload && typeof payload !== 'string') {
|
|
74
|
+
userId = payload.id;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const result = await actions.createFeedback(config, body, userId);
|
|
78
|
+
return NextResponse.json(result);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ====================================================================
|
|
84
|
+
// SETTINGS ROUTES
|
|
85
|
+
// ====================================================================
|
|
86
|
+
else if (route === 'settings') {
|
|
87
|
+
if (path[1] === 'maintenance') {
|
|
88
|
+
// /api/plugin-dep/settings/maintenance
|
|
89
|
+
if (method === 'GET') {
|
|
90
|
+
const result = await actions.getMaintenanceMode(config);
|
|
91
|
+
return NextResponse.json(result);
|
|
92
|
+
} else if (method === 'POST') {
|
|
93
|
+
const body = await req.json();
|
|
94
|
+
const result = await actions.setMaintenanceMode(config, body.active);
|
|
95
|
+
return NextResponse.json(result);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// /api/plugin-dep/settings
|
|
99
|
+
if (method === 'GET') {
|
|
100
|
+
const result = await actions.getSettings(config);
|
|
101
|
+
return NextResponse.json(result);
|
|
102
|
+
} else if (method === 'POST') {
|
|
103
|
+
const body = await req.json();
|
|
104
|
+
const result = await actions.updateSettings(config, body);
|
|
105
|
+
return NextResponse.json(result);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ====================================================================
|
|
111
|
+
// USER ROUTES
|
|
112
|
+
// ====================================================================
|
|
113
|
+
else if (route === 'users') {
|
|
114
|
+
if (path[1]) {
|
|
115
|
+
// /api/plugin-dep/users/[id]
|
|
116
|
+
const id = path[1];
|
|
117
|
+
if (method === 'PATCH') {
|
|
118
|
+
const body = await req.json();
|
|
119
|
+
const result = await actions.updateUser(config, id, body);
|
|
120
|
+
return NextResponse.json(result);
|
|
121
|
+
} else if (method === 'DELETE') {
|
|
122
|
+
const result = await actions.deleteUser(config, id);
|
|
123
|
+
return NextResponse.json(result);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// /api/plugin-dep/users
|
|
127
|
+
if (method === 'GET') {
|
|
128
|
+
const result = await actions.listUsers(config);
|
|
129
|
+
return NextResponse.json(result);
|
|
130
|
+
} else if (method === 'POST') {
|
|
131
|
+
const body = await req.json();
|
|
132
|
+
const result = await actions.createUser(config, body);
|
|
133
|
+
return NextResponse.json(result);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ====================================================================
|
|
139
|
+
// NEWSLETTER ROUTES
|
|
140
|
+
// ====================================================================
|
|
141
|
+
else if (route === 'newsletter') {
|
|
142
|
+
if (path[1] === 'subscribers') {
|
|
143
|
+
if (path[2]) {
|
|
144
|
+
// /api/plugin-dep/newsletter/subscribers/[user]
|
|
145
|
+
const email = decodeURIComponent(path[2]).toLowerCase();
|
|
146
|
+
if (method === 'GET') {
|
|
147
|
+
const result = await actions.getSubscriber(config, email);
|
|
148
|
+
return NextResponse.json(result);
|
|
149
|
+
} else if (method === 'DELETE') {
|
|
150
|
+
const result = await actions.deleteSubscriber(config, email);
|
|
151
|
+
return NextResponse.json(result);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
// /api/plugin-dep/newsletter/subscribers
|
|
155
|
+
if (method === 'GET') {
|
|
156
|
+
const result = await actions.listSubscribers(config);
|
|
157
|
+
return NextResponse.json(result);
|
|
158
|
+
} else if (method === 'POST') {
|
|
159
|
+
const body = await req.json();
|
|
160
|
+
const headers = await req.headers;
|
|
161
|
+
const host = headers.get('host') || undefined;
|
|
162
|
+
const result = await actions.subscribeNewsletter(config, body, host);
|
|
163
|
+
return NextResponse.json(result, { status: 201 });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else if (path[1] === 'settings') {
|
|
167
|
+
// /api/plugin-dep/newsletter/settings
|
|
168
|
+
if (method === 'GET') {
|
|
169
|
+
const result = await actions.getNewsletterSettings(config);
|
|
170
|
+
return NextResponse.json(result);
|
|
171
|
+
} else if (method === 'POST') {
|
|
172
|
+
const body = await req.json();
|
|
173
|
+
const result = await actions.updateNewsletterSettings(config, body);
|
|
174
|
+
return NextResponse.json(result);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ====================================================================
|
|
180
|
+
// NOTIFICATIONS ROUTES
|
|
181
|
+
// ====================================================================
|
|
182
|
+
else if (route === 'notifications') {
|
|
183
|
+
if (path[1] === 'stream') {
|
|
184
|
+
// /api/plugin-dep/notifications/stream
|
|
185
|
+
if (method === 'GET') {
|
|
186
|
+
return handleNotificationStream(req, config);
|
|
187
|
+
}
|
|
188
|
+
} else if (path[1] === 'action') {
|
|
189
|
+
// /api/plugin-dep/notifications/action
|
|
190
|
+
if (method === 'POST') {
|
|
191
|
+
const body = await req.json();
|
|
192
|
+
const cookies = await req.cookies;
|
|
193
|
+
const token = cookies.get('auth_token')?.value;
|
|
194
|
+
if (!token) {
|
|
195
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
196
|
+
}
|
|
197
|
+
const payload = verifyToken(token, config.jwtSecret);
|
|
198
|
+
if (!payload || typeof payload === 'string') {
|
|
199
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
200
|
+
}
|
|
201
|
+
const result = await actions.updateNotification(config, body, payload.id);
|
|
202
|
+
return NextResponse.json(result);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ====================================================================
|
|
208
|
+
// TRANSLATION ROUTES
|
|
209
|
+
// ====================================================================
|
|
210
|
+
else if (route === 'save-translation') {
|
|
211
|
+
if (method === 'POST') {
|
|
212
|
+
const body = await req.json();
|
|
213
|
+
const result = await actions.saveTranslation(config, body);
|
|
214
|
+
return NextResponse.json(result);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ====================================================================
|
|
219
|
+
// STATS ROUTES
|
|
220
|
+
// ====================================================================
|
|
221
|
+
else if (route === 'stats') {
|
|
222
|
+
if (path[1] === 'storage') {
|
|
223
|
+
// /api/plugin-dep/stats/storage
|
|
224
|
+
if (method === 'GET') {
|
|
225
|
+
const result = await actions.getStorageStats(config);
|
|
226
|
+
return NextResponse.json(result);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} else if (route === 'media') {
|
|
230
|
+
if (path[1] === 'stats') {
|
|
231
|
+
// /api/plugin-dep/media/stats
|
|
232
|
+
if (method === 'GET') {
|
|
233
|
+
const result = await actions.getMediaStats(config);
|
|
234
|
+
return NextResponse.json(result);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ====================================================================
|
|
240
|
+
// VERSION ROUTES
|
|
241
|
+
// ====================================================================
|
|
242
|
+
else if (route === 'version') {
|
|
243
|
+
if (method === 'GET') {
|
|
244
|
+
const result = await actions.getVersion();
|
|
245
|
+
return NextResponse.json(result);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ====================================================================
|
|
250
|
+
// GITHUB DEPLOYMENT ROUTES
|
|
251
|
+
// ====================================================================
|
|
252
|
+
else if (route === 'github') {
|
|
253
|
+
if (path[1] === 'deploy') {
|
|
254
|
+
// /api/plugin-dep/github/deploy
|
|
255
|
+
if (method === 'GET') {
|
|
256
|
+
const result = await actions.getDeploymentStatus(config);
|
|
257
|
+
return NextResponse.json(result);
|
|
258
|
+
} else if (method === 'POST') {
|
|
259
|
+
const signature = req.headers.get('x-hub-signature-256') || '';
|
|
260
|
+
let payload: any = null;
|
|
261
|
+
if (signature !== 'manual') {
|
|
262
|
+
const rawBody = await req.text();
|
|
263
|
+
const secret = config.githubWebhookSecret;
|
|
264
|
+
if (!secret) {
|
|
265
|
+
return NextResponse.json({ error: 'Server configuration error' }, { status: 500 });
|
|
266
|
+
}
|
|
267
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
268
|
+
const digest = 'sha256=' + hmac.update(rawBody).digest('hex');
|
|
269
|
+
if (signature !== digest) {
|
|
270
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
271
|
+
}
|
|
272
|
+
payload = rawBody.startsWith('payload=')
|
|
273
|
+
? JSON.parse(decodeURIComponent(rawBody.substring(8)))
|
|
274
|
+
: JSON.parse(rawBody);
|
|
275
|
+
}
|
|
276
|
+
const result = await actions.triggerDeployment(config, signature, payload);
|
|
277
|
+
return NextResponse.json(result);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Method not allowed
|
|
283
|
+
return NextResponse.json(
|
|
284
|
+
{ error: `Method ${method} not allowed for route: ${route || '/'}` },
|
|
285
|
+
{ status: 405 }
|
|
286
|
+
);
|
|
287
|
+
} catch (error: any) {
|
|
288
|
+
console.error('[DepApiRouter] Error:', error);
|
|
289
|
+
const status = error.message?.includes('not found') ? 404
|
|
290
|
+
: error.message?.includes('Unauthorized') ? 401
|
|
291
|
+
: error.message?.includes('already') ? 409
|
|
292
|
+
: error.message?.includes('too short') ? 400
|
|
293
|
+
: 500;
|
|
294
|
+
return NextResponse.json(
|
|
295
|
+
{ error: error.message || 'Internal server error' },
|
|
296
|
+
{ status }
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Handle Server-Sent Events stream for notifications
|
|
303
|
+
*/
|
|
304
|
+
async function handleNotificationStream(
|
|
305
|
+
req: NextRequest,
|
|
306
|
+
config: DepApiConfig
|
|
307
|
+
): Promise<NextResponse> {
|
|
308
|
+
const encoder = new TextEncoder();
|
|
309
|
+
const cookies = await req.cookies;
|
|
310
|
+
const token = cookies.get('auth_token')?.value;
|
|
311
|
+
|
|
312
|
+
let userRole = 'guest';
|
|
313
|
+
let currentUserId: string | null = null;
|
|
314
|
+
|
|
315
|
+
if (token) {
|
|
316
|
+
try {
|
|
317
|
+
const payload = verifyToken(token, config.jwtSecret);
|
|
318
|
+
if (payload && typeof payload !== 'string') {
|
|
319
|
+
userRole = payload.role || 'guest';
|
|
320
|
+
currentUserId = payload.id;
|
|
321
|
+
}
|
|
322
|
+
} catch (e) {
|
|
323
|
+
console.error('Token verification failed in stream');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let changeStream: any;
|
|
328
|
+
|
|
329
|
+
const stream = new ReadableStream({
|
|
330
|
+
async start(controller) {
|
|
331
|
+
try {
|
|
332
|
+
const client = await config.mongoClient;
|
|
333
|
+
const db = client.db();
|
|
334
|
+
const collection = db.collection('notifications');
|
|
335
|
+
|
|
336
|
+
const streamData = await actions.getNotificationStream(config, userRole, currentUserId);
|
|
337
|
+
|
|
338
|
+
// Send history
|
|
339
|
+
streamData.history.forEach((notif) => {
|
|
340
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(notif)}\n\n`));
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Send connection status
|
|
344
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ status: 'connected', role: userRole })}\n\n`));
|
|
345
|
+
|
|
346
|
+
// Watch for changes
|
|
347
|
+
changeStream = collection.watch(streamData.pipeline, { fullDocument: 'updateLookup' });
|
|
348
|
+
|
|
349
|
+
changeStream.on('change', (change: any) => {
|
|
350
|
+
if (change.operationType === 'insert') {
|
|
351
|
+
const data = `data: ${JSON.stringify(change.fullDocument)}\n\n`;
|
|
352
|
+
controller.enqueue(encoder.encode(data));
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const keepAlive = setInterval(() => {
|
|
357
|
+
try {
|
|
358
|
+
controller.enqueue(encoder.encode(': keep-alive\n\n'));
|
|
359
|
+
} catch {
|
|
360
|
+
clearInterval(keepAlive);
|
|
361
|
+
}
|
|
362
|
+
}, 20000);
|
|
363
|
+
|
|
364
|
+
changeStream.on('error', (err: any) => {
|
|
365
|
+
console.error('Mongo Stream Error:', err);
|
|
366
|
+
clearInterval(keepAlive);
|
|
367
|
+
if (changeStream) changeStream.close();
|
|
368
|
+
});
|
|
369
|
+
} catch (err) {
|
|
370
|
+
console.error('Stream Start Error:', err);
|
|
371
|
+
controller.error(err);
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
cancel() {
|
|
375
|
+
if (changeStream) {
|
|
376
|
+
changeStream.close();
|
|
377
|
+
console.log(`Stream connection closed for role: ${userRole}`);
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return new NextResponse(stream, {
|
|
383
|
+
headers: {
|
|
384
|
+
'Content-Type': 'text/event-stream',
|
|
385
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
386
|
+
Connection: 'keep-alive',
|
|
387
|
+
'X-Accel-Buffering': 'no',
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Deprecated - Types
|
|
3
|
+
* Type definitions for deprecated client API routes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { MongoClient } from 'mongodb';
|
|
7
|
+
import { NextRequest } from 'next/server';
|
|
8
|
+
|
|
9
|
+
export interface DepApiConfig {
|
|
10
|
+
/** MongoDB client promise */
|
|
11
|
+
mongoClient: Promise<MongoClient>;
|
|
12
|
+
/** JWT secret for token verification */
|
|
13
|
+
jwtSecret: string;
|
|
14
|
+
/** NextAuth auth options (for session-based auth) */
|
|
15
|
+
authOptions?: any;
|
|
16
|
+
/** Base URL for email links */
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
/** GitHub webhook secret */
|
|
19
|
+
githubWebhookSecret?: string;
|
|
20
|
+
/** Paths for deployment scripts */
|
|
21
|
+
deploymentPaths?: {
|
|
22
|
+
flagPath: string;
|
|
23
|
+
scriptPath: string;
|
|
24
|
+
};
|
|
25
|
+
/** Locales directory path */
|
|
26
|
+
localesDir?: string;
|
|
27
|
+
/** Uploads directory path */
|
|
28
|
+
uploadsDir?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ApiRequest {
|
|
32
|
+
method: string;
|
|
33
|
+
url: string;
|
|
34
|
+
body?: any;
|
|
35
|
+
headers: Headers;
|
|
36
|
+
cookies?: Map<string, string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ApiResponse {
|
|
40
|
+
status: number;
|
|
41
|
+
body: any;
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ApiHandler = (
|
|
46
|
+
req: ApiRequest,
|
|
47
|
+
config: DepApiConfig,
|
|
48
|
+
params?: Record<string, string>
|
|
49
|
+
) => Promise<ApiResponse>;
|
|
50
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth utilities for plugin-dep
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import jwt from 'jsonwebtoken';
|
|
6
|
+
|
|
7
|
+
export interface AuthPayload {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
role: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function verifyToken(token: string, secret: string): AuthPayload | null {
|
|
14
|
+
try {
|
|
15
|
+
return jwt.verify(token, secret) as AuthPayload;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|