@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.
Files changed (262) hide show
  1. package/.claude/rules/architecture.md +16 -0
  2. package/.claude/rules/configurable-features.md +11 -3
  3. package/.claude/rules/testing.md +101 -1
  4. package/CLAUDE.md +5 -0
  5. package/FRAMEWORK-API.md +58 -3
  6. package/bin/migrate.js +13 -3
  7. package/dist/core/common/helpers/content-disposition.helper.d.ts +1 -0
  8. package/dist/core/common/helpers/content-disposition.helper.js +14 -0
  9. package/dist/core/common/helpers/content-disposition.helper.js.map +1 -0
  10. package/dist/core/common/helpers/file.helper.d.ts +21 -2
  11. package/dist/core/common/helpers/file.helper.js +69 -13
  12. package/dist/core/common/helpers/file.helper.js.map +1 -1
  13. package/dist/core/common/helpers/graceful-shutdown.helper.d.ts +2 -0
  14. package/dist/core/common/helpers/graceful-shutdown.helper.js +55 -0
  15. package/dist/core/common/helpers/graceful-shutdown.helper.js.map +1 -0
  16. package/dist/core/common/helpers/project-name.helper.d.ts +2 -0
  17. package/dist/core/common/helpers/project-name.helper.js +38 -0
  18. package/dist/core/common/helpers/project-name.helper.js.map +1 -0
  19. package/dist/core/common/interfaces/cron-job-config.interface.d.ts +1 -0
  20. package/dist/core/common/interfaces/server-options.interface.d.ts +39 -0
  21. package/dist/core/common/services/core-cron-jobs.initializer.d.ts +10 -0
  22. package/dist/core/common/services/core-cron-jobs.initializer.js +46 -0
  23. package/dist/core/common/services/core-cron-jobs.initializer.js.map +1 -0
  24. package/dist/core/common/services/core-cron-jobs.registry.d.ts +8 -0
  25. package/dist/core/common/services/core-cron-jobs.registry.js +12 -0
  26. package/dist/core/common/services/core-cron-jobs.registry.js.map +1 -0
  27. package/dist/core/common/services/core-cron-jobs.service.d.ts +37 -7
  28. package/dist/core/common/services/core-cron-jobs.service.js +278 -27
  29. package/dist/core/common/services/core-cron-jobs.service.js.map +1 -1
  30. package/dist/core/common/services/core-redis-pubsub.d.ts +20 -0
  31. package/dist/core/common/services/core-redis-pubsub.js +112 -0
  32. package/dist/core/common/services/core-redis-pubsub.js.map +1 -0
  33. package/dist/core/common/services/core-redis.service.d.ts +25 -0
  34. package/dist/core/common/services/core-redis.service.js +154 -0
  35. package/dist/core/common/services/core-redis.service.js.map +1 -0
  36. package/dist/core/common/services/core-s3.service.d.ts +51 -0
  37. package/dist/core/common/services/core-s3.service.js +261 -0
  38. package/dist/core/common/services/core-s3.service.js.map +1 -0
  39. package/dist/core/common/services/core-trust-proxy.initializer.d.ts +16 -0
  40. package/dist/core/common/services/core-trust-proxy.initializer.js +77 -0
  41. package/dist/core/common/services/core-trust-proxy.initializer.js.map +1 -0
  42. package/dist/core/common/services/rate-limit-store.d.ts +61 -0
  43. package/dist/core/common/services/rate-limit-store.js +235 -0
  44. package/dist/core/common/services/rate-limit-store.js.map +1 -0
  45. package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +19 -3
  46. package/dist/core/modules/ai/core-ai-mcp.controller.js +148 -18
  47. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  48. package/dist/core/modules/ai/services/core-ai.service.d.ts +7 -2
  49. package/dist/core/modules/ai/services/core-ai.service.js +24 -20
  50. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  51. package/dist/core/modules/auth/core-auth.module.js +4 -1
  52. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  53. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +1 -1
  54. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +3 -14
  55. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -1
  56. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +12 -10
  57. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +30 -59
  58. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
  59. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +8 -2
  60. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +118 -45
  61. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  62. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +1 -1
  63. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -11
  64. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
  65. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +12 -10
  66. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +30 -85
  67. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
  68. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +0 -1
  69. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  70. package/dist/core/modules/file/core-file.controller.d.ts +15 -3
  71. package/dist/core/modules/file/core-file.controller.js +90 -20
  72. package/dist/core/modules/file/core-file.controller.js.map +1 -1
  73. package/dist/core/modules/file/core-file.resolver.d.ts +4 -4
  74. package/dist/core/modules/file/core-file.resolver.js +23 -16
  75. package/dist/core/modules/file/core-file.resolver.js.map +1 -1
  76. package/dist/core/modules/file/core-file.service.d.ts +41 -7
  77. package/dist/core/modules/file/core-file.service.js +227 -7
  78. package/dist/core/modules/file/core-file.service.js.map +1 -1
  79. package/dist/core/modules/file/file-metadata.helper.d.ts +16 -0
  80. package/dist/core/modules/file/file-metadata.helper.js +33 -0
  81. package/dist/core/modules/file/file-metadata.helper.js.map +1 -0
  82. package/dist/core/modules/file/file-roles.helper.d.ts +4 -0
  83. package/dist/core/modules/file/file-roles.helper.js +53 -0
  84. package/dist/core/modules/file/file-roles.helper.js.map +1 -0
  85. package/dist/core/modules/file/file-storage.helper.d.ts +13 -0
  86. package/dist/core/modules/file/file-storage.helper.js +65 -0
  87. package/dist/core/modules/file/file-storage.helper.js.map +1 -0
  88. package/dist/core/modules/file/filesystem-file.helper.d.ts +22 -0
  89. package/dist/core/modules/file/filesystem-file.helper.js +77 -0
  90. package/dist/core/modules/file/filesystem-file.helper.js.map +1 -0
  91. package/dist/core/modules/file/interfaces/file-service-options.interface.d.ts +1 -0
  92. package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +7 -1
  93. package/dist/core/modules/file/s3-file.helper.d.ts +37 -0
  94. package/dist/core/modules/file/s3-file.helper.js +81 -0
  95. package/dist/core/modules/file/s3-file.helper.js.map +1 -0
  96. package/dist/core/modules/hub/core-hub.controller.d.ts +8 -8
  97. package/dist/core/modules/hub/core-hub.controller.js +24 -22
  98. package/dist/core/modules/hub/core-hub.controller.js.map +1 -1
  99. package/dist/core/modules/hub/helpers/hub-mask.helper.js +2 -2
  100. package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -1
  101. package/dist/core/modules/hub/hub-buffer.d.ts +23 -0
  102. package/dist/core/modules/hub/hub-buffer.js +101 -0
  103. package/dist/core/modules/hub/hub-buffer.js.map +1 -0
  104. package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +7 -5
  105. package/dist/core/modules/hub/services/core-hub-mailbox.service.js +14 -10
  106. package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -1
  107. package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +5 -2
  108. package/dist/core/modules/hub/services/hub-log-buffer.service.js +17 -8
  109. package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -1
  110. package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +4 -2
  111. package/dist/core/modules/hub/services/hub-query-profiler.service.js +13 -7
  112. package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -1
  113. package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +4 -2
  114. package/dist/core/modules/hub/services/hub-trace-buffer.service.js +13 -8
  115. package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -1
  116. package/dist/core/modules/migrate/cli/migrate-cli.d.ts +3 -1
  117. package/dist/core/modules/migrate/cli/migrate-cli.js +29 -4
  118. package/dist/core/modules/migrate/cli/migrate-cli.js.map +1 -1
  119. package/dist/core/modules/migrate/helpers/migration.helper.d.ts +3 -6
  120. package/dist/core/modules/migrate/helpers/migration.helper.js +55 -8
  121. package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -1
  122. package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
  123. package/dist/core/modules/migrate/migration-runner.js +4 -0
  124. package/dist/core/modules/migrate/migration-runner.js.map +1 -1
  125. package/dist/core/modules/migrate/mongo-state-store.d.ts +1 -0
  126. package/dist/core/modules/migrate/mongo-state-store.js +98 -9
  127. package/dist/core/modules/migrate/mongo-state-store.js.map +1 -1
  128. package/dist/core/modules/system-setup/core-system-setup.service.d.ts +2 -0
  129. package/dist/core/modules/system-setup/core-system-setup.service.js +31 -1
  130. package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
  131. package/dist/core/modules/tenant/core-tenant.guard.d.ts +17 -3
  132. package/dist/core/modules/tenant/core-tenant.guard.js +92 -15
  133. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  134. package/dist/core/modules/tenant/core-tenant.module.js +3 -4
  135. package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
  136. package/dist/core/modules/tus/core-tus.controller.d.ts +2 -0
  137. package/dist/core/modules/tus/core-tus.controller.js +29 -3
  138. package/dist/core/modules/tus/core-tus.controller.js.map +1 -1
  139. package/dist/core/modules/tus/core-tus.service.d.ts +24 -1
  140. package/dist/core/modules/tus/core-tus.service.js +182 -28
  141. package/dist/core/modules/tus/core-tus.service.js.map +1 -1
  142. package/dist/core/modules/tus/interfaces/tus-config.interface.js +3 -0
  143. package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -1
  144. package/dist/core/modules/tus/tus-redis-locker.d.ts +25 -0
  145. package/dist/core/modules/tus/tus-redis-locker.js +87 -0
  146. package/dist/core/modules/tus/tus-redis-locker.js.map +1 -0
  147. package/dist/core/modules/tus/tus.module.d.ts +1 -0
  148. package/dist/core/modules/tus/tus.module.js +30 -4
  149. package/dist/core/modules/tus/tus.module.js.map +1 -1
  150. package/dist/core.module.js +18 -1
  151. package/dist/core.module.js.map +1 -1
  152. package/dist/index.d.ts +10 -0
  153. package/dist/index.js +10 -0
  154. package/dist/index.js.map +1 -1
  155. package/dist/main.js +2 -1
  156. package/dist/main.js.map +1 -1
  157. package/dist/server/modules/file/file.controller.js.map +1 -1
  158. package/dist/server/modules/file/file.resolver.js +1 -12
  159. package/dist/server/modules/file/file.resolver.js.map +1 -1
  160. package/dist/server/modules/file/file.service.d.ts +5 -1
  161. package/dist/server/modules/file/file.service.js +12 -3
  162. package/dist/server/modules/file/file.service.js.map +1 -1
  163. package/dist/server/modules/user/avatar.controller.d.ts +5 -1
  164. package/dist/server/modules/user/avatar.controller.js +26 -10
  165. package/dist/server/modules/user/avatar.controller.js.map +1 -1
  166. package/dist/server/modules/user/user.model.js +3 -2
  167. package/dist/server/modules/user/user.model.js.map +1 -1
  168. package/dist/server/modules/user/user.module.js +6 -2
  169. package/dist/server/modules/user/user.module.js.map +1 -1
  170. package/dist/server/modules/user/user.service.d.ts +1 -1
  171. package/dist/server/modules/user/user.service.js +6 -13
  172. package/dist/server/modules/user/user.service.js.map +1 -1
  173. package/dist/tsconfig.build.tsbuildinfo +1 -1
  174. package/docs/REQUEST-LIFECYCLE.md +57 -8
  175. package/docs/security-overrides.md +30 -5
  176. package/migration-guides/11.32.3-to-11.32.4.md +335 -0
  177. package/migration-guides/11.32.x-to-11.33.x.md +1551 -0
  178. package/package.json +74 -42
  179. package/src/core/common/helpers/content-disposition.helper.ts +90 -0
  180. package/src/core/common/helpers/file.helper.ts +201 -20
  181. package/src/core/common/helpers/graceful-shutdown.helper.ts +116 -0
  182. package/src/core/common/helpers/project-name.helper.ts +71 -0
  183. package/src/core/common/interfaces/cron-job-config.interface.ts +16 -0
  184. package/src/core/common/interfaces/server-options.interface.ts +439 -2
  185. package/src/core/common/services/core-cron-jobs.initializer.ts +39 -0
  186. package/src/core/common/services/core-cron-jobs.registry.ts +45 -0
  187. package/src/core/common/services/core-cron-jobs.service.ts +589 -47
  188. package/src/core/common/services/core-redis-pubsub.ts +183 -0
  189. package/src/core/common/services/core-redis.service.ts +249 -0
  190. package/src/core/common/services/core-s3.service.ts +465 -0
  191. package/src/core/common/services/core-trust-proxy.initializer.ts +112 -0
  192. package/src/core/common/services/rate-limit-store.ts +492 -0
  193. package/src/core/modules/ai/README.md +25 -0
  194. package/src/core/modules/ai/core-ai-mcp.controller.ts +253 -22
  195. package/src/core/modules/ai/services/core-ai.service.ts +33 -24
  196. package/src/core/modules/auth/core-auth.module.ts +7 -1
  197. package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +12 -21
  198. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +49 -93
  199. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +202 -66
  200. package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +11 -18
  201. package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +51 -132
  202. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -1
  203. package/src/core/modules/file/INTEGRATION-CHECKLIST.md +108 -0
  204. package/src/core/modules/file/README.md +324 -41
  205. package/src/core/modules/file/core-file.controller.ts +267 -19
  206. package/src/core/modules/file/core-file.resolver.ts +46 -12
  207. package/src/core/modules/file/core-file.service.ts +482 -16
  208. package/src/core/modules/file/file-metadata.helper.ts +142 -0
  209. package/src/core/modules/file/file-roles.helper.ts +110 -0
  210. package/src/core/modules/file/file-storage.helper.ts +163 -0
  211. package/src/core/modules/file/filesystem-file.helper.ts +184 -0
  212. package/src/core/modules/file/interfaces/file-service-options.interface.ts +14 -0
  213. package/src/core/modules/file/interfaces/file-upload.interface.ts +17 -1
  214. package/src/core/modules/file/s3-file.helper.ts +209 -0
  215. package/src/core/modules/hub/README.md +48 -0
  216. package/src/core/modules/hub/core-hub.controller.ts +18 -16
  217. package/src/core/modules/hub/helpers/hub-mask.helper.ts +21 -4
  218. package/src/core/modules/hub/hub-buffer.ts +198 -0
  219. package/src/core/modules/hub/services/core-hub-mailbox.service.ts +22 -12
  220. package/src/core/modules/hub/services/hub-log-buffer.service.ts +24 -8
  221. package/src/core/modules/hub/services/hub-query-profiler.service.ts +9 -6
  222. package/src/core/modules/hub/services/hub-trace-buffer.service.ts +14 -9
  223. package/src/core/modules/migrate/README.md +68 -6
  224. package/src/core/modules/migrate/cli/migrate-cli.ts +69 -6
  225. package/src/core/modules/migrate/helpers/migration.helper.ts +144 -10
  226. package/src/core/modules/migrate/migration-runner.ts +10 -1
  227. package/src/core/modules/migrate/mongo-state-store.ts +179 -11
  228. package/src/core/modules/system-setup/README.md +7 -2
  229. package/src/core/modules/system-setup/core-system-setup.service.ts +95 -1
  230. package/src/core/modules/tenant/README.md +21 -1
  231. package/src/core/modules/tenant/core-tenant.guard.ts +135 -14
  232. package/src/core/modules/tenant/core-tenant.module.ts +8 -4
  233. package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +93 -21
  234. package/src/core/modules/tus/README.md +35 -2
  235. package/src/core/modules/tus/core-tus.controller.ts +64 -12
  236. package/src/core/modules/tus/core-tus.service.ts +358 -34
  237. package/src/core/modules/tus/interfaces/tus-config.interface.ts +9 -0
  238. package/src/core/modules/tus/tus-redis-locker.ts +139 -0
  239. package/src/core/modules/tus/tus.module.ts +78 -6
  240. package/src/core.module.ts +26 -1
  241. package/src/index.ts +10 -0
  242. package/src/main.ts +7 -1
  243. package/src/server/modules/file/file.controller.ts +7 -3
  244. package/src/server/modules/file/file.resolver.ts +5 -17
  245. package/src/server/modules/file/file.service.ts +42 -3
  246. package/src/server/modules/user/avatar.controller.ts +39 -9
  247. package/src/server/modules/user/user.model.ts +9 -3
  248. package/src/server/modules/user/user.module.ts +12 -3
  249. package/src/server/modules/user/user.service.ts +20 -17
  250. package/src/test/README.md +7 -2
  251. package/src/core/common/helpers/logging.helper.spec.ts +0 -61
  252. package/src/core/common/helpers/process-diagnostics.helper.spec.ts +0 -310
  253. package/src/core/common/services/brevo.service.spec.ts +0 -266
  254. package/src/core/modules/hub/core-hub.module.spec.ts +0 -108
  255. package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +0 -48
  256. package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +0 -67
  257. package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +0 -61
  258. package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +0 -106
  259. package/src/core/modules/hub/hub-config.helper.spec.ts +0 -108
  260. package/src/core/modules/hub/hub-ring-buffer.spec.ts +0 -95
  261. package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +0 -116
  262. 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.S_EVERYONE)
27
- async getFileById(@Param('id') id: string, @Res() res: Response) {
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('Missing file ID for download');
181
+ throw new BadRequestException(ErrorCode.REQUIRED_FIELD_MISSING);
30
182
  }
31
183
 
32
- const file = await this.fileService.getFileInfo(id);
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('File not found');
192
+ throw new NotFoundException(ErrorCode.FILE_NOT_FOUND);
35
193
  }
36
- const filestream = await this.fileService.getFileStream(id);
37
- res.header('Content-Type', file.contentType || 'application/octet-stream');
38
- res.header('Content-Disposition', `attachment; filename=${file.filename}`);
39
- return filestream.pipe(res);
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.S_EVERYONE)
50
- async getFile(@Param('filename') filename: string, @Res() res: Response) {
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('Missing filename for download');
248
+ throw new BadRequestException(ErrorCode.REQUIRED_FIELD_MISSING);
53
249
  }
54
250
 
55
- const file = await this.fileService.getFileInfoByName(filename);
251
+ const serviceOptions = { currentUser };
252
+ const file = await this.fileService.getFileInfoByName(filename, serviceOptions);
56
253
  if (!file) {
57
- throw new NotFoundException('File not found');
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
- const filestream = await this.fileService.getFileStream(file.id);
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
- res.header('Content-Disposition', `attachment; filename=${file.filename}`);
62
- return filestream.pipe(res);
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.S_EVERYONE)
30
- async getFileInfo(@Args({ name: 'filename', type: () => String }) filename: string): Promise<any> {
31
- return await this.fileService.getFileInfoByName(filename);
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.S_EVERYONE)
43
- async deleteFile(@Args({ name: 'filename', type: () => String }) filename: string): Promise<any> {
44
- return await this.fileService.deleteFileByName(filename);
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.S_EVERYONE)
52
- async uploadFile(@Args({ name: 'file', type: () => GraphQLUpload }) file: FileUpload): Promise<any> {
53
- return await this.fileService.createFile(file);
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.S_EVERYONE)
61
- async uploadFiles(@Args({ name: 'files', type: () => [GraphQLUpload] }) files: FileUpload[]): Promise<any> {
62
- return await this.fileService.createFiles(files);
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
  }