mbkbucket 3.1.0 → 3.3.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/src/app.js CHANGED
@@ -9,6 +9,7 @@ import { notFoundHandler, globalErrorHandler } from "./middleware/error-handler.
9
9
  import bucketRoutes from "./routes/index.js";
10
10
  import { packageJson } from "./config/index.js";
11
11
  import { createLogger } from "./utils/logger.js";
12
+ import { commonHandlebarsHelpers } from "./utils/helpers.js";
12
13
 
13
14
  const debugServer = createLogger('server');
14
15
 
@@ -43,11 +44,17 @@ export function createApp() {
43
44
  server.get('/mbkbucket/bucketadmin.css', serveAsset('bucketadmin.css'));
44
45
  server.get('/mbkbucket/bucketadmin.js', serveAsset('bucketadmin.js'));
45
46
 
47
+ // Frontend helper utilities (drop-in for any HTML page)
48
+ server.get('/mbkbucket/mbkbucket-helper.css', serveAsset('mbkbucket-helper.css'));
49
+ server.get('/mbkbucket/mbkbucket-helper.js', serveAsset('mbkbucket-helper.js'));
50
+
51
+ // Helper component demo page (no auth — purely static HTML)
52
+ server.get('/mbkbucket/helper-demo', serveAsset('mbkbucket-helper-demo.html'));
53
+
46
54
  // Handlebars configuration
47
55
  server.engine("handlebars", engine({
48
56
  extname: ".handlebars",
49
- defaultLayout: "main",
50
- layoutsDir: path.join(__dirname, "..", "views", "layouts"),
57
+ defaultLayout: false,
51
58
  partialsDir: [
52
59
  path.join(__dirname, "..", "views", "templates"),
53
60
  path.join(__dirname, "..", "views", "templates", "notice"),
@@ -58,9 +65,7 @@ export function createApp() {
58
65
  ],
59
66
  cache: process.env.NODE_ENV === "production",
60
67
  helpers: {
61
- eq: function (a, b) {
62
- return a === b;
63
- },
68
+ ...commonHandlebarsHelpers,
64
69
  mbkbucket_cachebuster: function () {
65
70
  return "?v=" + packageJson.version;
66
71
  }
@@ -425,9 +425,11 @@ export async function downloadFileHandler(req, res) {
425
425
 
426
426
  try {
427
427
  const bucketName = req.activeBucket;
428
+ const rawKey = req.params.key;
429
+ const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
428
430
  let keyToUse;
429
431
  try {
430
- keyToUse = ensureKeyHasAppPrefix(req.params.key);
432
+ keyToUse = ensureKeyHasAppPrefix(key);
431
433
  } catch (e) {
432
434
  return res.status(400).json({ success: false, error: e.message });
433
435
  }
@@ -1,5 +1,7 @@
1
+ import path from "path";
1
2
  import { resolveBucketName, getAvailableBucketNames } from "../services/s3.service.js";
2
3
  import { renderPage, mbkautheVar } from "mbkauthe";
4
+ import { commonHandlebarsHelpers } from "../utils/helpers.js";
3
5
 
4
6
  /**
5
7
  * Render the main bucket dashboard page.
@@ -7,13 +9,9 @@ import { renderPage, mbkautheVar } from "mbkauthe";
7
9
  export function renderBucketDashboard(req, res) {
8
10
  const allBuckets = getAvailableBucketNames();
9
11
  const configuredDefaultBucket = mbkautheVar?.bucket;
10
-
11
- let bucketfilePath;
12
- if (mbkautheVar?.APP_NAME === "portal") {
13
- bucketfilePath = "bucketportal.handlebars";
14
- } else {
15
- bucketfilePath = "bucket.handlebars";
16
- }
12
+ const isPortal = mbkautheVar?.APP_NAME === "portal";
13
+ const bucketfilePath = isPortal ? "bucketportal.handlebars" : "bucket.handlebars";
14
+ const portalLayout = isPortal ? path.resolve(process.cwd(), "views", "layouts", "main") : false;
17
15
 
18
16
  let selectedBucket;
19
17
 
@@ -26,9 +24,10 @@ export function renderBucketDashboard(req, res) {
26
24
  isSelected: name === fallbackSelected,
27
25
  isDefault: name === configuredDefaultBucket
28
26
  }));
29
- return renderPage(req, res, bucketfilePath, mbkautheVar?.APP_NAME === "portal" ? true : false, {
27
+ return renderPage(req, res, bucketfilePath, isPortal, {
30
28
  page: "Admin Bucket",
31
- layout: true,
29
+ helpers: commonHandlebarsHelpers,
30
+ ...(isPortal ? { layout: portalLayout } : {}),
32
31
  bucketvar: mbkautheVar?.bucket,
33
32
  selectedBucket: fallbackSelected,
34
33
  bucketOptions,
@@ -45,8 +44,10 @@ export function renderBucketDashboard(req, res) {
45
44
  isDefault: name === configuredDefaultBucket
46
45
  }));
47
46
 
48
- renderPage(req, res, bucketfilePath, mbkautheVar?.APP_NAME === "portal" ? true : false, {
47
+ renderPage(req, res, bucketfilePath, isPortal, {
49
48
  page: "Admin Bucket",
49
+ helpers: commonHandlebarsHelpers,
50
+ ...(isPortal ? { layout: portalLayout } : {}),
50
51
  bucketvar: selectedBucket,
51
52
  selectedBucket,
52
53
  bucketOptions,
@@ -192,9 +192,11 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
192
192
  export async function viewFile(req, res) {
193
193
  try {
194
194
  const bucketName = req.activeBucket;
195
+ const rawKey = req.params.key;
196
+ const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
195
197
  let keyToUse;
196
198
  try {
197
- keyToUse = ensureKeyHasAppPrefix(req.params.key);
199
+ keyToUse = ensureKeyHasAppPrefix(key);
198
200
  } catch (e) {
199
201
  return res.status(400).json({ message: e.message });
200
202
  }
@@ -216,7 +218,8 @@ export async function viewFile(req, res) {
216
218
  export async function playerPage(req, res) {
217
219
  try {
218
220
  const bucketName = req.activeBucket;
219
- const key = req.params.key;
221
+ const rawKey = req.params.key;
222
+ const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
220
223
  let keyToUse;
221
224
  try {
222
225
  keyToUse = ensureKeyHasAppPrefix(key);
@@ -240,7 +243,8 @@ export async function playerPage(req, res) {
240
243
  export async function publicView(req, res) {
241
244
  try {
242
245
  const bucketName = resolveBucketName();
243
- const key = req.params.key;
246
+ const rawKey = req.params.key;
247
+ const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
244
248
 
245
249
  // No directory indexing
246
250
  if (!key || key.endsWith('/')) {
@@ -61,6 +61,6 @@ router.post('/create-folder', sessRole('SuperAdmin'), createFolder);
61
61
  router.post('/delete', sessRole('SuperAdmin'), deleteItems);
62
62
 
63
63
  // Download
64
- router.get('/download/:key(*)', sessRole('SuperAdmin'), downloadFileHandler);
64
+ router.get('/download/*key', sessRole('SuperAdmin'), downloadFileHandler);
65
65
 
66
66
  export default router;
@@ -12,14 +12,14 @@ const router = express.Router();
12
12
  router.use(requireBucketView);
13
13
 
14
14
  // Player page for video/audio
15
- router.get('/player/:key(*)', sessRole('SuperAdmin'), playerPage);
15
+ router.get('/player/*key', sessRole('SuperAdmin'), playerPage);
16
16
 
17
17
  // Inline file view
18
- router.get('/view/:key(*)', sessRole('SuperAdmin'), viewFile);
18
+ router.get('/view/*key', sessRole('SuperAdmin'), viewFile);
19
19
 
20
20
  // Public view (conditionally enabled)
21
21
  if (mbkbucketVar?.publiView_enabled) {
22
- router.get('/p_view/:key(*)', pviewRateLimit, pviewSecurity, publicView);
22
+ router.get('/p_view/*key', pviewRateLimit, pviewSecurity, publicView);
23
23
  }
24
24
 
25
25
  export default router;
@@ -44,9 +44,10 @@ function getDefaultBucketName() {
44
44
  }
45
45
 
46
46
  export function resolveBucketName(bucketName) {
47
- const candidate = (bucketName === undefined || bucketName === null)
47
+ const rawCandidate = (typeof bucketName === 'string') ? bucketName.trim() : bucketName;
48
+ const candidate = (rawCandidate === undefined || rawCandidate === null || rawCandidate === '')
48
49
  ? getDefaultBucketName()
49
- : String(bucketName).trim();
50
+ : String(rawCandidate);
50
51
 
51
52
  if (!candidate) {
52
53
  throw new Error('No bucket selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.');
@@ -74,6 +75,9 @@ function isRootModeApp(appName) {
74
75
  }
75
76
 
76
77
  export function ensureKeyHasAppPrefix(key = '') {
78
+ if (Array.isArray(key)) {
79
+ key = key.join('/');
80
+ }
77
81
  const app = getAppName();
78
82
  if (!key) throw new Error('Key is required');
79
83
 
@@ -103,6 +107,9 @@ export function ensureKeyHasAppPrefix(key = '') {
103
107
  }
104
108
 
105
109
  export function ensurePrefix(prefix = '') {
110
+ if (Array.isArray(prefix)) {
111
+ prefix = prefix.join('/');
112
+ }
106
113
  const app = getAppName();
107
114
 
108
115
  if (prefix && (prefix.includes('../') || prefix.includes('..\\') || /\.\.[\\/]/.test(prefix))) {
@@ -369,12 +376,13 @@ export async function downloadFile(key, options = {}) {
369
376
  key: keyToDownload,
370
377
  };
371
378
  } catch (error) {
372
- console.error(`Download failed for key ${key}:`, error);
373
-
374
- if (error && error.$metadata && error.$metadata.httpStatusCode === 304) {
379
+ if (error && (error.$metadata?.httpStatusCode === 304 || error.name === '304' || error.name === 'NotModified')) {
380
+ debugS3('File not modified (304) for key %s', keyToDownload);
375
381
  return { notModified: true, key: keyToDownload };
376
382
  }
377
383
 
384
+ console.error(`Download failed for key ${key}:`, error);
385
+
378
386
  if (error.name === 'NoSuchKey') {
379
387
  throw new Error(`File not found: ${key}`);
380
388
  } else if (error.name === 'AccessDenied') {
@@ -171,3 +171,27 @@ export function renderPlayerPage(safeFileName, encodedKey, bucketQuery) {
171
171
  </body>
172
172
  </html>`;
173
173
  }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Common Handlebars helpers
177
+ // ---------------------------------------------------------------------------
178
+
179
+ export const commonHandlebarsHelpers = {
180
+ eq: (a, b) => a === b,
181
+ neq: (a, b) => a !== b,
182
+ or: (...args) => args.slice(0, -1).some(Boolean),
183
+ and: (...args) => args.slice(0, -1).every(Boolean),
184
+ not: (val) => !val,
185
+ gt: (a, b) => a > b,
186
+ gte: (a, b) => a >= b,
187
+ lt: (a, b) => a < b,
188
+ lte: (a, b) => a <= b,
189
+ includes: (arr, val) => (Array.isArray(arr) ? arr.includes(val) : false),
190
+ json: (obj) => JSON.stringify(obj, null, 2),
191
+ jsonStringify: (obj) => JSON.stringify(obj),
192
+ getInitials: (username) => {
193
+ if (!username) return '?';
194
+ const parts = username.split(/[._]/);
195
+ return parts.map(p => p.charAt(0)).join('').toUpperCase().substring(0, 2);
196
+ },
197
+ };