@intranefr/superbackend 1.7.9 → 1.7.10
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/index.js +3 -1
- package/package.json +2 -1
- package/src/controllers/publicExport.controller.js +146 -0
- package/src/controllers/waitingList.controller.js +302 -0
- package/src/middleware.js +5 -10
- package/src/models/User.js +0 -5
- package/src/routes/publicExport.routes.js +9 -0
- package/src/routes/waitingList.routes.js +8 -0
- package/src/routes/waitingListAdmin.routes.js +6 -0
- package/src/services/waitingListPublicExports.service.js +465 -0
- package/views/admin-waiting-list.ejs +361 -11
- package/views/public-export-password.ejs +192 -0
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intranefr/superbackend",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.10",
|
|
4
4
|
"description": "Node.js middleware that gives your project backend superpowers",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"@aws-sdk/client-s3": "^3.0.0",
|
|
45
45
|
"@intranefr/superbackend": "^1.7.7",
|
|
46
46
|
"axios": "^1.13.2",
|
|
47
|
+
"basic-auth": "^2.0.1",
|
|
47
48
|
"bcryptjs": "^2.4.3",
|
|
48
49
|
"cheerio": "^1.0.0-rc.12",
|
|
49
50
|
"connect-mongo": "^5.1.0",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const waitingListPublicExportsService = require('../services/waitingListPublicExports.service');
|
|
6
|
+
const waitingListService = require('../services/waitingListJson.service');
|
|
7
|
+
const { logAudit } = require('../services/auditLogger');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GET /share/export/:name - Public export access page
|
|
11
|
+
*/
|
|
12
|
+
router.get('/:name', async (req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const { name } = req.params;
|
|
15
|
+
const { format = 'csv', error } = req.query;
|
|
16
|
+
|
|
17
|
+
// Validate format
|
|
18
|
+
if (!['csv', 'json'].includes(format)) {
|
|
19
|
+
return res.status(400).send('Invalid format. Must be csv or json.');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Find export configuration
|
|
23
|
+
const exportConfig = await waitingListPublicExportsService.getPublicExportByName(name);
|
|
24
|
+
if (!exportConfig) {
|
|
25
|
+
return res.status(404).send('Export not found');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// If no password protection, redirect to immediate download
|
|
29
|
+
if (!exportConfig.password) {
|
|
30
|
+
const downloadUrl = `/api/waiting-list/share/export?type=${encodeURIComponent(name)}&format=${format}`;
|
|
31
|
+
return res.redirect(downloadUrl);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Show password entry page
|
|
35
|
+
const templatePath = path.join(__dirname, '..', '..', 'views', 'public-export-password.ejs');
|
|
36
|
+
fs.readFile(templatePath, 'utf8', (err, template) => {
|
|
37
|
+
if (err) {
|
|
38
|
+
console.error('Error reading template:', err);
|
|
39
|
+
return res.status(500).send('Error loading page');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
let html = template
|
|
44
|
+
.replace(/<%= exportName %>/g, exportConfig.name)
|
|
45
|
+
.replace(/<%= exportType %>/g, exportConfig.type)
|
|
46
|
+
.replace(/<%= format %>/g, format)
|
|
47
|
+
.replace(/<%= format\.toUpperCase\(\) %>/g, format.toUpperCase())
|
|
48
|
+
.replace(/<%= format === 'csv' \? 'bg-green-100 text-green-800' : 'bg-blue-100 text-blue-800' %>/g,
|
|
49
|
+
format === 'csv' ? 'bg-green-100 text-green-800' : 'bg-blue-100 text-blue-800')
|
|
50
|
+
.replace(/<%= error %>/g, error || '');
|
|
51
|
+
|
|
52
|
+
// Handle error conditional
|
|
53
|
+
if (error) {
|
|
54
|
+
html = html.replace(/<% if \(error\) \{ %>/g, '').replace(/<% } %>/g, '');
|
|
55
|
+
} else {
|
|
56
|
+
// Remove the entire error div when no error
|
|
57
|
+
html = html.replace(/<% if \(error\) \{ %>[\s\S]*?<% } %>/g, '');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
res.send(html);
|
|
61
|
+
} catch (renderErr) {
|
|
62
|
+
console.error('Error rendering template:', renderErr);
|
|
63
|
+
res.status(500).send('Error rendering page');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Public export page error:', error);
|
|
69
|
+
res.status(500).send('Internal server error');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* POST /share/export/:name/auth - Password validation
|
|
75
|
+
*/
|
|
76
|
+
router.post('/:name/auth', async (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
const { name } = req.params;
|
|
79
|
+
const { password, format = 'csv' } = req.body;
|
|
80
|
+
|
|
81
|
+
// Validate required fields
|
|
82
|
+
if (!password) {
|
|
83
|
+
return res.status(400).json({ error: 'Password is required' });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!['csv', 'json'].includes(format)) {
|
|
87
|
+
return res.status(400).json({ error: 'Invalid format' });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Find export configuration
|
|
91
|
+
const exportConfig = await waitingListPublicExportsService.getPublicExportByName(name);
|
|
92
|
+
if (!exportConfig) {
|
|
93
|
+
return res.status(404).json({ error: 'Export not found' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate password
|
|
97
|
+
const isValidPassword = await waitingListPublicExportsService.validateExportPassword(exportConfig, password);
|
|
98
|
+
if (!isValidPassword) {
|
|
99
|
+
// Log failed attempt
|
|
100
|
+
await logAudit({
|
|
101
|
+
action: 'public.waiting_list.export.auth_failed',
|
|
102
|
+
entityType: 'WaitingListPublicExport',
|
|
103
|
+
req,
|
|
104
|
+
details: {
|
|
105
|
+
exportName: name,
|
|
106
|
+
ip: req.ip,
|
|
107
|
+
userAgent: req.headers['user-agent'],
|
|
108
|
+
authMethod: 'web_form'
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return res.status(401).json({ error: 'Invalid password' });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Set session for authenticated access
|
|
116
|
+
req.session.exportAuth = req.session.exportAuth || {};
|
|
117
|
+
req.session.exportAuth[name] = {
|
|
118
|
+
authenticated: true,
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
format: format
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Log successful authentication
|
|
124
|
+
await logAudit({
|
|
125
|
+
action: 'public.waiting_list.export.auth_success',
|
|
126
|
+
entityType: 'WaitingListPublicExport',
|
|
127
|
+
req,
|
|
128
|
+
details: {
|
|
129
|
+
exportName: name,
|
|
130
|
+
ip: req.ip,
|
|
131
|
+
userAgent: req.headers['user-agent'],
|
|
132
|
+
authMethod: 'web_form'
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Return download URL
|
|
137
|
+
const downloadUrl = `/api/waiting-list/share/export?type=${encodeURIComponent(name)}&format=${format}`;
|
|
138
|
+
res.json({ downloadUrl });
|
|
139
|
+
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('Password validation error:', error);
|
|
142
|
+
res.status(500).json({ error: 'Authentication failed' });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
module.exports = router;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const waitingListService = require('../services/waitingListJson.service');
|
|
2
|
+
const waitingListPublicExportsService = require('../services/waitingListPublicExports.service');
|
|
2
3
|
const { validateEmail, sanitizeString } = require('../utils/validation');
|
|
4
|
+
const basicAuth = require('basic-auth');
|
|
3
5
|
|
|
4
6
|
// Subscribe to waiting list
|
|
5
7
|
exports.subscribe = async (req, res) => {
|
|
@@ -272,3 +274,303 @@ exports.bulkRemove = async (req, res) => {
|
|
|
272
274
|
return res.status(500).json({ error: 'Failed to bulk remove entries' });
|
|
273
275
|
}
|
|
274
276
|
};
|
|
277
|
+
|
|
278
|
+
// Public export endpoint
|
|
279
|
+
exports.publicExport = async (req, res) => {
|
|
280
|
+
try {
|
|
281
|
+
const { type, format = 'csv', password: queryPassword } = req.query;
|
|
282
|
+
|
|
283
|
+
// Validate required parameters
|
|
284
|
+
if (!type) {
|
|
285
|
+
return res.status(400).json({
|
|
286
|
+
error: 'Type parameter is required',
|
|
287
|
+
field: 'type'
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Validate format
|
|
292
|
+
if (!['csv', 'json'].includes(format)) {
|
|
293
|
+
return res.status(400).json({
|
|
294
|
+
error: 'Format must be either "csv" or "json"',
|
|
295
|
+
field: 'format'
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Find export configuration by type
|
|
300
|
+
const exportConfig = await waitingListPublicExportsService.getPublicExportByName(type);
|
|
301
|
+
if (!exportConfig) {
|
|
302
|
+
return res.status(404).json({
|
|
303
|
+
error: 'Export configuration not found',
|
|
304
|
+
field: 'type'
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check password protection (support multiple authentication methods)
|
|
309
|
+
let providedPassword = null;
|
|
310
|
+
let authMethod = 'none';
|
|
311
|
+
|
|
312
|
+
if (exportConfig.password) {
|
|
313
|
+
// First check session authentication (web form)
|
|
314
|
+
if (req.session && req.session.exportAuth && req.session.exportAuth[type] &&
|
|
315
|
+
req.session.exportAuth[type].authenticated &&
|
|
316
|
+
req.session.exportAuth[type].format === format) {
|
|
317
|
+
// Session is valid (5 minute expiry)
|
|
318
|
+
const sessionAge = Date.now() - req.session.exportAuth[type].timestamp;
|
|
319
|
+
if (sessionAge < 5 * 60 * 1000) { // 5 minutes
|
|
320
|
+
authMethod = 'session';
|
|
321
|
+
} else {
|
|
322
|
+
// Session expired, remove it
|
|
323
|
+
delete req.session.exportAuth[type];
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// If no valid session, check query parameter
|
|
328
|
+
if (authMethod === 'none' && queryPassword) {
|
|
329
|
+
providedPassword = queryPassword;
|
|
330
|
+
authMethod = 'query';
|
|
331
|
+
} else if (authMethod === 'none') {
|
|
332
|
+
// Fall back to Basic Auth
|
|
333
|
+
const auth = basicAuth(req);
|
|
334
|
+
if (auth && auth.pass) {
|
|
335
|
+
providedPassword = auth.pass;
|
|
336
|
+
authMethod = 'basic';
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Validate password if we have one
|
|
341
|
+
if (authMethod !== 'session') {
|
|
342
|
+
if (!providedPassword || !await waitingListPublicExportsService.validateExportPassword(exportConfig, providedPassword)) {
|
|
343
|
+
// For Basic Auth, send proper challenge
|
|
344
|
+
if (authMethod === 'none' || authMethod === 'basic') {
|
|
345
|
+
res.setHeader('WWW-Authenticate', 'Basic realm="Waiting List Export"');
|
|
346
|
+
}
|
|
347
|
+
return res.status(401).json({
|
|
348
|
+
error: 'Authentication required',
|
|
349
|
+
field: 'password',
|
|
350
|
+
authMethod: authMethod === 'query' ? 'query' : 'basic'
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Get filtered waiting list entries
|
|
357
|
+
const { entries } = await waitingListService.getWaitingListEntriesAdmin({
|
|
358
|
+
type: exportConfig.type,
|
|
359
|
+
status: 'active',
|
|
360
|
+
limit: 100000, // Large limit to get all entries
|
|
361
|
+
offset: 0
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Record access for analytics
|
|
365
|
+
await waitingListPublicExportsService.recordExportAccess(exportConfig.name, req, authMethod);
|
|
366
|
+
|
|
367
|
+
// Export in requested format
|
|
368
|
+
if (format === 'json') {
|
|
369
|
+
res.setHeader('Content-Type', 'application/json');
|
|
370
|
+
res.setHeader('Content-Disposition', `attachment; filename="waiting-list-${exportConfig.type}-${new Date().toISOString().split('T')[0]}.json"`);
|
|
371
|
+
|
|
372
|
+
const exportData = {
|
|
373
|
+
export: {
|
|
374
|
+
name: exportConfig.name,
|
|
375
|
+
type: exportConfig.type,
|
|
376
|
+
exportedAt: new Date().toISOString(),
|
|
377
|
+
totalEntries: entries.length,
|
|
378
|
+
authMethod
|
|
379
|
+
},
|
|
380
|
+
entries: entries.map(entry => ({
|
|
381
|
+
email: entry.email,
|
|
382
|
+
type: entry.type,
|
|
383
|
+
status: entry.status,
|
|
384
|
+
referralSource: entry.referralSource,
|
|
385
|
+
createdAt: entry.createdAt,
|
|
386
|
+
updatedAt: entry.updatedAt
|
|
387
|
+
}))
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
return res.json(exportData);
|
|
391
|
+
} else {
|
|
392
|
+
// CSV format
|
|
393
|
+
res.setHeader('Content-Type', 'text/csv');
|
|
394
|
+
res.setHeader('Content-Disposition', `attachment; filename="waiting-list-${exportConfig.type}-${new Date().toISOString().split('T')[0]}.csv"`);
|
|
395
|
+
|
|
396
|
+
// CSV header row
|
|
397
|
+
const csvRows = [
|
|
398
|
+
['Email', 'Type', 'Status', 'Referral Source', 'Created At', 'Updated At']
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
// Data rows
|
|
402
|
+
entries.forEach(entry => {
|
|
403
|
+
csvRows.push([
|
|
404
|
+
entry.email || '',
|
|
405
|
+
entry.type || '',
|
|
406
|
+
entry.status || '',
|
|
407
|
+
entry.referralSource || '',
|
|
408
|
+
entry.createdAt ? new Date(entry.createdAt).toISOString() : '',
|
|
409
|
+
entry.updatedAt ? new Date(entry.updatedAt).toISOString() : ''
|
|
410
|
+
]);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Convert to CSV string with proper escaping
|
|
414
|
+
const csvContent = csvRows.map(row =>
|
|
415
|
+
row.map(cell => {
|
|
416
|
+
const str = String(cell || '');
|
|
417
|
+
// Escape quotes and wrap in quotes if contains comma, quote, or newline
|
|
418
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
419
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
420
|
+
}
|
|
421
|
+
return str;
|
|
422
|
+
}).join(',')
|
|
423
|
+
).join('\n');
|
|
424
|
+
|
|
425
|
+
return res.send(csvContent);
|
|
426
|
+
}
|
|
427
|
+
} catch (error) {
|
|
428
|
+
console.error('Public export error:', error);
|
|
429
|
+
return res.status(500).json({ error: 'Failed to export data' });
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Admin: Get all public exports
|
|
434
|
+
exports.getPublicExports = async (req, res) => {
|
|
435
|
+
try {
|
|
436
|
+
const result = await waitingListPublicExportsService.getPublicExports();
|
|
437
|
+
res.json(result);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.error('Get public exports error:', error);
|
|
440
|
+
return res.status(500).json({ error: 'Failed to get public exports' });
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// Admin: Create public export
|
|
445
|
+
exports.createPublicExport = async (req, res) => {
|
|
446
|
+
try {
|
|
447
|
+
const { name, type, password, format = 'csv' } = req.body;
|
|
448
|
+
|
|
449
|
+
// Validate required fields
|
|
450
|
+
if (!name) {
|
|
451
|
+
return res.status(400).json({
|
|
452
|
+
error: 'Name is required',
|
|
453
|
+
field: 'name'
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (!type) {
|
|
458
|
+
return res.status(400).json({
|
|
459
|
+
error: 'Type is required',
|
|
460
|
+
field: 'type'
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Hash password if provided
|
|
465
|
+
let hashedPassword = null;
|
|
466
|
+
if (password) {
|
|
467
|
+
hashedPassword = await waitingListPublicExportsService.hashPassword(password);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const exportConfig = await waitingListPublicExportsService.createPublicExport({
|
|
471
|
+
name: name.trim(),
|
|
472
|
+
type: type.trim(),
|
|
473
|
+
password: hashedPassword,
|
|
474
|
+
format
|
|
475
|
+
}, req.user?.username || 'admin');
|
|
476
|
+
|
|
477
|
+
// Don't return password hash in response
|
|
478
|
+
const response = { ...exportConfig };
|
|
479
|
+
delete response.password;
|
|
480
|
+
response.hasPassword = !!password;
|
|
481
|
+
|
|
482
|
+
res.status(201).json({
|
|
483
|
+
message: 'Public export created successfully',
|
|
484
|
+
data: response
|
|
485
|
+
});
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.error('Create public export error:', error);
|
|
488
|
+
|
|
489
|
+
if (error.code === 'VALIDATION') {
|
|
490
|
+
return res.status(400).json({
|
|
491
|
+
error: error.message,
|
|
492
|
+
field: 'general'
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (error.code === 'DUPLICATE_NAME') {
|
|
497
|
+
return res.status(409).json({
|
|
498
|
+
error: error.message,
|
|
499
|
+
field: 'name'
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return res.status(500).json({ error: 'Failed to create public export' });
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Admin: Update public export
|
|
508
|
+
exports.updatePublicExport = async (req, res) => {
|
|
509
|
+
try {
|
|
510
|
+
const { id } = req.params;
|
|
511
|
+
const { name, type, password, format } = req.body;
|
|
512
|
+
|
|
513
|
+
const updates = {};
|
|
514
|
+
if (name !== undefined) updates.name = name.trim();
|
|
515
|
+
if (type !== undefined) updates.type = type.trim();
|
|
516
|
+
if (format !== undefined) updates.format = format;
|
|
517
|
+
if (password !== undefined) {
|
|
518
|
+
updates.password = password ? await waitingListPublicExportsService.hashPassword(password) : null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const result = await waitingListPublicExportsService.updatePublicExport(id, updates, req.user?.username || 'admin');
|
|
522
|
+
|
|
523
|
+
// Don't return password hash in response
|
|
524
|
+
const updatedExport = result.exports.find(e => e.id === id);
|
|
525
|
+
const response = { ...updatedExport };
|
|
526
|
+
delete response.password;
|
|
527
|
+
response.hasPassword = !!updatedExport.password;
|
|
528
|
+
|
|
529
|
+
res.json({
|
|
530
|
+
message: 'Public export updated successfully',
|
|
531
|
+
data: response
|
|
532
|
+
});
|
|
533
|
+
} catch (error) {
|
|
534
|
+
console.error('Update public export error:', error);
|
|
535
|
+
|
|
536
|
+
if (error.code === 'VALIDATION') {
|
|
537
|
+
return res.status(400).json({
|
|
538
|
+
error: error.message,
|
|
539
|
+
field: 'general'
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (error.code === 'NOT_FOUND') {
|
|
544
|
+
return res.status(404).json({
|
|
545
|
+
error: 'Public export not found',
|
|
546
|
+
field: 'id'
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return res.status(500).json({ error: 'Failed to update public export' });
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// Admin: Delete public export
|
|
555
|
+
exports.deletePublicExport = async (req, res) => {
|
|
556
|
+
try {
|
|
557
|
+
const { id } = req.params;
|
|
558
|
+
|
|
559
|
+
await waitingListPublicExportsService.deletePublicExport(id, req.user?.username || 'admin');
|
|
560
|
+
|
|
561
|
+
res.json({
|
|
562
|
+
message: 'Public export deleted successfully'
|
|
563
|
+
});
|
|
564
|
+
} catch (error) {
|
|
565
|
+
console.error('Delete public export error:', error);
|
|
566
|
+
|
|
567
|
+
if (error.code === 'NOT_FOUND') {
|
|
568
|
+
return res.status(404).json({
|
|
569
|
+
error: 'Public export not found',
|
|
570
|
+
field: 'id'
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return res.status(500).json({ error: 'Failed to delete public export' });
|
|
575
|
+
}
|
|
576
|
+
};
|
package/src/middleware.js
CHANGED
|
@@ -228,7 +228,6 @@ function createMiddleware(options = {}) {
|
|
|
228
228
|
// Database connection
|
|
229
229
|
const mongoUri =
|
|
230
230
|
options.mongodbUri ||
|
|
231
|
-
options.dbConnection ||
|
|
232
231
|
process.env.MONGODB_URI ||
|
|
233
232
|
process.env.MONGO_URI;
|
|
234
233
|
|
|
@@ -539,15 +538,8 @@ function createMiddleware(options = {}) {
|
|
|
539
538
|
router.use(express.urlencoded({ extended: true }));
|
|
540
539
|
}
|
|
541
540
|
|
|
542
|
-
// Session middleware for
|
|
543
|
-
const
|
|
544
|
-
const sessionStore = !isJest && sessionMongoUrl
|
|
545
|
-
? MongoStore.create({
|
|
546
|
-
mongoUrl: sessionMongoUrl,
|
|
547
|
-
collectionName: 'admin_sessions',
|
|
548
|
-
ttl: 24 * 60 * 60 // 24 hours in seconds
|
|
549
|
-
})
|
|
550
|
-
: undefined;
|
|
541
|
+
// Session middleware - disabled for now (MongoDB at 27018 requires auth for writes)
|
|
542
|
+
const sessionStore = undefined;
|
|
551
543
|
|
|
552
544
|
const sessionMiddleware = session({
|
|
553
545
|
secret: process.env.SESSION_SECRET || 'superbackend-session-secret-fallback',
|
|
@@ -2290,6 +2282,9 @@ res.status(500).send("Error rendering page");
|
|
|
2290
2282
|
next();
|
|
2291
2283
|
});
|
|
2292
2284
|
|
|
2285
|
+
// Public export pages
|
|
2286
|
+
router.use("/share/export", require("./routes/publicExport.routes"));
|
|
2287
|
+
|
|
2293
2288
|
// Public pages router (catch-all, must be last before error handler)
|
|
2294
2289
|
router.use(require("./routes/pages.routes"));
|
|
2295
2290
|
|
package/src/models/User.js
CHANGED
|
@@ -92,11 +92,6 @@ const userSchema = new mongoose.Schema({
|
|
|
92
92
|
timestamps: true
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
// Indexes
|
|
96
|
-
userSchema.index({ email: 1 });
|
|
97
|
-
userSchema.index({ githubId: 1 });
|
|
98
|
-
userSchema.index({ clerkUserId: 1 });
|
|
99
|
-
|
|
100
95
|
// Hash password before saving
|
|
101
96
|
userSchema.pre('save', async function(next) {
|
|
102
97
|
if (!this.isModified('passwordHash')) return next();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const publicExportController = require('../controllers/publicExport.controller');
|
|
4
|
+
|
|
5
|
+
// Public export access page and authentication
|
|
6
|
+
router.get('/:name', publicExportController);
|
|
7
|
+
router.post('/:name/auth', publicExportController);
|
|
8
|
+
|
|
9
|
+
module.exports = router;
|
|
@@ -4,6 +4,7 @@ const waitingListController = require('../controllers/waitingList.controller');
|
|
|
4
4
|
const asyncHandler = require('../utils/asyncHandler');
|
|
5
5
|
const { auditMiddleware } = require('../services/auditLogger');
|
|
6
6
|
const rateLimiter = require('../services/rateLimiter.service');
|
|
7
|
+
const basicAuth = require('basic-auth');
|
|
7
8
|
|
|
8
9
|
// POST /api/waiting-list/subscribe - Subscribe to waiting list
|
|
9
10
|
// Rate limited by IP to prevent spam/abuse (1 request per minute)
|
|
@@ -20,4 +21,11 @@ router.get('/stats',
|
|
|
20
21
|
asyncHandler(waitingListController.getStats)
|
|
21
22
|
);
|
|
22
23
|
|
|
24
|
+
// GET /api/waiting-list/share/export - Public export endpoint
|
|
25
|
+
// Rate limited to prevent abuse (10 requests per minute)
|
|
26
|
+
router.get('/share/export',
|
|
27
|
+
rateLimiter.limit('waitingListPublicExportLimiter'),
|
|
28
|
+
asyncHandler(waitingListController.publicExport)
|
|
29
|
+
);
|
|
30
|
+
|
|
23
31
|
module.exports = router;
|
|
@@ -9,4 +9,10 @@ router.get('/types', adminSessionAuth, asyncHandler(waitingListController.getTyp
|
|
|
9
9
|
router.get('/export-csv', adminSessionAuth, asyncHandler(waitingListController.exportCsv));
|
|
10
10
|
router.post('/bulk-remove', adminSessionAuth, asyncHandler(waitingListController.bulkRemove));
|
|
11
11
|
|
|
12
|
+
// Public exports management
|
|
13
|
+
router.get('/public-exports', adminSessionAuth, asyncHandler(waitingListController.getPublicExports));
|
|
14
|
+
router.post('/public-exports', adminSessionAuth, asyncHandler(waitingListController.createPublicExport));
|
|
15
|
+
router.put('/public-exports/:id', adminSessionAuth, asyncHandler(waitingListController.updatePublicExport));
|
|
16
|
+
router.delete('/public-exports/:id', adminSessionAuth, asyncHandler(waitingListController.deletePublicExport));
|
|
17
|
+
|
|
12
18
|
module.exports = router;
|