@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
|
@@ -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: [
|
|
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 (
|
|
172
|
-
|
|
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
|
-
//
|
|
175
|
-
|
|
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
|
},
|
package/src/core.module.ts
CHANGED
|
@@ -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[] = [
|
|
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
|
-
|
|
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
|
|
24
|
-
* - GET /files/id/:id
|
|
25
|
-
* - GET /files/:filename
|
|
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
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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(
|
|
13
|
-
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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: '
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
useFactory: (redisService?: CoreRedisService) =>
|
|
37
|
+
redisService?.enabled ? new CoreRedisPubSub(redisService) : new PubSub(),
|
|
38
|
+
inject: [{ optional: true, token: CoreRedisService }],
|
|
30
39
|
},
|
|
31
40
|
],
|
|
32
41
|
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Inject, Injectable, Optional, UnauthorizedException, UnprocessableEntityException } from '@nestjs/common';
|
|
2
2
|
import { InjectModel } from '@nestjs/mongoose';
|
|
3
|
-
import fs = require('fs');
|
|
4
3
|
import { PubSub } from 'graphql-subscriptions';
|
|
5
4
|
import { Model } from 'mongoose';
|
|
6
5
|
|
|
@@ -94,10 +93,22 @@ export class UserService extends CoreUserService<User, UserInput, UserCreateInpu
|
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
/**
|
|
97
|
-
*
|
|
96
|
+
* Point the user's avatar at an already-stored file
|
|
97
|
+
*
|
|
98
|
+
* Takes the file ID rather than the upload itself: the bytes belong in the central
|
|
99
|
+
* file storage (GridFS/S3) so every replica can serve them, and putting that
|
|
100
|
+
* dependency here is not possible — `UserService` is instantiated by
|
|
101
|
+
* `CoreAuthModule`, which knows nothing about the project's `FileModule`.
|
|
102
|
+
* `AvatarController` therefore stores the file and calls this with the resulting id.
|
|
103
|
+
*
|
|
104
|
+
* @returns the PREVIOUS avatar id, so the caller can delete the orphaned file
|
|
98
105
|
*/
|
|
99
|
-
async setAvatar(
|
|
100
|
-
|
|
106
|
+
async setAvatar(avatarId: string, user: User): Promise<string> {
|
|
107
|
+
// `findById`, not `findOne({ id })`: `id` is a Mongoose virtual, so it exists on the
|
|
108
|
+
// document but not in MongoDB — a filter on it matches nothing and every call ended in
|
|
109
|
+
// "session user no longer exists". Broken since the Mongoose migration and never noticed,
|
|
110
|
+
// because no test covers the avatar endpoint.
|
|
111
|
+
const dbUser = await this.mainDbModel.findById(user.id).exec();
|
|
101
112
|
// Check user: the token is valid but the account no longer exists, so the session really is
|
|
102
113
|
// invalid — 401 is right here. (A permission error would have to be 403, see accessDeniedException.)
|
|
103
114
|
if (!dbUser) {
|
|
@@ -105,25 +116,17 @@ export class UserService extends CoreUserService<User, UserInput, UserCreateInpu
|
|
|
105
116
|
}
|
|
106
117
|
|
|
107
118
|
// Check file
|
|
108
|
-
if (!
|
|
119
|
+
if (!avatarId) {
|
|
109
120
|
throw new UnprocessableEntityException('Missing avatar file');
|
|
110
121
|
}
|
|
111
122
|
|
|
112
|
-
|
|
113
|
-
if (user.avatar) {
|
|
114
|
-
fs.unlink(`${this.configService.configFastButReadOnly.staticAssets.path}/avatars/${user.avatar}`, (err) => {
|
|
115
|
-
if (err) {
|
|
116
|
-
console.error(err);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
}
|
|
123
|
+
const previousAvatar = dbUser.avatar;
|
|
120
124
|
|
|
121
|
-
//
|
|
122
|
-
dbUser.avatar =
|
|
125
|
+
// The avatar is referenced by file id and served via GET /files/id/:id
|
|
126
|
+
dbUser.avatar = avatarId;
|
|
123
127
|
|
|
124
128
|
await dbUser.save();
|
|
125
129
|
|
|
126
|
-
|
|
127
|
-
return file.filename;
|
|
130
|
+
return previousAvatar;
|
|
128
131
|
}
|
|
129
132
|
}
|
package/src/test/README.md
CHANGED
|
@@ -181,10 +181,15 @@ await testHelper.rest('/protected-endpoint', {
|
|
|
181
181
|
Download a file and return the response with a `data` string property for content comparison.
|
|
182
182
|
|
|
183
183
|
```typescript
|
|
184
|
-
//
|
|
185
|
-
|
|
184
|
+
// The core download routes are gated by `file.downloadRoles` (default ADMIN),
|
|
185
|
+
// so a request without credentials answers 401 — pass a token or a session cookie.
|
|
186
|
+
const res = await testHelper.download('/files/id/abc123', { token: adminToken });
|
|
186
187
|
expect(res.statusCode).toEqual(200);
|
|
187
188
|
expect(res.data).toEqual('file content');
|
|
189
|
+
|
|
190
|
+
// Without credentials:
|
|
191
|
+
const anonymous = await testHelper.download('/files/id/abc123');
|
|
192
|
+
expect(anonymous.statusCode).toEqual(401);
|
|
188
193
|
```
|
|
189
194
|
|
|
190
195
|
### `downloadBuffer(url, tokenOrOptions?)`
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { redactSensitiveText } from './logging.helper';
|
|
4
|
-
|
|
5
|
-
describe('redactSensitiveText', () => {
|
|
6
|
-
it('redacts JWTs appearing anywhere in a line', () => {
|
|
7
|
-
const jwt = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U';
|
|
8
|
-
const out = redactSensitiveText(`token issued: ${jwt} done`);
|
|
9
|
-
|
|
10
|
-
expect(out).not.toContain(jwt);
|
|
11
|
-
expect(out).toContain('token issued:');
|
|
12
|
-
expect(out).toContain('done');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('redacts Authorization Bearer values', () => {
|
|
16
|
-
const out = redactSensitiveText('Authorization: Bearer abcdef1234567890secret');
|
|
17
|
-
|
|
18
|
-
expect(out).not.toContain('abcdef1234567890secret');
|
|
19
|
-
expect(out.toLowerCase()).toContain('authorization');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('redacts key/value secrets regardless of separator or quoting', () => {
|
|
23
|
-
expect(redactSensitiveText('password=SuperSecret123')).not.toContain('SuperSecret123');
|
|
24
|
-
expect(redactSensitiveText('token: "abcd1234efgh"')).not.toContain('abcd1234efgh');
|
|
25
|
-
expect(redactSensitiveText("apiKey='pk_live_9999'")).not.toContain('pk_live_9999');
|
|
26
|
-
expect(redactSensitiveText('client_secret=verySecretValue')).not.toContain('verySecretValue');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('redacts cookie headers', () => {
|
|
30
|
-
const out = redactSensitiveText('cookie: session=abc123; other=xyz789');
|
|
31
|
-
|
|
32
|
-
expect(out).not.toContain('abc123');
|
|
33
|
-
expect(out).not.toContain('xyz789');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('redacts reset/verification tokens carried as a URL PATH segment (not key=value)', () => {
|
|
37
|
-
const link = 'https://app.example.com/verify/AbCdEf0123456789XyZtoken please click';
|
|
38
|
-
const out = redactSensitiveText(link);
|
|
39
|
-
expect(out).not.toContain('AbCdEf0123456789XyZtoken');
|
|
40
|
-
expect(out).toContain('/verify/');
|
|
41
|
-
// also for reset / set-password style links
|
|
42
|
-
expect(redactSensitiveText('/reset/SUPERSECRETVALUE1234567890')).not.toContain('SUPERSECRETVALUE1234567890');
|
|
43
|
-
expect(redactSensitiveText('/set-password/ZZZ1112223334445556667')).not.toContain('ZZZ1112223334445556667');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('leaves harmless short path segments untouched', () => {
|
|
47
|
-
// Only long token-like segments (>= 16 chars) are masked — normal routes stay readable.
|
|
48
|
-
const text = 'GET /verify/email done';
|
|
49
|
-
expect(redactSensitiveText(text)).toBe(text);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('leaves harmless text untouched', () => {
|
|
53
|
-
const text = 'User signed in successfully with id 507f1f77bcf86cd799439011';
|
|
54
|
-
expect(redactSensitiveText(text)).toBe(text);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('handles empty / non-secret input without throwing', () => {
|
|
58
|
-
expect(redactSensitiveText('')).toBe('');
|
|
59
|
-
expect(redactSensitiveText('plain message')).toBe('plain message');
|
|
60
|
-
});
|
|
61
|
-
});
|