@modern-admin/feature-upload 0.2.1 → 0.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/LICENSE +21 -0
- package/dist/graphql.d.ts.map +1 -1
- package/dist/graphql.js +23 -5
- package/dist/graphql.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mime.d.ts +21 -0
- package/dist/mime.d.ts.map +1 -0
- package/dist/mime.js +46 -0
- package/dist/mime.js.map +1 -0
- package/dist/nest/upload-sweeper.service.d.ts.map +1 -1
- package/dist/nest/upload-sweeper.service.js +7 -0
- package/dist/nest/upload-sweeper.service.js.map +1 -1
- package/dist/nest/upload.controller.d.ts.map +1 -1
- package/dist/nest/upload.controller.js +82 -15
- package/dist/nest/upload.controller.js.map +1 -1
- package/dist/nest/upload.module.d.ts +5 -0
- package/dist/nest/upload.module.d.ts.map +1 -1
- package/dist/nest/upload.module.js +5 -0
- package/dist/nest/upload.module.js.map +1 -1
- package/dist/nest/upload.tokens.d.ts +23 -0
- package/dist/nest/upload.tokens.d.ts.map +1 -1
- package/dist/path-safety.d.ts +45 -0
- package/dist/path-safety.d.ts.map +1 -0
- package/dist/path-safety.js +73 -0
- package/dist/path-safety.js.map +1 -0
- package/dist/pending-registry.d.ts +19 -4
- package/dist/pending-registry.d.ts.map +1 -1
- package/dist/pending-registry.js +19 -4
- package/dist/pending-registry.js.map +1 -1
- package/dist/providers/local.d.ts.map +1 -1
- package/dist/providers/local.js +16 -3
- package/dist/providers/local.js.map +1 -1
- package/dist/providers/s3.d.ts.map +1 -1
- package/dist/providers/s3.js +14 -0
- package/dist/providers/s3.js.map +1 -1
- package/dist/registry.d.ts +8 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js.map +1 -1
- package/dist/types.d.ts +10 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/upload-feature.d.ts.map +1 -1
- package/dist/upload-feature.js +30 -31
- package/dist/upload-feature.js.map +1 -1
- package/package.json +8 -7
- package/src/graphql.ts +22 -5
- package/src/index.ts +5 -0
- package/src/mime.ts +42 -0
- package/src/nest/upload-sweeper.service.ts +9 -0
- package/src/nest/upload.controller.ts +95 -14
- package/src/nest/upload.module.ts +5 -0
- package/src/nest/upload.tokens.ts +23 -0
- package/src/path-safety.ts +73 -0
- package/src/pending-registry.ts +19 -4
- package/src/providers/local.ts +15 -3
- package/src/providers/s3.ts +17 -0
- package/src/registry.ts +8 -0
- package/src/types.ts +10 -2
- package/src/upload-feature.ts +31 -36
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path-safety helpers shared by the upload providers and the upload
|
|
3
|
+
* entry points (REST controller + GraphQL extension).
|
|
4
|
+
*
|
|
5
|
+
* Storage keys and filenames are attacker-controllable in two ways:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Upload** — the client-supplied `originalName` flows into key
|
|
8
|
+
* generation (default `uuid + extname`, or a custom `uploadPath`). A raw
|
|
9
|
+
* name like `../../evil.sh` must never be able to steer the key outside
|
|
10
|
+
* the intended storage space.
|
|
11
|
+
* 2. **Delete** — a `type: 'file'` property is just a string in the DB. An
|
|
12
|
+
* attacker who can edit a record can write `../../../../etc/passwd` into
|
|
13
|
+
* it; the next edit/delete would call `provider.delete(key)` with that
|
|
14
|
+
* value. Without containment the local provider's `unlink` escapes the
|
|
15
|
+
* upload directory.
|
|
16
|
+
*
|
|
17
|
+
* Defense is layered: keys are rejected at the transport boundary
|
|
18
|
+
* (`isUnsafeKey`), filenames are sanitised before they can influence a key
|
|
19
|
+
* (`sanitizeFilename`), and the local provider resolves every path against its
|
|
20
|
+
* upload directory (`resolveWithinDir`) as the authoritative backstop.
|
|
21
|
+
*/
|
|
22
|
+
import { resolve, sep } from 'node:path';
|
|
23
|
+
/**
|
|
24
|
+
* True when `key` must be refused: empty, containing a NUL byte, absolute
|
|
25
|
+
* (POSIX `/…` or Windows `C:\…`), or containing a `..` traversal segment.
|
|
26
|
+
*
|
|
27
|
+
* Forward and back slashes are both treated as separators so a key crafted on
|
|
28
|
+
* one platform cannot escape on another. Legitimate nested keys
|
|
29
|
+
* (`avatars/2024/uuid.jpg`) and prefixes are allowed — only traversal and
|
|
30
|
+
* absolute paths are rejected.
|
|
31
|
+
*/
|
|
32
|
+
export function isUnsafeKey(key) {
|
|
33
|
+
if (typeof key !== 'string' || key.length === 0)
|
|
34
|
+
return true;
|
|
35
|
+
if (key.includes('\0'))
|
|
36
|
+
return true;
|
|
37
|
+
const normalized = key.replace(/\\/g, '/');
|
|
38
|
+
if (normalized.startsWith('/'))
|
|
39
|
+
return true; // POSIX absolute
|
|
40
|
+
if (/^[a-zA-Z]:/.test(normalized))
|
|
41
|
+
return true; // Windows drive-absolute
|
|
42
|
+
return normalized.split('/').some((segment) => segment === '..');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reduce a client-supplied filename to a safe basename that cannot influence
|
|
46
|
+
* the directory a key resolves into. Strips any directory components (either
|
|
47
|
+
* separator), NUL bytes, and leading dots (so `..` / hidden-traversal names
|
|
48
|
+
* collapse). Falls back to `'upload'` when nothing safe remains.
|
|
49
|
+
*/
|
|
50
|
+
export function sanitizeFilename(name) {
|
|
51
|
+
if (typeof name !== 'string' || name.length === 0)
|
|
52
|
+
return 'upload';
|
|
53
|
+
const base = name.replace(/\\/g, '/').split('/').pop() ?? '';
|
|
54
|
+
const cleaned = base
|
|
55
|
+
.replace(/\0/g, '')
|
|
56
|
+
.replace(/^\.+/, '') // no leading dots → no bare '..' / hidden traversal
|
|
57
|
+
.trim();
|
|
58
|
+
return cleaned.length > 0 ? cleaned : 'upload';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve `key` against `baseDir` and guarantee the result stays inside it.
|
|
62
|
+
* Throws when the resolved path escapes the directory (traversal / absolute
|
|
63
|
+
* key). Returns the absolute, contained path for the caller to read/write.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveWithinDir(baseDir, key) {
|
|
66
|
+
const base = resolve(baseDir);
|
|
67
|
+
const full = resolve(base, key);
|
|
68
|
+
if (full !== base && !full.startsWith(base + sep)) {
|
|
69
|
+
throw new Error(`[modern-admin/feature-upload] refusing to access "${key}" outside the upload directory`);
|
|
70
|
+
}
|
|
71
|
+
return full;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=path-safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-safety.js","sourceRoot":"","sources":["../src/path-safety.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAExC;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC5D,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IACnC,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;IAC1C,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA,CAAC,iBAAiB;IAC7D,IAAI,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA,CAAC,yBAAyB;IACxE,OAAO,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,KAAK,IAAI,CAAC,CAAA;AAClE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAa;IAC5C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAA;IAClE,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAA;IAC5D,MAAM,OAAO,GAAG,IAAI;SACjB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;SAClB,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,oDAAoD;SACxE,IAAI,EAAE,CAAA;IACT,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAA;AAChD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,GAAW;IAC3D,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IAC/B,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CACb,qDAAqD,GAAG,gCAAgC,CACzF,CAAA;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
|
|
@@ -27,13 +27,28 @@
|
|
|
27
27
|
* The registry is a process-level singleton (same pattern as
|
|
28
28
|
* `UploadProviderRegistry`).
|
|
29
29
|
*
|
|
30
|
+
* ⚠️ SINGLE-INSTANCE ONLY (until a shared store lands).
|
|
31
|
+
* ------------------------------------------------------
|
|
32
|
+
* This `Map` lives in one process's memory. Behind a load balancer with ≥2
|
|
33
|
+
* replicas it is UNSAFE and can lose data:
|
|
34
|
+
*
|
|
35
|
+
* 1. Replica A handles `POST /upload` → `track(key)` in A's map only.
|
|
36
|
+
* 2. Replica B handles the form save → `confirm(key)` is a no-op (B never
|
|
37
|
+
* saw the pending entry).
|
|
38
|
+
* 3. A's sweeper still considers the key pending; once the TTL elapses it
|
|
39
|
+
* DELETES the file the record now references → broken/missing upload.
|
|
40
|
+
*
|
|
41
|
+
* The provider *registry* key is deterministic (`up_<resourceId>_<prop>`), so
|
|
42
|
+
* uploads still ROUTE correctly across replicas — but the pending lifecycle
|
|
43
|
+
* above does not. Run the upload feature single-instance, OR set a large
|
|
44
|
+
* `pendingTtlMs` to widen the confirm window, until the roadmap item below
|
|
45
|
+
* replaces this with a shared store.
|
|
46
|
+
*
|
|
30
47
|
* TODO(roadmap): swap the in-process `Map` for a Redis-backed store and move
|
|
31
48
|
* the sweeper driver (`UploadSweeperService`) onto BullMQ. This is required
|
|
32
49
|
* for multi-instance deployments where one Nest replica serves the upload
|
|
33
|
-
* request and a different replica processes the form submission
|
|
34
|
-
*
|
|
35
|
-
* confirm hook would no-op, and the file would be swept on the originator.
|
|
36
|
-
* BullMQ also gives us crash-safe scheduling and retries.
|
|
50
|
+
* request and a different replica processes the form submission. BullMQ also
|
|
51
|
+
* gives us crash-safe scheduling and retries.
|
|
37
52
|
*/
|
|
38
53
|
export declare const PendingUploadsRegistry: {
|
|
39
54
|
/** Mark `key` as a freshly-uploaded, unconfirmed file. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pending-registry.d.ts","sourceRoot":"","sources":["../src/pending-registry.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pending-registry.d.ts","sourceRoot":"","sources":["../src/pending-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAaH,eAAO,MAAM,sBAAsB;IACjC,0DAA0D;eAC/C,MAAM,cAAc,MAAM,SAAS,MAAM,GAAG,IAAI;IAI3D,iFAAiF;aACxE,MAAM,GAAG,OAAO;IAIzB,2EAA2E;kBAC7D,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI;IAI1C;;;OAGG;gBACe,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc3C;;;OAGG;gBACc,MAAM,GAAgB,OAAO,CAAC,MAAM,CAAC;IAkBtD,wBAAwB;aACf,IAAI;IAIb,uEAAuE;YAC/D,MAAM;CAGf,CAAA"}
|
package/dist/pending-registry.js
CHANGED
|
@@ -27,13 +27,28 @@
|
|
|
27
27
|
* The registry is a process-level singleton (same pattern as
|
|
28
28
|
* `UploadProviderRegistry`).
|
|
29
29
|
*
|
|
30
|
+
* ⚠️ SINGLE-INSTANCE ONLY (until a shared store lands).
|
|
31
|
+
* ------------------------------------------------------
|
|
32
|
+
* This `Map` lives in one process's memory. Behind a load balancer with ≥2
|
|
33
|
+
* replicas it is UNSAFE and can lose data:
|
|
34
|
+
*
|
|
35
|
+
* 1. Replica A handles `POST /upload` → `track(key)` in A's map only.
|
|
36
|
+
* 2. Replica B handles the form save → `confirm(key)` is a no-op (B never
|
|
37
|
+
* saw the pending entry).
|
|
38
|
+
* 3. A's sweeper still considers the key pending; once the TTL elapses it
|
|
39
|
+
* DELETES the file the record now references → broken/missing upload.
|
|
40
|
+
*
|
|
41
|
+
* The provider *registry* key is deterministic (`up_<resourceId>_<prop>`), so
|
|
42
|
+
* uploads still ROUTE correctly across replicas — but the pending lifecycle
|
|
43
|
+
* above does not. Run the upload feature single-instance, OR set a large
|
|
44
|
+
* `pendingTtlMs` to widen the confirm window, until the roadmap item below
|
|
45
|
+
* replaces this with a shared store.
|
|
46
|
+
*
|
|
30
47
|
* TODO(roadmap): swap the in-process `Map` for a Redis-backed store and move
|
|
31
48
|
* the sweeper driver (`UploadSweeperService`) onto BullMQ. This is required
|
|
32
49
|
* for multi-instance deployments where one Nest replica serves the upload
|
|
33
|
-
* request and a different replica processes the form submission
|
|
34
|
-
*
|
|
35
|
-
* confirm hook would no-op, and the file would be swept on the originator.
|
|
36
|
-
* BullMQ also gives us crash-safe scheduling and retries.
|
|
50
|
+
* request and a different replica processes the form submission. BullMQ also
|
|
51
|
+
* gives us crash-safe scheduling and retries.
|
|
37
52
|
*/
|
|
38
53
|
import { UploadProviderRegistry } from './registry.js';
|
|
39
54
|
const _pending = new Map();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pending-registry.js","sourceRoot":"","sources":["../src/pending-registry.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pending-registry.js","sourceRoot":"","sources":["../src/pending-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAStD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAwB,CAAA;AAEhD,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,0DAA0D;IAC1D,KAAK,CAAC,GAAW,EAAE,UAAkB,EAAE,KAAa;QAClD,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,CAAA;IAClE,CAAC;IAED,iFAAiF;IACjF,GAAG,CAAC,GAAW;QACb,OAAO,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAED,2EAA2E;IAC3E,OAAO,CAAC,IAA2B;QACjC,KAAK,MAAM,CAAC,IAAI,IAAI;YAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAC1C,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC/B,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAA;QACxB,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACpB,MAAM,GAAG,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;QACxD,IAAI,CAAC,GAAG;YAAE,OAAO,KAAK,CAAA;QACtB,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,mCAAmC;QACrC,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;QAClC,IAAI,KAAK,GAAG,CAAC,CAAA;QACb,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,QAAQ,EAAE,CAAC;YACpC,IAAI,KAAK,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC;gBAC3B,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;gBACpB,MAAM,GAAG,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;gBACxD,IAAI,CAAC,GAAG;oBAAE,SAAQ;gBAClB,IAAI,CAAC;oBACH,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;oBAC9B,KAAK,EAAE,CAAA;gBACT,CAAC;gBAAC,MAAM,CAAC;oBACP,aAAa;gBACf,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,wBAAwB;IACxB,KAAK;QACH,QAAQ,CAAC,KAAK,EAAE,CAAA;IAClB,CAAC;IAED,uEAAuE;IACvE,IAAI;QACF,OAAO,QAAQ,CAAC,IAAI,CAAA;IACtB,CAAC;CACF,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/providers/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/providers/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAEhE,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,qBAAa,mBAAoB,YAAW,eAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,kBAAkB;IAElD,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAW/D,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAKrB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBxC,WAAW,IAAI,MAAM;CAItB"}
|
package/dist/providers/local.js
CHANGED
|
@@ -9,15 +9,18 @@
|
|
|
9
9
|
* new LocalUploadProvider({ uploadDir: './public/uploads', baseUrl: '/uploads' })
|
|
10
10
|
*/
|
|
11
11
|
import { mkdir, writeFile, unlink } from 'node:fs/promises';
|
|
12
|
-
import {
|
|
12
|
+
import { extname, dirname } from 'node:path';
|
|
13
13
|
import { uuidv7 } from '@modern-admin/core';
|
|
14
|
+
import { resolveWithinDir } from '../path-safety.js';
|
|
14
15
|
export class LocalUploadProvider {
|
|
15
16
|
constructor(options) {
|
|
16
17
|
this.options = options;
|
|
17
18
|
}
|
|
18
19
|
async upload(file, key) {
|
|
19
20
|
const resolvedKey = key ?? `${uuidv7()}${extname(file.originalName)}`;
|
|
20
|
-
|
|
21
|
+
// Contain the key inside uploadDir — refuses traversal/absolute keys
|
|
22
|
+
// (e.g. a malicious `uploadPath` or `../../evil.sh`).
|
|
23
|
+
const dest = resolveWithinDir(this.options.uploadDir, resolvedKey);
|
|
21
24
|
// Create the full directory tree (handles nested keys like 'avatars/2024/01/uuid.jpg').
|
|
22
25
|
await mkdir(dirname(dest), { recursive: true });
|
|
23
26
|
await writeFile(dest, file.buffer);
|
|
@@ -28,8 +31,18 @@ export class LocalUploadProvider {
|
|
|
28
31
|
return `${base}/${key}`;
|
|
29
32
|
}
|
|
30
33
|
async delete(key) {
|
|
34
|
+
let dest;
|
|
31
35
|
try {
|
|
32
|
-
|
|
36
|
+
// Refuse to unlink anything outside uploadDir — a `type: 'file'` value
|
|
37
|
+
// is an arbitrary DB string, so `key` may be attacker-controlled
|
|
38
|
+
// (`../../../../app/src/main.ts`). Never delete outside the directory.
|
|
39
|
+
dest = resolveWithinDir(this.options.uploadDir, key);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await unlink(dest);
|
|
33
46
|
}
|
|
34
47
|
catch (err) {
|
|
35
48
|
if (err.code !== 'ENOENT')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/providers/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/providers/local.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAoBpD,MAAM,OAAO,mBAAmB;IAC9B,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;IAE5D,KAAK,CAAC,MAAM,CAAC,IAAkB,EAAE,GAAY;QAC3C,MAAM,WAAW,GAAG,GAAG,IAAI,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAA;QACrE,qEAAqE;QACrE,sDAAsD;QACtD,MAAM,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAA;QAClE,wFAAwF;QACxF,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAC/C,MAAM,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;QAClC,OAAO,WAAW,CAAA;IACpB,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,UAAU,CAAA;QAC/C,OAAO,GAAG,IAAI,IAAI,GAAG,EAAE,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,IAAI,IAAY,CAAA;QAChB,IAAI,CAAC;YACH,uEAAuE;YACvE,iEAAiE;YACjE,uEAAuE;YACvE,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;QACtD,CAAC;QAAC,MAAM,CAAC;YACP,OAAM;QACR,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,CAAA;QACpB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,GAAG,CAAA;QACjE,CAAC;IACH,CAAC;IAED,WAAW;QACT,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,UAAU,CAAA;QAC/C,OAAO,GAAG,IAAI,QAAQ,CAAA;IACxB,CAAC;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;
|
|
1
|
+
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAEhE,MAAM,WAAW,eAAe;IAC9B,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2EAA2E;IAC3E,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC1C;AAED,qBAAa,gBAAiB,YAAW,eAAe;IAKpD,OAAO,CAAC,QAAQ,CAAC,OAAO;IAIxB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;IAPlC,OAAO,CAAC,OAAO,CAAY;gBAGR,OAAO,EAAE,eAAe,EAIxB,cAAc,CAAC,EAAE,GAAG,YAAA;YAIzB,MAAM;IA4Bd,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmDzD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAYpC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBxC;;;;OAIG;IACH,WAAW,IAAI,MAAM,GAAG,SAAS;YAKnB,SAAS;IA0BvB,OAAO,CAAC,aAAa;CAKtB"}
|
package/dist/providers/s3.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { extname } from 'node:path';
|
|
29
29
|
import { uuidv7 } from '@modern-admin/core';
|
|
30
|
+
import { isUnsafeKey } from '../path-safety.js';
|
|
30
31
|
export class S3UploadProvider {
|
|
31
32
|
constructor(options,
|
|
32
33
|
// Accept a pre-configured S3Client instance (share across providers /
|
|
@@ -65,6 +66,12 @@ export class S3UploadProvider {
|
|
|
65
66
|
return this._client;
|
|
66
67
|
}
|
|
67
68
|
async upload(file, key) {
|
|
69
|
+
// Reject traversal / absolute keys before they reach the bucket. Keys
|
|
70
|
+
// legitimately contain `/` separators (prefixes), but never `..` or a
|
|
71
|
+
// leading slash.
|
|
72
|
+
if (key !== undefined && isUnsafeKey(key)) {
|
|
73
|
+
throw new Error(`[modern-admin/feature-upload] refusing to upload with unsafe key "${key}"`);
|
|
74
|
+
}
|
|
68
75
|
const c = await this.client();
|
|
69
76
|
const ext = extname(file.originalName);
|
|
70
77
|
const prefix = this.options.prefix ? `${this.options.prefix}/` : '';
|
|
@@ -109,12 +116,19 @@ export class S3UploadProvider {
|
|
|
109
116
|
return resolvedKey;
|
|
110
117
|
}
|
|
111
118
|
async getUrl(key) {
|
|
119
|
+
if (isUnsafeKey(key)) {
|
|
120
|
+
throw new Error(`[modern-admin/feature-upload] refusing to build a URL for unsafe key "${key}"`);
|
|
121
|
+
}
|
|
112
122
|
if (this.options.signed) {
|
|
113
123
|
return this.signedUrl(key);
|
|
114
124
|
}
|
|
115
125
|
return `${this.publicBaseUrl()}/${key}`;
|
|
116
126
|
}
|
|
117
127
|
async delete(key) {
|
|
128
|
+
// A `type: 'file'` value is an arbitrary DB string — never issue a
|
|
129
|
+
// DeleteObject for a traversal/absolute key.
|
|
130
|
+
if (isUnsafeKey(key))
|
|
131
|
+
return;
|
|
118
132
|
try {
|
|
119
133
|
const sdk = await import('@aws-sdk/client-s3');
|
|
120
134
|
const c = await this.client();
|
package/dist/providers/s3.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"s3.js","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"s3.js","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"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"}
|
package/dist/registry.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export interface RegisteredUploadConfig {
|
|
|
17
17
|
uploadPath?: (filename: string) => string;
|
|
18
18
|
/** True for multi-file properties — controller will accept N files per request. */
|
|
19
19
|
isArray?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Allowed MIME patterns (HTML `accept` syntax). Enforced server-side by the
|
|
22
|
+
* upload controller / GraphQL resolver — a request whose file declares a
|
|
23
|
+
* non-matching type is rejected. `undefined`/empty means no restriction.
|
|
24
|
+
*/
|
|
25
|
+
mimeTypes?: string[];
|
|
26
|
+
/** Maximum accepted file size in bytes. Enforced server-side (per file). */
|
|
27
|
+
maxSize?: number;
|
|
20
28
|
}
|
|
21
29
|
export declare const UploadProviderRegistry: {
|
|
22
30
|
register(id: string, config: RegisteredUploadConfig): void;
|
package/dist/registry.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD,0DAA0D;AAC1D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,eAAe,CAAA;IACzB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAA;IACzC,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD,0DAA0D;AAC1D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,eAAe,CAAA;IACzB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAA;IACzC,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAID,eAAO,MAAM,sBAAsB;iBACpB,MAAM,UAAU,sBAAsB,GAAG,IAAI;YAIlD,MAAM,GAAG,sBAAsB,GAAG,SAAS;IAInD,wBAAwB;aACf,IAAI;CAGd,CAAA"}
|
package/dist/registry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAqBH,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkC,CAAA;AAE3D,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,QAAQ,CAAC,EAAU,EAAE,MAA8B;QACjD,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;IAC3B,CAAC;IAED,GAAG,CAAC,EAAU;QACZ,OAAO,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAC1B,CAAC;IAED,wBAAwB;IACxB,KAAK;QACH,SAAS,CAAC,KAAK,EAAE,CAAA;IACnB,CAAC;CACF,CAAA"}
|
package/dist/types.d.ts
CHANGED
|
@@ -76,10 +76,18 @@ export interface UploadPropertyConfig {
|
|
|
76
76
|
/**
|
|
77
77
|
* Allowed MIME type patterns (same syntax as the HTML `accept` attribute).
|
|
78
78
|
* Examples: `['image/*']`, `['image/jpeg', 'application/pdf']`.
|
|
79
|
-
*
|
|
79
|
+
* Enforced both on the frontend (UX) and server-side — the upload controller
|
|
80
|
+
* / GraphQL resolver rejects a file whose declared type does not match. The
|
|
81
|
+
* server match is on the *declared* Content-Type (defense-in-depth, not
|
|
82
|
+
* content sniffing).
|
|
80
83
|
*/
|
|
81
84
|
mimeTypes?: string[];
|
|
82
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Maximum upload size in bytes. Enforced server-side per file (the multipart
|
|
87
|
+
* parser aborts an oversized stream) in addition to the frontend warning.
|
|
88
|
+
* A module-wide hard cap (`ModernAdminUploadModule.forRoot({ maxFileSize })`,
|
|
89
|
+
* default 25 MiB) always applies on top, even when this is unset.
|
|
90
|
+
*/
|
|
83
91
|
maxSize?: number;
|
|
84
92
|
/**
|
|
85
93
|
* Treat the property as an array of file keys (multi-file upload).
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,0DAA0D;AAC1D,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAA;IACpB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAA;IAChB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAA;IACX,iDAAiD;IACjD,GAAG,EAAE,MAAM,CAAA;IACX,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,iBAAiB;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAEzD;;;;OAIG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAE7C;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAElC;;;;;;OAMG;IACH,WAAW,CAAC,IAAI,MAAM,GAAG,SAAS,CAAA;CACnC;AAED,qEAAqE;AACrE,MAAM,WAAW,oBAAoB;IACnC,0CAA0C;IAC1C,QAAQ,EAAE,eAAe,CAAA;IACzB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,0DAA0D;AAC1D,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAA;IACpB,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAA;IAChB,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,gDAAgD;AAChD,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAA;IACX,iDAAiD;IACjD,GAAG,EAAE,MAAM,CAAA;IACX,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,iBAAiB;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAEzD;;;;OAIG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAE7C;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAElC;;;;;;OAMG;IACH,WAAW,CAAC,IAAI,MAAM,GAAG,SAAS,CAAA;CACnC;AAED,qEAAqE;AACrE,MAAM,WAAW,oBAAoB;IACnC,0CAA0C;IAC1C,QAAQ,EAAE,eAAe,CAAA;IACzB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;IACpB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C;AAED,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;;;;;OAYG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;CACjD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-feature.d.ts","sourceRoot":"","sources":["../src/upload-feature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"upload-feature.d.ts","sourceRoot":"","sources":["../src/upload-feature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAoE,KAAK,SAAS,EAAwB,MAAM,oBAAoB,CAAA;AAC3I,OAAO,KAAK,EAAE,oBAAoB,EAAwB,MAAM,YAAY,CAAA;AA8B5E,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CA+ItE"}
|
package/dist/upload-feature.js
CHANGED
|
@@ -25,24 +25,9 @@
|
|
|
25
25
|
* },
|
|
26
26
|
* })
|
|
27
27
|
*/
|
|
28
|
-
import { uuidv7 } from '@modern-admin/core';
|
|
28
|
+
import { appendAfterHook, uuidv7 } from '@modern-admin/core';
|
|
29
29
|
import { UploadProviderRegistry } from './registry.js';
|
|
30
30
|
import { PendingUploadsRegistry } from './pending-registry.js';
|
|
31
|
-
/** Normalise a hook value (fn | fn[] | undefined) into an array. */
|
|
32
|
-
function toArray(hook) {
|
|
33
|
-
if (!hook)
|
|
34
|
-
return [];
|
|
35
|
-
return Array.isArray(hook) ? hook : [hook];
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Merge `after` hooks from an existing action override with new hooks,
|
|
39
|
-
* producing an array that runs all of them in order.
|
|
40
|
-
* If `existing` already defines `after`, the new hooks are appended.
|
|
41
|
-
*/
|
|
42
|
-
function mergeAfterHook(existing, newHook) {
|
|
43
|
-
const existing_after = toArray(existing?.after);
|
|
44
|
-
return [...existing_after, newHook];
|
|
45
|
-
}
|
|
46
31
|
// ─── Value helpers ────────────────────────────────────────────────────────────
|
|
47
32
|
/** Return non-empty file keys for the given value (handles single + array). */
|
|
48
33
|
function toKeys(value) {
|
|
@@ -55,18 +40,32 @@ function toKeys(value) {
|
|
|
55
40
|
}
|
|
56
41
|
// ─── Feature function ─────────────────────────────────────────────────────────
|
|
57
42
|
export function uploadFeature(options) {
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
43
|
+
// Precompute per-property config plus a process-local fallback id, used only
|
|
44
|
+
// when the FeatureFn is invoked without a resource (e.g. unit tests calling
|
|
45
|
+
// it directly). In production the id is derived from `resource.id()` below.
|
|
46
|
+
const props = Object.entries(options.properties).map(([propPath, config]) => ({
|
|
47
|
+
propPath,
|
|
48
|
+
config,
|
|
49
|
+
fallbackId: `up_${uuidv7().replace(/-/g, '')}`,
|
|
50
|
+
}));
|
|
51
|
+
return (resourceOptions, resource) => {
|
|
52
|
+
// Register providers with a DETERMINISTIC id derived from the resource id
|
|
53
|
+
// + property path. Every replica behind a load balancer computes the same
|
|
54
|
+
// id, so a property served by replica A resolves on replica B's registry
|
|
55
|
+
// (a per-process UUID would 500 on the "wrong" replica). Registration
|
|
56
|
+
// happens here (at bootstrap decorate time), before any request is served.
|
|
57
|
+
const registered = new Map();
|
|
58
|
+
for (const { propPath, config, fallbackId } of props) {
|
|
59
|
+
const providerId = resource ? `up_${resource.id()}_${propPath}` : fallbackId;
|
|
60
|
+
UploadProviderRegistry.register(providerId, {
|
|
61
|
+
provider: config.provider,
|
|
62
|
+
uploadPath: config.uploadPath,
|
|
63
|
+
isArray: config.isArray ?? false,
|
|
64
|
+
...(config.mimeTypes ? { mimeTypes: config.mimeTypes } : {}),
|
|
65
|
+
...(config.maxSize != null ? { maxSize: config.maxSize } : {}),
|
|
66
|
+
});
|
|
67
|
+
registered.set(propPath, { providerId, config });
|
|
68
|
+
}
|
|
70
69
|
// --- Property overrides ---
|
|
71
70
|
const propOverrides = {};
|
|
72
71
|
for (const [propPath, { providerId, config }] of registered) {
|
|
@@ -146,15 +145,15 @@ export function uploadFeature(options) {
|
|
|
146
145
|
const actionOverrides = {
|
|
147
146
|
new: {
|
|
148
147
|
...existingNew,
|
|
149
|
-
after:
|
|
148
|
+
after: appendAfterHook(existingNew, newAfterHook),
|
|
150
149
|
},
|
|
151
150
|
edit: {
|
|
152
151
|
...existingEdit,
|
|
153
|
-
after:
|
|
152
|
+
after: appendAfterHook(existingEdit, editAfterHook),
|
|
154
153
|
},
|
|
155
154
|
delete: {
|
|
156
155
|
...existingDelete,
|
|
157
|
-
after:
|
|
156
|
+
after: appendAfterHook(existingDelete, deleteAfterHook),
|
|
158
157
|
},
|
|
159
158
|
};
|
|
160
159
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-feature.js","sourceRoot":"","sources":["../src/upload-feature.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,MAAM,EAAiF,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"upload-feature.js","sourceRoot":"","sources":["../src/upload-feature.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modern-admin/feature-upload",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "File upload feature plugin for Modern Admin (local + S3 providers).",
|
|
5
5
|
"homepage": "https://github.com/modern-admin/modern-admin/tree/main/packages/feature-upload#readme",
|
|
6
6
|
"keywords": [
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"feature"
|
|
15
15
|
],
|
|
16
16
|
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
17
18
|
"license": "MIT",
|
|
18
19
|
"repository": {
|
|
19
20
|
"type": "git",
|
|
@@ -61,7 +62,7 @@
|
|
|
61
62
|
}
|
|
62
63
|
},
|
|
63
64
|
"dependencies": {
|
|
64
|
-
"@modern-admin/core": "^0.
|
|
65
|
+
"@modern-admin/core": "^0.3.0",
|
|
65
66
|
"@nestjs/common": "^11.1.19",
|
|
66
67
|
"@nestjs/core": "^11.1.19",
|
|
67
68
|
"busboy": "^1.6.0",
|
|
@@ -69,19 +70,19 @@
|
|
|
69
70
|
"rxjs": "^7.8.2"
|
|
70
71
|
},
|
|
71
72
|
"devDependencies": {
|
|
72
|
-
"@modern-admin/graphql": "0.
|
|
73
|
-
"@modern-admin/nest": "0.
|
|
74
|
-
"@modern-admin/tsconfig": "0.
|
|
73
|
+
"@modern-admin/graphql": "0.3.0",
|
|
74
|
+
"@modern-admin/nest": "0.3.0",
|
|
75
|
+
"@modern-admin/tsconfig": "0.3.0",
|
|
75
76
|
"@nestjs/swagger": "^11.4.2",
|
|
76
77
|
"@types/bun": "^1.3.13",
|
|
77
78
|
"@types/busboy": "^1.5.4",
|
|
78
|
-
"graphql": "^
|
|
79
|
+
"graphql": "^17.0.2",
|
|
79
80
|
"typescript": "^6.0.3"
|
|
80
81
|
},
|
|
81
82
|
"peerDependencies": {
|
|
82
83
|
"@aws-sdk/client-s3": "^3.0.0",
|
|
83
84
|
"@nestjs/swagger": "^11.4.2",
|
|
84
|
-
"graphql": "^
|
|
85
|
+
"graphql": "^17.0.0"
|
|
85
86
|
},
|
|
86
87
|
"peerDependenciesMeta": {
|
|
87
88
|
"@aws-sdk/client-s3": {
|
package/src/graphql.ts
CHANGED
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
import { ForbiddenError, ResourceNotFoundError } from '@modern-admin/core'
|
|
40
40
|
import { UploadProviderRegistry } from './registry.js'
|
|
41
41
|
import { PendingUploadsRegistry } from './pending-registry.js'
|
|
42
|
+
import { isUnsafeKey, sanitizeFilename } from './path-safety.js'
|
|
43
|
+
import { mimeMatches } from './mime.js'
|
|
42
44
|
import type { UploadedFileInfo } from './types.js'
|
|
43
45
|
|
|
44
46
|
/** Default TTL for pending upload entries, mirrored from the controller. */
|
|
@@ -93,12 +95,26 @@ export function uploadGraphqlExtension(
|
|
|
93
95
|
size: number
|
|
94
96
|
buffer: Buffer
|
|
95
97
|
}
|
|
98
|
+
// Server-side MIME / size enforcement (the multipart parser has
|
|
99
|
+
// already applied the global hard size cap; this adds the tighter
|
|
100
|
+
// per-property limits).
|
|
101
|
+
if (!mimeMatches(upload.mimeType, registered.mimeTypes)) {
|
|
102
|
+
throw new Error(`File type "${upload.mimeType}" is not allowed`)
|
|
103
|
+
}
|
|
104
|
+
if (registered.maxSize != null && upload.size > registered.maxSize) {
|
|
105
|
+
throw new Error(`File exceeds the maximum size of ${registered.maxSize} bytes`)
|
|
106
|
+
}
|
|
107
|
+
// Sanitise the client filename before it can steer key generation.
|
|
108
|
+
const originalName = sanitizeFilename(upload.filename)
|
|
96
109
|
const computedKey = registered.uploadPath
|
|
97
|
-
? registered.uploadPath(
|
|
110
|
+
? registered.uploadPath(originalName)
|
|
98
111
|
: undefined
|
|
112
|
+
if (computedKey !== undefined && isUnsafeKey(computedKey)) {
|
|
113
|
+
throw new Error('Computed upload key is not allowed')
|
|
114
|
+
}
|
|
99
115
|
const key = await registered.provider.upload(
|
|
100
116
|
{
|
|
101
|
-
originalName
|
|
117
|
+
originalName,
|
|
102
118
|
mimeType: upload.mimeType,
|
|
103
119
|
size: upload.size,
|
|
104
120
|
buffer: upload.buffer,
|
|
@@ -110,7 +126,7 @@ export function uploadGraphqlExtension(
|
|
|
110
126
|
const info: UploadedFileInfo = {
|
|
111
127
|
key,
|
|
112
128
|
url,
|
|
113
|
-
name:
|
|
129
|
+
name: originalName,
|
|
114
130
|
size: upload.size,
|
|
115
131
|
mimeType: upload.mimeType,
|
|
116
132
|
}
|
|
@@ -137,6 +153,7 @@ export function uploadGraphqlExtension(
|
|
|
137
153
|
// the REST controller's check — prevents leaking arbitrary
|
|
138
154
|
// resource ids to authenticated clients.
|
|
139
155
|
resolveProperty(ctx, resourceId, field)
|
|
156
|
+
if (isUnsafeKey(key)) return false
|
|
140
157
|
return PendingUploadsRegistry.cancel(key)
|
|
141
158
|
},
|
|
142
159
|
},
|
|
@@ -160,8 +177,8 @@ function resolveProperty(
|
|
|
160
177
|
try {
|
|
161
178
|
resource = ctx.admin.findResource(resourceId)
|
|
162
179
|
} catch (err) {
|
|
163
|
-
if (err instanceof ResourceNotFoundError) throw new Error(err.message)
|
|
164
|
-
if (err instanceof ForbiddenError) throw new Error(err.message)
|
|
180
|
+
if (err instanceof ResourceNotFoundError) throw new Error(err.message, { cause: err })
|
|
181
|
+
if (err instanceof ForbiddenError) throw new Error(err.message, { cause: err })
|
|
165
182
|
throw err
|
|
166
183
|
}
|
|
167
184
|
const decorator = resource.decorate()
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,11 @@ export { uploadFeature } from './upload-feature.js'
|
|
|
8
8
|
export { UploadProviderRegistry } from './registry.js'
|
|
9
9
|
export { PendingUploadsRegistry } from './pending-registry.js'
|
|
10
10
|
|
|
11
|
+
// Path-safety helpers (shared by providers + custom upload backends).
|
|
12
|
+
export { isUnsafeKey, sanitizeFilename, resolveWithinDir } from './path-safety.js'
|
|
13
|
+
// Server-side MIME allow-list matcher (HTML `accept` syntax).
|
|
14
|
+
export { mimeMatches } from './mime.js'
|
|
15
|
+
|
|
11
16
|
// Built-in providers
|
|
12
17
|
export { LocalUploadProvider, type LocalUploadOptions } from './providers/local.js'
|
|
13
18
|
export { S3UploadProvider, type S3UploadOptions } from './providers/s3.js'
|