@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
|
@@ -1,42 +1,230 @@
|
|
|
1
|
-
import { BadRequestException, Controller, Get, NotFoundException, Param, Res } from '@nestjs/common';
|
|
2
|
-
import { Response } from 'express';
|
|
1
|
+
import { BadRequestException, Controller, Get, Logger, NotFoundException, Param, Res } from '@nestjs/common';
|
|
2
|
+
import type { Response } from 'express';
|
|
3
|
+
import type { Readable } from 'stream';
|
|
3
4
|
|
|
5
|
+
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
|
4
6
|
import { Roles } from '../../common/decorators/roles.decorator';
|
|
5
7
|
import { RoleEnum } from '../../common/enums/role.enum';
|
|
8
|
+
import { buildContentDisposition } from '../../common/helpers/content-disposition.helper';
|
|
9
|
+
import { ErrorCode } from '../error-code/error-codes';
|
|
10
|
+
import { SkipTenantCheck } from '../tenant/core-tenant.decorators';
|
|
6
11
|
import { CoreFileService } from './core-file.service';
|
|
12
|
+
import type { FileServiceOptions } from './interfaces/file-service-options.interface';
|
|
13
|
+
|
|
14
|
+
const fileStreamLogger = new Logger('CoreFileController');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Headers that describe the FILE and must not survive onto an error response.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately not the whole set: CORS headers are already on the response at
|
|
20
|
+
* this point, and removing those would turn a readable 404 into an opaque CORS
|
|
21
|
+
* failure in the browser — hiding the very answer this handler exists to give.
|
|
22
|
+
*
|
|
23
|
+
* `Content-Type` is the one most easily missed. Express only defaults it in
|
|
24
|
+
* `res.json()` when nothing is set yet (`if (!this.get('Content-Type'))`), so the
|
|
25
|
+
* file's own type would otherwise label a JSON body as `image/png` — and an
|
|
26
|
+
* ofetch/`$fetch` client picks its parser from that header and hands the caller a
|
|
27
|
+
* Blob instead of the error message.
|
|
28
|
+
*/
|
|
29
|
+
const FILE_DELIVERY_HEADERS = ['Cache-Control', 'Content-Disposition', 'Content-Type', 'ETag'];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Re-exported from the leaf it now lives in, so no import path broke.
|
|
33
|
+
*
|
|
34
|
+
* It moved because the S3 presigned-URL branch in `src/core/common/services/` needs the SAME
|
|
35
|
+
* value, and `src/core/common/**` must not import from `src/core/modules/**`. While the two
|
|
36
|
+
* rendered it separately, the same file downloaded under a different name depending on whether
|
|
37
|
+
* `s3.presignedDownloads` was on.
|
|
38
|
+
*/
|
|
39
|
+
export { buildContentDisposition };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pipe a GridFS download to the response without letting a read error kill the socket.
|
|
43
|
+
*
|
|
44
|
+
* A GridFS record and its chunks are two separate writes, so a file document can
|
|
45
|
+
* outlive its bytes — an interrupted upload, a restored backup, a manual cleanup.
|
|
46
|
+
* GridFS reports that asynchronously, on the stream, and `stream.pipe(res)` alone
|
|
47
|
+
* installs no error handler: the error goes unhandled, Node destroys the socket
|
|
48
|
+
* mid-response, and any reverse proxy in front turns that into **502 Bad Gateway**.
|
|
49
|
+
* That reads as "the server is down" — the one diagnosis that is wrong here, while
|
|
50
|
+
* every other route keeps answering normally.
|
|
51
|
+
*
|
|
52
|
+
* Nothing has been written when GridFS reports a missing file, so the status is
|
|
53
|
+
* still ours to set and the answer becomes an honest 404. Once bytes are on the
|
|
54
|
+
* wire there is no status left to send and closing the connection is all that
|
|
55
|
+
* remains — but at that point it genuinely is a truncated transfer, which is
|
|
56
|
+
* exactly what a dropped connection means to the client.
|
|
57
|
+
*
|
|
58
|
+
* Note on `pipe()` vs `stream.pipeline()`: pipeline would destroy BOTH streams on
|
|
59
|
+
* error, including the response — which is precisely the object still needed to
|
|
60
|
+
* send the 404. So the source is cleaned up explicitly on `res` close instead;
|
|
61
|
+
* `pipe()` only ever unpipes the destination and would otherwise leave the
|
|
62
|
+
* GridFS read stream and its server-side cursor open on every aborted download.
|
|
63
|
+
*/
|
|
64
|
+
export function pipeFileToResponse(stream: Readable, res: Response): Response {
|
|
65
|
+
res.on('close', () => {
|
|
66
|
+
if (!stream.destroyed) {
|
|
67
|
+
stream.destroy();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
stream.on('error', (err: Error) => {
|
|
72
|
+
// The answer to the CLIENT stays deliberately indistinguishable from an
|
|
73
|
+
// unknown id, but the server must not lose the event: a files document
|
|
74
|
+
// without its chunks is data corruption, and an error silently converted
|
|
75
|
+
// into a 404 is one nobody will ever notice.
|
|
76
|
+
fileStreamLogger.error(`GridFS download failed: ${err.message}`, err.stack);
|
|
77
|
+
|
|
78
|
+
if (res.headersSent) {
|
|
79
|
+
res.destroy();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const header of FILE_DELIVERY_HEADERS) {
|
|
83
|
+
res.removeHeader(header);
|
|
84
|
+
}
|
|
85
|
+
// No global nosniff on this route, and the body is now correctly typed —
|
|
86
|
+
// set it anyway so a mislabelled response can never be sniffed into markup.
|
|
87
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
88
|
+
res.status(404).json({ error: 'Not Found', message: ErrorCode.FILE_NOT_FOUND, statusCode: 404 });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return stream.pipe(res);
|
|
92
|
+
}
|
|
7
93
|
|
|
8
94
|
/**
|
|
9
95
|
* File controller
|
|
96
|
+
*
|
|
97
|
+
* TENANT SCOPING: the class carries `@SkipTenantCheck()`, so the roles below are
|
|
98
|
+
* checked against `user.roles` and never against `membership.role`.
|
|
99
|
+
*
|
|
100
|
+
* That is not a convenience — it is required for the gate to mean what it says.
|
|
101
|
+
* GridFS is reached through the NATIVE MongoDB driver, so `mongooseTenantPlugin`
|
|
102
|
+
* never runs on `fs.files`: one bucket holds every tenant's blobs, unscoped.
|
|
103
|
+
* Without this decorator and with `multiTenancy` active, a role string like
|
|
104
|
+
* `'admin'` would be satisfied by any member whose MEMBERSHIP role is `admin` —
|
|
105
|
+
* a workspace admin of tenant A could then read tenant B's files.
|
|
106
|
+
*
|
|
107
|
+
* A genuinely tenant-aware policy therefore cannot be expressed by a role name.
|
|
108
|
+
* Write `tenantId` into the file metadata at upload time and compare it in an
|
|
109
|
+
* overridden `CoreFileService.checkRights()`.
|
|
10
110
|
*/
|
|
11
111
|
@Controller('files')
|
|
12
112
|
@Roles(RoleEnum.ADMIN)
|
|
113
|
+
@SkipTenantCheck()
|
|
13
114
|
export abstract class CoreFileController {
|
|
14
115
|
/**
|
|
15
116
|
* Include services
|
|
16
117
|
*/
|
|
17
118
|
protected constructor(protected fileService: CoreFileService) {}
|
|
18
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Stream a file to the response.
|
|
122
|
+
*
|
|
123
|
+
* Delegates to the module-level {@link pipeFileToResponse} (which stays exported
|
|
124
|
+
* so it can be unit-tested without a Nest context). Override this to change the
|
|
125
|
+
* error status, the body shape or the logging for a project — the free function
|
|
126
|
+
* alone would not be reachable from a subclass.
|
|
127
|
+
*/
|
|
128
|
+
protected pipeFileToResponse(stream: Readable, res: Response): Response {
|
|
129
|
+
return pipeFileToResponse(stream, res);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Presigned S3 URL for a download, or undefined to stream through the API.
|
|
134
|
+
*
|
|
135
|
+
* Deliberately fail-soft. A presigned URL is an OPTIMIZATION — it offloads bytes
|
|
136
|
+
* from the API — so nothing about it may cost the caller their download: a
|
|
137
|
+
* missing `@aws-sdk/s3-request-presigner`, an S3 outage or a project service
|
|
138
|
+
* predating this method must all fall through to the streaming path, which
|
|
139
|
+
* still runs the same rights check and answers a refusal exactly like an
|
|
140
|
+
* unknown id. Turning any of those into a 500 would also leak that the file
|
|
141
|
+
* exists, which the streaming path takes care never to do.
|
|
142
|
+
*/
|
|
143
|
+
protected async resolveDownloadUrl(id: string, serviceOptions?: FileServiceOptions): Promise<string | undefined> {
|
|
144
|
+
try {
|
|
145
|
+
// `serviceOptions` carries the current user into the service's own rights
|
|
146
|
+
// check. Omitting it made this second check run with `currentUser:
|
|
147
|
+
// undefined`: for the ownership rule documented in the README that fails
|
|
148
|
+
// CLOSED, so presigned downloads silently never fired — not even for
|
|
149
|
+
// admins, since `currentUser?.hasRole(...)` was undefined too. For an
|
|
150
|
+
// override that reads a missing user as "system-internal, allow" it was a
|
|
151
|
+
// no-op instead. Neither is what the path that mints a session-less
|
|
152
|
+
// capability should be doing.
|
|
153
|
+
return await this.fileService.getDownloadUrl?.(id, serviceOptions);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
fileStreamLogger.warn(
|
|
156
|
+
`Presigned download URL unavailable, falling back to streaming: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
157
|
+
);
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
19
162
|
/**
|
|
20
163
|
* Download file by ID
|
|
21
164
|
*
|
|
22
165
|
* More reliable than filename-based download as IDs are unique.
|
|
23
166
|
* Recommended for TUS uploads and when filename uniqueness cannot be guaranteed.
|
|
167
|
+
*
|
|
168
|
+
* SECURITY: gated by `file.downloadRoles` (default `[ADMIN]`). The decorator
|
|
169
|
+
* below is the fallback — `CoreModule.forRoot()` rewrites it from config.
|
|
170
|
+
* See `src/core/modules/file/README.md` § Access control for the full model
|
|
171
|
+
* and the 11.32.4 → 11.33.0 migration guide for why the default changed.
|
|
24
172
|
*/
|
|
25
173
|
@Get('id/:id')
|
|
26
|
-
@Roles(RoleEnum.
|
|
27
|
-
async getFileById(
|
|
174
|
+
@Roles(RoleEnum.ADMIN)
|
|
175
|
+
async getFileById(
|
|
176
|
+
@Param('id') id: string,
|
|
177
|
+
@Res() res: Response,
|
|
178
|
+
@CurrentUser() currentUser?: any,
|
|
179
|
+
): Promise<Response> {
|
|
28
180
|
if (!id) {
|
|
29
|
-
throw new BadRequestException(
|
|
181
|
+
throw new BadRequestException(ErrorCode.REQUIRED_FIELD_MISSING);
|
|
30
182
|
}
|
|
31
183
|
|
|
32
|
-
const
|
|
184
|
+
const serviceOptions = { currentUser };
|
|
185
|
+
// One lookup for both answers — metadata AND which store holds the bytes.
|
|
186
|
+
// getFileInfo() followed by getFileStream() had each probe the three stores
|
|
187
|
+
// from scratch, since neither told the other which one had answered. Same
|
|
188
|
+
// rights check, same 404-on-refusal.
|
|
189
|
+
const resolved = await this.fileService.resolveFile(id, serviceOptions);
|
|
190
|
+
const file = resolved?.info;
|
|
33
191
|
if (!file) {
|
|
34
|
-
throw new NotFoundException(
|
|
192
|
+
throw new NotFoundException(ErrorCode.FILE_NOT_FOUND);
|
|
35
193
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
194
|
+
// S3-stored file with presigned downloads enabled: let the client fetch the
|
|
195
|
+
// bytes from S3 directly instead of streaming them through the API.
|
|
196
|
+
//
|
|
197
|
+
// AUTHORIZATION on this branch rests entirely on the `resolveFile()` call
|
|
198
|
+
// above, which runs the very same `checkRights()` `getFileInfo()` did and
|
|
199
|
+
// answers null on refusal, so the throw above turns it into a 404 —
|
|
200
|
+
// `getFileStream()` is never reached here. Keep that call before this
|
|
201
|
+
// block. Note also that the issued URL is a bearer capability: anyone
|
|
202
|
+
// holding it can fetch the object until it expires, without a session. Keep
|
|
203
|
+
// the expiry short, and do not enable presigned downloads for files whose
|
|
204
|
+
// audience is narrower than "anyone who was once allowed to see the link".
|
|
205
|
+
// Only S3-stored bytes can be handed over as a presigned URL, and `resolveFile`
|
|
206
|
+
// already established the store — so a GridFS or filesystem download no longer
|
|
207
|
+
// pays a pointless S3 metadata lookup to find that out.
|
|
208
|
+
const url = resolved.store === 's3' ? await this.resolveDownloadUrl(id, serviceOptions) : undefined;
|
|
209
|
+
if (url) {
|
|
210
|
+
this.setNoStore(res);
|
|
211
|
+
// `res.redirect()` is typed `void`, so returning it directly widened this method's inferred
|
|
212
|
+
// return type to `Promise<void | Response>` — a source-invisible BREAKING change for every
|
|
213
|
+
// project that overrides `getFileById`/`getFile` with an explicit `Promise<Response>` and
|
|
214
|
+
// delegates to super. Nest ignores the returned value once `@Res()` is used, so returning
|
|
215
|
+
// `res` is equivalent and keeps the published contract intact. The explicit annotation on
|
|
216
|
+
// both methods pins it, so inference can never silently widen it again.
|
|
217
|
+
res.redirect(302, url);
|
|
218
|
+
return res;
|
|
219
|
+
}
|
|
220
|
+
const filestream = await this.fileService.getFileStream(id, serviceOptions, resolved.store);
|
|
221
|
+
// `getFileStream` answers null when the service's own rights check refuses.
|
|
222
|
+
// Same answer as an unknown id: never confirm that the file exists.
|
|
223
|
+
if (!filestream) {
|
|
224
|
+
throw new NotFoundException(ErrorCode.FILE_NOT_FOUND);
|
|
225
|
+
}
|
|
226
|
+
this.setFileHeaders(res, file);
|
|
227
|
+
return this.pipeFileToResponse(filestream, res);
|
|
40
228
|
}
|
|
41
229
|
|
|
42
230
|
/**
|
|
@@ -44,21 +232,81 @@ export abstract class CoreFileController {
|
|
|
44
232
|
*
|
|
45
233
|
* Note: If multiple files have the same filename, only the first match is returned.
|
|
46
234
|
* For unique file access, use GET /files/id/:id instead.
|
|
235
|
+
*
|
|
236
|
+
* SECURITY: gated by `file.downloadRoles` — see `getFileById()`. Prefer the
|
|
237
|
+
* id route when widening: this one resolves the FIRST match for a name a
|
|
238
|
+
* caller may be able to guess, so it leaks across files that share a name.
|
|
47
239
|
*/
|
|
48
240
|
@Get(':filename')
|
|
49
|
-
@Roles(RoleEnum.
|
|
50
|
-
async getFile(
|
|
241
|
+
@Roles(RoleEnum.ADMIN)
|
|
242
|
+
async getFile(
|
|
243
|
+
@Param('filename') filename: string,
|
|
244
|
+
@Res() res: Response,
|
|
245
|
+
@CurrentUser() currentUser?: any,
|
|
246
|
+
): Promise<Response> {
|
|
51
247
|
if (!filename) {
|
|
52
|
-
throw new BadRequestException(
|
|
248
|
+
throw new BadRequestException(ErrorCode.REQUIRED_FIELD_MISSING);
|
|
53
249
|
}
|
|
54
250
|
|
|
55
|
-
const
|
|
251
|
+
const serviceOptions = { currentUser };
|
|
252
|
+
const file = await this.fileService.getFileInfoByName(filename, serviceOptions);
|
|
56
253
|
if (!file) {
|
|
57
|
-
throw new NotFoundException(
|
|
254
|
+
throw new NotFoundException(ErrorCode.FILE_NOT_FOUND);
|
|
255
|
+
}
|
|
256
|
+
// See getFileById(): authorization on the presigned branch rests on the
|
|
257
|
+
// getFileInfoByName() call above.
|
|
258
|
+
const url = await this.resolveDownloadUrl(file.id, serviceOptions);
|
|
259
|
+
if (url) {
|
|
260
|
+
this.setNoStore(res);
|
|
261
|
+
res.redirect(302, url);
|
|
262
|
+
return res;
|
|
263
|
+
}
|
|
264
|
+
const filestream = await this.fileService.getFileStream(file.id, serviceOptions);
|
|
265
|
+
if (!filestream) {
|
|
266
|
+
throw new NotFoundException(ErrorCode.FILE_NOT_FOUND);
|
|
58
267
|
}
|
|
59
|
-
|
|
268
|
+
this.setFileHeaders(res, file);
|
|
269
|
+
return this.pipeFileToResponse(filestream, res);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Set the response headers that describe the file being delivered.
|
|
274
|
+
*
|
|
275
|
+
* `Cache-Control: private, no-store` is the security-relevant one. These
|
|
276
|
+
* routes are authorization-gated, and RFC 9111 lets a shared cache store a
|
|
277
|
+
* response that carries no cache directive. A reverse proxy or CDN with a
|
|
278
|
+
* blanket `/files/*` rule would then be free to hand an authorized response
|
|
279
|
+
* to the next, unauthorized requester — reopening at the proxy layer exactly
|
|
280
|
+
* what the role gate closes at the application layer. The directive costs one
|
|
281
|
+
* header and removes that entire class of misconfiguration.
|
|
282
|
+
*
|
|
283
|
+
* `no-store` also suppresses browser disk caching, which is the conservative
|
|
284
|
+
* choice for a bucket that may hold documents. A project serving public,
|
|
285
|
+
* immutable assets can override this to `public, max-age=…` — GridFS blobs
|
|
286
|
+
* are immutable once written, so a validator built from `_id` + `uploadDate`
|
|
287
|
+
* is sound. Do that only for files that are genuinely public.
|
|
288
|
+
*/
|
|
289
|
+
protected setFileHeaders(res: Response, file: { contentType?: string; filename?: string }): void {
|
|
290
|
+
this.setNoStore(res);
|
|
60
291
|
res.header('Content-Type', file.contentType || 'application/octet-stream');
|
|
61
|
-
|
|
62
|
-
|
|
292
|
+
// See {@link buildContentDisposition} for why the two filename parameters are
|
|
293
|
+
// rendered differently, and for the injection cases the sanitiser closes.
|
|
294
|
+
res.header('Content-Disposition', buildContentDisposition(file.filename));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Mark a download response as uncacheable by any shared cache.
|
|
299
|
+
*
|
|
300
|
+
* Applies to BOTH delivery paths. On the streaming path it stops a reverse
|
|
301
|
+
* proxy or CDN with a blanket `/files/*` rule from handing an authorized
|
|
302
|
+
* response to the next, unauthorized requester. On the presigned path it
|
|
303
|
+
* matters more, not less: that response's `Location` is a bearer capability
|
|
304
|
+
* that works with no session, from any IP, until it expires — and while a 302
|
|
305
|
+
* is not heuristically cacheable per RFC 9111, `proxy_cache_valid 200 302 …`
|
|
306
|
+
* is the single most-copied nginx caching snippet, and Cloudflare's "Cache
|
|
307
|
+
* Everything" stores it too.
|
|
308
|
+
*/
|
|
309
|
+
protected setNoStore(res: Response): void {
|
|
310
|
+
res.header('Cache-Control', 'private, no-store');
|
|
63
311
|
}
|
|
64
312
|
}
|
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
|
2
2
|
import * as GraphQLUpload from 'graphql-upload/GraphQLUpload.js';
|
|
3
3
|
|
|
4
|
+
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
|
4
5
|
import { Roles } from '../../common/decorators/roles.decorator';
|
|
5
6
|
import { RoleEnum } from '../../common/enums/role.enum';
|
|
7
|
+
import { SkipTenantCheck } from '../tenant/core-tenant.decorators';
|
|
6
8
|
import { CoreFileInfo } from './core-file-info.model';
|
|
7
9
|
import { CoreFileService } from './core-file.service';
|
|
8
10
|
import { FileUpload } from './interfaces/file-upload.interface';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* File resolver
|
|
14
|
+
*
|
|
15
|
+
* SECURITY: gated by `file.downloadRoles` (`getFileInfo`), `file.uploadRoles`
|
|
16
|
+
* (`uploadFile`, `uploadFiles`) and `file.deleteRoles` (`deleteFile`), each
|
|
17
|
+
* defaulting to `[ADMIN]`. The decorators below are the fallback —
|
|
18
|
+
* `CoreModule.forRoot()` rewrites them from config.
|
|
19
|
+
*
|
|
20
|
+
* All four previously carried a handler-level `@Roles(RoleEnum.S_EVERYONE)`,
|
|
21
|
+
* which the roles guard turns into an unconditional `return true` WITHOUT
|
|
22
|
+
* authenticating, defeating the class-level `@Roles(ADMIN)` above (handler and
|
|
23
|
+
* class roles are UNIONed, and `S_EVERYONE` in that union short-circuits).
|
|
24
|
+
* That exposed reads, uploads and DELETES to anonymous callers — but only on a
|
|
25
|
+
* consumer that registers this resolver or a subclass of it: the framework
|
|
26
|
+
* itself registers no `CoreFileModule`, so enabling GraphQL alone did not.
|
|
27
|
+
*
|
|
28
|
+
* TENANT SCOPING: `@SkipTenantCheck()` for the same reason as
|
|
29
|
+
* `CoreFileController` — GridFS is not tenant-scoped, so a role name alone can
|
|
30
|
+
* never express a per-tenant rule. See that class for the full explanation.
|
|
31
|
+
*
|
|
32
|
+
* See `src/core/modules/file/README.md` § Access control.
|
|
12
33
|
*/
|
|
13
34
|
@Resolver()
|
|
14
35
|
@Roles(RoleEnum.ADMIN)
|
|
36
|
+
@SkipTenantCheck()
|
|
15
37
|
export class CoreFileResolver {
|
|
16
38
|
/**
|
|
17
39
|
* Integrate services
|
|
@@ -26,9 +48,12 @@ export class CoreFileResolver {
|
|
|
26
48
|
* Get file info
|
|
27
49
|
*/
|
|
28
50
|
@Query(() => CoreFileInfo, { nullable: true })
|
|
29
|
-
@Roles(RoleEnum.
|
|
30
|
-
async getFileInfo(
|
|
31
|
-
|
|
51
|
+
@Roles(RoleEnum.ADMIN)
|
|
52
|
+
async getFileInfo(
|
|
53
|
+
@Args({ name: 'filename', type: () => String }) filename: string,
|
|
54
|
+
@CurrentUser() currentUser?: any,
|
|
55
|
+
): Promise<any> {
|
|
56
|
+
return await this.fileService.getFileInfoByName(filename, { currentUser });
|
|
32
57
|
}
|
|
33
58
|
|
|
34
59
|
// ===========================================================================
|
|
@@ -39,26 +64,35 @@ export class CoreFileResolver {
|
|
|
39
64
|
* Delete file
|
|
40
65
|
*/
|
|
41
66
|
@Mutation(() => CoreFileInfo)
|
|
42
|
-
@Roles(RoleEnum.
|
|
43
|
-
async deleteFile(
|
|
44
|
-
|
|
67
|
+
@Roles(RoleEnum.ADMIN)
|
|
68
|
+
async deleteFile(
|
|
69
|
+
@Args({ name: 'filename', type: () => String }) filename: string,
|
|
70
|
+
@CurrentUser() currentUser?: any,
|
|
71
|
+
): Promise<any> {
|
|
72
|
+
return await this.fileService.deleteFileByName(filename, { currentUser });
|
|
45
73
|
}
|
|
46
74
|
|
|
47
75
|
/**
|
|
48
76
|
* Upload file
|
|
49
77
|
*/
|
|
50
78
|
@Mutation(() => CoreFileInfo)
|
|
51
|
-
@Roles(RoleEnum.
|
|
52
|
-
async uploadFile(
|
|
53
|
-
|
|
79
|
+
@Roles(RoleEnum.ADMIN)
|
|
80
|
+
async uploadFile(
|
|
81
|
+
@Args({ name: 'file', type: () => GraphQLUpload }) file: FileUpload,
|
|
82
|
+
@CurrentUser() currentUser?: any,
|
|
83
|
+
): Promise<any> {
|
|
84
|
+
return await this.fileService.createFile(file, { currentUser });
|
|
54
85
|
}
|
|
55
86
|
|
|
56
87
|
/**
|
|
57
88
|
* Upload files
|
|
58
89
|
*/
|
|
59
90
|
@Mutation(() => [CoreFileInfo])
|
|
60
|
-
@Roles(RoleEnum.
|
|
61
|
-
async uploadFiles(
|
|
62
|
-
|
|
91
|
+
@Roles(RoleEnum.ADMIN)
|
|
92
|
+
async uploadFiles(
|
|
93
|
+
@Args({ name: 'files', type: () => [GraphQLUpload] }) files: FileUpload[],
|
|
94
|
+
@CurrentUser() currentUser?: any,
|
|
95
|
+
): Promise<any> {
|
|
96
|
+
return await this.fileService.createFiles(files, { currentUser });
|
|
63
97
|
}
|
|
64
98
|
}
|