mbkbucket 1.5.1 → 2.1.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.
package/lib/routes/api.js CHANGED
@@ -2,12 +2,60 @@ import express from "express";
2
2
  import { validateSessionAndRole, } from "mbkauthe";
3
3
  import { listfiles, uploadFile, deleteFile, deleteFiles, deleteFolder, downloadFile, getFileMetadata, fileExists, ensureKeyHasAppPrefix, ensurePrefix, getAppName, createMultipartUpload, uploadPart, completeMultipartUpload, abortMultipartUpload, listIncompleteMultipartUploads, cleanupIncompleteMultipartUploads } from "../s3.js";
4
4
  import multer from "multer";
5
+ import { createLogger } from "../debug.js";
5
6
 
6
7
  const router = express.Router();
8
+ const debugApi = createLogger('api');
7
9
 
8
- // This is the maximum size we allow per chunk upload. It should be >=
9
- // the client chunk size (5MB) and leave room for any encoding overhead.
10
- // If you need to support larger chunks, increase this accordingly.
10
+ function classifyApiError(error, fallbackMessage = 'Request failed') {
11
+ const message = String(error?.message || fallbackMessage);
12
+ const lower = message.toLowerCase();
13
+
14
+ if (lower.includes('bucketconnection environment variable is not set')) {
15
+ return { status: 503, code: 'BUCKET_CONFIG_ERROR', message };
16
+ }
17
+ if (lower.includes('no bucket selected') || lower.includes('not found in bucketconnection')) {
18
+ return { status: 400, code: 'INVALID_BUCKET', message };
19
+ }
20
+ if (lower.includes('already exists')) {
21
+ return { status: 409, code: 'CONFLICT', message };
22
+ }
23
+ if (lower.includes('not found')) {
24
+ return { status: 404, code: 'NOT_FOUND', message };
25
+ }
26
+ if (lower.includes('access denied')) {
27
+ return { status: 403, code: 'ACCESS_DENIED', message };
28
+ }
29
+ if (lower.includes('required') || lower.includes('invalid') || lower.includes('must be')) {
30
+ return { status: 400, code: 'VALIDATION_ERROR', message };
31
+ }
32
+ return { status: 500, code: 'INTERNAL_ERROR', message };
33
+ }
34
+
35
+ function sendApiError(res, error, fallbackMessage = 'Request failed') {
36
+ const mapped = classifyApiError(error, fallbackMessage);
37
+ return res.status(mapped.status).json({ success: false, code: mapped.code, error: mapped.message });
38
+ }
39
+
40
+ function getBaseName(path = '') {
41
+ const value = String(path || '');
42
+ const lastSlashIndex = value.lastIndexOf('/');
43
+ return lastSlashIndex === -1 ? value : value.substring(lastSlashIndex + 1);
44
+ }
45
+
46
+ // Central bucket guard for all API routes under /mbkbucket
47
+ router.use((req, res, next) => {
48
+ if (req.bucketResolveError) {
49
+ return sendApiError(res, req.bucketResolveError, 'Invalid bucket selection');
50
+ }
51
+ if (!req.activeBucket) {
52
+ return sendApiError(res, new Error('No bucket selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.'));
53
+ }
54
+ next();
55
+ });
56
+
57
+ // This is the maximum size we allow per file part. The dashboard uses the same
58
+ // size as its single-upload threshold and multipart chunk size.
11
59
  const MAX_CHUNK_SIZE = 50 * 1024 * 1024; // 50 MB
12
60
 
13
61
  const upload = multer({
@@ -31,10 +79,25 @@ const uploadChunk = (req, res, next) => {
31
79
  });
32
80
  };
33
81
 
82
+ async function cleanupFolderMarker(folderPath, bucketName) {
83
+ if (!folderPath) return;
84
+
85
+ try {
86
+ const metadata = await getFileMetadata(folderPath, bucketName);
87
+ if (metadata.exists && (metadata.ContentLength === 0 || metadata.Metadata?.marker === 'true')) {
88
+ await deleteFile(folderPath, bucketName);
89
+ debugApi('Cleaned up folder marker: %s', folderPath);
90
+ }
91
+ } catch (cleanupErr) {
92
+ debugApi('Failed to clean up folder marker: %s', cleanupErr.message);
93
+ }
94
+ }
95
+
34
96
  // API endpoint for multipart upload cleanup
35
97
  router.post('/api/cleanup-uploads', validateSessionAndRole('SuperAdmin'), async (req, res) => {
36
98
  try {
37
99
  const { olderThanDays = 7, prefix = '' } = req.body;
100
+ const bucketName = req.activeBucket;
38
101
 
39
102
  // Validate olderThanDays
40
103
  const days = parseInt(olderThanDays, 10);
@@ -42,8 +105,8 @@ router.post('/api/cleanup-uploads', validateSessionAndRole('SuperAdmin'), async
42
105
  return res.status(400).json({ success: false, error: 'olderThanDays must be a positive integer' });
43
106
  }
44
107
 
45
- console.log(`[mbkbucket] Starting cleanup of incomplete uploads older than ${days} days`);
46
- const result = await cleanupIncompleteMultipartUploads(days, prefix);
108
+ debugApi('Starting cleanup of incomplete uploads older than %s days', days);
109
+ const result = await cleanupIncompleteMultipartUploads(days, prefix, bucketName);
47
110
 
48
111
  res.json({
49
112
  success: true,
@@ -52,10 +115,7 @@ router.post('/api/cleanup-uploads', validateSessionAndRole('SuperAdmin'), async
52
115
  });
53
116
  } catch (error) {
54
117
  console.error('[mbkbucket] Cleanup failed:', error);
55
- res.status(500).json({
56
- success: false,
57
- error: error.message
58
- });
118
+ return sendApiError(res, error, 'Cleanup failed');
59
119
  }
60
120
  });
61
121
 
@@ -63,7 +123,8 @@ router.post('/api/cleanup-uploads', validateSessionAndRole('SuperAdmin'), async
63
123
  router.get('/api/incomplete-uploads', validateSessionAndRole('SuperAdmin'), async (req, res) => {
64
124
  try {
65
125
  const { prefix = '' } = req.query;
66
- const uploads = await listIncompleteMultipartUploads(prefix);
126
+ const bucketName = req.activeBucket;
127
+ const uploads = await listIncompleteMultipartUploads(prefix, bucketName);
67
128
 
68
129
  res.json({
69
130
  success: true,
@@ -77,10 +138,7 @@ router.get('/api/incomplete-uploads', validateSessionAndRole('SuperAdmin'), asyn
77
138
  });
78
139
  } catch (error) {
79
140
  console.error('[mbkbucket] Failed to list incomplete uploads:', error);
80
- res.status(500).json({
81
- success: false,
82
- error: error.message
83
- });
141
+ return sendApiError(res, error, 'Failed to list incomplete uploads');
84
142
  }
85
143
  });
86
144
 
@@ -88,6 +146,7 @@ router.get('/api/incomplete-uploads', validateSessionAndRole('SuperAdmin'), asyn
88
146
  router.get('/api/files', validateSessionAndRole('SuperAdmin'), async (req, res) => {
89
147
  const { prefix = '', page = '1', search = '', recursive = 'true', token = '' } = req.query;
90
148
  const pageSize = 100;
149
+ const bucketName = req.activeBucket;
91
150
  let effectivePrefix;
92
151
  try {
93
152
  effectivePrefix = ensurePrefix(prefix);
@@ -124,14 +183,16 @@ router.get('/api/files', validateSessionAndRole('SuperAdmin'), async (req, res)
124
183
  const result = await listfiles(listPrefix, {
125
184
  continuationToken: token || undefined, // Use token from query if provided
126
185
  delimiter: '/',
127
- maxKeys: 1000 // Fetch a healthy batch.
186
+ maxKeys: 1000, // Fetch a healthy batch.
187
+ bucketName
128
188
  });
129
189
 
130
- files = result.Contents || [];
131
190
  // Filter out folder marker objects (zero-byte files ending with /)
132
- files = files.filter(f => !f.Key.endsWith('/'));
191
+ files = (result.Contents || []).filter(f => !f.Key.endsWith('/'));
133
192
  // S3 returns CommonPrefixes for folders when delimiter is used
134
- folders = (result.CommonPrefixes || []).map(p => p.Prefix).filter(p => p !== effectivePrefix);
193
+ folders = (result.CommonPrefixes || [])
194
+ .filter(p => p.Prefix !== effectivePrefix)
195
+ .map(p => p.Prefix);
135
196
  nextContinuationToken = result.NextContinuationToken;
136
197
 
137
198
  // Note: In optimized mode, totalFiles is unknown without scanning everything.
@@ -141,11 +202,12 @@ router.get('/api/files', validateSessionAndRole('SuperAdmin'), async (req, res)
141
202
 
142
203
  // Fetch all files recursively (handling S3 pagination)
143
204
  do {
144
- const result = await listfiles(effectivePrefix, { continuationToken });
145
- if (result.Contents) {
205
+ const result = await listfiles(effectivePrefix, { continuationToken, bucketName });
206
+ if (result.Contents?.length) {
146
207
  // Filter out folder marker objects (zero-byte files ending with /)
147
- const actualFiles = result.Contents.filter(f => !f.Key.endsWith('/'));
148
- files = files.concat(actualFiles);
208
+ for (const file of result.Contents) {
209
+ if (!file.Key.endsWith('/')) files.push(file);
210
+ }
149
211
  }
150
212
  continuationToken = result.nextToken;
151
213
  } while (continuationToken);
@@ -153,23 +215,21 @@ router.get('/api/files', validateSessionAndRole('SuperAdmin'), async (req, res)
153
215
  // If no files found under the app prefixed path, try root as a legacy fallback
154
216
  if (!files.length && !search) {
155
217
  try {
156
- const rootResult = await listfiles('');
218
+ const rootResult = await listfiles('', { bucketName });
157
219
  if ((rootResult.Contents || []).length) {
158
220
  files = rootResult.Contents || [];
159
221
  usedPrefix = '';
160
222
  legacyFallback = true;
161
223
  }
162
224
  } catch (fallbackErr) {
163
- console.warn('Fallback root listing failed:', fallbackErr.message);
225
+ debugApi('Fallback root listing failed: %s', fallbackErr.message);
164
226
  }
165
227
  }
166
228
 
167
229
  // Apply search filter if provided
168
230
  if (isSearch) {
169
- const searchLower = search.toLowerCase();
170
- files = files.filter(file => {
171
- return file.Key.toLowerCase().includes(searchLower);
172
- });
231
+ const searchLower = search.trim().toLowerCase();
232
+ files = files.filter(file => file.Key.toLowerCase().includes(searchLower));
173
233
  }
174
234
  }
175
235
 
@@ -218,9 +278,11 @@ router.get('/api/files', validateSessionAndRole('SuperAdmin'), async (req, res)
218
278
 
219
279
  } catch (error) {
220
280
  console.error("Error listing files:", error);
221
- res.status(500).json({
281
+ const mapped = classifyApiError(error, 'Error listing files');
282
+ return res.status(mapped.status).json({
222
283
  success: false,
223
- error: "Error listing files: " + error.message,
284
+ code: mapped.code,
285
+ error: mapped.message,
224
286
  files: [],
225
287
  totalFiles: 0
226
288
  });
@@ -230,6 +292,7 @@ router.get('/api/files', validateSessionAndRole('SuperAdmin'), async (req, res)
230
292
  // Standard single-file upload
231
293
  router.post('/upload', validateSessionAndRole('SuperAdmin'), upload.single('file'), async (req, res) => {
232
294
  try {
295
+ const bucketName = req.activeBucket;
233
296
  if (!req.file) {
234
297
  return res.status(400).json({ success: false, error: 'No file selected' });
235
298
  }
@@ -250,32 +313,16 @@ router.post('/upload', validateSessionAndRole('SuperAdmin'), upload.single('file
250
313
  'upload-source': 'web-portal',
251
314
  'user-agent': req.headers['user-agent'] || 'unknown'
252
315
  },
316
+ bucketName,
253
317
  preventOverwrite: true // Use atomic operation to prevent race conditions
254
318
  };
255
319
 
256
320
  try {
257
321
  await uploadFile(key, req.file.buffer, req.file.mimetype, uploadOptions);
258
322
 
259
- // Clean up folder marker if it exists in the parent directory
260
- // This happens when a file is uploaded to a folder that was previously empty
323
+ // Clean up folder marker if it exists in the parent directory.
261
324
  const folderPath = key.substring(0, key.lastIndexOf('/') + 1);
262
- if (folderPath) {
263
- const folderMarkerKey = folderPath; // Already ends with /
264
- try {
265
- const markerExists = await fileExists(folderMarkerKey);
266
- if (markerExists) {
267
- // Check if this is actually a folder marker (zero-byte file)
268
- const metadata = await getFileMetadata(folderMarkerKey);
269
- if (metadata.ContentLength === 0 || metadata.Metadata?.marker === 'true') {
270
- await deleteFile(folderMarkerKey);
271
- console.log(`[mbkbucket] Cleaned up folder marker: ${folderMarkerKey}`);
272
- }
273
- }
274
- } catch (cleanupErr) {
275
- // Non-critical error, just log it
276
- console.warn('[mbkbucket] Failed to clean up folder marker:', cleanupErr.message);
277
- }
278
- }
325
+ await cleanupFolderMarker(folderPath, bucketName);
279
326
 
280
327
  res.json({ success: true, message: 'File uploaded successfully', key: key });
281
328
  } catch (uploadError) {
@@ -287,7 +334,7 @@ router.post('/upload', validateSessionAndRole('SuperAdmin'), upload.single('file
287
334
  }
288
335
  } catch (error) {
289
336
  console.error("Error uploading file:", error);
290
- res.status(500).json({ success: false, error: `Upload failed: ${error.message}` });
337
+ return sendApiError(res, error, 'Upload failed');
291
338
  }
292
339
  });
293
340
 
@@ -296,6 +343,7 @@ router.post('/upload', validateSessionAndRole('SuperAdmin'), upload.single('file
296
343
  // Returns: { uploadId, key }
297
344
  router.post('/upload-init', validateSessionAndRole('SuperAdmin'), async (req, res) => {
298
345
  try {
346
+ const bucketName = req.activeBucket;
299
347
  const { fileName, prefix = '', contentType = 'application/octet-stream' } = req.body;
300
348
  if (!fileName) return res.status(400).json({ success: false, error: 'fileName is required' });
301
349
 
@@ -306,14 +354,14 @@ router.post('/upload-init', validateSessionAndRole('SuperAdmin'), async (req, re
306
354
  // Check if file exists before initiating multipart upload
307
355
  // Note: This is still slightly racy, but multipart uploads take time so collision risk is lower
308
356
  // A complete solution would require server-side locking or unique upload IDs
309
- const exists = await fileExists(key);
357
+ const exists = await fileExists(key, bucketName);
310
358
  if (exists) return res.status(409).json({ success: false, error: `File already exists: ${fileName}` });
311
359
 
312
- const result = await createMultipartUpload(key, contentType);
360
+ const result = await createMultipartUpload(key, contentType, {}, bucketName);
313
361
  return res.json({ success: true, uploadId: result.uploadId, key: result.key });
314
362
  } catch (err) {
315
363
  console.error('[mbkbucket] upload-init failed:', err);
316
- return res.status(500).json({ success: false, error: err.message });
364
+ return sendApiError(res, err, 'upload-init failed');
317
365
  }
318
366
  });
319
367
 
@@ -322,6 +370,7 @@ router.post('/upload-init', validateSessionAndRole('SuperAdmin'), async (req, re
322
370
  // Returns: { partNumber, ETag }
323
371
  router.post('/upload-chunk', validateSessionAndRole('SuperAdmin'), uploadChunk, async (req, res) => {
324
372
  try {
373
+ const bucketName = req.activeBucket;
325
374
  const { uploadId, key, partNumber } = req.body;
326
375
  if (!req.file || !uploadId || !key || !partNumber) {
327
376
  return res.status(400).json({ success: false, error: 'chunk file, uploadId, key, and partNumber are required' });
@@ -331,11 +380,11 @@ router.post('/upload-chunk', validateSessionAndRole('SuperAdmin'), uploadChunk,
331
380
  return res.status(400).json({ success: false, error: 'partNumber must be an integer between 1 and 10000' });
332
381
  }
333
382
 
334
- const result = await uploadPart(key, uploadId, part, req.file.buffer);
383
+ const result = await uploadPart(key, uploadId, part, req.file.buffer, bucketName);
335
384
  return res.json({ success: true, partNumber: result.partNumber, ETag: result.ETag });
336
385
  } catch (err) {
337
386
  console.error('[mbkbucket] upload-chunk failed:', err);
338
- return res.status(500).json({ success: false, error: err.message });
387
+ return sendApiError(res, err, 'upload-chunk failed');
339
388
  }
340
389
  });
341
390
 
@@ -344,35 +393,22 @@ router.post('/upload-chunk', validateSessionAndRole('SuperAdmin'), uploadChunk,
344
393
  // Returns: { key }
345
394
  router.post('/upload-complete', validateSessionAndRole('SuperAdmin'), async (req, res) => {
346
395
  try {
396
+ const bucketName = req.activeBucket;
347
397
  const { uploadId, key, parts } = req.body;
348
398
  if (!uploadId || !key || !Array.isArray(parts) || !parts.length) {
349
399
  return res.status(400).json({ success: false, error: 'uploadId, key, and parts array are required' });
350
400
  }
351
401
 
352
- const result = await completeMultipartUpload(key, uploadId, parts);
402
+ const result = await completeMultipartUpload(key, uploadId, parts, bucketName);
353
403
 
354
404
  // Clean up folder marker if it exists in the parent directory
355
405
  const folderPath = result.key.substring(0, result.key.lastIndexOf('/') + 1);
356
- if (folderPath) {
357
- const folderMarkerKey = folderPath;
358
- try {
359
- const markerExists = await fileExists(folderMarkerKey);
360
- if (markerExists) {
361
- const metadata = await getFileMetadata(folderMarkerKey);
362
- if (metadata.ContentLength === 0 || metadata.Metadata?.marker === 'true') {
363
- await deleteFile(folderMarkerKey);
364
- console.log(`[mbkbucket] Cleaned up folder marker: ${folderMarkerKey}`);
365
- }
366
- }
367
- } catch (cleanupErr) {
368
- console.warn('[mbkbucket] Failed to clean up folder marker:', cleanupErr.message);
369
- }
370
- }
406
+ await cleanupFolderMarker(folderPath, bucketName);
371
407
 
372
408
  return res.json({ success: true, message: 'File uploaded successfully', key: result.key });
373
409
  } catch (err) {
374
410
  console.error('[mbkbucket] upload-complete failed:', err);
375
- return res.status(500).json({ success: false, error: err.message });
411
+ return sendApiError(res, err, 'upload-complete failed');
376
412
  }
377
413
  });
378
414
 
@@ -380,20 +416,22 @@ router.post('/upload-complete', validateSessionAndRole('SuperAdmin'), async (req
380
416
  // Body: { uploadId, key }
381
417
  router.post('/upload-abort', validateSessionAndRole('SuperAdmin'), async (req, res) => {
382
418
  try {
419
+ const bucketName = req.activeBucket;
383
420
  const { uploadId, key } = req.body;
384
421
  if (!uploadId || !key) return res.status(400).json({ success: false, error: 'uploadId and key are required' });
385
422
 
386
- await abortMultipartUpload(key, uploadId);
423
+ await abortMultipartUpload(key, uploadId, bucketName);
387
424
  return res.json({ success: true, message: 'Multipart upload aborted' });
388
425
  } catch (err) {
389
426
  console.error('[mbkbucket] upload-abort failed:', err);
390
- return res.status(500).json({ success: false, error: err.message });
427
+ return sendApiError(res, err, 'upload-abort failed');
391
428
  }
392
429
  });
393
430
 
394
431
  // Create folder (zero-byte object with trailing slash)
395
432
  router.post('/create-folder', validateSessionAndRole('SuperAdmin'), async (req, res) => {
396
433
  try {
434
+ const bucketName = req.activeBucket;
397
435
  const { prefix = '', folderName } = req.body;
398
436
  if (!folderName) return res.status(400).json({ success: false, error: 'folderName is required' });
399
437
 
@@ -403,11 +441,11 @@ router.post('/create-folder', validateSessionAndRole('SuperAdmin'), async (req,
403
441
  const folderKey = `${effectivePrefix.replace(/\/+$/, '')}/${folderName}/`;
404
442
 
405
443
  // Check if folder marker already exists
406
- const markerExists = await fileExists(folderKey);
444
+ const markerExists = await fileExists(folderKey, bucketName);
407
445
  if (markerExists) return res.status(409).json({ success: false, error: 'Folder already exists' });
408
446
 
409
447
  // Check if any files already exist under this prefix (folder already has content)
410
- const existingFiles = await listfiles(folderKey, { maxKeys: 1 });
448
+ const existingFiles = await listfiles(folderKey, { maxKeys: 1, bucketName });
411
449
  const hasContent = existingFiles.Contents && existingFiles.Contents.length > 0;
412
450
 
413
451
  if (hasContent) {
@@ -422,23 +460,25 @@ router.post('/create-folder', validateSessionAndRole('SuperAdmin'), async (req,
422
460
 
423
461
  // Create zero-byte object to represent empty folder
424
462
  await uploadFile(folderKey, Buffer.alloc(0), 'application/x-empty', {
425
- metadata: { 'folder': 'true', 'marker': 'true' }
463
+ metadata: { 'folder': 'true', 'marker': 'true' },
464
+ bucketName
426
465
  });
427
466
 
428
467
  return res.json({ success: true, message: 'Folder created', key: folderKey });
429
468
  } catch (err) {
430
469
  console.error('Create folder failed:', err);
431
- return res.status(500).json({ success: false, error: err.message });
470
+ return sendApiError(res, err, 'Create folder failed');
432
471
  }
433
472
  });
434
473
 
435
474
  router.post('/delete', validateSessionAndRole('SuperAdmin'), async (req, res) => {
436
475
  try {
476
+ const bucketName = req.activeBucket;
437
477
  const { key, keys, folder } = req.body;
438
478
 
439
479
  if (keys && Array.isArray(keys) && keys.length) {
440
480
  // Bulk delete
441
- await deleteFiles(keys);
481
+ await deleteFiles(keys, bucketName);
442
482
  return res.json({ success: true, message: 'Files deleted successfully' });
443
483
  }
444
484
 
@@ -449,21 +489,22 @@ router.post('/delete', validateSessionAndRole('SuperAdmin'), async (req, res) =>
449
489
 
450
490
  // If caller indicates it's a folder (or key ends with '/'), delete all objects under the prefix
451
491
  if (folder === true || String(keyToDelete).endsWith('/')) {
452
- await deleteFolder(keyToDelete);
492
+ await deleteFolder(keyToDelete, bucketName);
453
493
  return res.json({ success: true, message: 'Folder deleted successfully' });
454
494
  }
455
495
 
456
- await deleteFile(keyToDelete);
496
+ await deleteFile(keyToDelete, bucketName);
457
497
  res.json({ success: true, message: 'File deleted successfully' });
458
498
  } catch (error) {
459
499
  console.error("Error deleting file:", error);
460
- res.status(500).json({ success: false, error: `Delete failed: ${error.message}` });
500
+ return sendApiError(res, error, 'Delete failed');
461
501
  }
462
502
  });
463
503
 
464
504
  router.get('/download/:key(*)', validateSessionAndRole('SuperAdmin'), async (req, res) => {
465
505
  let stream = null;
466
506
  try {
507
+ const bucketName = req.activeBucket;
467
508
  const key = req.params.key;
468
509
  let keyToUse;
469
510
  try {
@@ -471,19 +512,37 @@ router.get('/download/:key(*)', validateSessionAndRole('SuperAdmin'), async (req
471
512
  } catch (e) {
472
513
  return res.status(400).json({ success: false, error: e.message });
473
514
  }
474
- const result = await downloadFile(keyToUse);
515
+ const result = await downloadFile(keyToUse, { bucketName });
475
516
  stream = result.Body;
476
517
 
477
- res.setHeader('Content-Disposition', `attachment; filename="${keyToUse.split('/').pop()}"`);
478
- res.setHeader('Content-Type', result.ContentType || 'application/octet-stream');
518
+ res.setHeader('Content-Disposition', `attachment; filename="${getBaseName(keyToUse).replace(/"/g, '\\"')}"`);
519
+ const contentType = result.ContentType || 'application/octet-stream';
520
+ res.setHeader('Content-Type', contentType);
479
521
 
480
522
  // Set Content-Length header for proper download progress indication
481
523
  if (result.ContentLength) {
482
524
  res.setHeader('Content-Length', result.ContentLength);
483
525
  }
484
526
 
485
- // Add cache headers for downloads
486
- res.setHeader('Cache-Control', 'private, max-age=3600');
527
+ if (result.ETag) res.setHeader('ETag', result.ETag);
528
+ if (result.LastModified) res.setHeader('Last-Modified', result.LastModified.toUTCString());
529
+
530
+ const typeLower = String(contentType).toLowerCase();
531
+ const isSensitiveText = typeLower.startsWith('text/')
532
+ || typeLower.includes('json')
533
+ || typeLower.includes('xml')
534
+ || typeLower.includes('javascript')
535
+ || typeLower.includes('typescript');
536
+ const isLargeFile = Number(result.ContentLength || 0) > 50 * 1024 * 1024;
537
+
538
+ // Smarter cache policy for authenticated downloads.
539
+ if (isSensitiveText) {
540
+ res.setHeader('Cache-Control', 'private, no-store');
541
+ } else if (isLargeFile) {
542
+ res.setHeader('Cache-Control', 'private, max-age=600, must-revalidate');
543
+ } else {
544
+ res.setHeader('Cache-Control', 'private, max-age=1800, must-revalidate');
545
+ }
487
546
 
488
547
  // Proper stream error handling
489
548
  stream.on('error', (err) => {
@@ -523,4 +582,4 @@ router.get('/download/:key(*)', validateSessionAndRole('SuperAdmin'), async (req
523
582
  }
524
583
  });
525
584
 
526
- export default router;
585
+ export default router;
@@ -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);