@lenne.tech/nest-server 11.32.3 → 11.33.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/.claude/rules/architecture.md +16 -0
- package/.claude/rules/configurable-features.md +11 -3
- package/.claude/rules/testing.md +101 -1
- package/CLAUDE.md +5 -0
- package/FRAMEWORK-API.md +58 -3
- package/bin/migrate.js +13 -3
- package/dist/core/common/helpers/content-disposition.helper.d.ts +1 -0
- package/dist/core/common/helpers/content-disposition.helper.js +14 -0
- package/dist/core/common/helpers/content-disposition.helper.js.map +1 -0
- package/dist/core/common/helpers/file.helper.d.ts +21 -2
- package/dist/core/common/helpers/file.helper.js +69 -13
- package/dist/core/common/helpers/file.helper.js.map +1 -1
- package/dist/core/common/helpers/graceful-shutdown.helper.d.ts +2 -0
- package/dist/core/common/helpers/graceful-shutdown.helper.js +55 -0
- package/dist/core/common/helpers/graceful-shutdown.helper.js.map +1 -0
- package/dist/core/common/helpers/project-name.helper.d.ts +2 -0
- package/dist/core/common/helpers/project-name.helper.js +38 -0
- package/dist/core/common/helpers/project-name.helper.js.map +1 -0
- package/dist/core/common/interfaces/cron-job-config.interface.d.ts +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +39 -0
- package/dist/core/common/services/core-cron-jobs.initializer.d.ts +10 -0
- package/dist/core/common/services/core-cron-jobs.initializer.js +46 -0
- package/dist/core/common/services/core-cron-jobs.initializer.js.map +1 -0
- package/dist/core/common/services/core-cron-jobs.registry.d.ts +8 -0
- package/dist/core/common/services/core-cron-jobs.registry.js +12 -0
- package/dist/core/common/services/core-cron-jobs.registry.js.map +1 -0
- package/dist/core/common/services/core-cron-jobs.service.d.ts +37 -7
- package/dist/core/common/services/core-cron-jobs.service.js +278 -27
- package/dist/core/common/services/core-cron-jobs.service.js.map +1 -1
- package/dist/core/common/services/core-redis-pubsub.d.ts +20 -0
- package/dist/core/common/services/core-redis-pubsub.js +112 -0
- package/dist/core/common/services/core-redis-pubsub.js.map +1 -0
- package/dist/core/common/services/core-redis.service.d.ts +25 -0
- package/dist/core/common/services/core-redis.service.js +154 -0
- package/dist/core/common/services/core-redis.service.js.map +1 -0
- package/dist/core/common/services/core-s3.service.d.ts +51 -0
- package/dist/core/common/services/core-s3.service.js +261 -0
- package/dist/core/common/services/core-s3.service.js.map +1 -0
- package/dist/core/common/services/core-trust-proxy.initializer.d.ts +16 -0
- package/dist/core/common/services/core-trust-proxy.initializer.js +77 -0
- package/dist/core/common/services/core-trust-proxy.initializer.js.map +1 -0
- package/dist/core/common/services/rate-limit-store.d.ts +61 -0
- package/dist/core/common/services/rate-limit-store.js +235 -0
- package/dist/core/common/services/rate-limit-store.js.map +1 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +19 -3
- package/dist/core/modules/ai/core-ai-mcp.controller.js +148 -18
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai.service.d.ts +7 -2
- package/dist/core/modules/ai/services/core-ai.service.js +24 -20
- package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
- package/dist/core/modules/auth/core-auth.module.js +4 -1
- package/dist/core/modules/auth/core-auth.module.js.map +1 -1
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +1 -1
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +3 -14
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -1
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +12 -10
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +30 -59
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +8 -2
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +118 -45
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -11
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +12 -10
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +30 -85
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +0 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/file/core-file.controller.d.ts +15 -3
- package/dist/core/modules/file/core-file.controller.js +90 -20
- package/dist/core/modules/file/core-file.controller.js.map +1 -1
- package/dist/core/modules/file/core-file.resolver.d.ts +4 -4
- package/dist/core/modules/file/core-file.resolver.js +23 -16
- package/dist/core/modules/file/core-file.resolver.js.map +1 -1
- package/dist/core/modules/file/core-file.service.d.ts +41 -7
- package/dist/core/modules/file/core-file.service.js +227 -7
- package/dist/core/modules/file/core-file.service.js.map +1 -1
- package/dist/core/modules/file/file-metadata.helper.d.ts +16 -0
- package/dist/core/modules/file/file-metadata.helper.js +33 -0
- package/dist/core/modules/file/file-metadata.helper.js.map +1 -0
- package/dist/core/modules/file/file-roles.helper.d.ts +4 -0
- package/dist/core/modules/file/file-roles.helper.js +53 -0
- package/dist/core/modules/file/file-roles.helper.js.map +1 -0
- package/dist/core/modules/file/file-storage.helper.d.ts +13 -0
- package/dist/core/modules/file/file-storage.helper.js +65 -0
- package/dist/core/modules/file/file-storage.helper.js.map +1 -0
- package/dist/core/modules/file/filesystem-file.helper.d.ts +22 -0
- package/dist/core/modules/file/filesystem-file.helper.js +77 -0
- package/dist/core/modules/file/filesystem-file.helper.js.map +1 -0
- package/dist/core/modules/file/interfaces/file-service-options.interface.d.ts +1 -0
- package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +7 -1
- package/dist/core/modules/file/s3-file.helper.d.ts +37 -0
- package/dist/core/modules/file/s3-file.helper.js +81 -0
- package/dist/core/modules/file/s3-file.helper.js.map +1 -0
- package/dist/core/modules/hub/core-hub.controller.d.ts +8 -8
- package/dist/core/modules/hub/core-hub.controller.js +24 -22
- package/dist/core/modules/hub/core-hub.controller.js.map +1 -1
- package/dist/core/modules/hub/helpers/hub-mask.helper.js +2 -2
- package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -1
- package/dist/core/modules/hub/hub-buffer.d.ts +23 -0
- package/dist/core/modules/hub/hub-buffer.js +101 -0
- package/dist/core/modules/hub/hub-buffer.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +7 -5
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js +14 -10
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +5 -2
- package/dist/core/modules/hub/services/hub-log-buffer.service.js +17 -8
- package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +4 -2
- package/dist/core/modules/hub/services/hub-query-profiler.service.js +13 -7
- package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +4 -2
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js +13 -8
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -1
- package/dist/core/modules/migrate/cli/migrate-cli.d.ts +3 -1
- package/dist/core/modules/migrate/cli/migrate-cli.js +29 -4
- package/dist/core/modules/migrate/cli/migrate-cli.js.map +1 -1
- package/dist/core/modules/migrate/helpers/migration.helper.d.ts +3 -6
- package/dist/core/modules/migrate/helpers/migration.helper.js +55 -8
- package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -1
- package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
- package/dist/core/modules/migrate/migration-runner.js +4 -0
- package/dist/core/modules/migrate/migration-runner.js.map +1 -1
- package/dist/core/modules/migrate/mongo-state-store.d.ts +1 -0
- package/dist/core/modules/migrate/mongo-state-store.js +98 -9
- package/dist/core/modules/migrate/mongo-state-store.js.map +1 -1
- package/dist/core/modules/system-setup/core-system-setup.service.d.ts +2 -0
- package/dist/core/modules/system-setup/core-system-setup.service.js +31 -1
- package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +17 -3
- package/dist/core/modules/tenant/core-tenant.guard.js +92 -15
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.module.js +3 -4
- package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
- package/dist/core/modules/tus/core-tus.controller.d.ts +2 -0
- package/dist/core/modules/tus/core-tus.controller.js +29 -3
- package/dist/core/modules/tus/core-tus.controller.js.map +1 -1
- package/dist/core/modules/tus/core-tus.service.d.ts +24 -1
- package/dist/core/modules/tus/core-tus.service.js +182 -28
- package/dist/core/modules/tus/core-tus.service.js.map +1 -1
- package/dist/core/modules/tus/interfaces/tus-config.interface.js +3 -0
- package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -1
- package/dist/core/modules/tus/tus-redis-locker.d.ts +25 -0
- package/dist/core/modules/tus/tus-redis-locker.js +87 -0
- package/dist/core/modules/tus/tus-redis-locker.js.map +1 -0
- package/dist/core/modules/tus/tus.module.d.ts +1 -0
- package/dist/core/modules/tus/tus.module.js +30 -4
- package/dist/core/modules/tus/tus.module.js.map +1 -1
- package/dist/core.module.js +18 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +2 -1
- package/dist/main.js.map +1 -1
- package/dist/server/modules/file/file.controller.js.map +1 -1
- package/dist/server/modules/file/file.resolver.js +1 -12
- package/dist/server/modules/file/file.resolver.js.map +1 -1
- package/dist/server/modules/file/file.service.d.ts +5 -1
- package/dist/server/modules/file/file.service.js +12 -3
- package/dist/server/modules/file/file.service.js.map +1 -1
- package/dist/server/modules/user/avatar.controller.d.ts +5 -1
- package/dist/server/modules/user/avatar.controller.js +26 -10
- package/dist/server/modules/user/avatar.controller.js.map +1 -1
- package/dist/server/modules/user/user.model.js +3 -2
- package/dist/server/modules/user/user.model.js.map +1 -1
- package/dist/server/modules/user/user.module.js +6 -2
- package/dist/server/modules/user/user.module.js.map +1 -1
- package/dist/server/modules/user/user.service.d.ts +1 -1
- package/dist/server/modules/user/user.service.js +6 -13
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +57 -8
- package/docs/security-overrides.md +30 -5
- package/migration-guides/11.32.3-to-11.32.4.md +335 -0
- package/migration-guides/11.32.x-to-11.33.x.md +1551 -0
- package/package.json +74 -42
- package/src/core/common/helpers/content-disposition.helper.ts +90 -0
- package/src/core/common/helpers/file.helper.ts +201 -20
- package/src/core/common/helpers/graceful-shutdown.helper.ts +116 -0
- package/src/core/common/helpers/project-name.helper.ts +71 -0
- package/src/core/common/interfaces/cron-job-config.interface.ts +16 -0
- package/src/core/common/interfaces/server-options.interface.ts +439 -2
- package/src/core/common/services/core-cron-jobs.initializer.ts +39 -0
- package/src/core/common/services/core-cron-jobs.registry.ts +45 -0
- package/src/core/common/services/core-cron-jobs.service.ts +589 -47
- package/src/core/common/services/core-redis-pubsub.ts +183 -0
- package/src/core/common/services/core-redis.service.ts +249 -0
- package/src/core/common/services/core-s3.service.ts +465 -0
- package/src/core/common/services/core-trust-proxy.initializer.ts +112 -0
- package/src/core/common/services/rate-limit-store.ts +492 -0
- package/src/core/modules/ai/README.md +25 -0
- package/src/core/modules/ai/core-ai-mcp.controller.ts +253 -22
- package/src/core/modules/ai/services/core-ai.service.ts +33 -24
- package/src/core/modules/auth/core-auth.module.ts +7 -1
- package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +12 -21
- package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +49 -93
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +202 -66
- package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +11 -18
- package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +51 -132
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -1
- package/src/core/modules/file/INTEGRATION-CHECKLIST.md +108 -0
- package/src/core/modules/file/README.md +324 -41
- package/src/core/modules/file/core-file.controller.ts +267 -19
- package/src/core/modules/file/core-file.resolver.ts +46 -12
- package/src/core/modules/file/core-file.service.ts +482 -16
- package/src/core/modules/file/file-metadata.helper.ts +142 -0
- package/src/core/modules/file/file-roles.helper.ts +110 -0
- package/src/core/modules/file/file-storage.helper.ts +163 -0
- package/src/core/modules/file/filesystem-file.helper.ts +184 -0
- package/src/core/modules/file/interfaces/file-service-options.interface.ts +14 -0
- package/src/core/modules/file/interfaces/file-upload.interface.ts +17 -1
- package/src/core/modules/file/s3-file.helper.ts +209 -0
- package/src/core/modules/hub/README.md +48 -0
- package/src/core/modules/hub/core-hub.controller.ts +18 -16
- package/src/core/modules/hub/helpers/hub-mask.helper.ts +21 -4
- package/src/core/modules/hub/hub-buffer.ts +198 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.ts +22 -12
- package/src/core/modules/hub/services/hub-log-buffer.service.ts +24 -8
- package/src/core/modules/hub/services/hub-query-profiler.service.ts +9 -6
- package/src/core/modules/hub/services/hub-trace-buffer.service.ts +14 -9
- package/src/core/modules/migrate/README.md +68 -6
- package/src/core/modules/migrate/cli/migrate-cli.ts +69 -6
- package/src/core/modules/migrate/helpers/migration.helper.ts +144 -10
- package/src/core/modules/migrate/migration-runner.ts +10 -1
- package/src/core/modules/migrate/mongo-state-store.ts +179 -11
- package/src/core/modules/system-setup/README.md +7 -2
- package/src/core/modules/system-setup/core-system-setup.service.ts +95 -1
- package/src/core/modules/tenant/README.md +21 -1
- package/src/core/modules/tenant/core-tenant.guard.ts +135 -14
- package/src/core/modules/tenant/core-tenant.module.ts +8 -4
- package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +93 -21
- package/src/core/modules/tus/README.md +35 -2
- package/src/core/modules/tus/core-tus.controller.ts +64 -12
- package/src/core/modules/tus/core-tus.service.ts +358 -34
- package/src/core/modules/tus/interfaces/tus-config.interface.ts +9 -0
- package/src/core/modules/tus/tus-redis-locker.ts +139 -0
- package/src/core/modules/tus/tus.module.ts +78 -6
- package/src/core.module.ts +26 -1
- package/src/index.ts +10 -0
- package/src/main.ts +7 -1
- package/src/server/modules/file/file.controller.ts +7 -3
- package/src/server/modules/file/file.resolver.ts +5 -17
- package/src/server/modules/file/file.service.ts +42 -3
- package/src/server/modules/user/avatar.controller.ts +39 -9
- package/src/server/modules/user/user.model.ts +9 -3
- package/src/server/modules/user/user.module.ts +12 -3
- package/src/server/modules/user/user.service.ts +20 -17
- package/src/test/README.md +7 -2
- package/src/core/common/helpers/logging.helper.spec.ts +0 -61
- package/src/core/common/helpers/process-diagnostics.helper.spec.ts +0 -310
- package/src/core/common/services/brevo.service.spec.ts +0 -266
- package/src/core/modules/hub/core-hub.module.spec.ts +0 -108
- package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +0 -48
- package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +0 -67
- package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +0 -61
- package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +0 -106
- package/src/core/modules/hub/hub-config.helper.spec.ts +0 -108
- package/src/core/modules/hub/hub-ring-buffer.spec.ts +0 -95
- package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +0 -116
- package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +0 -112
|
@@ -4,14 +4,260 @@ File upload and download functionality with MongoDB GridFS storage.
|
|
|
4
4
|
|
|
5
5
|
## Endpoints
|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### Download Endpoints (via CoreFileController)
|
|
8
8
|
|
|
9
|
-
| Method | Endpoint | Description |
|
|
10
|
-
| ------ | ------------------ | ------------------------- |
|
|
11
|
-
| GET | `/files/id/:id` | Download file by ID |
|
|
12
|
-
| GET | `/files/:filename` | Download file by filename |
|
|
9
|
+
| Method | Endpoint | Description | Gated by |
|
|
10
|
+
| ------ | ------------------ | ------------------------- | --------------- |
|
|
11
|
+
| GET | `/files/id/:id` | Download file by ID | `downloadRoles` |
|
|
12
|
+
| GET | `/files/:filename` | Download file by filename | `downloadRoles` |
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
### GraphQL members (via CoreFileResolver)
|
|
15
|
+
|
|
16
|
+
| Member | Kind | Gated by |
|
|
17
|
+
| ------------- | -------- | --------------- |
|
|
18
|
+
| `getFileInfo` | Query | `downloadRoles` |
|
|
19
|
+
| `uploadFile` | Mutation | `uploadRoles` |
|
|
20
|
+
| `uploadFiles` | Mutation | `uploadRoles` |
|
|
21
|
+
| `deleteFile` | Mutation | `deleteRoles` |
|
|
22
|
+
|
|
23
|
+
> `CoreFileResolver` is registered by no core module. It only takes effect in a project that
|
|
24
|
+
> registers it (or a subclass) itself — enabling GraphQL alone does not.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
One `file` object configures the module — where the bytes live and who may reach them:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
file: {
|
|
34
|
+
storage: 'gridfs', // 'filesystem' | 'gridfs' | 's3' — omit to derive it
|
|
35
|
+
storageDir: 'uploads/files', // only for 'filesystem'
|
|
36
|
+
downloadRoles: [RoleEnum.ADMIN], // GET /files/id/:id, GET /files/:filename, getFileInfo
|
|
37
|
+
uploadRoles: [RoleEnum.ADMIN], // uploadFile, uploadFiles
|
|
38
|
+
deleteRoles: [RoleEnum.ADMIN], // deleteFile
|
|
39
|
+
},
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`storage` and the role knobs are orthogonal: one picks the driver, the others the access.
|
|
43
|
+
|
|
44
|
+
## Storage drivers
|
|
45
|
+
|
|
46
|
+
Three equivalent options. They differ only in where the bytes end up:
|
|
47
|
+
|
|
48
|
+
| Driver | Bytes | Survives a restart | Shared between replicas | Needs |
|
|
49
|
+
| -------------- | ------------------------- | ------------------------ | ----------------------- | ---------------------------------- |
|
|
50
|
+
| `'s3'` | S3-compatible bucket | yes | **yes** | `s3` config + `@aws-sdk/client-s3` |
|
|
51
|
+
| `'gridfs'` | MongoDB GridFS | yes | yes | nothing beyond the database |
|
|
52
|
+
| `'filesystem'` | local disk (`storageDir`) | only on a mounted volume | **no** | nothing |
|
|
53
|
+
|
|
54
|
+
**Metadata always lives in the database**, whichever driver holds the bytes. Filename, content type,
|
|
55
|
+
length and the custom `metadata` a per-file rule reads have to be queryable — `findFileInfo()` filters
|
|
56
|
+
and pages over them, `checkRights()` reads them per request. A directory listing answers none of
|
|
57
|
+
that, and sidecar files would reinvent an index the database already is. So `'filesystem'` moves the
|
|
58
|
+
bytes off the database, not the bookkeeping.
|
|
59
|
+
|
|
60
|
+
### Choosing the driver
|
|
61
|
+
|
|
62
|
+
**Set it explicitly and it is enforced.** If the chosen store is not available, the boot **fails**:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
file.storage is set to 's3', but that storage is not available.
|
|
66
|
+
Configure `s3` (bucket, credentials/endpoint) and install `@aws-sdk/client-s3`, …
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
That is deliberate. The previous behaviour fell back to GridFS whenever S3 was selected but
|
|
70
|
+
unusable — the application kept working, so nothing looked broken, while files landed in a store the
|
|
71
|
+
operator did not believe they were in. Afterwards nobody can tell which file went where.
|
|
72
|
+
|
|
73
|
+
**Leave it unset and it is derived**, most capable first:
|
|
74
|
+
|
|
75
|
+
1. `'s3'` — when `s3.bucket` is configured
|
|
76
|
+
2. `'gridfs'` — when a database is configured
|
|
77
|
+
3. `'filesystem'` — when neither is
|
|
78
|
+
|
|
79
|
+
A configured-but-**unreachable** database is an error in its own right (Mongoose fails the boot), never
|
|
80
|
+
a reason to fall through to the disk. Only a database that is not configured **at all** reaches step 3 —
|
|
81
|
+
which today is theoretical, since `CoreModule` always registers Mongoose.
|
|
82
|
+
|
|
83
|
+
A derived driver is enforced too: `s3.bucket` in the config makes S3 the default, but the bytes still
|
|
84
|
+
go nowhere unless your `FileService` forwards the services to `super()`:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
super(connection, 'fs', { configService, s3Service });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Switching drivers is forward-only
|
|
91
|
+
|
|
92
|
+
Reads consult **every** store, so files written under a previous driver stay readable and there is no
|
|
93
|
+
migration step and no cut-over moment. New files go to the active driver; `findFileInfo()` returns
|
|
94
|
+
the union, paged once over the merged result.
|
|
95
|
+
|
|
96
|
+
The boot log names the driver in use, so it never has to be inferred from where files stopped
|
|
97
|
+
appearing:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
[CoreFileStorage] File storage: s3 (defaulted — s3.bucket is configured)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Access control
|
|
106
|
+
|
|
107
|
+
Two layers, and they answer different questions.
|
|
108
|
+
|
|
109
|
+
### 1. Roles — _may this caller reach the endpoint at all?_
|
|
110
|
+
|
|
111
|
+
All three role knobs take **plain role strings**, so your own project roles work exactly as they
|
|
112
|
+
would in a hand-written `@Roles()`.
|
|
113
|
+
|
|
114
|
+
All three default to `[ADMIN]`. That default is restrictive on purpose: the file store is a **single
|
|
115
|
+
bucket shared by every feature** of the project — GridFS or S3, whichever `file.storage` selects —
|
|
116
|
+
and the ids naming its blobs are not secrets. An id is an ObjectId (4-byte timestamp, 5-byte
|
|
117
|
+
per-process random, 3-byte counter), so one upload of your own discloses the per-process value in
|
|
118
|
+
full and bounds the counter, collapsing the id space to a brute-forceable range. `/files/:filename`
|
|
119
|
+
is weaker still: it resolves the **first** match for a name a caller may be able to guess.
|
|
120
|
+
|
|
121
|
+
Three properties worth knowing:
|
|
122
|
+
|
|
123
|
+
- **ADMIN always keeps access.** Handler and class roles are UNIONed (`mergeRolesMetadata`), and both
|
|
124
|
+
classes carry a class-level `@Roles(ADMIN)`. So `downloadRoles: ['editor']` grants editors _in
|
|
125
|
+
addition to_ admins. These knobs cannot exclude admins.
|
|
126
|
+
- **`[]` is rejected, not honoured.** An all-empty role set reads to the guards as "no roles
|
|
127
|
+
required" and would OPEN the route. An empty array logs a warning and falls back to the default.
|
|
128
|
+
- **Roles are checked against `user.roles`, never `membership.role`.** Both classes carry
|
|
129
|
+
`@SkipTenantCheck()`. No store is reached through Mongoose — GridFS goes through the native driver,
|
|
130
|
+
the S3 and filesystem metadata live in their own `s3-files` / `filesystem-files` collections — so
|
|
131
|
+
`mongooseTenantPlugin` never scopes them. None is tenant-scoped, and a role name alone therefore
|
|
132
|
+
cannot express a per-tenant rule. Use layer 2 for that.
|
|
133
|
+
|
|
134
|
+
### 2. `checkRights()` — _may this caller have THIS file?_
|
|
135
|
+
|
|
136
|
+
Roles cannot express "…but only their own". That is what the service hook is for:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
export class FileService extends CoreFileService {
|
|
140
|
+
protected override async checkRights(
|
|
141
|
+
input: any,
|
|
142
|
+
options?: FileServiceOptions & { checkInputType: FileInputCheckType },
|
|
143
|
+
): Promise<boolean> {
|
|
144
|
+
if (options?.checkInputType !== 'id' || options.force) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (options.currentUser?.hasRole([RoleEnum.ADMIN])) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
const raw = await this.getRawFileInfo(input);
|
|
151
|
+
return !!raw && String(raw.metadata?.ownerId) === String(options.currentUser?.id);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Three pieces make this work, and all three are needed:
|
|
157
|
+
|
|
158
|
+
1. **Write the metadata at upload time** — `createFile(file, { metadata: { ownerId: user.id } })`.
|
|
159
|
+
`CoreFileService.createFile()` passes `serviceOptions.metadata` straight to the storage driver,
|
|
160
|
+
so it works for both GridFS and S3.
|
|
161
|
+
2. **Read it back with `getRawFileInfo()`** (or `getRawFileInfoByName()`), not `getFileInfo()`. The
|
|
162
|
+
public getter runs `prepareOutput` → `check()`, which strips fields the caller may not see —
|
|
163
|
+
including the very field the decision rests on. `getRawFileInfo()` consults every metadata store
|
|
164
|
+
in the same order `getFileInfo()` uses, so a rule sees the file the download would actually
|
|
165
|
+
serve — under any driver.
|
|
166
|
+
3. **`options.currentUser` is supplied by the core controller and resolver** for every member.
|
|
167
|
+
|
|
168
|
+
When `checkRights()` refuses, `getFileStream()` returns `null` and the controller answers **404** —
|
|
169
|
+
deliberately the same answer as an unknown id, so the endpoint cannot be used to probe which files
|
|
170
|
+
exist. Do not turn that into a 403 in an override without accepting that trade-off.
|
|
171
|
+
|
|
172
|
+
### Serving files to a browser `<img>` / `<a download>`
|
|
173
|
+
|
|
174
|
+
A markup-driven request cannot send an `Authorization` header, and a CORS preflight sends no
|
|
175
|
+
credentials at all. So anything stricter than `S_EVERYONE` only works from markup when the session
|
|
176
|
+
travels as a **cookie** on a same-site request.
|
|
177
|
+
|
|
178
|
+
If some files are genuinely public and others are not, the answer is not a role list — it is a
|
|
179
|
+
per-file rule. Record a visibility flag in the metadata at upload time and branch on it in
|
|
180
|
+
`checkRights()`, or expose a separate public route for exactly the files that are meant to be
|
|
181
|
+
public and leave these two gated.
|
|
182
|
+
|
|
183
|
+
**Presigned S3 downloads** (`file.storage: 's3'` with presigned downloads enabled) sidestep the
|
|
184
|
+
`<img>` problem, because the browser fetches the object directly from S3. Understand what you are
|
|
185
|
+
issuing, though: the URL is a **bearer capability** — anyone holding it can fetch the object until
|
|
186
|
+
it expires, with no session. Authorization happens once, when the URL is issued (through the
|
|
187
|
+
`resolveFile()` call that precedes the redirect on `GET /files/id/:id`, and the
|
|
188
|
+
`getFileInfoByName()` call on `GET /files/:filename`; both run the same `checkRights()`). Keep the
|
|
189
|
+
expiry short, and do not enable it for files whose audience is narrower than "anyone who was ever
|
|
190
|
+
given the link".
|
|
191
|
+
|
|
192
|
+
### Overriding: read this before you re-declare a member
|
|
193
|
+
|
|
194
|
+
`CoreFileController` and `CoreFileResolver` are meant to be extended — but role metadata lives on
|
|
195
|
+
the **function object**, so an override carries its own and thereby **opts out of `file.*Roles`
|
|
196
|
+
entirely**. Two consequences that bite in practice:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// WRONG — the route DISAPPEARS. Nest reads PATH_METADATA off the subclass
|
|
200
|
+
// function, which has none, so it is never registered: 404 for everyone,
|
|
201
|
+
// no error, no warning.
|
|
202
|
+
override async getFileById(@Param('id') id: string, @Res() res: Response) {
|
|
203
|
+
return super.getFileById(id, res);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// WRONG — pins the policy and silently ignores `file.downloadRoles` forever.
|
|
207
|
+
// This exact shape kept generated projects public after the framework default
|
|
208
|
+
// had already been closed.
|
|
209
|
+
@Get('id/:id')
|
|
210
|
+
@Roles(RoleEnum.S_EVERYONE)
|
|
211
|
+
override async getFileById(@Param('id') id: string, @Res() res: Response) {
|
|
212
|
+
return super.getFileById(id, res);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// RIGHT — don't override at all. Configure it:
|
|
216
|
+
// file: { downloadRoles: [RoleEnum.S_USER] }
|
|
217
|
+
// and put per-file rules in checkRights().
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
A class-level `@Roles()` on your subclass cannot relax an inherited member either: the inherited
|
|
221
|
+
function carries its own handler-level roles, and the two are unioned rather than overridden.
|
|
222
|
+
|
|
223
|
+
### If you override `getFileInfo()`: `GET /files/id/:id` no longer calls it (11.33.0)
|
|
224
|
+
|
|
225
|
+
Up to 11.32.x, `GET /files/id/:id` called the public `CoreFileService.getFileInfo()` and then let
|
|
226
|
+
`getFileStream()` work out on its own which store held the bytes. With three stores to consult
|
|
227
|
+
(§ Storage drivers) that resolves the same id up to three times per download, so since 11.33.0 the
|
|
228
|
+
route calls **`resolveFile()`** instead, which answers the metadata and the store in one pass.
|
|
229
|
+
|
|
230
|
+
An override of `getFileInfo()` is therefore **no longer on that route's path**. It is still honoured
|
|
231
|
+
by every other caller it ever had — `deleteFile()`, `duplicateById()` and any project code that
|
|
232
|
+
calls it, such as the `GET /files/info/:id` endpoint projects usually add — which is exactly what
|
|
233
|
+
makes the gap easy to miss: the behaviour disappears on one route while everything else keeps it.
|
|
234
|
+
(The GraphQL `getFileInfo(filename:)` member resolves by NAME and has always gone through
|
|
235
|
+
`getFileInfoByName()`, so it is unaffected either way.)
|
|
236
|
+
|
|
237
|
+
This does **not** weaken authorization. `resolveFile()` runs the same `checkRights()` with the same
|
|
238
|
+
`serviceOptions`, and answers `null` on refusal, which the controller turns into the same 404. Only
|
|
239
|
+
work you added **on top** of the base `getFileInfo()` — a decorated field, a counter, a log line —
|
|
240
|
+
stops happening on the id route.
|
|
241
|
+
|
|
242
|
+
The remedy is to override `resolveFile()` as well, keeping the two consistent:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
export class FileService extends CoreFileService {
|
|
246
|
+
override async getFileInfo(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions) {
|
|
247
|
+
return this.decorate(await super.getFileInfo(id, serviceOptions));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Same treatment for the download route. `store` must be passed through
|
|
251
|
+
// untouched — the controller hands it to getFileStream() to pick the store.
|
|
252
|
+
override async resolveFile(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions) {
|
|
253
|
+
const resolved = await super.resolveFile(id, serviceOptions);
|
|
254
|
+
return resolved && { ...resolved, info: this.decorate(resolved.info) };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Per-file **authorization** needs none of this: it belongs in `checkRights()`, which both methods
|
|
260
|
+
call.
|
|
15
261
|
|
|
16
262
|
### Admin Endpoints (project-specific)
|
|
17
263
|
|
|
@@ -46,44 +292,71 @@ export class FileController extends CoreFileController {
|
|
|
46
292
|
}
|
|
47
293
|
```
|
|
48
294
|
|
|
49
|
-
###
|
|
295
|
+
### Error responses
|
|
296
|
+
|
|
297
|
+
| Situation | Status | Body |
|
|
298
|
+
| -------------------------------------------------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------- |
|
|
299
|
+
| Unknown id / filename, or `checkRights()` refused | `404` | `NotFoundException` with `ErrorCode.FILE_NOT_FOUND` |
|
|
300
|
+
| Missing id / filename in the route | `400` | `BadRequestException` with `ErrorCode.REQUIRED_FIELD_MISSING` |
|
|
301
|
+
| GridFS read fails **before** any byte was sent (file document exists, chunks are gone) | `404` | `{ "error": "Not Found", "message": "<FILE_NOT_FOUND>", "statusCode": 404 }` |
|
|
302
|
+
| GridFS read fails **after** streaming started | — | The connection is closed; a truncated transfer is the only signal left once the status is on the wire |
|
|
303
|
+
|
|
304
|
+
The mid-stream failure case is handled by `pipeFileToResponse()`. Without it the stream error would
|
|
305
|
+
go unhandled, Node would destroy the socket, and a reverse proxy would report **502 Bad Gateway** —
|
|
306
|
+
i.e. "the server is down", while every other route keeps answering. On the error path the headers
|
|
307
|
+
describing the file (`Content-Type`, `Content-Disposition`, `Cache-Control`, `ETag`) are removed, so
|
|
308
|
+
the JSON body is not labelled as the image it failed to deliver. The error itself is logged
|
|
309
|
+
server-side even though the client answer stays deliberately generic.
|
|
310
|
+
|
|
311
|
+
To change the status, the body or the logging, override the `protected pipeFileToResponse()` method
|
|
312
|
+
on the controller rather than the exported function of the same name.
|
|
50
313
|
|
|
51
|
-
|
|
314
|
+
### Upload filtering
|
|
315
|
+
|
|
316
|
+
Upload endpoints are project-specific, but the filter they install comes from the framework
|
|
317
|
+
(`multerOptionsForImageUpload()` / `multerFileFilter()` in `common/helpers/file.helper.ts`). Name what
|
|
318
|
+
the endpoint accepts as an `UploadAllowList` — both the mimetype and the extension are compared as
|
|
319
|
+
WHOLE values:
|
|
52
320
|
|
|
53
321
|
```typescript
|
|
54
|
-
@
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
322
|
+
@UseInterceptors(FileInterceptor('file', multerOptionsForImageUpload({
|
|
323
|
+
allowList: {
|
|
324
|
+
extensions: ['.jpeg', '.jpg', '.pdf', '.png'],
|
|
325
|
+
mimeTypes: ['application/pdf', 'image/jpeg', 'image/png'],
|
|
326
|
+
},
|
|
327
|
+
})))
|
|
328
|
+
```
|
|
60
329
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return super.getFileById(id, res);
|
|
66
|
-
}
|
|
330
|
+
The two conditions are **independent**: either one alone rejects the file, while a pair that is odd
|
|
331
|
+
yet individually allowed (`report.txt` announced as `application/pdf`) passes. An extension→mimetype
|
|
332
|
+
MAPPING is deliberately not enforced, because user agents genuinely disagree about office and audio
|
|
333
|
+
types (macOS reports `.csv` as `text/plain`) and a mapping would reject legitimate uploads.
|
|
67
334
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
335
|
+
The legacy `fileTypeRegex` option still works and keeps precedence, but is **deprecated**: one
|
|
336
|
+
expression is `.test()`ed against both the mimetype and the extension, so every alternative matches
|
|
337
|
+
as a SUBSTRING — an allow-list containing `te?xt` also accepts `text/html`.
|
|
338
|
+
|
|
339
|
+
Types a browser may execute as script (`text/html`, `image/svg+xml`, `application/xhtml+xml`, XML and
|
|
340
|
+
JavaScript types, plus the matching extensions) are rejected **before** the allow-list is consulted,
|
|
341
|
+
on both forms. A stored upload served back from the API origin with one of these content types runs
|
|
342
|
+
in that origin, with the victim's session. Opt out only when the file never reaches an origin that
|
|
343
|
+
carries a session:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
multerFileFilter({ extensions: ['.svg'], mimeTypes: ['image/svg+xml'] }, { allowScriptableTypes: true });
|
|
75
347
|
```
|
|
76
348
|
|
|
77
349
|
---
|
|
78
350
|
|
|
79
351
|
## GraphQL Support
|
|
80
352
|
|
|
81
|
-
File operations are also available via GraphQL through `CoreFileResolver
|
|
353
|
+
File operations are also available via GraphQL through `CoreFileResolver` — in a project that
|
|
354
|
+
registers it. See [Access control](#access-control) for which knob gates which member.
|
|
82
355
|
|
|
83
356
|
```graphql
|
|
84
|
-
#
|
|
357
|
+
# Read file info by filename (downloadRoles)
|
|
85
358
|
query {
|
|
86
|
-
|
|
359
|
+
getFileInfo(filename: "...") {
|
|
87
360
|
id
|
|
88
361
|
filename
|
|
89
362
|
contentType
|
|
@@ -91,24 +364,23 @@ query {
|
|
|
91
364
|
}
|
|
92
365
|
}
|
|
93
366
|
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
367
|
+
# Upload a file via the GraphQL Upload scalar (uploadRoles)
|
|
368
|
+
mutation {
|
|
369
|
+
uploadFile(file: Upload!) {
|
|
97
370
|
id
|
|
98
371
|
filename
|
|
99
|
-
contentType
|
|
100
372
|
}
|
|
101
373
|
}
|
|
102
374
|
|
|
103
|
-
# Upload
|
|
375
|
+
# Upload several files (uploadRoles)
|
|
104
376
|
mutation {
|
|
105
|
-
|
|
377
|
+
uploadFiles(files: [Upload!]!) {
|
|
106
378
|
id
|
|
107
379
|
filename
|
|
108
380
|
}
|
|
109
381
|
}
|
|
110
382
|
|
|
111
|
-
# Delete
|
|
383
|
+
# Delete by filename (deleteRoles)
|
|
112
384
|
mutation {
|
|
113
385
|
deleteFile(filename: "...") {
|
|
114
386
|
id
|
|
@@ -116,21 +388,32 @@ mutation {
|
|
|
116
388
|
}
|
|
117
389
|
```
|
|
118
390
|
|
|
391
|
+
> Earlier revisions of this file documented `file(id:)` and `fileByFilename(filename:)`. Neither has
|
|
392
|
+
> ever existed on `CoreFileResolver`; the query is `getFileInfo(filename:)`.
|
|
393
|
+
|
|
119
394
|
---
|
|
120
395
|
|
|
121
396
|
## Integration with TUS
|
|
122
397
|
|
|
123
|
-
Files uploaded via TUS
|
|
398
|
+
Files uploaded via TUS land in the same GridFS bucket and are read back through the same endpoints —
|
|
399
|
+
which means the same `downloadRoles` gate applies:
|
|
124
400
|
|
|
125
401
|
```bash
|
|
126
|
-
# After TUS upload completes, download by ID
|
|
402
|
+
# After the TUS upload completes, download by ID (requires downloadRoles)
|
|
127
403
|
GET /files/id/<gridfs-file-id>
|
|
128
404
|
|
|
129
|
-
# Or by filename
|
|
405
|
+
# Or by filename, if unique (requires downloadRoles)
|
|
130
406
|
GET /files/<original-filename>
|
|
131
407
|
```
|
|
132
408
|
|
|
133
|
-
**Recommendation:**
|
|
409
|
+
**Recommendation:** use ID-based downloads for TUS uploads, since filenames may not be unique.
|
|
410
|
+
|
|
411
|
+
**Watch the pairing.** TUS uploads are gated separately by `tus.roles` (default `S_USER`). The
|
|
412
|
+
common "user uploads their own file, then views it" flow therefore needs both sides to line up: a
|
|
413
|
+
signed-in user may upload, but with the default `downloadRoles: [ADMIN]` they cannot read the result
|
|
414
|
+
back. Either widen `downloadRoles`, or — better — write an owner into the metadata at upload time
|
|
415
|
+
and authorize per file in `checkRights()`. TUS uploads already carry `metadata.tusUploadId` and the
|
|
416
|
+
original TUS metadata, so there is a natural place to add one.
|
|
134
417
|
|
|
135
418
|
---
|
|
136
419
|
|