mbkbucket 3.3.0 → 3.3.2
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 +8 -18
- package/lib/cli.js +642 -0
- package/{public → lib/public}/bucketadmin.js +1 -1
- package/{src → lib/src}/app.js +37 -50
- package/{src → lib/src}/cli/auth-helper.js +24 -103
- package/lib/src/config/index.js +108 -0
- package/{src → lib/src}/config/validation.js +14 -30
- package/{src → lib/src}/controllers/api.controller.js +85 -176
- package/{src → lib/src}/controllers/dashboard.controller.js +12 -23
- package/{src → lib/src}/controllers/info.controller.js +17 -20
- package/{src → lib/src}/controllers/view.controller.js +23 -70
- package/{src → lib/src}/middleware/async-handler.js +1 -16
- package/{src → lib/src}/middleware/bucket-resolver.js +7 -13
- package/{src → lib/src}/middleware/error-handler.js +6 -14
- package/{src → lib/src}/middleware/rate-limiter.js +2 -10
- package/{src → lib/src}/middleware/security.js +1 -17
- package/lib/src/routes/api.routes.js +40 -0
- package/{src → lib/src}/routes/index.js +1 -9
- package/{src → lib/src}/routes/info.routes.js +2 -3
- package/{src → lib/src}/routes/view.routes.js +3 -8
- package/{src → lib/src}/services/s3.service.js +157 -349
- package/lib/src/utils/errors.js +45 -0
- package/lib/src/utils/helpers.js +251 -0
- package/package.json +18 -3
- package/.github/PACKAGE.md +0 -19
- package/.github/workflows/publish.yml +0 -88
- package/cli.js +0 -977
- package/index.d.ts +0 -444
- package/lib/index.js +0 -17
- package/src/config/index.js +0 -133
- package/src/routes/api.routes.js +0 -66
- package/src/utils/errors.js +0 -76
- package/src/utils/helpers.js +0 -197
- package/test/config-validation.test.js +0 -116
- /package/{public → lib/public}/bucketadmin.css +0 -0
- /package/{public → lib/public}/mbkbucket-helper-demo.html +0 -0
- /package/{public → lib/public}/mbkbucket-helper.css +0 -0
- /package/{public → lib/public}/mbkbucket-helper.js +0 -0
- /package/{src → lib/src}/utils/logger.js +0 -0
|
@@ -1,28 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
downloadFile, getFileMetadata, ensureKeyHasAppPrefix,
|
|
3
|
-
generateSignedUrl, resolveBucketName
|
|
4
|
-
} from "../services/s3.service.js";
|
|
1
|
+
import { downloadFile, getFileMetadata, ensureKeyHasAppPrefix, generateSignedUrl, resolveBucketName } from "../services/s3.service.js";
|
|
5
2
|
import { mbkbucketVar } from "../config/index.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
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";
|
|
3
|
+
import { escapeHtml, getBaseName, getFileExt, getMimeType, normalizeKeyParam, destroyStream, VIEWABLE_TYPES, VIEWABLE_TYPES_SORTED, STATIC_ASSET_TYPES, VIDEO_TYPES, AUDIO_TYPES, buildCacheControl, parseRangeHeader, renderPlayerPage } from "../utils/helpers.js";
|
|
4
|
+
import { createLogger } from "#logger";
|
|
12
5
|
|
|
13
6
|
const debugView = createLogger('view');
|
|
14
7
|
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Stream helpers
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
8
|
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
|
-
};
|
|
9
|
+
const cleanup = () => destroyStream(stream);
|
|
26
10
|
|
|
27
11
|
stream.on('error', (err) => {
|
|
28
12
|
console.error('Stream error:', err);
|
|
@@ -32,25 +16,18 @@ function attachStreamCleanup(req, res, stream, timeoutMs = 120000) {
|
|
|
32
16
|
|
|
33
17
|
req.on('close', cleanup);
|
|
34
18
|
req.on('aborted', cleanup);
|
|
35
|
-
|
|
36
19
|
req.setTimeout(timeoutMs, () => {
|
|
37
20
|
console.error('Request timeout for stream');
|
|
38
21
|
cleanup();
|
|
39
22
|
if (!res.headersSent) res.status(408).end('Request timeout');
|
|
40
23
|
});
|
|
41
|
-
|
|
42
24
|
res.on('finish', cleanup);
|
|
43
25
|
res.on('close', cleanup);
|
|
44
26
|
}
|
|
45
27
|
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Inline file serving (used by /view/:key and /p_view/:key)
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
28
|
async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName, publicCache = false } = {}) {
|
|
51
29
|
const fileName = getBaseName(keyToUse);
|
|
52
|
-
const
|
|
53
|
-
const fileExtension = dotIndex === -1 ? '' : fileName.substring(dotIndex + 1).toLowerCase();
|
|
30
|
+
const fileExtension = getFileExt(fileName);
|
|
54
31
|
|
|
55
32
|
debugView('Request for %s (type: %s, range: %s)', fileName, fileExtension, req.headers.range || 'none');
|
|
56
33
|
|
|
@@ -70,7 +47,7 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
|
|
|
70
47
|
const isAudio = AUDIO_TYPES.has(fileExtension);
|
|
71
48
|
const isPdf = fileExtension === 'pdf';
|
|
72
49
|
const supportsRanges = isVideo || isAudio || isPdf;
|
|
73
|
-
const contentType =
|
|
50
|
+
const contentType = getMimeType(fileName);
|
|
74
51
|
|
|
75
52
|
// Fast path for range requests (video / audio / PDF streaming)
|
|
76
53
|
if (supportsRanges && rangeHeader) {
|
|
@@ -83,7 +60,7 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
|
|
|
83
60
|
return res.status(500).json({ message: 'Failed to fetch file metadata', error: err.message });
|
|
84
61
|
}
|
|
85
62
|
|
|
86
|
-
if (!meta
|
|
63
|
+
if (!meta?.exists) {
|
|
87
64
|
return res.status(404).json({ message: 'File not found', key: keyToUse });
|
|
88
65
|
}
|
|
89
66
|
|
|
@@ -91,21 +68,16 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
|
|
|
91
68
|
if (ifNoneMatch === etag) return res.status(304).end();
|
|
92
69
|
|
|
93
70
|
const total = Number(meta.ContentLength);
|
|
94
|
-
if (
|
|
95
|
-
// No content-length — fall through to full-file path
|
|
96
|
-
} else {
|
|
71
|
+
if (total) {
|
|
97
72
|
const range = parseRangeHeader(rangeHeader, total);
|
|
98
73
|
if (!range) {
|
|
99
|
-
// Invalid or unsatisfiable range
|
|
100
74
|
res.status(416).setHeader('Content-Range', `bytes */${total}`);
|
|
101
75
|
return res.end();
|
|
102
76
|
}
|
|
103
77
|
|
|
104
78
|
const { start, end, size: chunksize } = range;
|
|
105
|
-
|
|
106
79
|
try {
|
|
107
80
|
const rangeResult = await downloadFile(keyToUse, { range: `bytes=${start}-${end}`, bucketName });
|
|
108
|
-
|
|
109
81
|
if (typeof rangeResult.ContentLength === 'number' && rangeResult.ContentLength !== chunksize) {
|
|
110
82
|
debugView('Range size mismatch for %s: expected=%s got=%s', keyToUse, chunksize, rangeResult.ContentLength);
|
|
111
83
|
}
|
|
@@ -125,10 +97,8 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
|
|
|
125
97
|
|
|
126
98
|
attachStreamCleanup(req, res, rangeResult.Body);
|
|
127
99
|
return rangeResult.Body.pipe(res);
|
|
128
|
-
|
|
129
100
|
} catch (rangeError) {
|
|
130
101
|
console.error('Range request failed:', rangeError);
|
|
131
|
-
// fall through to full-file path
|
|
132
102
|
}
|
|
133
103
|
}
|
|
134
104
|
}
|
|
@@ -140,9 +110,7 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
|
|
|
140
110
|
bucketName
|
|
141
111
|
};
|
|
142
112
|
|
|
143
|
-
if (isPdf)
|
|
144
|
-
debugView('Serving full PDF (no range header) for %s', fileName);
|
|
145
|
-
}
|
|
113
|
+
if (isPdf) debugView('Serving full PDF (no range header) for %s', fileName);
|
|
146
114
|
|
|
147
115
|
let result;
|
|
148
116
|
try {
|
|
@@ -150,14 +118,15 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
|
|
|
150
118
|
} catch (err) {
|
|
151
119
|
if (err.name === 'NoSuchKey' || err.name === 'NotFound') {
|
|
152
120
|
return res.status(404).json({ message: 'File not found', key: keyToUse });
|
|
153
|
-
}
|
|
121
|
+
}
|
|
122
|
+
if (err.name === 'AccessDenied') {
|
|
154
123
|
return res.status(403).json({ message: 'Access denied' });
|
|
155
124
|
}
|
|
156
125
|
console.error('[mbkbucket] serveFileInline download error:', err);
|
|
157
126
|
return res.status(500).json({ message: 'Failed to fetch file', error: err.message });
|
|
158
127
|
}
|
|
159
128
|
|
|
160
|
-
if (result
|
|
129
|
+
if (result?.notModified) return res.status(304).end();
|
|
161
130
|
|
|
162
131
|
const etag = `"${result.LastModified?.getTime() || Date.now()}-${result.ContentLength || 0}"`;
|
|
163
132
|
if (ifNoneMatch === etag) return res.status(304).end();
|
|
@@ -174,26 +143,17 @@ async function serveFileInline(req, res, keyToUse, { noindex = true, bucketName,
|
|
|
174
143
|
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'none'; object-src 'none';");
|
|
175
144
|
}
|
|
176
145
|
if (supportsRanges) res.setHeader('Accept-Ranges', 'bytes');
|
|
177
|
-
|
|
178
146
|
res.setHeader('Cache-Control', buildCacheControl(contentType, { isStaticAsset, supportsRanges, publicCache }));
|
|
179
|
-
|
|
180
147
|
res.status(200);
|
|
181
148
|
|
|
182
149
|
const timeoutDuration = result.ContentLength > 50 * 1024 * 1024 ? 300000 : 120000;
|
|
183
150
|
attachStreamCleanup(req, res, result.Body, timeoutDuration);
|
|
184
|
-
|
|
185
151
|
result.Body.pipe(res);
|
|
186
152
|
}
|
|
187
153
|
|
|
188
|
-
// ---------------------------------------------------------------------------
|
|
189
|
-
// Controller handlers
|
|
190
|
-
// ---------------------------------------------------------------------------
|
|
191
|
-
|
|
192
154
|
export async function viewFile(req, res) {
|
|
193
155
|
try {
|
|
194
|
-
const
|
|
195
|
-
const rawKey = req.params.key;
|
|
196
|
-
const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
|
|
156
|
+
const key = normalizeKeyParam(req.params.key);
|
|
197
157
|
let keyToUse;
|
|
198
158
|
try {
|
|
199
159
|
keyToUse = ensureKeyHasAppPrefix(key);
|
|
@@ -201,8 +161,7 @@ export async function viewFile(req, res) {
|
|
|
201
161
|
return res.status(400).json({ message: e.message });
|
|
202
162
|
}
|
|
203
163
|
|
|
204
|
-
await serveFileInline(req, res, keyToUse, { noindex: true, bucketName, publicCache: false });
|
|
205
|
-
|
|
164
|
+
await serveFileInline(req, res, keyToUse, { noindex: true, bucketName: req.activeBucket, publicCache: false });
|
|
206
165
|
} catch (error) {
|
|
207
166
|
console.error("Error viewing file:", error);
|
|
208
167
|
if (error.name === 'NoSuchKey' || error.name === 'NotFound') {
|
|
@@ -217,19 +176,19 @@ export async function viewFile(req, res) {
|
|
|
217
176
|
|
|
218
177
|
export async function playerPage(req, res) {
|
|
219
178
|
try {
|
|
220
|
-
const
|
|
221
|
-
const rawKey = req.params.key;
|
|
222
|
-
const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
|
|
179
|
+
const key = normalizeKeyParam(req.params.key);
|
|
223
180
|
let keyToUse;
|
|
224
181
|
try {
|
|
225
182
|
keyToUse = ensureKeyHasAppPrefix(key);
|
|
226
183
|
} catch (e) {
|
|
227
184
|
return res.status(400).send(e.message);
|
|
228
185
|
}
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
186
|
+
|
|
187
|
+
const html = renderPlayerPage(
|
|
188
|
+
escapeHtml(getBaseName(keyToUse)),
|
|
189
|
+
encodeURIComponent(keyToUse),
|
|
190
|
+
`?bucket=${encodeURIComponent(req.activeBucket)}`
|
|
191
|
+
);
|
|
233
192
|
|
|
234
193
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
235
194
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
@@ -243,10 +202,8 @@ export async function playerPage(req, res) {
|
|
|
243
202
|
export async function publicView(req, res) {
|
|
244
203
|
try {
|
|
245
204
|
const bucketName = resolveBucketName();
|
|
246
|
-
const
|
|
247
|
-
const key = Array.isArray(rawKey) ? rawKey.join('/') : (rawKey || '');
|
|
205
|
+
const key = normalizeKeyParam(req.params.key);
|
|
248
206
|
|
|
249
|
-
// No directory indexing
|
|
250
207
|
if (!key || key.endsWith('/')) {
|
|
251
208
|
return res.status(403).json({ message: 'Directory indexing is not allowed' });
|
|
252
209
|
}
|
|
@@ -258,10 +215,7 @@ export async function publicView(req, res) {
|
|
|
258
215
|
return res.status(400).json({ message: e.message });
|
|
259
216
|
}
|
|
260
217
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (!p_view_inline) {
|
|
264
|
-
// Signed URL mode
|
|
218
|
+
if (!mbkbucketVar?.p_view_inline) {
|
|
265
219
|
try {
|
|
266
220
|
const signed = await generateSignedUrl(keyToUse, 'getObject', 3600, bucketName);
|
|
267
221
|
return res.redirect(302, signed.url);
|
|
@@ -271,7 +225,6 @@ export async function publicView(req, res) {
|
|
|
271
225
|
}
|
|
272
226
|
}
|
|
273
227
|
|
|
274
|
-
// Direct serve mode
|
|
275
228
|
await serveFileInline(req, res, keyToUse, { noindex: true, bucketName, publicCache: true });
|
|
276
229
|
} catch (error) {
|
|
277
230
|
console.error('[mbkbucket] p_view error:', error);
|
|
@@ -1,30 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wraps an async Express route handler so that any thrown/rejected errors
|
|
3
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
4
|
*/
|
|
11
5
|
export function wrap(handler, onError) {
|
|
12
6
|
return (req, res, next) => {
|
|
13
|
-
|
|
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
|
-
}
|
|
7
|
+
Promise.resolve(handler(req, res, next)).catch(err => (onError ? onError(err, req, res, next) : next(err)));
|
|
22
8
|
};
|
|
23
9
|
}
|
|
24
10
|
|
|
25
11
|
/**
|
|
26
12
|
* Combines wrap() with a standard JSON error response for API routes.
|
|
27
|
-
* The `fallbackMessage` is used when the error has no message.
|
|
28
13
|
*/
|
|
29
14
|
export function wrapApi(handler, fallbackMessage = 'Request failed') {
|
|
30
15
|
return wrap(handler, (err, _req, res) => {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { resolveBucketName } from "../services/s3.service.js";
|
|
2
2
|
import { sendApiError, sendViewError } from "../utils/errors.js";
|
|
3
3
|
|
|
4
|
+
const NO_BUCKET_MSG = 'No bucket selected. Provide ?bucket=<name> or configure a default bucket in mbkautheVar.bucket.';
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
|
-
*
|
|
6
|
-
* `req.activeBucket` and `req.bucketResolveError` to every request under /mbkbucket.
|
|
7
|
+
* Resolves `?bucket=<name>` query param and attaches `req.activeBucket` and `req.bucketResolveError`.
|
|
7
8
|
*/
|
|
8
9
|
export function bucketResolver(req, _res, next) {
|
|
9
10
|
try {
|
|
@@ -20,24 +21,17 @@ export function bucketResolver(req, _res, next) {
|
|
|
20
21
|
* Guard middleware for API routes — returns 400/503 JSON if bucket is invalid.
|
|
21
22
|
*/
|
|
22
23
|
export function requireBucketApi(req, res, next) {
|
|
23
|
-
if (req.bucketResolveError)
|
|
24
|
-
|
|
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
|
-
}
|
|
24
|
+
if (req.bucketResolveError) return sendApiError(res, req.bucketResolveError, 'Invalid bucket selection');
|
|
25
|
+
if (!req.activeBucket) return sendApiError(res, new Error(NO_BUCKET_MSG));
|
|
29
26
|
next();
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
/**
|
|
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.
|
|
30
|
+
* Guard middleware for view routes — returns 400/503 JSON if bucket is invalid (skips /p_view/...).
|
|
35
31
|
*/
|
|
36
32
|
export function requireBucketView(req, res, next) {
|
|
37
33
|
if (req.path.startsWith('/p_view/')) return next();
|
|
38
34
|
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
|
-
}
|
|
35
|
+
if (!req.activeBucket) return sendViewError(res, new Error(NO_BUCKET_MSG));
|
|
42
36
|
next();
|
|
43
37
|
}
|
|
@@ -1,34 +1,26 @@
|
|
|
1
|
-
import { createLogger } from "
|
|
1
|
+
import { createLogger } from "#logger";
|
|
2
2
|
|
|
3
3
|
const debugServer = createLogger('server');
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*/
|
|
5
|
+
const BASE_ERROR_DATA = { layout: false, pagename: "Home", page: "/" };
|
|
6
|
+
|
|
8
7
|
export function notFoundHandler(req, res) {
|
|
9
8
|
debugServer("Path not found: %s %s", req.method, req.url);
|
|
10
9
|
return res.status(404).render("Error/dError.handlebars", {
|
|
11
|
-
|
|
10
|
+
...BASE_ERROR_DATA,
|
|
12
11
|
code: 404,
|
|
13
12
|
error: "Not Found",
|
|
14
13
|
message: "The requested page was not found.",
|
|
15
|
-
pagename: "Home",
|
|
16
|
-
page: "/",
|
|
17
14
|
});
|
|
18
15
|
}
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
* Global error handler — catches unhandled errors from routes/middleware.
|
|
22
|
-
*/
|
|
23
|
-
export function globalErrorHandler(err, req, res, _next) {
|
|
17
|
+
export function globalErrorHandler(err, _req, res, _next) {
|
|
24
18
|
console.error(`[mbkbucket] ${err.stack}`);
|
|
25
19
|
return res.status(500).render("Error/dError.handlebars", {
|
|
26
|
-
|
|
20
|
+
...BASE_ERROR_DATA,
|
|
27
21
|
code: 500,
|
|
28
22
|
error: "Internal app Error",
|
|
29
23
|
message: "An unexpected error occurred on the app.",
|
|
30
24
|
details: err.message,
|
|
31
|
-
pagename: "Home",
|
|
32
|
-
page: "/",
|
|
33
25
|
});
|
|
34
26
|
}
|
|
@@ -1,29 +1,21 @@
|
|
|
1
1
|
import rateLimit from "express-rate-limit";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* General rate limiter for typical browsing/API usage.
|
|
5
|
-
* 150 requests per 2 minutes per IP.
|
|
6
|
-
*/
|
|
7
3
|
export const generalLimiter = rateLimit({
|
|
8
4
|
windowMs: 2 * 60 * 1000, // 2 minutes
|
|
9
5
|
max: 150,
|
|
10
6
|
standardHeaders: true,
|
|
11
7
|
legacyHeaders: false,
|
|
12
|
-
handler: (
|
|
8
|
+
handler: (_req, res) => {
|
|
13
9
|
res.status(429).render('error.handlebars', { message: 'Too many requests from your IP. Try again later.', code: 429 });
|
|
14
10
|
}
|
|
15
11
|
});
|
|
16
12
|
|
|
17
|
-
/**
|
|
18
|
-
* Stricter rate limiter for public view (p_view) routes.
|
|
19
|
-
* 10 requests per minute per IP.
|
|
20
|
-
*/
|
|
21
13
|
export const pviewRateLimit = rateLimit({
|
|
22
14
|
windowMs: 60 * 1000, // 1 minute
|
|
23
15
|
max: 10,
|
|
24
16
|
standardHeaders: 'draft-7',
|
|
25
17
|
legacyHeaders: false,
|
|
26
|
-
keyGenerator:
|
|
18
|
+
keyGenerator: req => req.headers['x-forwarded-for']?.split(',')[0].trim() || req.socket.remoteAddress || 'unknown',
|
|
27
19
|
handler: (_req, res) => {
|
|
28
20
|
res.status(429).json({ message: 'Too many requests. Please slow down and try again later.' });
|
|
29
21
|
}
|
|
@@ -11,29 +11,13 @@ const SUSPICIOUS_HEADERS = [
|
|
|
11
11
|
'x-automated-tool'
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* p_view security middleware: blocks bots, sets security headers.
|
|
16
|
-
*/
|
|
17
14
|
export function pviewSecurity(req, res, next) {
|
|
18
15
|
const ua = req.headers['user-agent'] || '';
|
|
19
16
|
|
|
20
|
-
|
|
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']) {
|
|
17
|
+
if (!ua || BLOCKED_UA_PATTERNS.test(ua) || !req.headers.accept || SUSPICIOUS_HEADERS.some(h => h in req.headers)) {
|
|
33
18
|
return res.status(403).json({ message: 'Forbidden' });
|
|
34
19
|
}
|
|
35
20
|
|
|
36
|
-
// Set security headers for the public view response
|
|
37
21
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
38
22
|
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
39
23
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
@@ -0,0 +1,40 @@
|
|
|
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 { cleanupUploads, listIncompleteUploads, listFiles, uploadSingleFile, initiateMultipartUpload, uploadChunk, completeUpload, abortUpload, createFolder, deleteItems, downloadFileHandler } from "../controllers/api.controller.js";
|
|
6
|
+
|
|
7
|
+
const router = express.Router();
|
|
8
|
+
const MAX_CHUNK_SIZE = 50 * 1024 * 1024; // 50 MB
|
|
9
|
+
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: MAX_CHUNK_SIZE } });
|
|
10
|
+
|
|
11
|
+
const uploadChunkMiddleware = (req, res, next) => {
|
|
12
|
+
upload.single('chunk')(req, res, (err) => {
|
|
13
|
+
if (err?.code === 'LIMIT_FILE_SIZE') {
|
|
14
|
+
return res.status(413).json({
|
|
15
|
+
success: false,
|
|
16
|
+
error: `Chunk too large (max ${Math.round(MAX_CHUNK_SIZE / (1024 * 1024))} MB).`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (err) return next(err);
|
|
20
|
+
next();
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const adminAuth = sessRole('SuperAdmin');
|
|
25
|
+
|
|
26
|
+
router.use(requireBucketApi);
|
|
27
|
+
|
|
28
|
+
router.post('/api/cleanup-uploads', adminAuth, cleanupUploads);
|
|
29
|
+
router.get('/api/incomplete-uploads', adminAuth, listIncompleteUploads);
|
|
30
|
+
router.get('/api/files', adminAuth, listFiles);
|
|
31
|
+
router.post('/upload', adminAuth, upload.single('file'), uploadSingleFile);
|
|
32
|
+
router.post('/upload-init', adminAuth, initiateMultipartUpload);
|
|
33
|
+
router.post('/upload-chunk', adminAuth, uploadChunkMiddleware, uploadChunk);
|
|
34
|
+
router.post('/upload-complete', adminAuth, completeUpload);
|
|
35
|
+
router.post('/upload-abort', adminAuth, abortUpload);
|
|
36
|
+
router.post('/create-folder', adminAuth, createFolder);
|
|
37
|
+
router.post('/delete', adminAuth, deleteItems);
|
|
38
|
+
router.get('/download/*key', adminAuth, downloadFileHandler);
|
|
39
|
+
|
|
40
|
+
export default router;
|
|
@@ -8,15 +8,7 @@ import apiRoutes from "./api.routes.js";
|
|
|
8
8
|
|
|
9
9
|
const router = express.Router();
|
|
10
10
|
|
|
11
|
-
// Dashboard home
|
|
12
11
|
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);
|
|
12
|
+
router.use('/mbkbucket', bucketResolver, infoRoutes, viewRoutes, apiRoutes);
|
|
21
13
|
|
|
22
14
|
export default router;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
-
import { sessRole } from "mbkauthe";
|
|
3
2
|
import { infoPage, infoJson } from "../controllers/info.controller.js";
|
|
4
3
|
|
|
5
4
|
const router = express.Router();
|
|
6
5
|
|
|
7
|
-
router.get(['/info',
|
|
8
|
-
router.get(['/info.json',
|
|
6
|
+
router.get(['/info', '/i'], infoPage);
|
|
7
|
+
router.get(['/info.json', '/i.json'], infoJson);
|
|
9
8
|
|
|
10
9
|
export default router;
|
|
@@ -7,17 +7,12 @@ import { pviewSecurity } from "../middleware/security.js";
|
|
|
7
7
|
import { viewFile, playerPage, publicView } from "../controllers/view.controller.js";
|
|
8
8
|
|
|
9
9
|
const router = express.Router();
|
|
10
|
+
const adminAuth = sessRole('SuperAdmin');
|
|
10
11
|
|
|
11
|
-
// Bucket guard for view routes (skips p_view)
|
|
12
12
|
router.use(requireBucketView);
|
|
13
|
+
router.get('/player/*key', adminAuth, playerPage);
|
|
14
|
+
router.get('/view/*key', adminAuth, viewFile);
|
|
13
15
|
|
|
14
|
-
// Player page for video/audio
|
|
15
|
-
router.get('/player/*key', sessRole('SuperAdmin'), playerPage);
|
|
16
|
-
|
|
17
|
-
// Inline file view
|
|
18
|
-
router.get('/view/*key', sessRole('SuperAdmin'), viewFile);
|
|
19
|
-
|
|
20
|
-
// Public view (conditionally enabled)
|
|
21
16
|
if (mbkbucketVar?.publiView_enabled) {
|
|
22
17
|
router.get('/p_view/*key', pviewRateLimit, pviewSecurity, publicView);
|
|
23
18
|
}
|