@lenne.tech/nest-server 11.32.4 → 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 (260) 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/dist/core/common/helpers/content-disposition.helper.d.ts +1 -0
  7. package/dist/core/common/helpers/content-disposition.helper.js +14 -0
  8. package/dist/core/common/helpers/content-disposition.helper.js.map +1 -0
  9. package/dist/core/common/helpers/file.helper.d.ts +7 -0
  10. package/dist/core/common/helpers/file.helper.js +21 -4
  11. package/dist/core/common/helpers/file.helper.js.map +1 -1
  12. package/dist/core/common/helpers/graceful-shutdown.helper.d.ts +2 -0
  13. package/dist/core/common/helpers/graceful-shutdown.helper.js +55 -0
  14. package/dist/core/common/helpers/graceful-shutdown.helper.js.map +1 -0
  15. package/dist/core/common/helpers/project-name.helper.d.ts +2 -0
  16. package/dist/core/common/helpers/project-name.helper.js +38 -0
  17. package/dist/core/common/helpers/project-name.helper.js.map +1 -0
  18. package/dist/core/common/interfaces/cron-job-config.interface.d.ts +1 -0
  19. package/dist/core/common/interfaces/server-options.interface.d.ts +39 -0
  20. package/dist/core/common/services/core-cron-jobs.initializer.d.ts +10 -0
  21. package/dist/core/common/services/core-cron-jobs.initializer.js +46 -0
  22. package/dist/core/common/services/core-cron-jobs.initializer.js.map +1 -0
  23. package/dist/core/common/services/core-cron-jobs.registry.d.ts +8 -0
  24. package/dist/core/common/services/core-cron-jobs.registry.js +12 -0
  25. package/dist/core/common/services/core-cron-jobs.registry.js.map +1 -0
  26. package/dist/core/common/services/core-cron-jobs.service.d.ts +37 -7
  27. package/dist/core/common/services/core-cron-jobs.service.js +278 -27
  28. package/dist/core/common/services/core-cron-jobs.service.js.map +1 -1
  29. package/dist/core/common/services/core-redis-pubsub.d.ts +20 -0
  30. package/dist/core/common/services/core-redis-pubsub.js +112 -0
  31. package/dist/core/common/services/core-redis-pubsub.js.map +1 -0
  32. package/dist/core/common/services/core-redis.service.d.ts +25 -0
  33. package/dist/core/common/services/core-redis.service.js +154 -0
  34. package/dist/core/common/services/core-redis.service.js.map +1 -0
  35. package/dist/core/common/services/core-s3.service.d.ts +51 -0
  36. package/dist/core/common/services/core-s3.service.js +261 -0
  37. package/dist/core/common/services/core-s3.service.js.map +1 -0
  38. package/dist/core/common/services/core-trust-proxy.initializer.d.ts +16 -0
  39. package/dist/core/common/services/core-trust-proxy.initializer.js +77 -0
  40. package/dist/core/common/services/core-trust-proxy.initializer.js.map +1 -0
  41. package/dist/core/common/services/rate-limit-store.d.ts +61 -0
  42. package/dist/core/common/services/rate-limit-store.js +235 -0
  43. package/dist/core/common/services/rate-limit-store.js.map +1 -0
  44. package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +19 -3
  45. package/dist/core/modules/ai/core-ai-mcp.controller.js +148 -18
  46. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  47. package/dist/core/modules/ai/services/core-ai.service.d.ts +7 -2
  48. package/dist/core/modules/ai/services/core-ai.service.js +24 -20
  49. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  50. package/dist/core/modules/auth/core-auth.module.js +4 -1
  51. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  52. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +1 -1
  53. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +3 -14
  54. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -1
  55. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +12 -10
  56. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +30 -59
  57. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
  58. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +8 -2
  59. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +118 -45
  60. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  61. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +1 -1
  62. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -11
  63. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
  64. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +12 -10
  65. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +30 -85
  66. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
  67. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +0 -1
  68. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  69. package/dist/core/modules/file/core-file.controller.d.ts +11 -2
  70. package/dist/core/modules/file/core-file.controller.js +52 -15
  71. package/dist/core/modules/file/core-file.controller.js.map +1 -1
  72. package/dist/core/modules/file/core-file.resolver.d.ts +4 -4
  73. package/dist/core/modules/file/core-file.resolver.js +23 -16
  74. package/dist/core/modules/file/core-file.resolver.js.map +1 -1
  75. package/dist/core/modules/file/core-file.service.d.ts +41 -7
  76. package/dist/core/modules/file/core-file.service.js +227 -7
  77. package/dist/core/modules/file/core-file.service.js.map +1 -1
  78. package/dist/core/modules/file/file-metadata.helper.d.ts +16 -0
  79. package/dist/core/modules/file/file-metadata.helper.js +33 -0
  80. package/dist/core/modules/file/file-metadata.helper.js.map +1 -0
  81. package/dist/core/modules/file/file-roles.helper.d.ts +4 -0
  82. package/dist/core/modules/file/file-roles.helper.js +53 -0
  83. package/dist/core/modules/file/file-roles.helper.js.map +1 -0
  84. package/dist/core/modules/file/file-storage.helper.d.ts +13 -0
  85. package/dist/core/modules/file/file-storage.helper.js +65 -0
  86. package/dist/core/modules/file/file-storage.helper.js.map +1 -0
  87. package/dist/core/modules/file/filesystem-file.helper.d.ts +22 -0
  88. package/dist/core/modules/file/filesystem-file.helper.js +77 -0
  89. package/dist/core/modules/file/filesystem-file.helper.js.map +1 -0
  90. package/dist/core/modules/file/interfaces/file-service-options.interface.d.ts +1 -0
  91. package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +7 -1
  92. package/dist/core/modules/file/s3-file.helper.d.ts +37 -0
  93. package/dist/core/modules/file/s3-file.helper.js +81 -0
  94. package/dist/core/modules/file/s3-file.helper.js.map +1 -0
  95. package/dist/core/modules/hub/core-hub.controller.d.ts +8 -8
  96. package/dist/core/modules/hub/core-hub.controller.js +24 -22
  97. package/dist/core/modules/hub/core-hub.controller.js.map +1 -1
  98. package/dist/core/modules/hub/helpers/hub-mask.helper.js +2 -2
  99. package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -1
  100. package/dist/core/modules/hub/hub-buffer.d.ts +23 -0
  101. package/dist/core/modules/hub/hub-buffer.js +101 -0
  102. package/dist/core/modules/hub/hub-buffer.js.map +1 -0
  103. package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +7 -5
  104. package/dist/core/modules/hub/services/core-hub-mailbox.service.js +14 -10
  105. package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -1
  106. package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +5 -2
  107. package/dist/core/modules/hub/services/hub-log-buffer.service.js +17 -8
  108. package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -1
  109. package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +4 -2
  110. package/dist/core/modules/hub/services/hub-query-profiler.service.js +13 -7
  111. package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -1
  112. package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +4 -2
  113. package/dist/core/modules/hub/services/hub-trace-buffer.service.js +13 -8
  114. package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -1
  115. package/dist/core/modules/migrate/helpers/migration.helper.d.ts +2 -6
  116. package/dist/core/modules/migrate/helpers/migration.helper.js +4 -4
  117. package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -1
  118. package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
  119. package/dist/core/modules/migrate/migration-runner.js +4 -0
  120. package/dist/core/modules/migrate/migration-runner.js.map +1 -1
  121. package/dist/core/modules/migrate/mongo-state-store.d.ts +1 -0
  122. package/dist/core/modules/migrate/mongo-state-store.js +98 -9
  123. package/dist/core/modules/migrate/mongo-state-store.js.map +1 -1
  124. package/dist/core/modules/system-setup/core-system-setup.service.d.ts +2 -0
  125. package/dist/core/modules/system-setup/core-system-setup.service.js +31 -1
  126. package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
  127. package/dist/core/modules/tenant/core-tenant.guard.d.ts +17 -3
  128. package/dist/core/modules/tenant/core-tenant.guard.js +92 -15
  129. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  130. package/dist/core/modules/tenant/core-tenant.module.js +3 -4
  131. package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
  132. package/dist/core/modules/tus/core-tus.controller.d.ts +2 -0
  133. package/dist/core/modules/tus/core-tus.controller.js +29 -3
  134. package/dist/core/modules/tus/core-tus.controller.js.map +1 -1
  135. package/dist/core/modules/tus/core-tus.service.d.ts +24 -1
  136. package/dist/core/modules/tus/core-tus.service.js +182 -28
  137. package/dist/core/modules/tus/core-tus.service.js.map +1 -1
  138. package/dist/core/modules/tus/interfaces/tus-config.interface.js +3 -0
  139. package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -1
  140. package/dist/core/modules/tus/tus-redis-locker.d.ts +25 -0
  141. package/dist/core/modules/tus/tus-redis-locker.js +87 -0
  142. package/dist/core/modules/tus/tus-redis-locker.js.map +1 -0
  143. package/dist/core/modules/tus/tus.module.d.ts +1 -0
  144. package/dist/core/modules/tus/tus.module.js +30 -4
  145. package/dist/core/modules/tus/tus.module.js.map +1 -1
  146. package/dist/core.module.js +18 -1
  147. package/dist/core.module.js.map +1 -1
  148. package/dist/index.d.ts +10 -0
  149. package/dist/index.js +10 -0
  150. package/dist/index.js.map +1 -1
  151. package/dist/main.js +2 -1
  152. package/dist/main.js.map +1 -1
  153. package/dist/server/modules/file/file.controller.js.map +1 -1
  154. package/dist/server/modules/file/file.resolver.js +1 -12
  155. package/dist/server/modules/file/file.resolver.js.map +1 -1
  156. package/dist/server/modules/file/file.service.d.ts +5 -1
  157. package/dist/server/modules/file/file.service.js +12 -3
  158. package/dist/server/modules/file/file.service.js.map +1 -1
  159. package/dist/server/modules/user/avatar.controller.d.ts +5 -1
  160. package/dist/server/modules/user/avatar.controller.js +26 -10
  161. package/dist/server/modules/user/avatar.controller.js.map +1 -1
  162. package/dist/server/modules/user/user.model.js +3 -2
  163. package/dist/server/modules/user/user.model.js.map +1 -1
  164. package/dist/server/modules/user/user.module.js +6 -2
  165. package/dist/server/modules/user/user.module.js.map +1 -1
  166. package/dist/server/modules/user/user.service.d.ts +1 -1
  167. package/dist/server/modules/user/user.service.js +6 -13
  168. package/dist/server/modules/user/user.service.js.map +1 -1
  169. package/dist/tsconfig.build.tsbuildinfo +1 -1
  170. package/docs/REQUEST-LIFECYCLE.md +57 -8
  171. package/docs/security-overrides.md +21 -3
  172. package/migration-guides/11.32.3-to-11.32.4.md +15 -3
  173. package/migration-guides/11.32.x-to-11.33.x.md +1551 -0
  174. package/package.json +74 -42
  175. package/src/core/common/helpers/content-disposition.helper.ts +90 -0
  176. package/src/core/common/helpers/file.helper.ts +53 -10
  177. package/src/core/common/helpers/graceful-shutdown.helper.ts +116 -0
  178. package/src/core/common/helpers/project-name.helper.ts +71 -0
  179. package/src/core/common/interfaces/cron-job-config.interface.ts +16 -0
  180. package/src/core/common/interfaces/server-options.interface.ts +439 -2
  181. package/src/core/common/services/core-cron-jobs.initializer.ts +39 -0
  182. package/src/core/common/services/core-cron-jobs.registry.ts +45 -0
  183. package/src/core/common/services/core-cron-jobs.service.ts +589 -47
  184. package/src/core/common/services/core-redis-pubsub.ts +183 -0
  185. package/src/core/common/services/core-redis.service.ts +249 -0
  186. package/src/core/common/services/core-s3.service.ts +465 -0
  187. package/src/core/common/services/core-trust-proxy.initializer.ts +112 -0
  188. package/src/core/common/services/rate-limit-store.ts +492 -0
  189. package/src/core/modules/ai/README.md +25 -0
  190. package/src/core/modules/ai/core-ai-mcp.controller.ts +253 -22
  191. package/src/core/modules/ai/services/core-ai.service.ts +33 -24
  192. package/src/core/modules/auth/core-auth.module.ts +7 -1
  193. package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +12 -21
  194. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +49 -93
  195. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +202 -66
  196. package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +11 -18
  197. package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +51 -132
  198. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -1
  199. package/src/core/modules/file/INTEGRATION-CHECKLIST.md +108 -0
  200. package/src/core/modules/file/README.md +277 -53
  201. package/src/core/modules/file/core-file.controller.ts +168 -12
  202. package/src/core/modules/file/core-file.resolver.ts +46 -12
  203. package/src/core/modules/file/core-file.service.ts +482 -16
  204. package/src/core/modules/file/file-metadata.helper.ts +142 -0
  205. package/src/core/modules/file/file-roles.helper.ts +110 -0
  206. package/src/core/modules/file/file-storage.helper.ts +163 -0
  207. package/src/core/modules/file/filesystem-file.helper.ts +184 -0
  208. package/src/core/modules/file/interfaces/file-service-options.interface.ts +14 -0
  209. package/src/core/modules/file/interfaces/file-upload.interface.ts +17 -1
  210. package/src/core/modules/file/s3-file.helper.ts +209 -0
  211. package/src/core/modules/hub/README.md +48 -0
  212. package/src/core/modules/hub/core-hub.controller.ts +18 -16
  213. package/src/core/modules/hub/helpers/hub-mask.helper.ts +21 -4
  214. package/src/core/modules/hub/hub-buffer.ts +198 -0
  215. package/src/core/modules/hub/services/core-hub-mailbox.service.ts +22 -12
  216. package/src/core/modules/hub/services/hub-log-buffer.service.ts +24 -8
  217. package/src/core/modules/hub/services/hub-query-profiler.service.ts +9 -6
  218. package/src/core/modules/hub/services/hub-trace-buffer.service.ts +14 -9
  219. package/src/core/modules/migrate/README.md +33 -6
  220. package/src/core/modules/migrate/helpers/migration.helper.ts +13 -6
  221. package/src/core/modules/migrate/migration-runner.ts +10 -1
  222. package/src/core/modules/migrate/mongo-state-store.ts +179 -11
  223. package/src/core/modules/system-setup/README.md +7 -2
  224. package/src/core/modules/system-setup/core-system-setup.service.ts +95 -1
  225. package/src/core/modules/tenant/README.md +21 -1
  226. package/src/core/modules/tenant/core-tenant.guard.ts +135 -14
  227. package/src/core/modules/tenant/core-tenant.module.ts +8 -4
  228. package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +93 -21
  229. package/src/core/modules/tus/README.md +35 -2
  230. package/src/core/modules/tus/core-tus.controller.ts +64 -12
  231. package/src/core/modules/tus/core-tus.service.ts +358 -34
  232. package/src/core/modules/tus/interfaces/tus-config.interface.ts +9 -0
  233. package/src/core/modules/tus/tus-redis-locker.ts +139 -0
  234. package/src/core/modules/tus/tus.module.ts +78 -6
  235. package/src/core.module.ts +26 -1
  236. package/src/index.ts +10 -0
  237. package/src/main.ts +7 -1
  238. package/src/server/modules/file/file.controller.ts +7 -3
  239. package/src/server/modules/file/file.resolver.ts +5 -17
  240. package/src/server/modules/file/file.service.ts +42 -3
  241. package/src/server/modules/user/avatar.controller.ts +39 -9
  242. package/src/server/modules/user/user.model.ts +9 -3
  243. package/src/server/modules/user/user.module.ts +12 -3
  244. package/src/server/modules/user/user.service.ts +20 -17
  245. package/src/test/README.md +7 -2
  246. package/src/core/common/helpers/file.helper.spec.ts +0 -145
  247. package/src/core/common/helpers/logging.helper.spec.ts +0 -61
  248. package/src/core/common/helpers/process-diagnostics.helper.spec.ts +0 -310
  249. package/src/core/common/services/brevo.service.spec.ts +0 -266
  250. package/src/core/modules/file/core-file.controller.spec.ts +0 -164
  251. package/src/core/modules/hub/core-hub.module.spec.ts +0 -108
  252. package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +0 -48
  253. package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +0 -67
  254. package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +0 -61
  255. package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +0 -106
  256. package/src/core/modules/hub/hub-config.helper.spec.ts +0 -108
  257. package/src/core/modules/hub/hub-ring-buffer.spec.ts +0 -95
  258. package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +0 -116
  259. package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +0 -112
  260. package/src/core/modules/migrate/helpers/migration.helper.spec.ts +0 -85
@@ -0,0 +1,139 @@
1
+ import { Logger } from '@nestjs/common';
2
+ import { randomUUID } from 'node:crypto';
3
+
4
+ import { CoreRedisService } from '../../common/services/core-redis.service';
5
+
6
+ /** Release callback handed to the current lock holder when someone else wants the resource */
7
+ type RequestRelease = () => Promise<void> | void;
8
+
9
+ /**
10
+ * TUS lock that is exclusive across REPLICAS, not just within one process.
11
+ *
12
+ * `@tus/server` defaults to an in-memory locker, which is exactly right for one process and
13
+ * useless behind a load balancer: two replicas each hold their own lock for the same upload id
14
+ * and both accept a PATCH, so two byte ranges are written to one upload and the file ends up
15
+ * interleaved. Resumable uploads are long-lived and clients retry aggressively, so a request
16
+ * pair landing on different replicas is the normal case, not a rare one.
17
+ *
18
+ * The lock is a Redis key with a TTL held only while a request is being served, refreshed by a
19
+ * heartbeat so a slow but live PATCH keeps it, and released in `unlock()`. A holder that dies
20
+ * mid-request stops refreshing and the key expires — the upload becomes writable again instead
21
+ * of being stuck until someone intervenes.
22
+ */
23
+ export class TusRedisLock {
24
+ protected heartbeat?: NodeJS.Timeout;
25
+
26
+ /** Random per-acquisition value, so only the actual holder can release or refresh the lock */
27
+ protected token?: string;
28
+
29
+ constructor(
30
+ protected readonly redisService: CoreRedisService,
31
+ protected readonly key: string,
32
+ protected readonly ttlMs: number,
33
+ protected readonly acquireTimeoutMs: number,
34
+ ) {}
35
+
36
+ /**
37
+ * Wait for exclusive access to the upload, or throw once the timeout is reached.
38
+ *
39
+ * `cancelReq` is the TUS mechanism for cooperative handover: when a newer request wants an
40
+ * upload we already hold, the holder is asked to let go rather than made to wait it out.
41
+ */
42
+ async lock(signal: AbortSignal, cancelReq: RequestRelease): Promise<void> {
43
+ const deadline = Date.now() + this.acquireTimeoutMs;
44
+ // randomUUID, not Math.random: this token is the ONLY thing separating "holds the lock" from
45
+ // "does not" in the compare-and-delete below and in the heartbeat's PEXPIRE. A collision lets
46
+ // one holder refresh or release another's lock, and two replicas then interleave byte ranges
47
+ // into one upload — the exact corruption this locker exists to prevent.
48
+ const token = `${process.pid}-${randomUUID()}`;
49
+
50
+ for (;;) {
51
+ const acquired = await this.redisService.getClient().set(this.key, token, 'PX', this.ttlMs, 'NX');
52
+
53
+ if (acquired === 'OK') {
54
+ this.token = token;
55
+ this.startHeartbeat();
56
+ return;
57
+ }
58
+
59
+ // Tell whoever holds it that someone else is waiting. Signalling on every attempt is
60
+ // deliberate: the holder may be on another replica and only learns of us through this.
61
+ await cancelReq();
62
+
63
+ if (signal.aborted || Date.now() > deadline) {
64
+ throw new Error(`Could not acquire the upload lock for "${this.key}" within ${this.acquireTimeoutMs}ms`);
65
+ }
66
+
67
+ await new Promise((resolve) => setTimeout(resolve, 100));
68
+ }
69
+ }
70
+
71
+ /** Release the lock — only if we still hold it */
72
+ async unlock(): Promise<void> {
73
+ this.stopHeartbeat();
74
+ const token = this.token;
75
+ this.token = undefined;
76
+ if (!token) {
77
+ return;
78
+ }
79
+
80
+ // Compare-and-delete: after our TTL expired the key may already belong to someone else,
81
+ // and deleting it blindly would unlock an upload another replica is actively writing.
82
+ const script = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0`;
83
+ try {
84
+ await this.redisService.getClient().eval(script, 1, this.key, token);
85
+ } catch {
86
+ // The key expires on its own; a failed release costs at most one TTL of waiting.
87
+ }
88
+ }
89
+
90
+ /** Keep the lock alive while the request is still being served */
91
+ protected startHeartbeat(): void {
92
+ this.heartbeat = setInterval(
93
+ () => {
94
+ const token = this.token;
95
+ if (!token) {
96
+ return;
97
+ }
98
+ const script = `if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) end return 0`;
99
+ this.redisService
100
+ .getClient()
101
+ .eval(script, 1, this.key, token, this.ttlMs)
102
+ .catch(() => undefined);
103
+ },
104
+ Math.max(1000, Math.floor(this.ttlMs / 3)),
105
+ );
106
+ this.heartbeat.unref?.();
107
+ }
108
+
109
+ protected stopHeartbeat(): void {
110
+ if (this.heartbeat) {
111
+ clearInterval(this.heartbeat);
112
+ this.heartbeat = undefined;
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Locker handed to `@tus/server` when Redis is configured (see {@link TusRedisLock}).
119
+ */
120
+ export class TusRedisLocker {
121
+ protected readonly logger = new Logger(TusRedisLocker.name);
122
+
123
+ constructor(
124
+ protected readonly redisService: CoreRedisService,
125
+ /** How long a lock survives without a heartbeat — a dead holder's upload frees itself */
126
+ protected readonly ttlMs = 30_000,
127
+ /** How long a request waits for a busy upload before giving up with 500 */
128
+ protected readonly acquireTimeoutMs = 10_000,
129
+ ) {}
130
+
131
+ newLock(id: string): TusRedisLock {
132
+ return new TusRedisLock(
133
+ this.redisService,
134
+ this.redisService.key('tus-lock', id),
135
+ this.ttlMs,
136
+ this.acquireTimeoutMs,
137
+ );
138
+ }
139
+ }
@@ -3,10 +3,13 @@ import { getConnectionToken } from '@nestjs/mongoose';
3
3
  import { Connection } from 'mongoose';
4
4
 
5
5
  import { ITusConfig } from '../../common/interfaces/server-options.interface';
6
+ import { ConfigService } from '../../common/services/config.service';
7
+ import { CoreRedisService } from '../../common/services/core-redis.service';
8
+ import { CoreS3Service } from '../../common/services/core-s3.service';
6
9
  import { CoreTusController } from './core-tus.controller';
7
10
  import { CoreTusService } from './core-tus.service';
8
11
  import { TUS_CONFIG } from './tus.constants';
9
- import { normalizeTusConfig } from './interfaces/tus-config.interface';
12
+ import { DEFAULT_TUS_CONFIG, normalizeTusConfig } from './interfaces/tus-config.interface';
10
13
 
11
14
  /**
12
15
  * @deprecated Import from `./tus.constants` instead. Re-exported only so existing deep imports keep
@@ -111,6 +114,42 @@ export class TusModule implements OnModuleInit {
111
114
  return this.customController || CoreTusController;
112
115
  }
113
116
 
117
+ /**
118
+ * Write the configured roles onto the tus handlers.
119
+ *
120
+ * An empty array is rejected rather than honoured: the guards read an
121
+ * all-empty role set as "no roles required" and return true, so `roles: []`
122
+ * would OPEN the endpoints instead of closing them.
123
+ */
124
+ private static applyRoles(controller: Type<CoreTusController>, roles?: string[]): void {
125
+ if (
126
+ roles !== undefined &&
127
+ (!Array.isArray(roles) || roles.length === 0 || roles.some((r) => typeof r !== 'string'))
128
+ ) {
129
+ this.logger.warn(
130
+ `Ignoring tus.roles: expected a non-empty array of role strings, got ${JSON.stringify(roles)}. ` +
131
+ `Falling back to ${JSON.stringify(DEFAULT_TUS_CONFIG.roles)}.`,
132
+ );
133
+ }
134
+
135
+ const effective =
136
+ Array.isArray(roles) && roles.length > 0 && roles.every((r) => typeof r === 'string')
137
+ ? roles
138
+ : DEFAULT_TUS_CONFIG.roles;
139
+
140
+ Reflect.defineMetadata('roles', effective, controller);
141
+ // NOTE: handleTusOptions / handleTusOptionsWithId are deliberately absent.
142
+ // They answer the CORS preflight, which a browser sends WITHOUT credentials,
143
+ // so gating them would break every browser upload. They expose capabilities
144
+ // only, never upload data — see CoreTusController.
145
+ for (const member of ['handleTus', 'handleTusWithId']) {
146
+ const handler = (controller.prototype as Record<string, unknown>)[member];
147
+ if (typeof handler === 'function') {
148
+ Reflect.defineMetadata('roles', effective, handler);
149
+ }
150
+ }
151
+ }
152
+
114
153
  /**
115
154
  * Creates a dynamic module for TUS uploads
116
155
  *
@@ -156,6 +195,25 @@ export class TusModule implements OnModuleInit {
156
195
  // Enable TUS
157
196
  this.tusEnabled = true;
158
197
 
198
+ // Apply the configured roles to the controller that will actually be
199
+ // registered. Same mechanism as CorePermissionsModule: the value is only
200
+ // known at runtime, and the guards read exactly this metadata key.
201
+ //
202
+ // A custom controller is covered whether it inherits the handlers or
203
+ // re-declares them. Re-declaring @All()/@Roles() does NOT opt out: this
204
+ // writes onto `controller.prototype[member]`, which for an override resolves
205
+ // to the SUBCLASS's own function, and forRoot() runs after decorator
206
+ // evaluation — so config wins either way.
207
+ //
208
+ // That differs from the file module on purpose. `applyFileRoles()` targets
209
+ // the BASE class by name, so a subclass override there genuinely does keep
210
+ // its own metadata. Do not reason from one to the other.
211
+ //
212
+ // The real opt-outs here are: give the handler a different name (this only
213
+ // touches the members it knows), or register the controller outside
214
+ // TusModule entirely.
215
+ this.applyRoles(this.getControllerClass(), config.roles);
216
+
159
217
  return {
160
218
  controllers: [this.getControllerClass()],
161
219
  exports: [TUS_CONFIG, CoreTusService],
@@ -166,13 +224,27 @@ export class TusModule implements OnModuleInit {
166
224
  useValue: config,
167
225
  },
168
226
  {
169
- inject: [getConnectionToken(), TUS_CONFIG],
227
+ inject: [
228
+ getConnectionToken(),
229
+ TUS_CONFIG,
230
+ ConfigService,
231
+ { optional: true, token: CoreS3Service },
232
+ { optional: true, token: CoreRedisService },
233
+ ],
170
234
  provide: CoreTusService,
171
- useFactory: async (connection: Connection, tusConfig: ITusConfig) => {
172
- const service = new CoreTusService(connection);
235
+ useFactory: async (
236
+ connection: Connection,
237
+ tusConfig: ITusConfig,
238
+ configService: ConfigService,
239
+ s3Service?: CoreS3Service,
240
+ redisService?: CoreRedisService,
241
+ ) => {
242
+ const service = new CoreTusService(connection, { configService, redisService, s3Service });
173
243
  service.configure(tusConfig);
174
- // Manually call onModuleInit since useFactory bypasses lifecycle hooks
175
- await service.onModuleInit();
244
+ // NestJS DOES call onModuleInit on a factory-provided instance — its hook iterates
245
+ // every non-alias provider, however it was constructed. Calling it here as well ran
246
+ // init TWICE per boot: two TUS servers, two S3 stores, and two hourly expiration
247
+ // intervals of which onModuleDestroy clears only the second.
176
248
  return service;
177
249
  },
178
250
  },
@@ -33,6 +33,10 @@ import { mongoosePasswordPlugin } from './core/common/plugins/mongoose-password.
33
33
  import { mongooseRoleGuardPlugin } from './core/common/plugins/mongoose-role-guard.plugin';
34
34
  import { mongooseTenantPlugin } from './core/common/plugins/mongoose-tenant.plugin';
35
35
  import { ConfigService } from './core/common/services/config.service';
36
+ import { CoreCronJobsInitializer } from './core/common/services/core-cron-jobs.initializer';
37
+ import { CoreRedisService } from './core/common/services/core-redis.service';
38
+ import { CoreS3Service } from './core/common/services/core-s3.service';
39
+ import { CoreTrustProxyInitializer } from './core/common/services/core-trust-proxy.initializer';
36
40
  import { EmailService } from './core/common/services/email.service';
37
41
  import { MailjetService } from './core/common/services/mailjet.service';
38
42
  import { ModelDocService } from './core/common/services/model-doc.service';
@@ -42,6 +46,7 @@ import { CoreBetterAuthUserMapper } from './core/modules/better-auth/core-better
42
46
  import { CoreBetterAuthModule } from './core/modules/better-auth/core-better-auth.module';
43
47
  import { CoreBetterAuthService } from './core/modules/better-auth/core-better-auth.service';
44
48
  import { ErrorCodeModule } from './core/modules/error-code/error-code.module';
49
+ import { applyFileRoles } from './core/modules/file/file-roles.helper';
45
50
  import { CoreHealthCheckModule } from './core/modules/health-check/core-health-check.module';
46
51
  import { CoreHubModule } from './core/modules/hub/core-hub.module';
47
52
  import { isHubEnabled, isHubQueriesEnabled } from './core/modules/hub/hub-config.helper';
@@ -259,6 +264,15 @@ export class CoreModule implements NestModule {
259
264
  return connection;
260
265
  };
261
266
 
267
+ // Apply the configured roles to the core file endpoints.
268
+ //
269
+ // The file module ships abstract classes only — there is no CoreFileModule
270
+ // with its own forRoot() — so this is the one place that runs early enough
271
+ // to rewrite the role metadata before Nest builds its route table.
272
+ // Consumers that OVERRIDE a member re-declare the metadata on their own
273
+ // function and opt out of this; see applyFileRoles() for why.
274
+ applyFileRoles(config.file);
275
+
262
276
  // Check secrets
263
277
  const jwtConfig = config.jwt;
264
278
  if (jwtConfig?.secret && jwtConfig.secret && jwtConfig.refresh && jwtConfig.refresh.secret === jwtConfig.secret) {
@@ -274,6 +288,10 @@ export class CoreModule implements NestModule {
274
288
  },
275
289
 
276
290
  // Core Services
291
+ CoreCronJobsInitializer,
292
+ CoreRedisService,
293
+ CoreS3Service,
294
+ CoreTrustProxyInitializer,
277
295
  EmailService,
278
296
  TemplateService,
279
297
  MailjetService,
@@ -530,7 +548,14 @@ export class CoreModule implements NestModule {
530
548
  }
531
549
 
532
550
  // Set exports
533
- const exports: any[] = [ConfigService, EmailService, TemplateService, MailjetService];
551
+ const exports: any[] = [
552
+ ConfigService,
553
+ CoreRedisService,
554
+ CoreS3Service,
555
+ EmailService,
556
+ TemplateService,
557
+ MailjetService,
558
+ ];
534
559
  if (!process.env.VITEST && isGraphQlEnabled) {
535
560
  exports.push(ComplexityPlugin);
536
561
  }
package/src/index.ts CHANGED
@@ -30,8 +30,10 @@ export * from './core/common/exceptions/access-denied.exception';
30
30
  export * from './core/common/filters/http-exception-log.filter';
31
31
  export * from './core/common/helpers/common.helper';
32
32
  export * from './core/common/helpers/config.helper';
33
+ export * from './core/common/helpers/content-disposition.helper';
33
34
  export * from './core/common/helpers/context.helper';
34
35
  export * from './core/common/helpers/cookies.helper';
36
+ export * from './core/common/helpers/graceful-shutdown.helper';
35
37
  export * from './core/common/helpers/db.helper';
36
38
  export * from './core/common/helpers/decorator.helper';
37
39
  export * from './core/common/helpers/file.helper';
@@ -86,7 +88,12 @@ export * from './core/common/scalars/date.scalar';
86
88
  export * from './core/common/scalars/json.scalar';
87
89
  export * from './core/common/services/brevo.service';
88
90
  export * from './core/common/services/config.service';
91
+ export * from './core/common/services/core-cron-jobs.registry';
89
92
  export * from './core/common/services/core-cron-jobs.service';
93
+ export * from './core/common/services/core-redis.service';
94
+ export * from './core/common/services/core-redis-pubsub';
95
+ export * from './core/common/services/core-s3.service';
96
+ export * from './core/common/services/rate-limit-store';
90
97
  export * from './core/common/services/crud.service';
91
98
  export * from './core/common/services/email.service';
92
99
  export * from './core/common/services/mailjet.service';
@@ -169,6 +176,9 @@ export * from './core/modules/file/core-file-info.model';
169
176
  export * from './core/modules/file/core-file.controller';
170
177
  export * from './core/modules/file/core-file.resolver';
171
178
  export * from './core/modules/file/core-file.service';
179
+ export * from './core/modules/file/file-roles.helper';
180
+ export * from './core/modules/file/file-storage.helper';
181
+ export * from './core/modules/file/filesystem-file.helper';
172
182
  export * from './core/modules/file/interfaces/file-service-options.interface';
173
183
  export * from './core/modules/file/interfaces/file-upload.interface';
174
184
 
package/src/main.ts CHANGED
@@ -9,6 +9,7 @@ import envConfig from './config.env';
9
9
  import { FilterArgs } from './core/common/args/filter.args';
10
10
  import { buildCorsConfig, isCookiesEnabled, isCorsDisabled } from './core/common/helpers/cookies.helper';
11
11
  import { HttpExceptionLogFilter } from './core/common/filters/http-exception-log.filter';
12
+ import { installGracefulShutdown } from './core/common/helpers/graceful-shutdown.helper';
12
13
  import { handleFatalBootstrapError, installProcessDiagnostics } from './core/common/helpers/process-diagnostics.helper';
13
14
  import { CorePersistenceModel } from './core/common/models/core-persistence.model';
14
15
  import { CoreAuthModel } from './core/modules/auth/core-auth.model';
@@ -121,7 +122,12 @@ async function bootstrap() {
121
122
  // and `docker stop` waits out its full grace period before SIGKILL — dropping in-flight requests
122
123
  // and skipping every onModuleDestroy(). enableShutdownHooks() is what closes the app and drains
123
124
  // the loop; installProcessDiagnostics() then correctly defers to it instead of re-raising.
124
- server.enableShutdownHooks();
125
+ //
126
+ // installGracefulShutdown() IS enableShutdownHooks() when no `shutdownDelayMs` is configured.
127
+ // With one, it additionally keeps the instance fully healthy for that long before closing —
128
+ // which a NestJS lifecycle hook cannot do, because every one of them already runs inside
129
+ // close(), after the modules are destroyed. See the helper for the ordering.
130
+ installGracefulShutdown(server);
125
131
 
126
132
  // Start server on configured port
127
133
  await server.listen(envConfig.port, envConfig.hostname);
@@ -20,9 +20,13 @@ import { FileService } from './file.service';
20
20
  /**
21
21
  * File controller
22
22
  *
23
- * Extends CoreFileController to provide public download endpoints:
24
- * - GET /files/id/:id - Download file by ID (public)
25
- * - GET /files/:filename - Download file by filename (public)
23
+ * Extends CoreFileController and INHERITS its download endpoints unchanged:
24
+ * - GET /files/id/:id - Download file by ID
25
+ * - GET /files/:filename - Download file by filename
26
+ *
27
+ * Both are gated by `file.downloadRoles` (default ADMIN). They are deliberately
28
+ * not overridden here: role metadata lives on the function object, so an
29
+ * override would carry its own and silently opt the route out of that config.
26
30
  *
27
31
  * Adds admin-only endpoints:
28
32
  * - POST /files/upload - Upload file (admin)
@@ -1,6 +1,4 @@
1
1
  import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
2
- import { createWriteStream } from 'fs';
3
- import fs = require('fs');
4
2
  import GraphQLUpload = require('graphql-upload/GraphQLUpload.js');
5
3
 
6
4
  import { Roles } from '../../../core/common/decorators/roles.decorator';
@@ -61,21 +59,11 @@ export class FileResolver {
61
59
  @Mutation(() => Boolean)
62
60
  @Roles(RoleEnum.ADMIN)
63
61
  async uploadFiles(@Args({ name: 'files', type: () => [GraphQLUpload] }) files: FileUpload[]) {
64
- // Save files in filesystem
65
- const promises: Promise<any>[] = [];
66
- for (const file of files) {
67
- const { createReadStream, filename } = await file;
68
- await fs.promises.mkdir('./uploads', { recursive: true });
69
- promises.push(
70
- new Promise((resolve, reject) =>
71
- createReadStream()
72
- .pipe(createWriteStream(`./uploads/${filename}`))
73
- .on('finish', () => resolve(true))
74
- .on('error', (error) => reject(error)),
75
- ),
76
- );
77
- }
78
- await Promise.allSettled(promises);
62
+ // Store in the central file storage (GridFS/S3), never on the pod's own disk:
63
+ // `./uploads` is relative to the process working directory, so with more than
64
+ // one replica the file lands wherever the request happened to be routed and is
65
+ // unreadable everywhere else and gone after a restart.
66
+ await this.fileService.createFiles(files);
79
67
  return true;
80
68
  }
81
69
  }
@@ -1,7 +1,9 @@
1
- import { Injectable } from '@nestjs/common';
1
+ import { Injectable, Optional } from '@nestjs/common';
2
2
  import { InjectConnection } from '@nestjs/mongoose';
3
3
  import { Connection } from 'mongoose';
4
4
 
5
+ import { ConfigService } from '../../../core/common/services/config.service';
6
+ import { CoreS3Service } from '../../../core/common/services/core-s3.service';
5
7
  import { CoreFileService } from '../../../core/modules/file/core-file.service';
6
8
 
7
9
  /**
@@ -9,8 +11,12 @@ import { CoreFileService } from '../../../core/modules/file/core-file.service';
9
11
  */
10
12
  @Injectable()
11
13
  export class FileService extends CoreFileService {
12
- constructor(@InjectConnection() protected override readonly connection: Connection) {
13
- super(connection);
14
+ constructor(
15
+ @InjectConnection() protected override readonly connection: Connection,
16
+ protected readonly configService: ConfigService,
17
+ @Optional() protected readonly s3Service?: CoreS3Service,
18
+ ) {
19
+ super(connection, 'fs', { configService, s3Service });
14
20
  }
15
21
 
16
22
  /**
@@ -19,4 +25,37 @@ export class FileService extends CoreFileService {
19
25
  async duplicate(fileName: string, newName: string): Promise<any> {
20
26
  return this.files.openDownloadStreamByName(fileName).pipe(this.files.openUploadStream(newName));
21
27
  }
28
+
29
+ /**
30
+ * NOTE — where the per-file rule would go.
31
+ *
32
+ * This reference server deliberately does NOT override `checkRights()`. With
33
+ * `file.downloadRoles` at its default `[ADMIN]`, the roles guard answers before
34
+ * the service is reached, so an owner rule here could never fire — and a rule
35
+ * that cannot fire is worse than none, because it reads as protection.
36
+ *
37
+ * The pairing only becomes useful once the coarse gate is widened. A project
38
+ * that wants "signed-in users may fetch THEIR OWN files" sets
39
+ * `file: { downloadRoles: [RoleEnum.S_USER] }` and then overrides:
40
+ *
41
+ * ```typescript
42
+ * protected override async checkRights(input, options) {
43
+ * if (options?.force || options?.checkInputType !== 'id') {
44
+ * return true; // writes and filename reads stay on the role gate
45
+ * }
46
+ * if (!options.currentUser) {
47
+ * return true; // system-internal call: the guard already decided
48
+ * }
49
+ * if (options.currentUser.hasRole?.([RoleEnum.ADMIN])) {
50
+ * return true;
51
+ * }
52
+ * const raw = await this.getRawFileInfo(input);
53
+ * return !!raw?.metadata?.ownerId && String(raw.metadata.ownerId) === String(options.currentUser.id);
54
+ * }
55
+ * ```
56
+ *
57
+ * `AvatarController` already writes the `metadata.ownerId` such a rule reads,
58
+ * so enabling it in a downstream project is a config change plus this method.
59
+ * See `src/core/modules/file/README.md` § Access control.
60
+ */
22
61
  }
@@ -1,12 +1,13 @@
1
- import { Post, UploadedFile, UseInterceptors } from '@nestjs/common';
1
+ import { Logger, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
2
2
  import { Controller } from '@nestjs/common/decorators/core/controller.decorator';
3
3
  import { FileInterceptor } from '@nestjs/platform-express';
4
4
 
5
- import envConfig from '../../../config.env';
6
5
  import { CurrentUser } from '../../../core/common/decorators/current-user.decorator';
7
6
  import { Roles } from '../../../core/common/decorators/roles.decorator';
8
7
  import { RoleEnum } from '../../../core/common/enums/role.enum';
9
- import { multerOptionsForImageUpload } from '../../../core/common/helpers/file.helper';
8
+ import { getStringIds } from '../../../core/common/helpers/db.helper';
9
+ import { multerFileToUpload, multerOptionsForImageUpload } from '../../../core/common/helpers/file.helper';
10
+ import { FileService } from '../file/file.service';
10
11
  import { User } from './user.model';
11
12
  import { UserService } from './user.service';
12
13
 
@@ -16,10 +17,15 @@ import { UserService } from './user.service';
16
17
  @Controller('avatar')
17
18
  @Roles(RoleEnum.ADMIN)
18
19
  export class AvatarController {
20
+ protected readonly logger = new Logger(AvatarController.name);
21
+
19
22
  /**
20
23
  * Import services
21
24
  */
22
- constructor(protected readonly usersService: UserService) {}
25
+ constructor(
26
+ protected readonly usersService: UserService,
27
+ protected readonly fileService: FileService,
28
+ ) {}
23
29
 
24
30
  /**
25
31
  * Upload files
@@ -29,12 +35,36 @@ export class AvatarController {
29
35
  @UseInterceptors(
30
36
  FileInterceptor(
31
37
  'file',
32
- multerOptionsForImageUpload({
33
- destination: `${envConfig.staticAssets.path}/avatars`,
34
- }),
38
+ // `memory: true`, NOT a disk destination: the avatar goes into the central
39
+ // file storage (GridFS/S3) so every replica can serve it and a restart or
40
+ // reschedule does not lose it. A pod-local `staticAssets` path is readable
41
+ // by exactly one replica.
42
+ multerOptionsForImageUpload({ memory: true }),
35
43
  ),
36
44
  )
37
- uploadFile(@UploadedFile() file: Express.Multer.File, @CurrentUser() user: User): Promise<string> {
38
- return this.usersService.setAvatar(file, user);
45
+ async uploadFile(@UploadedFile() file: Express.Multer.File, @CurrentUser() user: User): Promise<string> {
46
+ // Record the owner. `file.downloadRoles` defaults to ADMIN, so without this the
47
+ // uploader could not fetch their own avatar back — roles answer "may this caller
48
+ // reach the route", never "may this caller have THIS file". FileService.checkRights()
49
+ // reads this metadata to answer the second question.
50
+ const stored = await this.fileService.createFile(multerFileToUpload(file), {
51
+ metadata: { ownerId: getStringIds(user.id) },
52
+ });
53
+ const previousAvatar = await this.usersService.setAvatar(stored.id, user);
54
+
55
+ // Drop the replaced file. A failure here must not fail the upload: the new avatar
56
+ // is already stored and referenced, so an orphaned object is a cleanup concern,
57
+ // not a request error.
58
+ if (previousAvatar) {
59
+ try {
60
+ await this.fileService.deleteFile(previousAvatar);
61
+ } catch (error) {
62
+ this.logger.warn(
63
+ `Could not remove previous avatar ${previousAvatar}: ${error instanceof Error ? error.message : 'Unknown error'}`,
64
+ );
65
+ }
66
+ }
67
+
68
+ return stored.id;
39
69
  }
40
70
  }
@@ -6,6 +6,7 @@ import { Document, Schema } from 'mongoose';
6
6
  import { Restricted } from '../../../core/common/decorators/restricted.decorator';
7
7
  import { Translatable } from '../../../core/common/decorators/translatable.decorator';
8
8
  import { UnifiedField } from '../../../core/common/decorators/unified-field.decorator';
9
+ import { ProcessType } from '../../../core/common/enums/process-type.enum';
9
10
  import { RoleEnum } from '../../../core/common/enums/role.enum';
10
11
  import { CoreUserModel } from '../../../core/modules/user/core-user.model';
11
12
  import { PersistenceModel } from '../../common/models/persistence.model';
@@ -24,13 +25,18 @@ export class User extends CoreUserModel implements PersistenceModel {
24
25
  // ===================================================================================================================
25
26
 
26
27
  /**
27
- * URL to avatar file of the user
28
+ * ID of the user's avatar file, assigned by POST /avatar/upload
29
+ *
30
+ * Readable by everyone, writable by NO ONE through a generic update: it is a file id the
31
+ * server hands out, and the avatar endpoint deletes the file the previous value points at.
32
+ * While it was freely writable, a signed-in user could put someone else's file id here and
33
+ * have the next upload delete that file for them.
28
34
  */
29
35
  @UnifiedField({
30
- description: 'URL to avatar file of the user',
36
+ description: "ID of the user's avatar file (set via POST /avatar/upload)",
31
37
  isOptional: true,
32
38
  mongoose: true,
33
- roles: RoleEnum.S_EVERYONE,
39
+ roles: [{ processType: ProcessType.INPUT, roles: RoleEnum.S_NO_ONE }],
34
40
  })
35
41
  avatar: string = undefined;
36
42
 
@@ -1,8 +1,11 @@
1
- import { Module } from '@nestjs/common';
1
+ import { forwardRef, Module } from '@nestjs/common';
2
2
  import { MongooseModule } from '@nestjs/mongoose';
3
3
  import { PubSub } from 'graphql-subscriptions';
4
4
 
5
5
  import { ConfigService } from '../../../core/common/services/config.service';
6
+ import { CoreRedisPubSub } from '../../../core/common/services/core-redis-pubsub';
7
+ import { CoreRedisService } from '../../../core/common/services/core-redis.service';
8
+ import { FileModule } from '../file/file.module';
6
9
  import { AvatarController } from './avatar.controller';
7
10
  import { UserController } from './user.controller';
8
11
  import { User, UserSchema } from './user.model';
@@ -15,7 +18,9 @@ import { UserService } from './user.service';
15
18
  @Module({
16
19
  controllers: [AvatarController, UserController],
17
20
  exports: [MongooseModule, UserResolver, UserService, 'USER_CLASS'],
18
- imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
21
+ // forwardRef: FileModule already imports UserModule, and the avatar upload now
22
+ // needs FileService to reach the central storage — so the two reference each other.
23
+ imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), forwardRef(() => FileModule)],
19
24
  providers: [
20
25
  UserResolver,
21
26
  ConfigService,
@@ -25,8 +30,12 @@ import { UserService } from './user.service';
25
30
  useValue: User,
26
31
  },
27
32
  {
33
+ // Redis-backed when Redis is enabled (subscriptions reach all replicas),
34
+ // process-local in-memory PubSub otherwise
28
35
  provide: 'PUB_SUB',
29
- useValue: new PubSub(),
36
+ useFactory: (redisService?: CoreRedisService) =>
37
+ redisService?.enabled ? new CoreRedisPubSub(redisService) : new PubSub(),
38
+ inject: [{ optional: true, token: CoreRedisService }],
30
39
  },
31
40
  ],
32
41
  })