@lenne.tech/nest-server 11.41.8 → 11.42.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/configurable-features.md +4 -3
- package/.claude/rules/role-system.md +31 -0
- package/.claude/rules/testing.md +4 -4
- package/CLAUDE.md +6 -0
- package/FRAMEWORK-API.md +3 -2
- package/dist/core/common/helpers/graphql-ws-context.helper.js +1 -1
- package/dist/core/common/helpers/graphql-ws-context.helper.js.map +1 -1
- package/dist/core/common/helpers/outbound-url.helper.d.ts +14 -0
- package/dist/core/common/helpers/outbound-url.helper.js +234 -0
- package/dist/core/common/helpers/outbound-url.helper.js.map +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +5 -0
- package/dist/core/common/middleware/request-context.middleware.js +1 -0
- package/dist/core/common/middleware/request-context.middleware.js.map +1 -1
- package/dist/core/common/services/request-context.service.d.ts +1 -0
- package/dist/core/common/services/request-context.service.js.map +1 -1
- package/dist/core/modules/ai/core-ai-mcp.controller.js +5 -2
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +1 -1
- package/dist/core/modules/ai/services/core-ai-mcp.service.js +3 -2
- package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -1
- package/dist/core/modules/ai/tools/ai-tool.registry.js +15 -2
- package/dist/core/modules/ai/tools/ai-tool.registry.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant-role.helper.d.ts +2 -0
- package/dist/core/modules/tenant/core-tenant-role.helper.js +36 -0
- package/dist/core/modules/tenant/core-tenant-role.helper.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +7 -1
- package/dist/core/modules/tenant/core-tenant.guard.js +56 -4
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tus/core-tus.service.d.ts +11 -0
- package/dist/core/modules/tus/core-tus.service.js +54 -14
- package/dist/core/modules/tus/core-tus.service.js.map +1 -1
- package/dist/core/modules/tus/tus.constants.d.ts +1 -0
- package/dist/core/modules/tus/tus.constants.js +2 -1
- package/dist/core/modules/tus/tus.constants.js.map +1 -1
- package/dist/core/modules/tus/tus.module.d.ts +2 -0
- package/dist/core/modules/tus/tus.module.js +14 -8
- package/dist/core/modules/tus/tus.module.js.map +1 -1
- package/dist/core/modules/user/core-user.service.d.ts +1 -0
- package/dist/core/modules/user/core-user.service.js +24 -0
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +5 -3
- package/migration-guides/11.41.8-to-11.41.9.md +116 -0
- package/migration-guides/11.41.x-to-11.42.x.md +161 -0
- package/migration-guides/11.9.x-to-11.10.x.md +3 -2
- package/package.json +2 -1
- package/src/core/common/helpers/graphql-ws-context.helper.ts +1 -1
- package/src/core/common/helpers/outbound-url.helper.ts +310 -0
- package/src/core/common/interfaces/server-options.interface.ts +54 -0
- package/src/core/common/middleware/request-context.middleware.ts +1 -0
- package/src/core/common/services/request-context.service.ts +9 -0
- package/src/core/modules/ai/README.md +25 -0
- package/src/core/modules/ai/core-ai-mcp.controller.ts +22 -3
- package/src/core/modules/ai/services/core-ai-mcp.service.ts +9 -3
- package/src/core/modules/ai/tools/ai-tool.registry.ts +33 -3
- package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +18 -11
- package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +14 -0
- package/src/core/modules/tenant/README.md +80 -8
- package/src/core/modules/tenant/core-tenant-role.helper.ts +75 -0
- package/src/core/modules/tenant/core-tenant.guard.ts +78 -5
- package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +38 -11
- package/src/core/modules/tus/README.md +88 -14
- package/src/core/modules/tus/core-tus.service.ts +125 -30
- package/src/core/modules/tus/tus.constants.ts +12 -0
- package/src/core/modules/tus/tus.module.ts +41 -8
- package/src/core/modules/user/core-user.service.ts +53 -0
- package/src/index.ts +2 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { ModuleRef } from '@nestjs/core';
|
|
2
3
|
import { FileStore } from '@tus/file-store';
|
|
3
4
|
import { Server, Upload } from '@tus/server';
|
|
4
5
|
import * as fs from 'fs';
|
|
@@ -11,6 +12,7 @@ import { ITusConfig } from '../../common/interfaces/server-options.interface';
|
|
|
11
12
|
import { ConfigService } from '../../common/services/config.service';
|
|
12
13
|
import { CoreRedisService } from '../../common/services/core-redis.service';
|
|
13
14
|
import { CoreS3Service } from '../../common/services/core-s3.service';
|
|
15
|
+
import { getTenantContextResolver } from '../../common/services/core-tenant-context.registry';
|
|
14
16
|
import { resolveFileStorage } from '../file/file-storage.helper';
|
|
15
17
|
import {
|
|
16
18
|
DEFAULT_FILESYSTEM_DIR,
|
|
@@ -18,7 +20,7 @@ import {
|
|
|
18
20
|
FilesystemFileHelper,
|
|
19
21
|
} from '../file/filesystem-file.helper';
|
|
20
22
|
import { S3_FILES_COLLECTION, S3FileHelper } from '../file/s3-file.helper';
|
|
21
|
-
import { TUS_OWNER_METADATA_KEY } from './tus.constants';
|
|
23
|
+
import { TUS_OWNER_METADATA_KEY, TUS_TENANT_METADATA_KEY } from './tus.constants';
|
|
22
24
|
import { TusRedisLocker } from './tus-redis-locker';
|
|
23
25
|
import {
|
|
24
26
|
DEFAULT_TUS_ALLOWED_HEADERS,
|
|
@@ -37,6 +39,14 @@ import {
|
|
|
37
39
|
export interface CoreTusServiceOptions {
|
|
38
40
|
configService?: ConfigService;
|
|
39
41
|
|
|
42
|
+
/**
|
|
43
|
+
* How a subclass reaches project providers. `TusModule` constructs the service itself, with
|
|
44
|
+
* exactly `(connection, options)`, so a subclass cannot declare constructor dependencies of its
|
|
45
|
+
* own — resolve them at call time instead:
|
|
46
|
+
* `this.options?.moduleRef?.get(QuotaService, { strict: false })`.
|
|
47
|
+
*/
|
|
48
|
+
moduleRef?: ModuleRef;
|
|
49
|
+
|
|
40
50
|
/** Central Redis; when present, upload locks are shared across replicas */
|
|
41
51
|
redisService?: CoreRedisService;
|
|
42
52
|
|
|
@@ -187,6 +197,10 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
|
|
|
187
197
|
// ADMIN, so a project following the documented pattern ended up with undownloadable files.
|
|
188
198
|
// Written from the SERVER-recorded owner, never from the client's own metadata.
|
|
189
199
|
...(metadata[TUS_OWNER_METADATA_KEY] ? { ownerId: metadata[TUS_OWNER_METADATA_KEY] } : {}),
|
|
200
|
+
// The tenant the upload was created in — validated against an active membership in
|
|
201
|
+
// onUploadCreate(), never taken from the client. Present only with multi-tenancy active; it
|
|
202
|
+
// is what the file module's `'tenant'` access preset decides on.
|
|
203
|
+
...(metadata[TUS_TENANT_METADATA_KEY] ? { tenantId: metadata[TUS_TENANT_METADATA_KEY] } : {}),
|
|
190
204
|
tusUploadId: upload.id,
|
|
191
205
|
uploadedAt: new Date(),
|
|
192
206
|
};
|
|
@@ -579,6 +593,115 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
|
|
|
579
593
|
return false;
|
|
580
594
|
}
|
|
581
595
|
|
|
596
|
+
/**
|
|
597
|
+
* Runs when a client CREATES an upload — the one point where the declared `Upload-Length` is known
|
|
598
|
+
* and no byte has been written yet. Returning `{ metadata }` is what PERSISTS it: the tus server
|
|
599
|
+
* writes the returned value onto the upload (returning `{}` would keep the client's metadata
|
|
600
|
+
* verbatim). Throw an error carrying `status_code` to refuse the upload.
|
|
601
|
+
*
|
|
602
|
+
* Records two framework-owned keys, both OVERWRITTEN rather than merged, because the client's
|
|
603
|
+
* `Upload-Metadata` header must never be able to set them:
|
|
604
|
+
* - {@link TUS_OWNER_METADATA_KEY} — the authenticated creator (see `assertUploadOwnership()`).
|
|
605
|
+
* - {@link TUS_TENANT_METADATA_KEY} — the validated tenant, only while multi-tenancy is active
|
|
606
|
+
* (see `resolveUploadTenantId()`); `null` otherwise.
|
|
607
|
+
*
|
|
608
|
+
* Override to add project rules — a quota, a per-tenant type policy — and call `super` to keep the
|
|
609
|
+
* owner, the tenant and `allowedTypes`. Register the subclass via `TusModule.forRoot({ service })`.
|
|
610
|
+
*/
|
|
611
|
+
protected async onUploadCreate(req: any, upload: Upload): Promise<{ metadata: Record<string, null | string> }> {
|
|
612
|
+
// Record the creator and the tenant FIRST, so the checks below and later requests have something
|
|
613
|
+
// to compare against.
|
|
614
|
+
const metadata: Record<string, null | string> = {
|
|
615
|
+
...upload.metadata,
|
|
616
|
+
[TUS_OWNER_METADATA_KEY]: this.readRequestUserId(req) ?? null,
|
|
617
|
+
[TUS_TENANT_METADATA_KEY]: (await this.resolveUploadTenantId(req)) ?? null,
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
// Validate file type if allowedTypes is configured
|
|
621
|
+
if (this.config.allowedTypes && this.config.allowedTypes.length > 0) {
|
|
622
|
+
const filetype = this.parseMetadata(upload.metadata).filetype;
|
|
623
|
+
|
|
624
|
+
if (!this.validateFileType(filetype)) {
|
|
625
|
+
const allowedList = this.config.allowedTypes.join(', ');
|
|
626
|
+
this.logger.warn(
|
|
627
|
+
`Upload rejected: file type '${filetype || 'unknown'}' not allowed. Allowed types: ${allowedList}`,
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
// Throw error to reject the upload
|
|
631
|
+
// @tus/server v2 expects throwing an error with status_code
|
|
632
|
+
const error = new Error(`File type '${filetype || 'unknown'}' is not allowed. Allowed types: ${allowedList}`);
|
|
633
|
+
(error as any).status_code = 415; // Unsupported Media Type
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return { metadata };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* The VALIDATED tenant a new upload belongs to, or `undefined`.
|
|
643
|
+
*
|
|
644
|
+
* Only while multi-tenancy is active AND the tenant guard is registered — without it nothing can
|
|
645
|
+
* validate a tenant, and a project without multi-tenancy sees no change at all. The tus routes
|
|
646
|
+
* carry `@SkipTenantCheck()`, so the guard did NOT validate the request's tenant header; this asks
|
|
647
|
+
* the same membership logic directly (`core-tenant-context.registry.ts`, the path GraphQL
|
|
648
|
+
* subscriptions use).
|
|
649
|
+
*
|
|
650
|
+
* - No tenant header: `undefined` — the upload stays tenant-less, as before.
|
|
651
|
+
* - A header naming a tenant the caller is an active member of (or any tenant, for an admin under
|
|
652
|
+
* `adminBypass`): that tenant.
|
|
653
|
+
* - A header naming a tenant the caller is NOT a member of: the upload is REFUSED with 403. Taking
|
|
654
|
+
* the header verbatim would let anybody place a file in somebody else's tenant, and silently
|
|
655
|
+
* dropping it would store the file somewhere the caller did not ask for.
|
|
656
|
+
*
|
|
657
|
+
* Override to derive the tenant from somewhere else (a subdomain, an API key).
|
|
658
|
+
*/
|
|
659
|
+
protected async resolveUploadTenantId(req: any): Promise<string | undefined> {
|
|
660
|
+
const multiTenancy = ConfigService.configFastButReadOnly?.multiTenancy;
|
|
661
|
+
const resolver = getTenantContextResolver();
|
|
662
|
+
if (!multiTenancy || multiTenancy.enabled === false || !resolver) {
|
|
663
|
+
return undefined;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const headerName = (multiTenancy.headerName ?? 'x-tenant-id').toLowerCase();
|
|
667
|
+
const header = this.readRequestHeader(req, headerName)?.trim();
|
|
668
|
+
if (!header) {
|
|
669
|
+
return undefined;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const resolved = await resolver.resolve(this.readRequestUser(req), header);
|
|
673
|
+
if (!resolved?.tenantId) {
|
|
674
|
+
const error = new Error('Not a member of the requested tenant');
|
|
675
|
+
(error as any).status_code = 403;
|
|
676
|
+
throw error;
|
|
677
|
+
}
|
|
678
|
+
return resolved.tenantId;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* The authenticated caller with roles, read the same way as {@link readRequestUserId} — the roles
|
|
683
|
+
* decide the admin bypass of the tenant resolution.
|
|
684
|
+
*/
|
|
685
|
+
protected readRequestUser(req: any): undefined | { id: string; roles?: string[] } {
|
|
686
|
+
const id = this.readRequestUserId(req);
|
|
687
|
+
if (!id) {
|
|
688
|
+
return undefined;
|
|
689
|
+
}
|
|
690
|
+
const user = req?.user ?? req?.runtime?.node?.req?.user ?? req?.context?.user;
|
|
691
|
+
return { id, roles: Array.isArray(user?.roles) ? user.roles : undefined };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* A request header, from either request shape `@tus/server` v2 hands its hooks: the WHATWG
|
|
696
|
+
* `ServerRequest` (a `Headers` object) or the original Node request behind `runtime.node.req`.
|
|
697
|
+
*/
|
|
698
|
+
protected readRequestHeader(req: any, name: string): string | undefined {
|
|
699
|
+
const fromHeaders = typeof req?.headers?.get === 'function' ? req.headers.get(name) : req?.headers?.[name];
|
|
700
|
+
const value = fromHeaders ?? req?.runtime?.node?.req?.headers?.[name];
|
|
701
|
+
const single = Array.isArray(value) ? value[0] : value;
|
|
702
|
+
return typeof single === 'string' && single.length > 0 ? single : undefined;
|
|
703
|
+
}
|
|
704
|
+
|
|
582
705
|
/**
|
|
583
706
|
* Create the TUS server instance with configured extensions
|
|
584
707
|
*/
|
|
@@ -604,35 +727,7 @@ export class CoreTusService implements OnModuleDestroy, OnModuleInit {
|
|
|
604
727
|
// protocol is built around a per-upload URL. Appending is the sharp end — bytes PATCHed into
|
|
605
728
|
// somebody else's upload are migrated into the file store under THEIR filename.
|
|
606
729
|
onIncomingRequest: async (req, uploadId) => this.assertUploadOwnership(req, uploadId),
|
|
607
|
-
onUploadCreate: async (req, upload) =>
|
|
608
|
-
// Record the creator FIRST, so the ownership check below has something to compare against.
|
|
609
|
-
// Overwrites rather than merges: see TUS_OWNER_METADATA_KEY.
|
|
610
|
-
const metadata = { ...upload.metadata, [TUS_OWNER_METADATA_KEY]: this.readRequestUserId(req) ?? null };
|
|
611
|
-
|
|
612
|
-
// Validate file type if allowedTypes is configured
|
|
613
|
-
if (this.config.allowedTypes && this.config.allowedTypes.length > 0) {
|
|
614
|
-
const filetype = this.parseMetadata(upload.metadata).filetype;
|
|
615
|
-
|
|
616
|
-
if (!this.validateFileType(filetype)) {
|
|
617
|
-
const allowedList = this.config.allowedTypes.join(', ');
|
|
618
|
-
this.logger.warn(
|
|
619
|
-
`Upload rejected: file type '${filetype || 'unknown'}' not allowed. Allowed types: ${allowedList}`,
|
|
620
|
-
);
|
|
621
|
-
|
|
622
|
-
// Throw error to reject the upload
|
|
623
|
-
// @tus/server v2 expects throwing an error with status_code
|
|
624
|
-
const error = new Error(
|
|
625
|
-
`File type '${filetype || 'unknown'}' is not allowed. Allowed types: ${allowedList}`,
|
|
626
|
-
);
|
|
627
|
-
(error as any).status_code = 415; // Unsupported Media Type
|
|
628
|
-
throw error;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Returning the metadata is what PERSISTS the owner — the tus server writes the returned
|
|
633
|
-
// value onto the upload. Returning `{}` here would keep the client's own metadata verbatim.
|
|
634
|
-
return { metadata };
|
|
635
|
-
},
|
|
730
|
+
onUploadCreate: async (req, upload) => this.onUploadCreate(req, upload),
|
|
636
731
|
onUploadFinish: async (_req, upload) => {
|
|
637
732
|
try {
|
|
638
733
|
await this.onUploadComplete(upload);
|
|
@@ -39,3 +39,15 @@ export const TUS_CONFIG = 'TUS_CONFIG';
|
|
|
39
39
|
* `tests/unit/import-cycle-invariants.spec.ts` enforces exactly that placement.
|
|
40
40
|
*/
|
|
41
41
|
export const TUS_OWNER_METADATA_KEY = 'ltOwnerId';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Metadata key recording the VALIDATED tenant a tus upload was created in, in the upload's own
|
|
45
|
+
* (staged) metadata. On completion it becomes the finished file's `metadata.tenantId` — the field the
|
|
46
|
+
* file module's `'tenant'` access preset decides on.
|
|
47
|
+
*
|
|
48
|
+
* Framework-owned and ALWAYS overwritten in `onUploadCreate`, exactly like
|
|
49
|
+
* {@link TUS_OWNER_METADATA_KEY}: the client's `Upload-Metadata` header must never be able to place
|
|
50
|
+
* a file in a tenant. It is set only while multi-tenancy is active and the caller's tenant header
|
|
51
|
+
* resolves to a tenant they are an active member of; otherwise it is `null`.
|
|
52
|
+
*/
|
|
53
|
+
export const TUS_TENANT_METADATA_KEY = 'ltTenantId';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DynamicModule, Global, Logger, Module, OnModuleInit, Type } from '@nestjs/common';
|
|
2
|
+
import { ModuleRef } from '@nestjs/core';
|
|
2
3
|
import { getConnectionToken } from '@nestjs/mongoose';
|
|
3
4
|
import { Connection } from 'mongoose';
|
|
4
5
|
|
|
@@ -54,6 +55,32 @@ export interface TusModuleOptions {
|
|
|
54
55
|
* ```
|
|
55
56
|
*/
|
|
56
57
|
controller?: Type<CoreTusController>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Custom service class to use instead of CoreTusService.
|
|
61
|
+
* The class must extend CoreTusService and keep its constructor signature
|
|
62
|
+
* `(connection, options?)`: the module constructs it with exactly those arguments, so it cannot
|
|
63
|
+
* declare constructor dependencies of its own. Project providers are reached at call time through
|
|
64
|
+
* `this.options?.moduleRef`.
|
|
65
|
+
*
|
|
66
|
+
* The seam for project-specific upload rules — e.g. override `onUploadCreate()` to enforce a quota
|
|
67
|
+
* from the declared `Upload-Length` before a single byte is written, or `onUploadComplete()` to
|
|
68
|
+
* post-process the finished file.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* export class TusService extends CoreTusService {
|
|
73
|
+
* protected override async onUploadCreate(req: any, upload: Upload) {
|
|
74
|
+
* const quota = this.options?.moduleRef?.get(QuotaService, { strict: false });
|
|
75
|
+
* await quota?.assertRoomFor(req, upload.size); // throw an Error with status_code 413 to refuse
|
|
76
|
+
* return super.onUploadCreate(req, upload);
|
|
77
|
+
* }
|
|
78
|
+
* }
|
|
79
|
+
*
|
|
80
|
+
* TusModule.forRoot({ service: TusService })
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
service?: Type<CoreTusService>;
|
|
57
84
|
}
|
|
58
85
|
|
|
59
86
|
/**
|
|
@@ -98,6 +125,7 @@ export class TusModule implements OnModuleInit {
|
|
|
98
125
|
private static tusEnabled = false;
|
|
99
126
|
private static currentConfig: ITusConfig | null = null;
|
|
100
127
|
private static customController: null | Type<CoreTusController> = null;
|
|
128
|
+
private static customService: null | Type<CoreTusService> = null;
|
|
101
129
|
|
|
102
130
|
constructor(private readonly tusService?: CoreTusService) {}
|
|
103
131
|
|
|
@@ -157,15 +185,17 @@ export class TusModule implements OnModuleInit {
|
|
|
157
185
|
* @returns Dynamic module configuration
|
|
158
186
|
*/
|
|
159
187
|
static forRoot(options: TusModuleOptions = {}): DynamicModule {
|
|
160
|
-
const { config: rawConfig, controller } = options;
|
|
188
|
+
const { config: rawConfig, controller, service } = options;
|
|
161
189
|
|
|
162
190
|
// Normalize config: undefined/true → enabled with defaults, false → disabled
|
|
163
191
|
const config = normalizeTusConfig(rawConfig);
|
|
164
192
|
|
|
165
193
|
// Store config for service configuration
|
|
166
194
|
this.currentConfig = config;
|
|
167
|
-
// Store custom controller if provided
|
|
195
|
+
// Store custom controller and service if provided
|
|
168
196
|
this.customController = controller || null;
|
|
197
|
+
this.customService = service || null;
|
|
198
|
+
const ServiceClass = this.customService || CoreTusService;
|
|
169
199
|
|
|
170
200
|
// If TUS is disabled, return minimal module
|
|
171
201
|
if (config === null) {
|
|
@@ -183,9 +213,9 @@ export class TusModule implements OnModuleInit {
|
|
|
183
213
|
inject: [getConnectionToken()],
|
|
184
214
|
provide: CoreTusService,
|
|
185
215
|
useFactory: (connection: Connection) => {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
return
|
|
216
|
+
const tusService = new ServiceClass(connection);
|
|
217
|
+
tusService.configure(false);
|
|
218
|
+
return tusService;
|
|
189
219
|
},
|
|
190
220
|
},
|
|
191
221
|
],
|
|
@@ -230,6 +260,7 @@ export class TusModule implements OnModuleInit {
|
|
|
230
260
|
ConfigService,
|
|
231
261
|
{ optional: true, token: CoreS3Service },
|
|
232
262
|
{ optional: true, token: CoreRedisService },
|
|
263
|
+
ModuleRef,
|
|
233
264
|
],
|
|
234
265
|
provide: CoreTusService,
|
|
235
266
|
useFactory: async (
|
|
@@ -238,14 +269,15 @@ export class TusModule implements OnModuleInit {
|
|
|
238
269
|
configService: ConfigService,
|
|
239
270
|
s3Service?: CoreS3Service,
|
|
240
271
|
redisService?: CoreRedisService,
|
|
272
|
+
moduleRef?: ModuleRef,
|
|
241
273
|
) => {
|
|
242
|
-
const
|
|
243
|
-
|
|
274
|
+
const tusService = new ServiceClass(connection, { configService, moduleRef, redisService, s3Service });
|
|
275
|
+
tusService.configure(tusConfig);
|
|
244
276
|
// NestJS DOES call onModuleInit on a factory-provided instance — its hook iterates
|
|
245
277
|
// every non-alias provider, however it was constructed. Calling it here as well ran
|
|
246
278
|
// init TWICE per boot: two TUS servers, two S3 stores, and two hourly expiration
|
|
247
279
|
// intervals of which onModuleDestroy clears only the second.
|
|
248
|
-
return
|
|
280
|
+
return tusService;
|
|
249
281
|
},
|
|
250
282
|
},
|
|
251
283
|
],
|
|
@@ -260,5 +292,6 @@ export class TusModule implements OnModuleInit {
|
|
|
260
292
|
this.tusEnabled = false;
|
|
261
293
|
this.currentConfig = null;
|
|
262
294
|
this.customController = null;
|
|
295
|
+
this.customService = null;
|
|
263
296
|
}
|
|
264
297
|
}
|
|
@@ -5,6 +5,7 @@ import { sha256 } from 'js-sha256';
|
|
|
5
5
|
import { Document, Model } from 'mongoose';
|
|
6
6
|
|
|
7
7
|
import { looksLikeSystemRole, SYSTEM_ROLE_PREFIX } from '../../common/enums/role.enum';
|
|
8
|
+
import { accessDeniedException } from '../../common/exceptions/access-denied.exception';
|
|
8
9
|
import { resolveAppUrlFromConfig } from '../../common/helpers/cookies.helper';
|
|
9
10
|
import { maskEmail } from '../../common/helpers/logging.helper';
|
|
10
11
|
import { assignPlain, isQueryableString, prepareServiceOptionsForCreate } from '../../common/helpers/input.helper';
|
|
@@ -673,6 +674,8 @@ export abstract class CoreUserService<
|
|
|
673
674
|
const oldUser = (await this.mainDbModel.findById(id).lean().exec()) as null | TUser;
|
|
674
675
|
const oldEmail = oldUser?.email;
|
|
675
676
|
|
|
677
|
+
this.assertCredentialChangeAllowed(oldUser, input, serviceOptions);
|
|
678
|
+
|
|
676
679
|
// Capture the submitted password for the IAM sync before super.update()
|
|
677
680
|
// hashes it in place.
|
|
678
681
|
//
|
|
@@ -738,6 +741,56 @@ export abstract class CoreUserService<
|
|
|
738
741
|
return updatedUser;
|
|
739
742
|
}
|
|
740
743
|
|
|
744
|
+
/**
|
|
745
|
+
* Refuses a change of another account's e-mail address or password where the configuration promises
|
|
746
|
+
* tenant isolation from platform administrators.
|
|
747
|
+
*
|
|
748
|
+
* With multi-tenancy active and `adminBypass: false`, a platform administrator deliberately has no
|
|
749
|
+
* way into a tenant's data. Setting a tenant user's address or password is a way around that: change
|
|
750
|
+
* the address, request a reset, sign in as that person. The same holds for whoever created the
|
|
751
|
+
* account, because `S_CREATOR` may update it and the e-mail field is open to everyone who may.
|
|
752
|
+
*
|
|
753
|
+
* Untouched by design:
|
|
754
|
+
* - the account changing its own credentials,
|
|
755
|
+
* - profile fields, and an e-mail address sent along unchanged,
|
|
756
|
+
* - work without a signed-in user (invitation, password reset), which is the system's own,
|
|
757
|
+
* - every configuration other than multi-tenancy with `adminBypass: false`.
|
|
758
|
+
*
|
|
759
|
+
* `force` does not lift it: the rule is about WHO acts, and system work is defined by the absence of
|
|
760
|
+
* a current user. Protected so a project can widen or narrow it.
|
|
761
|
+
*/
|
|
762
|
+
protected assertCredentialChangeAllowed(
|
|
763
|
+
target: null | TUser,
|
|
764
|
+
input: TUserInput,
|
|
765
|
+
serviceOptions?: ServiceOptions,
|
|
766
|
+
): void {
|
|
767
|
+
const multiTenancy = ConfigService.configFastButReadOnly?.multiTenancy;
|
|
768
|
+
if (!multiTenancy || multiTenancy.enabled === false || multiTenancy.adminBypass !== false) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const currentUser = serviceOptions?.currentUser;
|
|
772
|
+
if (!currentUser?.id || !target) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
// No exemption for administrator TARGETS: an administrator may write `roles`, so "promote the
|
|
776
|
+
// tenant account to ADMIN, then change its address" would open every account in two requests.
|
|
777
|
+
const targetId = String((target as any)._id ?? (target as any).id);
|
|
778
|
+
if (String(currentUser.id) === targetId) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const submitted = input as { email?: unknown; password?: unknown };
|
|
783
|
+
const normalize = (value: unknown) => (typeof value === 'string' ? value.trim().toLowerCase() : value);
|
|
784
|
+
const changesEmail = submitted?.email !== undefined && normalize(submitted.email) !== normalize(target.email);
|
|
785
|
+
const changesPassword = submitted?.password !== undefined;
|
|
786
|
+
if (changesEmail || changesPassword) {
|
|
787
|
+
throw accessDeniedException(
|
|
788
|
+
currentUser,
|
|
789
|
+
'The e-mail address and password of a tenant account can only be changed by the account itself',
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
741
794
|
/**
|
|
742
795
|
* Delete user with automatic cleanup of IAM auth data
|
|
743
796
|
*
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ export * from './core/common/helpers/input.helper';
|
|
|
47
47
|
export * from './core/common/helpers/logging.helper';
|
|
48
48
|
export * from './core/common/helpers/meta.helper';
|
|
49
49
|
export * from './core/common/helpers/model.helper';
|
|
50
|
+
export * from './core/common/helpers/outbound-url.helper';
|
|
50
51
|
export * from './core/common/helpers/process-diagnostics.helper';
|
|
51
52
|
export * from './core/common/helpers/register-enum.helper';
|
|
52
53
|
export * from './core/common/helpers/scim.helper';
|
|
@@ -258,6 +259,7 @@ export * from './core/modules/tenant/core-tenant.decorators';
|
|
|
258
259
|
export * from './core/modules/tenant/core-tenant.enums';
|
|
259
260
|
export * from './core/modules/tenant/core-tenant.guard';
|
|
260
261
|
export * from './core/modules/tenant/core-role-scope.registry';
|
|
262
|
+
export * from './core/modules/tenant/core-tenant-role.helper';
|
|
261
263
|
export * from './core/modules/tenant/core-tenant.helpers';
|
|
262
264
|
export * from './core/modules/tenant/core-tenant.module';
|
|
263
265
|
export * from './core/modules/tenant/core-tenant.service';
|