mbkbucket 2.1.0 → 3.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.
@@ -1,25 +1,20 @@
1
- import express from "express";
2
- import infoRoutes from "./info.js";
3
- import viewRoutes from "./view.js";
4
- import apiRoutes from "./api.js";
5
- import { validateSessionAndRole, renderPage, mbkautheVar } from "mbkauthe";
6
- import { resolveBucketName, getAvailableBucketNames } from "../s3.js";
1
+ import { resolveBucketName, getAvailableBucketNames } from "../services/s3.service.js";
2
+ import { renderPage, mbkautheVar } from "mbkauthe";
7
3
 
8
- const router = express.Router();
9
-
10
- function renderBucketDashboard(req, res) {
4
+ /**
5
+ * Render the main bucket dashboard page.
6
+ */
7
+ export function renderBucketDashboard(req, res) {
11
8
  const allBuckets = getAvailableBucketNames();
12
9
  const configuredDefaultBucket = mbkautheVar?.bucket;
13
10
 
14
11
  let bucketfilePath;
15
-
16
- if(mbkautheVar?.APP_NAME === "portal") {
17
- bucketfilePath ="bucketportal.handlebars";
12
+ if (mbkautheVar?.APP_NAME === "portal") {
13
+ bucketfilePath = "bucketportal.handlebars";
18
14
  } else {
19
15
  bucketfilePath = "bucket.handlebars";
20
16
  }
21
17
 
22
-
23
18
  let selectedBucket;
24
19
 
25
20
  try {
@@ -61,26 +56,3 @@ function renderBucketDashboard(req, res) {
61
56
  availableBuckets: allBuckets
62
57
  });
63
58
  }
64
-
65
- router.get('/mbkbucket', validateSessionAndRole('SuperAdmin'), async (req, res) => {
66
- return renderBucketDashboard(req, res);
67
- });
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
-
82
- router.use("/mbkbucket", infoRoutes);
83
- router.use("/mbkbucket", viewRoutes);
84
- router.use("/mbkbucket", apiRoutes);
85
-
86
- export default router;
@@ -0,0 +1,37 @@
1
+ import { renderPage, mbkautheVar } from "mbkauthe";
2
+ import { packageJson, getLatestVersion } from "../config/index.js";
3
+
4
+ /**
5
+ * Render the mbkbucket info page (Handlebars).
6
+ */
7
+ export async function infoPage(req, res) {
8
+ let latestVersion = 'unknown';
9
+ try {
10
+ latestVersion = await getLatestVersion();
11
+ } catch (err) {
12
+ console.error("[mbkauthe] Error fetching package-lock.json:", err);
13
+ }
14
+ renderPage(req, res, "mbkbucket_info.handlebars", false, {
15
+ page: "MBK Bucket Info",
16
+ CurrentVersion: packageJson.version,
17
+ latestVersion,
18
+ APP_NAME: mbkautheVar?.APP_NAME
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Return bucket info as JSON.
24
+ */
25
+ export async function infoJson(req, res) {
26
+ let latestVersion = 'unknown';
27
+ try {
28
+ latestVersion = await getLatestVersion();
29
+ } catch (err) {
30
+ console.error("[mbkauthe] Error fetching package-lock.json:", err);
31
+ }
32
+ res.json({
33
+ CurrentVersion: packageJson.version,
34
+ latestVersion,
35
+ APP_NAME: mbkautheVar?.APP_NAME
36
+ });
37
+ }
@@ -0,0 +1,276 @@
1
+ import {
2
+ downloadFile, getFileMetadata, ensureKeyHasAppPrefix,
3
+ generateSignedUrl, resolveBucketName
4
+ } from "../services/s3.service.js";
5
+ import { mbkbucketVar } from "../config/index.js";
6
+ import {
7
+ escapeHtml, getBaseName, VIEWABLE_TYPES, VIEWABLE_TYPES_SORTED,
8
+ STATIC_ASSET_TYPES, VIDEO_TYPES, AUDIO_TYPES, MIME_TYPES,
9
+ buildCacheControl, parseRangeHeader, renderPlayerPage
10
+ } from "../utils/helpers.js";
11
+ import { createLogger } from "../utils/logger.js";
12
+
13
+ const debugView = createLogger('view');
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Stream helpers
17
+ // ---------------------------------------------------------------------------
18
+
19
+ function attachStreamCleanup(req, res, stream, timeoutMs = 120000) {
20
+ const cleanup = () => {
21
+ if (stream && typeof stream.destroy === 'function') {
22
+ stream.removeAllListeners();
23
+ stream.destroy();
24
+ }
25
+ };
26
+
27
+ stream.on('error', (err) => {
28
+ console.error('Stream error:', err);
29
+ cleanup();
30
+ if (!res.headersSent) res.status(500).end('Stream error');
31
+ });
32
+
33
+ req.on('close', cleanup);
34
+ req.on('aborted', cleanup);
35
+
36
+ req.setTimeout(timeoutMs, () => {
37
+ console.error('Request timeout for stream');
38
+ cleanup();
39
+ if (!res.headersSent) res.status(408).end('Request timeout');
40
+ });
41
+
42
+ res.on('finish', cleanup);
43
+ res.on('close', cleanup);
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Inline file serving (used by /view/:key and /p_view/:key)
48
+ // ---------------------------------------------------------------------------
49
+
50
+ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName, publicCache = false } = {}) {
51
+ const fileName = getBaseName(keyToUse);
52
+ const dotIndex = fileName.lastIndexOf('.');
53
+ const fileExtension = dotIndex === -1 ? '' : fileName.substring(dotIndex + 1).toLowerCase();
54
+
55
+ debugView('Request for %s (type: %s, range: %s)', fileName, fileExtension, req.headers.range || 'none');
56
+
57
+ if (!VIEWABLE_TYPES.has(fileExtension)) {
58
+ return res.status(415).json({
59
+ message: 'File type not supported for viewing',
60
+ supportedTypes: VIEWABLE_TYPES_SORTED
61
+ });
62
+ }
63
+
64
+ const ifNoneMatch = req.headers['if-none-match'];
65
+ const ifModifiedSince = req.headers['if-modified-since'];
66
+ const rangeHeader = req.headers.range;
67
+
68
+ const isStaticAsset = STATIC_ASSET_TYPES.has(fileExtension);
69
+ const isVideo = VIDEO_TYPES.has(fileExtension);
70
+ const isAudio = AUDIO_TYPES.has(fileExtension);
71
+ const isPdf = fileExtension === 'pdf';
72
+ const supportsRanges = isVideo || isAudio || isPdf;
73
+ const contentType = MIME_TYPES[fileExtension] || 'application/octet-stream';
74
+
75
+ // Fast path for range requests (video / audio / PDF streaming)
76
+ if (supportsRanges && rangeHeader) {
77
+ debugView('Processing range request: %s for %s', rangeHeader, fileName);
78
+ let meta;
79
+ try {
80
+ meta = await getFileMetadata(keyToUse, bucketName);
81
+ } catch (err) {
82
+ console.error('[mbkbucket] serveFileInline metadata error:', err);
83
+ return res.status(500).json({ message: 'Failed to fetch file metadata', error: err.message });
84
+ }
85
+
86
+ if (!meta || !meta.exists) {
87
+ return res.status(404).json({ message: 'File not found', key: keyToUse });
88
+ }
89
+
90
+ const etag = `"${meta.LastModified?.getTime() || Date.now()}-${meta.ContentLength || 0}"`;
91
+ if (ifNoneMatch === etag) return res.status(304).end();
92
+
93
+ const total = Number(meta.ContentLength);
94
+ if (!total) {
95
+ // No content-length — fall through to full-file path
96
+ } else {
97
+ const range = parseRangeHeader(rangeHeader, total);
98
+ if (!range) {
99
+ // Invalid or unsatisfiable range
100
+ res.status(416).setHeader('Content-Range', `bytes */${total}`);
101
+ return res.end();
102
+ }
103
+
104
+ const { start, end, size: chunksize } = range;
105
+
106
+ try {
107
+ const rangeResult = await downloadFile(keyToUse, { range: `bytes=${start}-${end}`, bucketName });
108
+
109
+ if (typeof rangeResult.ContentLength === 'number' && rangeResult.ContentLength !== chunksize) {
110
+ debugView('Range size mismatch for %s: expected=%s got=%s', keyToUse, chunksize, rangeResult.ContentLength);
111
+ }
112
+
113
+ debugView('Serving range bytes %s-%s/%s (%s bytes) for %s', start, end, total, chunksize, fileName);
114
+
115
+ res.status(206);
116
+ res.setHeader('Content-Range', `bytes ${start}-${end}/${total}`);
117
+ res.setHeader('Content-Length', chunksize);
118
+ res.setHeader('Content-Type', contentType);
119
+ res.setHeader('Cache-Control', buildCacheControl(contentType, { supportsRanges: true, publicCache }));
120
+ res.setHeader('ETag', etag);
121
+ res.setHeader('Accept-Ranges', 'bytes');
122
+ if (noindex) res.setHeader('X-Robots-Tag', 'noindex, nofollow');
123
+ res.setHeader('X-Content-Type-Options', 'nosniff');
124
+ if (meta.LastModified) res.setHeader('Last-Modified', meta.LastModified.toUTCString());
125
+
126
+ attachStreamCleanup(req, res, rangeResult.Body);
127
+ return rangeResult.Body.pipe(res);
128
+
129
+ } catch (rangeError) {
130
+ console.error('Range request failed:', rangeError);
131
+ // fall through to full-file path
132
+ }
133
+ }
134
+ }
135
+
136
+ // Standard path: full-file download
137
+ const downloadOptions = {
138
+ ...(ifNoneMatch && { ifNoneMatch }),
139
+ ...(ifModifiedSince && { ifModifiedSince: new Date(ifModifiedSince) }),
140
+ bucketName
141
+ };
142
+
143
+ if (isPdf) {
144
+ debugView('Serving full PDF (no range header) for %s', fileName);
145
+ }
146
+
147
+ let result;
148
+ try {
149
+ result = await downloadFile(keyToUse, downloadOptions);
150
+ } catch (err) {
151
+ if (err.name === 'NoSuchKey' || err.name === 'NotFound') {
152
+ return res.status(404).json({ message: 'File not found', key: keyToUse });
153
+ } else if (err.name === 'AccessDenied') {
154
+ return res.status(403).json({ message: 'Access denied' });
155
+ }
156
+ console.error('[mbkbucket] serveFileInline download error:', err);
157
+ return res.status(500).json({ message: 'Failed to fetch file', error: err.message });
158
+ }
159
+
160
+ if (result && result.notModified) return res.status(304).end();
161
+
162
+ const etag = `"${result.LastModified?.getTime() || Date.now()}-${result.ContentLength || 0}"`;
163
+ if (ifNoneMatch === etag) return res.status(304).end();
164
+
165
+ res.setHeader('Content-Type', contentType);
166
+ res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
167
+ res.setHeader('ETag', etag);
168
+ res.setHeader('X-Content-Type-Options', 'nosniff');
169
+ res.setHeader('X-Frame-Options', 'SAMEORIGIN');
170
+ if (noindex) res.setHeader('X-Robots-Tag', 'noindex, nofollow');
171
+ if (result.ContentLength) res.setHeader('Content-Length', result.ContentLength);
172
+ if (result.LastModified) res.setHeader('Last-Modified', result.LastModified.toUTCString());
173
+ if (fileExtension === 'html' || fileExtension === 'htm') {
174
+ res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';");
175
+ }
176
+ if (supportsRanges) res.setHeader('Accept-Ranges', 'bytes');
177
+
178
+ res.setHeader('Cache-Control', buildCacheControl(contentType, { isStaticAsset, supportsRanges, publicCache }));
179
+
180
+ res.status(200);
181
+
182
+ const timeoutDuration = result.ContentLength > 50 * 1024 * 1024 ? 300000 : 120000;
183
+ attachStreamCleanup(req, res, result.Body, timeoutDuration);
184
+
185
+ result.Body.pipe(res);
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Controller handlers
190
+ // ---------------------------------------------------------------------------
191
+
192
+ export async function viewFile(req, res) {
193
+ try {
194
+ const bucketName = req.activeBucket;
195
+ let keyToUse;
196
+ try {
197
+ keyToUse = ensureKeyHasAppPrefix(req.params.key);
198
+ } catch (e) {
199
+ return res.status(400).json({ message: e.message });
200
+ }
201
+
202
+ await serveFileInline(req, res, keyToUse, { noindex: true, bucketName, publicCache: false });
203
+
204
+ } catch (error) {
205
+ console.error("Error viewing file:", error);
206
+ if (error.name === 'NoSuchKey' || error.name === 'NotFound') {
207
+ res.status(404).json({ message: "File not found", key: req.params.key });
208
+ } else if (error.name === 'AccessDenied') {
209
+ res.status(403).json({ message: "Access denied" });
210
+ } else {
211
+ res.status(500).json({ message: "View failed", error: error.message });
212
+ }
213
+ }
214
+ }
215
+
216
+ export async function playerPage(req, res) {
217
+ try {
218
+ const bucketName = req.activeBucket;
219
+ const key = req.params.key;
220
+ let keyToUse;
221
+ try {
222
+ keyToUse = ensureKeyHasAppPrefix(key);
223
+ } catch (e) {
224
+ return res.status(400).send(e.message);
225
+ }
226
+ const encoded = encodeURIComponent(keyToUse);
227
+ const safeFileName = escapeHtml(getBaseName(keyToUse));
228
+ const bucketQuery = `?bucket=${encodeURIComponent(bucketName)}`;
229
+ const html = renderPlayerPage(safeFileName, encoded, bucketQuery);
230
+
231
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
232
+ res.setHeader('X-Content-Type-Options', 'nosniff');
233
+ return res.status(200).send(html);
234
+ } catch (err) {
235
+ console.error('Error rendering player page:', err);
236
+ return res.status(500).send('Failed to render player');
237
+ }
238
+ }
239
+
240
+ export async function publicView(req, res) {
241
+ try {
242
+ const bucketName = resolveBucketName();
243
+ const key = req.params.key;
244
+
245
+ // No directory indexing
246
+ if (!key || key.endsWith('/')) {
247
+ return res.status(403).json({ message: 'Directory indexing is not allowed' });
248
+ }
249
+
250
+ let keyToUse;
251
+ try {
252
+ keyToUse = ensureKeyHasAppPrefix(key);
253
+ } catch (e) {
254
+ return res.status(400).json({ message: e.message });
255
+ }
256
+
257
+ let p_view_inline = !!mbkbucketVar?.p_view_inline;
258
+
259
+ if (!p_view_inline) {
260
+ // Signed URL mode
261
+ try {
262
+ const signed = await generateSignedUrl(keyToUse, 'getObject', 3600, bucketName);
263
+ return res.redirect(302, signed.url);
264
+ } catch (err) {
265
+ console.error('[mbkbucket] p_view signed URL error:', err);
266
+ return res.status(500).json({ message: 'Failed to generate access URL' });
267
+ }
268
+ }
269
+
270
+ // Direct serve mode
271
+ await serveFileInline(req, res, keyToUse, { noindex: true, bucketName, publicCache: true });
272
+ } catch (error) {
273
+ console.error('[mbkbucket] p_view error:', error);
274
+ res.status(500).json({ message: 'Internal server error', error: error.message });
275
+ }
276
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Wraps an async Express route handler so that any thrown/rejected errors
3
+ * are forwarded to `next(err)` instead of crashing the process.
4
+ *
5
+ * Usage:
6
+ * router.get('/path', wrap(myAsyncHandler));
7
+ *
8
+ * If a custom error handler is provided, it receives (error, req, res, next)
9
+ * and can send a custom response instead of forwarding to Express error middleware.
10
+ */
11
+ export function wrap(handler, onError) {
12
+ return (req, res, next) => {
13
+ const result = handler(req, res, next);
14
+ if (result && typeof result.catch === 'function') {
15
+ result.catch((err) => {
16
+ if (onError) {
17
+ return onError(err, req, res, next);
18
+ }
19
+ next(err);
20
+ });
21
+ }
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Combines wrap() with a standard JSON error response for API routes.
27
+ * The `fallbackMessage` is used when the error has no message.
28
+ */
29
+ export function wrapApi(handler, fallbackMessage = 'Request failed') {
30
+ return wrap(handler, (err, _req, res) => {
31
+ console.error(`[mbkbucket] ${err.stack || err.message}`);
32
+ res.status(500).json({
33
+ success: false,
34
+ code: 'INTERNAL_ERROR',
35
+ error: err.message || fallbackMessage,
36
+ });
37
+ });
38
+ }
@@ -0,0 +1,43 @@
1
+ import { resolveBucketName } from "../services/s3.service.js";
2
+ import { sendApiError, sendViewError } from "../utils/errors.js";
3
+
4
+ /**
5
+ * Middleware that resolves `?bucket=<name>` query param and attaches
6
+ * `req.activeBucket` and `req.bucketResolveError` to every request under /mbkbucket.
7
+ */
8
+ export function bucketResolver(req, _res, next) {
9
+ try {
10
+ req.activeBucket = resolveBucketName(req.query.bucket);
11
+ req.bucketResolveError = null;
12
+ } catch (err) {
13
+ req.activeBucket = null;
14
+ req.bucketResolveError = err;
15
+ }
16
+ next();
17
+ }
18
+
19
+ /**
20
+ * Guard middleware for API routes — returns 400/503 JSON if bucket is invalid.
21
+ */
22
+ export function requireBucketApi(req, res, next) {
23
+ if (req.bucketResolveError) {
24
+ return sendApiError(res, req.bucketResolveError, 'Invalid bucket selection');
25
+ }
26
+ if (!req.activeBucket) {
27
+ return sendApiError(res, new Error('No bucket selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.'));
28
+ }
29
+ next();
30
+ }
31
+
32
+ /**
33
+ * Guard middleware for view routes — returns 400/503 JSON if bucket is invalid.
34
+ * Skips public view routes (/p_view/...) which use the default bucket.
35
+ */
36
+ export function requireBucketView(req, res, next) {
37
+ if (req.path.startsWith('/p_view/')) return next();
38
+ if (req.bucketResolveError) return sendViewError(res, req.bucketResolveError, 'Invalid bucket selection');
39
+ if (!req.activeBucket) {
40
+ return sendViewError(res, new Error('No bucket selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.'));
41
+ }
42
+ next();
43
+ }
@@ -0,0 +1,34 @@
1
+ import { createLogger } from "../utils/logger.js";
2
+
3
+ const debugServer = createLogger('server');
4
+
5
+ /**
6
+ * Global 404 handler — renders error page via Handlebars.
7
+ */
8
+ export function notFoundHandler(req, res) {
9
+ debugServer("Path not found: %s %s", req.method, req.url);
10
+ return res.status(404).render("Error/dError.handlebars", {
11
+ layout: false,
12
+ code: 404,
13
+ error: "Not Found",
14
+ message: "The requested page was not found.",
15
+ pagename: "Home",
16
+ page: "/",
17
+ });
18
+ }
19
+
20
+ /**
21
+ * Global error handler — catches unhandled errors from routes/middleware.
22
+ */
23
+ export function globalErrorHandler(err, req, res, _next) {
24
+ console.error(`[mbkbucket] ${err.stack}`);
25
+ return res.status(500).render("Error/dError.handlebars", {
26
+ layout: false,
27
+ code: 500,
28
+ error: "Internal app Error",
29
+ message: "An unexpected error occurred on the app.",
30
+ details: err.message,
31
+ pagename: "Home",
32
+ page: "/",
33
+ });
34
+ }
@@ -0,0 +1,30 @@
1
+ import rateLimit from "express-rate-limit";
2
+
3
+ /**
4
+ * General rate limiter for typical browsing/API usage.
5
+ * 150 requests per 2 minutes per IP.
6
+ */
7
+ export const generalLimiter = rateLimit({
8
+ windowMs: 2 * 60 * 1000, // 2 minutes
9
+ max: 150,
10
+ standardHeaders: true,
11
+ legacyHeaders: false,
12
+ handler: (req, res) => {
13
+ res.status(429).render('error.handlebars', { message: 'Too many requests from your IP. Try again later.', code: 429 });
14
+ }
15
+ });
16
+
17
+ /**
18
+ * Stricter rate limiter for public view (p_view) routes.
19
+ * 10 requests per minute per IP.
20
+ */
21
+ export const pviewRateLimit = rateLimit({
22
+ windowMs: 60 * 1000, // 1 minute
23
+ max: 10,
24
+ standardHeaders: 'draft-7',
25
+ legacyHeaders: false,
26
+ keyGenerator: (req) => req.headers['x-forwarded-for']?.split(',')[0].trim() || req.socket.remoteAddress || 'unknown',
27
+ handler: (_req, res) => {
28
+ res.status(429).json({ message: 'Too many requests. Please slow down and try again later.' });
29
+ }
30
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Security middleware for public view (p_view) routes.
3
+ * Blocks bot/scraper User-Agents and sets security headers.
4
+ */
5
+
6
+ const BLOCKED_UA_PATTERNS = /curl|wget|python-requests|python-urllib|go-http|scrapy|libwww|java\/|bot|crawl|spider|headless|phantom|selenium|puppeteer|playwright|postman|insomnia|httpie/i;
7
+
8
+ const SUSPICIOUS_HEADERS = [
9
+ 'x-devtools-emulate-network-conditions-client-id',
10
+ 'x-chromedriver-clientid',
11
+ 'x-automated-tool'
12
+ ];
13
+
14
+ /**
15
+ * p_view security middleware: blocks bots, sets security headers.
16
+ */
17
+ export function pviewSecurity(req, res, next) {
18
+ const ua = req.headers['user-agent'] || '';
19
+
20
+ // Block empty or obviously automated UAs
21
+ if (!ua || BLOCKED_UA_PATTERNS.test(ua)) {
22
+ return res.status(403).json({ message: 'Forbidden' });
23
+ }
24
+
25
+ // Block suspicious headers that indicate automation
26
+ const hasSuspiciousHeaders = SUSPICIOUS_HEADERS.some(header => header in req.headers);
27
+ if (hasSuspiciousHeaders) {
28
+ return res.status(403).json({ message: 'Forbidden' });
29
+ }
30
+
31
+ // Verify Accept header is present (legitimate browsers always send this)
32
+ if (!req.headers['accept']) {
33
+ return res.status(403).json({ message: 'Forbidden' });
34
+ }
35
+
36
+ // Set security headers for the public view response
37
+ res.setHeader('X-Content-Type-Options', 'nosniff');
38
+ res.setHeader('X-Frame-Options', 'SAMEORIGIN');
39
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
40
+ res.removeHeader('Access-Control-Allow-Origin');
41
+
42
+ next();
43
+ }
@@ -0,0 +1,66 @@
1
+ import express from "express";
2
+ import { sessRole } from "mbkauthe";
3
+ import multer from "multer";
4
+ import { requireBucketApi } from "../middleware/bucket-resolver.js";
5
+ import {
6
+ cleanupUploads, listIncompleteUploads, listFiles,
7
+ uploadSingleFile, initiateMultipartUpload, uploadChunk,
8
+ completeUpload, abortUpload, createFolder, deleteItems,
9
+ downloadFileHandler
10
+ } from "../controllers/api.controller.js";
11
+
12
+ const router = express.Router();
13
+
14
+ const MAX_CHUNK_SIZE = 50 * 1024 * 1024; // 50 MB
15
+
16
+ const upload = multer({
17
+ storage: multer.memoryStorage(),
18
+ limits: { fileSize: MAX_CHUNK_SIZE }
19
+ });
20
+
21
+ const uploadChunkMiddleware = (req, res, next) => {
22
+ upload.single('chunk')(req, res, (err) => {
23
+ if (err) {
24
+ if (err.code === 'LIMIT_FILE_SIZE') {
25
+ return res.status(413).json({
26
+ success: false,
27
+ error: `Chunk too large (max ${Math.round(MAX_CHUNK_SIZE / (1024 * 1024))} MB).`,
28
+ });
29
+ }
30
+ return next(err);
31
+ }
32
+ next();
33
+ });
34
+ };
35
+
36
+ // Bucket guard for all API routes
37
+ router.use(requireBucketApi);
38
+
39
+ // Multipart upload cleanup
40
+ router.post('/api/cleanup-uploads', sessRole('SuperAdmin'), cleanupUploads);
41
+
42
+ // List incomplete multipart uploads
43
+ router.get('/api/incomplete-uploads', sessRole('SuperAdmin'), listIncompleteUploads);
44
+
45
+ // List files
46
+ router.get('/api/files', sessRole('SuperAdmin'), listFiles);
47
+
48
+ // Single-file upload
49
+ router.post('/upload', sessRole('SuperAdmin'), upload.single('file'), uploadSingleFile);
50
+
51
+ // Multipart upload flow
52
+ router.post('/upload-init', sessRole('SuperAdmin'), initiateMultipartUpload);
53
+ router.post('/upload-chunk', sessRole('SuperAdmin'), uploadChunkMiddleware, uploadChunk);
54
+ router.post('/upload-complete', sessRole('SuperAdmin'), completeUpload);
55
+ router.post('/upload-abort', sessRole('SuperAdmin'), abortUpload);
56
+
57
+ // Folder operations
58
+ router.post('/create-folder', sessRole('SuperAdmin'), createFolder);
59
+
60
+ // Delete operations
61
+ router.post('/delete', sessRole('SuperAdmin'), deleteItems);
62
+
63
+ // Download
64
+ router.get('/download/:key(*)', sessRole('SuperAdmin'), downloadFileHandler);
65
+
66
+ export default router;
@@ -0,0 +1,22 @@
1
+ import express from "express";
2
+ import { sessRole } from "mbkauthe";
3
+ import { bucketResolver } from "../middleware/bucket-resolver.js";
4
+ import { renderBucketDashboard } from "../controllers/dashboard.controller.js";
5
+ import infoRoutes from "./info.routes.js";
6
+ import viewRoutes from "./view.routes.js";
7
+ import apiRoutes from "./api.routes.js";
8
+
9
+ const router = express.Router();
10
+
11
+ // Dashboard home
12
+ router.get('/mbkbucket', sessRole('SuperAdmin'), renderBucketDashboard);
13
+
14
+ // Central bucket resolution for all /mbkbucket subroutes
15
+ router.use('/mbkbucket', bucketResolver);
16
+
17
+ // Mount sub-routers
18
+ router.use("/mbkbucket", infoRoutes);
19
+ router.use("/mbkbucket", viewRoutes);
20
+ router.use("/mbkbucket", apiRoutes);
21
+
22
+ export default router;
@@ -0,0 +1,10 @@
1
+ import express from "express";
2
+ import { sessRole } from "mbkauthe";
3
+ import { infoPage, infoJson } from "../controllers/info.controller.js";
4
+
5
+ const router = express.Router();
6
+
7
+ router.get(['/info', "/i"], infoPage);
8
+ router.get(['/info.json', "/i.json"], infoJson);
9
+
10
+ export default router;