mbkbucket 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ function normalizeBooleanLike(value) {
2
+ if (typeof value === 'boolean') return value;
3
+ if (typeof value === 'string') {
4
+ const normalized = value.trim().toLowerCase();
5
+ if (['true', 't', '1', 'yes', 'y', 'on'].includes(normalized)) return true;
6
+ if (['false', 'f', '0', 'no', 'n', 'off'].includes(normalized)) return false;
7
+ }
8
+ return value;
9
+ }
10
+
11
+ export function parseAndValidateMbkbucketVar(rawValue) {
12
+ const errors = [];
13
+ let parsed;
14
+
15
+ try {
16
+ parsed = rawValue ? JSON.parse(rawValue) : {};
17
+ } catch {
18
+ throw new Error('Invalid JSON in process.env.mbkbucketVar');
19
+ }
20
+
21
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
22
+ throw new Error('mbkbucketVar must be a valid object');
23
+ }
24
+
25
+ const defaults = {
26
+ p_view_inline: true,
27
+ publiView_enabled: false
28
+ };
29
+
30
+ for (const [key, defaultVal] of Object.entries(defaults)) {
31
+ const current = parsed[key];
32
+ if (current === undefined || (typeof current === 'string' && current.trim() === '')) {
33
+ parsed[key] = defaultVal;
34
+ }
35
+ }
36
+
37
+ parsed.p_view_inline = normalizeBooleanLike(parsed.p_view_inline);
38
+ parsed.publiView_enabled = normalizeBooleanLike(parsed.publiView_enabled);
39
+
40
+ if (typeof parsed.p_view_inline !== 'boolean') {
41
+ errors.push('mbkbucketVar.p_view_inline must be a boolean if provided');
42
+ }
43
+ if (typeof parsed.publiView_enabled !== 'boolean') {
44
+ errors.push('mbkbucketVar.publiView_enabled must be a boolean if provided');
45
+ }
46
+
47
+ if (errors.length) {
48
+ throw new Error(errors.join(' | '));
49
+ }
50
+
51
+ return parsed;
52
+ }
53
+
54
+ export function parseAndValidateBucketConnection(rawValue) {
55
+ if (!rawValue) {
56
+ return null;
57
+ }
58
+
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(rawValue);
62
+ } catch {
63
+ throw new Error('BucketConnection is not valid JSON');
64
+ }
65
+
66
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || Object.keys(parsed).length === 0) {
67
+ throw new Error('BucketConnection must be a non-empty object');
68
+ }
69
+
70
+ const requiredFields = ['BUCKET_NAME', 'ACCESS_KEY_ID', 'SECRET_ACCESS_KEY', 'ENDPOINT'];
71
+
72
+ for (const [bucketName, config] of Object.entries(parsed)) {
73
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
74
+ throw new Error(`Bucket '${bucketName}' configuration must be an object, not a string. Remove quotes around the inner JSON object.`);
75
+ }
76
+
77
+ const missingFields = requiredFields.filter((field) => !config[field]);
78
+ if (missingFields.length) {
79
+ throw new Error(`Bucket '${bucketName}' is missing required fields: ${missingFields.join(', ')}`);
80
+ }
81
+ }
82
+
83
+ return parsed;
84
+ }
85
+
86
+ export { normalizeBooleanLike };
package/lib/routes/api.js CHANGED
@@ -5,6 +5,47 @@ import multer from "multer";
5
5
 
6
6
  const router = express.Router();
7
7
 
8
+ function classifyApiError(error, fallbackMessage = 'Request failed') {
9
+ const message = String(error?.message || fallbackMessage);
10
+ const lower = message.toLowerCase();
11
+
12
+ if (lower.includes('bucketconnection environment variable is not set')) {
13
+ return { status: 503, code: 'BUCKET_CONFIG_ERROR', message };
14
+ }
15
+ if (lower.includes('no bucket selected') || lower.includes('not found in bucketconnection')) {
16
+ return { status: 400, code: 'INVALID_BUCKET', message };
17
+ }
18
+ if (lower.includes('already exists')) {
19
+ return { status: 409, code: 'CONFLICT', message };
20
+ }
21
+ if (lower.includes('not found')) {
22
+ return { status: 404, code: 'NOT_FOUND', message };
23
+ }
24
+ if (lower.includes('access denied')) {
25
+ return { status: 403, code: 'ACCESS_DENIED', message };
26
+ }
27
+ if (lower.includes('required') || lower.includes('invalid') || lower.includes('must be')) {
28
+ return { status: 400, code: 'VALIDATION_ERROR', message };
29
+ }
30
+ return { status: 500, code: 'INTERNAL_ERROR', message };
31
+ }
32
+
33
+ function sendApiError(res, error, fallbackMessage = 'Request failed') {
34
+ const mapped = classifyApiError(error, fallbackMessage);
35
+ return res.status(mapped.status).json({ success: false, code: mapped.code, error: mapped.message });
36
+ }
37
+
38
+ // Central bucket guard for all API routes under /mbkbucket
39
+ router.use((req, res, next) => {
40
+ if (req.bucketResolveError) {
41
+ return sendApiError(res, req.bucketResolveError, 'Invalid bucket selection');
42
+ }
43
+ if (!req.activeBucket) {
44
+ return sendApiError(res, new Error('No bucket selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.'));
45
+ }
46
+ next();
47
+ });
48
+
8
49
  // This is the maximum size we allow per chunk upload. It should be >=
9
50
  // the client chunk size (5MB) and leave room for any encoding overhead.
10
51
  // If you need to support larger chunks, increase this accordingly.
@@ -35,6 +76,7 @@ const uploadChunk = (req, res, next) => {
35
76
  router.post('/api/cleanup-uploads', validateSessionAndRole('SuperAdmin'), async (req, res) => {
36
77
  try {
37
78
  const { olderThanDays = 7, prefix = '' } = req.body;
79
+ const bucketName = req.activeBucket;
38
80
 
39
81
  // Validate olderThanDays
40
82
  const days = parseInt(olderThanDays, 10);
@@ -43,7 +85,7 @@ router.post('/api/cleanup-uploads', validateSessionAndRole('SuperAdmin'), async
43
85
  }
44
86
 
45
87
  console.log(`[mbkbucket] Starting cleanup of incomplete uploads older than ${days} days`);
46
- const result = await cleanupIncompleteMultipartUploads(days, prefix);
88
+ const result = await cleanupIncompleteMultipartUploads(days, prefix, bucketName);
47
89
 
48
90
  res.json({
49
91
  success: true,
@@ -52,10 +94,7 @@ router.post('/api/cleanup-uploads', validateSessionAndRole('SuperAdmin'), async
52
94
  });
53
95
  } catch (error) {
54
96
  console.error('[mbkbucket] Cleanup failed:', error);
55
- res.status(500).json({
56
- success: false,
57
- error: error.message
58
- });
97
+ return sendApiError(res, error, 'Cleanup failed');
59
98
  }
60
99
  });
61
100
 
@@ -63,7 +102,8 @@ router.post('/api/cleanup-uploads', validateSessionAndRole('SuperAdmin'), async
63
102
  router.get('/api/incomplete-uploads', validateSessionAndRole('SuperAdmin'), async (req, res) => {
64
103
  try {
65
104
  const { prefix = '' } = req.query;
66
- const uploads = await listIncompleteMultipartUploads(prefix);
105
+ const bucketName = req.activeBucket;
106
+ const uploads = await listIncompleteMultipartUploads(prefix, bucketName);
67
107
 
68
108
  res.json({
69
109
  success: true,
@@ -77,10 +117,7 @@ router.get('/api/incomplete-uploads', validateSessionAndRole('SuperAdmin'), asyn
77
117
  });
78
118
  } catch (error) {
79
119
  console.error('[mbkbucket] Failed to list incomplete uploads:', error);
80
- res.status(500).json({
81
- success: false,
82
- error: error.message
83
- });
120
+ return sendApiError(res, error, 'Failed to list incomplete uploads');
84
121
  }
85
122
  });
86
123
 
@@ -88,6 +125,7 @@ router.get('/api/incomplete-uploads', validateSessionAndRole('SuperAdmin'), asyn
88
125
  router.get('/api/files', validateSessionAndRole('SuperAdmin'), async (req, res) => {
89
126
  const { prefix = '', page = '1', search = '', recursive = 'true', token = '' } = req.query;
90
127
  const pageSize = 100;
128
+ const bucketName = req.activeBucket;
91
129
  let effectivePrefix;
92
130
  try {
93
131
  effectivePrefix = ensurePrefix(prefix);
@@ -124,7 +162,8 @@ router.get('/api/files', validateSessionAndRole('SuperAdmin'), async (req, res)
124
162
  const result = await listfiles(listPrefix, {
125
163
  continuationToken: token || undefined, // Use token from query if provided
126
164
  delimiter: '/',
127
- maxKeys: 1000 // Fetch a healthy batch.
165
+ maxKeys: 1000, // Fetch a healthy batch.
166
+ bucketName
128
167
  });
129
168
 
130
169
  files = result.Contents || [];
@@ -141,7 +180,7 @@ router.get('/api/files', validateSessionAndRole('SuperAdmin'), async (req, res)
141
180
 
142
181
  // Fetch all files recursively (handling S3 pagination)
143
182
  do {
144
- const result = await listfiles(effectivePrefix, { continuationToken });
183
+ const result = await listfiles(effectivePrefix, { continuationToken, bucketName });
145
184
  if (result.Contents) {
146
185
  // Filter out folder marker objects (zero-byte files ending with /)
147
186
  const actualFiles = result.Contents.filter(f => !f.Key.endsWith('/'));
@@ -153,7 +192,7 @@ router.get('/api/files', validateSessionAndRole('SuperAdmin'), async (req, res)
153
192
  // If no files found under the app prefixed path, try root as a legacy fallback
154
193
  if (!files.length && !search) {
155
194
  try {
156
- const rootResult = await listfiles('');
195
+ const rootResult = await listfiles('', { bucketName });
157
196
  if ((rootResult.Contents || []).length) {
158
197
  files = rootResult.Contents || [];
159
198
  usedPrefix = '';
@@ -218,9 +257,11 @@ router.get('/api/files', validateSessionAndRole('SuperAdmin'), async (req, res)
218
257
 
219
258
  } catch (error) {
220
259
  console.error("Error listing files:", error);
221
- res.status(500).json({
260
+ const mapped = classifyApiError(error, 'Error listing files');
261
+ return res.status(mapped.status).json({
222
262
  success: false,
223
- error: "Error listing files: " + error.message,
263
+ code: mapped.code,
264
+ error: mapped.message,
224
265
  files: [],
225
266
  totalFiles: 0
226
267
  });
@@ -230,6 +271,7 @@ router.get('/api/files', validateSessionAndRole('SuperAdmin'), async (req, res)
230
271
  // Standard single-file upload
231
272
  router.post('/upload', validateSessionAndRole('SuperAdmin'), upload.single('file'), async (req, res) => {
232
273
  try {
274
+ const bucketName = req.activeBucket;
233
275
  if (!req.file) {
234
276
  return res.status(400).json({ success: false, error: 'No file selected' });
235
277
  }
@@ -250,6 +292,7 @@ router.post('/upload', validateSessionAndRole('SuperAdmin'), upload.single('file
250
292
  'upload-source': 'web-portal',
251
293
  'user-agent': req.headers['user-agent'] || 'unknown'
252
294
  },
295
+ bucketName,
253
296
  preventOverwrite: true // Use atomic operation to prevent race conditions
254
297
  };
255
298
 
@@ -262,12 +305,12 @@ router.post('/upload', validateSessionAndRole('SuperAdmin'), upload.single('file
262
305
  if (folderPath) {
263
306
  const folderMarkerKey = folderPath; // Already ends with /
264
307
  try {
265
- const markerExists = await fileExists(folderMarkerKey);
308
+ const markerExists = await fileExists(folderMarkerKey, bucketName);
266
309
  if (markerExists) {
267
310
  // Check if this is actually a folder marker (zero-byte file)
268
- const metadata = await getFileMetadata(folderMarkerKey);
311
+ const metadata = await getFileMetadata(folderMarkerKey, bucketName);
269
312
  if (metadata.ContentLength === 0 || metadata.Metadata?.marker === 'true') {
270
- await deleteFile(folderMarkerKey);
313
+ await deleteFile(folderMarkerKey, bucketName);
271
314
  console.log(`[mbkbucket] Cleaned up folder marker: ${folderMarkerKey}`);
272
315
  }
273
316
  }
@@ -287,7 +330,7 @@ router.post('/upload', validateSessionAndRole('SuperAdmin'), upload.single('file
287
330
  }
288
331
  } catch (error) {
289
332
  console.error("Error uploading file:", error);
290
- res.status(500).json({ success: false, error: `Upload failed: ${error.message}` });
333
+ return sendApiError(res, error, 'Upload failed');
291
334
  }
292
335
  });
293
336
 
@@ -296,6 +339,7 @@ router.post('/upload', validateSessionAndRole('SuperAdmin'), upload.single('file
296
339
  // Returns: { uploadId, key }
297
340
  router.post('/upload-init', validateSessionAndRole('SuperAdmin'), async (req, res) => {
298
341
  try {
342
+ const bucketName = req.activeBucket;
299
343
  const { fileName, prefix = '', contentType = 'application/octet-stream' } = req.body;
300
344
  if (!fileName) return res.status(400).json({ success: false, error: 'fileName is required' });
301
345
 
@@ -306,14 +350,14 @@ router.post('/upload-init', validateSessionAndRole('SuperAdmin'), async (req, re
306
350
  // Check if file exists before initiating multipart upload
307
351
  // Note: This is still slightly racy, but multipart uploads take time so collision risk is lower
308
352
  // A complete solution would require server-side locking or unique upload IDs
309
- const exists = await fileExists(key);
353
+ const exists = await fileExists(key, bucketName);
310
354
  if (exists) return res.status(409).json({ success: false, error: `File already exists: ${fileName}` });
311
355
 
312
- const result = await createMultipartUpload(key, contentType);
356
+ const result = await createMultipartUpload(key, contentType, {}, bucketName);
313
357
  return res.json({ success: true, uploadId: result.uploadId, key: result.key });
314
358
  } catch (err) {
315
359
  console.error('[mbkbucket] upload-init failed:', err);
316
- return res.status(500).json({ success: false, error: err.message });
360
+ return sendApiError(res, err, 'upload-init failed');
317
361
  }
318
362
  });
319
363
 
@@ -322,6 +366,7 @@ router.post('/upload-init', validateSessionAndRole('SuperAdmin'), async (req, re
322
366
  // Returns: { partNumber, ETag }
323
367
  router.post('/upload-chunk', validateSessionAndRole('SuperAdmin'), uploadChunk, async (req, res) => {
324
368
  try {
369
+ const bucketName = req.activeBucket;
325
370
  const { uploadId, key, partNumber } = req.body;
326
371
  if (!req.file || !uploadId || !key || !partNumber) {
327
372
  return res.status(400).json({ success: false, error: 'chunk file, uploadId, key, and partNumber are required' });
@@ -331,11 +376,11 @@ router.post('/upload-chunk', validateSessionAndRole('SuperAdmin'), uploadChunk,
331
376
  return res.status(400).json({ success: false, error: 'partNumber must be an integer between 1 and 10000' });
332
377
  }
333
378
 
334
- const result = await uploadPart(key, uploadId, part, req.file.buffer);
379
+ const result = await uploadPart(key, uploadId, part, req.file.buffer, bucketName);
335
380
  return res.json({ success: true, partNumber: result.partNumber, ETag: result.ETag });
336
381
  } catch (err) {
337
382
  console.error('[mbkbucket] upload-chunk failed:', err);
338
- return res.status(500).json({ success: false, error: err.message });
383
+ return sendApiError(res, err, 'upload-chunk failed');
339
384
  }
340
385
  });
341
386
 
@@ -344,23 +389,24 @@ router.post('/upload-chunk', validateSessionAndRole('SuperAdmin'), uploadChunk,
344
389
  // Returns: { key }
345
390
  router.post('/upload-complete', validateSessionAndRole('SuperAdmin'), async (req, res) => {
346
391
  try {
392
+ const bucketName = req.activeBucket;
347
393
  const { uploadId, key, parts } = req.body;
348
394
  if (!uploadId || !key || !Array.isArray(parts) || !parts.length) {
349
395
  return res.status(400).json({ success: false, error: 'uploadId, key, and parts array are required' });
350
396
  }
351
397
 
352
- const result = await completeMultipartUpload(key, uploadId, parts);
398
+ const result = await completeMultipartUpload(key, uploadId, parts, bucketName);
353
399
 
354
400
  // Clean up folder marker if it exists in the parent directory
355
401
  const folderPath = result.key.substring(0, result.key.lastIndexOf('/') + 1);
356
402
  if (folderPath) {
357
403
  const folderMarkerKey = folderPath;
358
404
  try {
359
- const markerExists = await fileExists(folderMarkerKey);
405
+ const markerExists = await fileExists(folderMarkerKey, bucketName);
360
406
  if (markerExists) {
361
- const metadata = await getFileMetadata(folderMarkerKey);
407
+ const metadata = await getFileMetadata(folderMarkerKey, bucketName);
362
408
  if (metadata.ContentLength === 0 || metadata.Metadata?.marker === 'true') {
363
- await deleteFile(folderMarkerKey);
409
+ await deleteFile(folderMarkerKey, bucketName);
364
410
  console.log(`[mbkbucket] Cleaned up folder marker: ${folderMarkerKey}`);
365
411
  }
366
412
  }
@@ -372,7 +418,7 @@ router.post('/upload-complete', validateSessionAndRole('SuperAdmin'), async (req
372
418
  return res.json({ success: true, message: 'File uploaded successfully', key: result.key });
373
419
  } catch (err) {
374
420
  console.error('[mbkbucket] upload-complete failed:', err);
375
- return res.status(500).json({ success: false, error: err.message });
421
+ return sendApiError(res, err, 'upload-complete failed');
376
422
  }
377
423
  });
378
424
 
@@ -380,20 +426,22 @@ router.post('/upload-complete', validateSessionAndRole('SuperAdmin'), async (req
380
426
  // Body: { uploadId, key }
381
427
  router.post('/upload-abort', validateSessionAndRole('SuperAdmin'), async (req, res) => {
382
428
  try {
429
+ const bucketName = req.activeBucket;
383
430
  const { uploadId, key } = req.body;
384
431
  if (!uploadId || !key) return res.status(400).json({ success: false, error: 'uploadId and key are required' });
385
432
 
386
- await abortMultipartUpload(key, uploadId);
433
+ await abortMultipartUpload(key, uploadId, bucketName);
387
434
  return res.json({ success: true, message: 'Multipart upload aborted' });
388
435
  } catch (err) {
389
436
  console.error('[mbkbucket] upload-abort failed:', err);
390
- return res.status(500).json({ success: false, error: err.message });
437
+ return sendApiError(res, err, 'upload-abort failed');
391
438
  }
392
439
  });
393
440
 
394
441
  // Create folder (zero-byte object with trailing slash)
395
442
  router.post('/create-folder', validateSessionAndRole('SuperAdmin'), async (req, res) => {
396
443
  try {
444
+ const bucketName = req.activeBucket;
397
445
  const { prefix = '', folderName } = req.body;
398
446
  if (!folderName) return res.status(400).json({ success: false, error: 'folderName is required' });
399
447
 
@@ -403,11 +451,11 @@ router.post('/create-folder', validateSessionAndRole('SuperAdmin'), async (req,
403
451
  const folderKey = `${effectivePrefix.replace(/\/+$/, '')}/${folderName}/`;
404
452
 
405
453
  // Check if folder marker already exists
406
- const markerExists = await fileExists(folderKey);
454
+ const markerExists = await fileExists(folderKey, bucketName);
407
455
  if (markerExists) return res.status(409).json({ success: false, error: 'Folder already exists' });
408
456
 
409
457
  // Check if any files already exist under this prefix (folder already has content)
410
- const existingFiles = await listfiles(folderKey, { maxKeys: 1 });
458
+ const existingFiles = await listfiles(folderKey, { maxKeys: 1, bucketName });
411
459
  const hasContent = existingFiles.Contents && existingFiles.Contents.length > 0;
412
460
 
413
461
  if (hasContent) {
@@ -422,23 +470,25 @@ router.post('/create-folder', validateSessionAndRole('SuperAdmin'), async (req,
422
470
 
423
471
  // Create zero-byte object to represent empty folder
424
472
  await uploadFile(folderKey, Buffer.alloc(0), 'application/x-empty', {
425
- metadata: { 'folder': 'true', 'marker': 'true' }
473
+ metadata: { 'folder': 'true', 'marker': 'true' },
474
+ bucketName
426
475
  });
427
476
 
428
477
  return res.json({ success: true, message: 'Folder created', key: folderKey });
429
478
  } catch (err) {
430
479
  console.error('Create folder failed:', err);
431
- return res.status(500).json({ success: false, error: err.message });
480
+ return sendApiError(res, err, 'Create folder failed');
432
481
  }
433
482
  });
434
483
 
435
484
  router.post('/delete', validateSessionAndRole('SuperAdmin'), async (req, res) => {
436
485
  try {
486
+ const bucketName = req.activeBucket;
437
487
  const { key, keys, folder } = req.body;
438
488
 
439
489
  if (keys && Array.isArray(keys) && keys.length) {
440
490
  // Bulk delete
441
- await deleteFiles(keys);
491
+ await deleteFiles(keys, bucketName);
442
492
  return res.json({ success: true, message: 'Files deleted successfully' });
443
493
  }
444
494
 
@@ -449,21 +499,22 @@ router.post('/delete', validateSessionAndRole('SuperAdmin'), async (req, res) =>
449
499
 
450
500
  // If caller indicates it's a folder (or key ends with '/'), delete all objects under the prefix
451
501
  if (folder === true || String(keyToDelete).endsWith('/')) {
452
- await deleteFolder(keyToDelete);
502
+ await deleteFolder(keyToDelete, bucketName);
453
503
  return res.json({ success: true, message: 'Folder deleted successfully' });
454
504
  }
455
505
 
456
- await deleteFile(keyToDelete);
506
+ await deleteFile(keyToDelete, bucketName);
457
507
  res.json({ success: true, message: 'File deleted successfully' });
458
508
  } catch (error) {
459
509
  console.error("Error deleting file:", error);
460
- res.status(500).json({ success: false, error: `Delete failed: ${error.message}` });
510
+ return sendApiError(res, error, 'Delete failed');
461
511
  }
462
512
  });
463
513
 
464
514
  router.get('/download/:key(*)', validateSessionAndRole('SuperAdmin'), async (req, res) => {
465
515
  let stream = null;
466
516
  try {
517
+ const bucketName = req.activeBucket;
467
518
  const key = req.params.key;
468
519
  let keyToUse;
469
520
  try {
@@ -471,19 +522,37 @@ router.get('/download/:key(*)', validateSessionAndRole('SuperAdmin'), async (req
471
522
  } catch (e) {
472
523
  return res.status(400).json({ success: false, error: e.message });
473
524
  }
474
- const result = await downloadFile(keyToUse);
525
+ const result = await downloadFile(keyToUse, { bucketName });
475
526
  stream = result.Body;
476
527
 
477
528
  res.setHeader('Content-Disposition', `attachment; filename="${keyToUse.split('/').pop()}"`);
478
- res.setHeader('Content-Type', result.ContentType || 'application/octet-stream');
529
+ const contentType = result.ContentType || 'application/octet-stream';
530
+ res.setHeader('Content-Type', contentType);
479
531
 
480
532
  // Set Content-Length header for proper download progress indication
481
533
  if (result.ContentLength) {
482
534
  res.setHeader('Content-Length', result.ContentLength);
483
535
  }
484
536
 
485
- // Add cache headers for downloads
486
- res.setHeader('Cache-Control', 'private, max-age=3600');
537
+ if (result.ETag) res.setHeader('ETag', result.ETag);
538
+ if (result.LastModified) res.setHeader('Last-Modified', result.LastModified.toUTCString());
539
+
540
+ const typeLower = String(contentType).toLowerCase();
541
+ const isSensitiveText = typeLower.startsWith('text/')
542
+ || typeLower.includes('json')
543
+ || typeLower.includes('xml')
544
+ || typeLower.includes('javascript')
545
+ || typeLower.includes('typescript');
546
+ const isLargeFile = Number(result.ContentLength || 0) > 50 * 1024 * 1024;
547
+
548
+ // Smarter cache policy for authenticated downloads.
549
+ if (isSensitiveText) {
550
+ res.setHeader('Cache-Control', 'private, no-store');
551
+ } else if (isLargeFile) {
552
+ res.setHeader('Cache-Control', 'private, max-age=600, must-revalidate');
553
+ } else {
554
+ res.setHeader('Cache-Control', 'private, max-age=1800, must-revalidate');
555
+ }
487
556
 
488
557
  // Proper stream error handling
489
558
  stream.on('error', (err) => {
@@ -3,13 +3,82 @@ import infoRoutes from "./info.js";
3
3
  import viewRoutes from "./view.js";
4
4
  import apiRoutes from "./api.js";
5
5
  import { validateSessionAndRole, renderPage, mbkautheVar } from "mbkauthe";
6
+ import { resolveBucketName, getAvailableBucketNames } from "../s3.js";
6
7
 
7
8
  const router = express.Router();
8
9
 
10
+ function renderBucketDashboard(req, res) {
11
+ const allBuckets = getAvailableBucketNames();
12
+ const configuredDefaultBucket = mbkautheVar?.bucket;
13
+
14
+ let bucketfilePath;
15
+
16
+ if(mbkautheVar?.APP_NAME === "portal") {
17
+ bucketfilePath ="bucketportal.handlebars";
18
+ } else {
19
+ bucketfilePath = "bucket.handlebars";
20
+ }
21
+
22
+
23
+ let selectedBucket;
24
+
25
+ try {
26
+ selectedBucket = resolveBucketName(req.query.bucket);
27
+ } catch (err) {
28
+ const fallbackSelected = mbkautheVar?.bucket;
29
+ const bucketOptions = allBuckets.map((name) => ({
30
+ name,
31
+ isSelected: name === fallbackSelected,
32
+ isDefault: name === configuredDefaultBucket
33
+ }));
34
+ return renderPage(req, res, bucketfilePath, mbkautheVar?.APP_NAME === "portal" ? true : false, {
35
+ page: "Admin Bucket",
36
+ layout: true,
37
+ bucketvar: mbkautheVar?.bucket,
38
+ selectedBucket: fallbackSelected,
39
+ bucketOptions,
40
+ APP_NAME: mbkautheVar?.APP_NAME,
41
+ message: req.query.message,
42
+ error: req.query.error || err.message,
43
+ availableBuckets: allBuckets
44
+ });
45
+ }
46
+
47
+ const bucketOptions = allBuckets.map((name) => ({
48
+ name,
49
+ isSelected: name === selectedBucket,
50
+ isDefault: name === configuredDefaultBucket
51
+ }));
52
+
53
+ renderPage(req, res, bucketfilePath, mbkautheVar?.APP_NAME === "portal" ? true : false, {
54
+ page: "Admin Bucket",
55
+ bucketvar: selectedBucket,
56
+ selectedBucket,
57
+ bucketOptions,
58
+ APP_NAME: mbkautheVar?.APP_NAME,
59
+ message: req.query.message,
60
+ error: req.query.error,
61
+ availableBuckets: allBuckets
62
+ });
63
+ }
64
+
9
65
  router.get('/mbkbucket', validateSessionAndRole('SuperAdmin'), async (req, res) => {
10
- renderPage(req, res, "bucket.handlebars", false, { page: "Admin Bucket", bucketvar: mbkautheVar?.bucket, APP_NAME: mbkautheVar?.APP_NAME, message: req.query.message, error: req.query.error });
66
+ return renderBucketDashboard(req, res);
11
67
  });
12
68
 
69
+ // Central bucket resolution for all /mbkbucket subroutes.
70
+ router.use('/mbkbucket', (req, _res, next) => {
71
+ try {
72
+ req.activeBucket = resolveBucketName(req.query.bucket);
73
+ req.bucketResolveError = null;
74
+ } catch (err) {
75
+ req.activeBucket = null;
76
+ req.bucketResolveError = err;
77
+ }
78
+ next();
79
+ });
80
+
81
+
13
82
  router.use("/mbkbucket", infoRoutes);
14
83
  router.use("/mbkbucket", viewRoutes);
15
84
  router.use("/mbkbucket", apiRoutes);