@lenne.tech/nest-server 11.17.0 → 11.19.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/dist/config.env.js +2 -2
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/decorators/response-model.decorator.d.ts +3 -0
- package/dist/core/common/decorators/response-model.decorator.js +8 -0
- package/dist/core/common/decorators/response-model.decorator.js.map +1 -0
- package/dist/core/common/helpers/db.helper.js +2 -2
- package/dist/core/common/helpers/db.helper.js.map +1 -1
- package/dist/core/common/helpers/filter.helper.js +3 -3
- package/dist/core/common/helpers/filter.helper.js.map +1 -1
- package/dist/core/common/helpers/input.helper.js +2 -2
- package/dist/core/common/helpers/input.helper.js.map +1 -1
- package/dist/core/common/helpers/interceptor.helper.d.ts +3 -0
- package/dist/core/common/helpers/interceptor.helper.js +84 -0
- package/dist/core/common/helpers/interceptor.helper.js.map +1 -0
- package/dist/core/common/helpers/service.helper.d.ts +1 -0
- package/dist/core/common/helpers/service.helper.js +1 -0
- package/dist/core/common/helpers/service.helper.js.map +1 -1
- package/dist/core/common/interceptors/check-security.interceptor.d.ts +2 -0
- package/dist/core/common/interceptors/check-security.interceptor.js +43 -1
- package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
- package/dist/core/common/interceptors/response-model.interceptor.d.ts +13 -0
- package/dist/core/common/interceptors/response-model.interceptor.js +107 -0
- package/dist/core/common/interceptors/response-model.interceptor.js.map +1 -0
- package/dist/core/common/interceptors/translate-response.interceptor.d.ts +8 -0
- package/dist/core/common/interceptors/translate-response.interceptor.js +85 -0
- package/dist/core/common/interceptors/translate-response.interceptor.js.map +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +14 -0
- package/dist/core/common/middleware/request-context.middleware.d.ts +5 -0
- package/dist/core/common/middleware/request-context.middleware.js +29 -0
- package/dist/core/common/middleware/request-context.middleware.js.map +1 -0
- package/dist/core/common/pipes/map-and-validate.pipe.js +2 -2
- package/dist/core/common/pipes/map-and-validate.pipe.js.map +1 -1
- package/dist/core/common/plugins/complexity.plugin.d.ts +2 -2
- package/dist/core/common/plugins/mongoose-audit-fields.plugin.d.ts +1 -0
- package/dist/core/common/plugins/mongoose-audit-fields.plugin.js +51 -0
- package/dist/core/common/plugins/mongoose-audit-fields.plugin.js.map +1 -0
- package/dist/core/common/plugins/mongoose-password.plugin.d.ts +4 -0
- package/dist/core/common/plugins/mongoose-password.plugin.js +69 -0
- package/dist/core/common/plugins/mongoose-password.plugin.js.map +1 -0
- package/dist/core/common/plugins/mongoose-role-guard.plugin.d.ts +1 -0
- package/dist/core/common/plugins/mongoose-role-guard.plugin.js +80 -0
- package/dist/core/common/plugins/mongoose-role-guard.plugin.js.map +1 -0
- package/dist/core/common/services/config.service.js +2 -2
- package/dist/core/common/services/config.service.js.map +1 -1
- package/dist/core/common/services/model-registry.service.d.ts +8 -0
- package/dist/core/common/services/model-registry.service.js +20 -0
- package/dist/core/common/services/model-registry.service.js.map +1 -0
- package/dist/core/common/services/module.service.d.ts +2 -0
- package/dist/core/common/services/module.service.js +36 -1
- package/dist/core/common/services/module.service.js.map +1 -1
- package/dist/core/common/services/request-context.service.d.ts +18 -0
- package/dist/core/common/services/request-context.service.js +32 -0
- package/dist/core/common/services/request-context.service.js.map +1 -0
- package/dist/core/modules/auth/guards/auth.guard.js +2 -2
- package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.resolver.js +4 -2
- package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -1
- package/dist/core/modules/error-code/core-error-code.controller.js +1 -1
- package/dist/core/modules/error-code/core-error-code.controller.js.map +1 -1
- package/dist/core/modules/system-setup/core-system-setup.controller.js +1 -1
- package/dist/core/modules/system-setup/core-system-setup.controller.js.map +1 -1
- package/dist/core.module.js +36 -0
- 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/server/modules/error-code/error-code.controller.js +1 -1
- package/dist/server/modules/error-code/error-code.controller.js.map +1 -1
- package/dist/server/modules/file/file-info.model.d.ts +12 -12
- package/dist/server/modules/user/user.model.d.ts +33 -33
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +35 -30
- package/src/config.env.ts +2 -2
- package/src/core/common/decorators/response-model.decorator.ts +31 -0
- package/src/core/common/helpers/db.helper.ts +2 -2
- package/src/core/common/helpers/filter.helper.ts +3 -3
- package/src/core/common/helpers/input.helper.ts +2 -2
- package/src/core/common/helpers/interceptor.helper.ts +132 -0
- package/src/core/common/helpers/service.helper.ts +1 -1
- package/src/core/common/interceptors/check-security.interceptor.ts +44 -1
- package/src/core/common/interceptors/response-model.interceptor.ts +135 -0
- package/src/core/common/interceptors/translate-response.interceptor.ts +104 -0
- package/src/core/common/interfaces/server-options.interface.ts +162 -2
- package/src/core/common/middleware/request-context.middleware.ts +25 -0
- package/src/core/common/pipes/map-and-validate.pipe.ts +2 -2
- package/src/core/common/plugins/complexity.plugin.ts +2 -2
- package/src/core/common/plugins/mongoose-audit-fields.plugin.ts +74 -0
- package/src/core/common/plugins/mongoose-password.plugin.ts +100 -0
- package/src/core/common/plugins/mongoose-role-guard.plugin.ts +150 -0
- package/src/core/common/services/config.service.ts +2 -2
- package/src/core/common/services/model-registry.service.ts +25 -0
- package/src/core/common/services/module.service.ts +91 -1
- package/src/core/common/services/request-context.service.ts +69 -0
- package/src/core/modules/auth/guards/auth.guard.ts +2 -2
- package/src/core/modules/better-auth/core-better-auth.resolver.ts +4 -2
- package/src/core/modules/error-code/INTEGRATION-CHECKLIST.md +8 -8
- package/src/core/modules/error-code/core-error-code.controller.ts +3 -3
- package/src/core/modules/error-code/interfaces/error-code.interfaces.ts +1 -1
- package/src/core/modules/system-setup/INTEGRATION-CHECKLIST.md +5 -5
- package/src/core/modules/system-setup/README.md +9 -9
- package/src/core/modules/system-setup/core-system-setup.controller.ts +1 -1
- package/src/core.module.ts +55 -4
- package/src/index.ts +10 -0
- package/src/server/modules/error-code/README.md +5 -5
- package/src/server/modules/error-code/error-code.controller.ts +3 -3
|
@@ -5,10 +5,13 @@ import { ProcessType } from '../enums/process-type.enum';
|
|
|
5
5
|
import { getStringIds, popAndMap } from '../helpers/db.helper';
|
|
6
6
|
import { check } from '../helpers/input.helper';
|
|
7
7
|
import { prepareInput, prepareOutput } from '../helpers/service.helper';
|
|
8
|
+
import { getTranslatablePropertyKeys, updateLanguage } from '../decorators/translatable.decorator';
|
|
8
9
|
import { ServiceOptions } from '../interfaces/service-options.interface';
|
|
9
10
|
import { CoreModel } from '../models/core-model.model';
|
|
10
11
|
import { FieldSelection } from '../types/field-selection.type';
|
|
11
12
|
import { ConfigService } from './config.service';
|
|
13
|
+
import { ModelRegistry } from './model-registry.service';
|
|
14
|
+
import { RequestContext } from './request-context.service';
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Module service class to be extended by concrete module services
|
|
@@ -40,6 +43,11 @@ export abstract class ModuleService<T extends CoreModel = any> {
|
|
|
40
43
|
this.configService = options?.configService;
|
|
41
44
|
this.mainDbModel = options?.mainDbModel;
|
|
42
45
|
this.mainModelConstructor = options?.mainModelConstructor;
|
|
46
|
+
|
|
47
|
+
// Auto-register model class for ResponseModelInterceptor lookups
|
|
48
|
+
if (options?.mainModelConstructor && options?.mainDbModel?.modelName) {
|
|
49
|
+
ModelRegistry.register(options.mainDbModel.modelName, options.mainModelConstructor);
|
|
50
|
+
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
/**
|
|
@@ -191,7 +199,10 @@ export abstract class ModuleService<T extends CoreModel = any> {
|
|
|
191
199
|
}
|
|
192
200
|
|
|
193
201
|
// Run service function
|
|
194
|
-
|
|
202
|
+
// When force is enabled, bypass the Mongoose role guard plugin as well
|
|
203
|
+
let result = config.force
|
|
204
|
+
? await RequestContext.runWithBypassRoleGuard(() => serviceFunc(config))
|
|
205
|
+
: await serviceFunc(config);
|
|
195
206
|
|
|
196
207
|
// Pop and map main model
|
|
197
208
|
if (config.processFieldSelection && config.fieldSelection && this.processFieldSelection) {
|
|
@@ -242,6 +253,85 @@ export abstract class ModuleService<T extends CoreModel = any> {
|
|
|
242
253
|
return result;
|
|
243
254
|
}
|
|
244
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Simplified result processing for direct Mongoose queries.
|
|
258
|
+
* Handles population and output preparation without the full process() pipeline.
|
|
259
|
+
*
|
|
260
|
+
* Security (password hashing, role guard, createdBy/updatedBy, type mapping,
|
|
261
|
+
* field filtering, secret removal, translations) is handled automatically
|
|
262
|
+
* by the safety net (Mongoose plugins + interceptors).
|
|
263
|
+
*
|
|
264
|
+
* Use this when bypassing CrudService methods but still wanting
|
|
265
|
+
* population and custom prepareOutput() transformations.
|
|
266
|
+
*
|
|
267
|
+
* Note: This method does NOT perform authorization checks (checkRights).
|
|
268
|
+
* The caller is responsible for verifying permissions before calling this method.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```typescript
|
|
272
|
+
* // Direct Mongoose query with result processing
|
|
273
|
+
* const doc = await this.mainDbModel.findById(id).exec();
|
|
274
|
+
* return this.processResult(doc, serviceOptions);
|
|
275
|
+
*
|
|
276
|
+
* // Aggregate with result processing
|
|
277
|
+
* const results = await this.mainDbModel.aggregate(pipeline).exec();
|
|
278
|
+
* return this.processResult(results, serviceOptions);
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
async processResult(result: any, serviceOptions: ServiceOptions = {}): Promise<any> {
|
|
282
|
+
if (!result) {
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Population (if GraphQL field selection is available)
|
|
287
|
+
if (serviceOptions.fieldSelection && this.processFieldSelection) {
|
|
288
|
+
const populateOpts = serviceOptions.processFieldSelection || {};
|
|
289
|
+
const items = Array.isArray(result) ? result : [result];
|
|
290
|
+
for (const item of items) {
|
|
291
|
+
await this.processFieldSelection(item, serviceOptions.fieldSelection, populateOpts);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Custom prepareOutput (type mapping, ObjectId conversion, custom overrides)
|
|
296
|
+
if (this.prepareOutput) {
|
|
297
|
+
result = await this.prepareOutput(result, serviceOptions);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Apply translation-aware input processing.
|
|
305
|
+
* Auto-detects @Translatable fields on the model and routes non-base-language
|
|
306
|
+
* values into _translations.
|
|
307
|
+
*
|
|
308
|
+
* @param input - The input data to transform
|
|
309
|
+
* @param oldDoc - The existing document from DB (needed for value comparison)
|
|
310
|
+
* @param language - Language code (defaults to RequestContext.getLanguage())
|
|
311
|
+
* @returns The transformed input with translations stored in _translations
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```typescript
|
|
315
|
+
* // In a custom update method:
|
|
316
|
+
* const oldDoc = await this.mainDbModel.findById(id).lean();
|
|
317
|
+
* input = this.applyTranslationInput(input, oldDoc, serviceOptions.language);
|
|
318
|
+
* return this.mainDbModel.findByIdAndUpdate(id, input, { returnDocument: 'after' }).exec();
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
protected applyTranslationInput(input: any, oldDoc: any, language?: string): any {
|
|
322
|
+
language = language || RequestContext.getLanguage();
|
|
323
|
+
if (!language) {
|
|
324
|
+
return input;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const translatableFields = getTranslatablePropertyKeys(this.mainModelConstructor);
|
|
328
|
+
if (translatableFields.length === 0) {
|
|
329
|
+
return input;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return updateLanguage(language, input, oldDoc, translatableFields);
|
|
333
|
+
}
|
|
334
|
+
|
|
245
335
|
/**
|
|
246
336
|
* Prepare input before save
|
|
247
337
|
*/
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
|
|
3
|
+
export interface IRequestContext {
|
|
4
|
+
currentUser?: {
|
|
5
|
+
id: string;
|
|
6
|
+
hasRole?: (roles: string[]) => boolean;
|
|
7
|
+
roles?: string[];
|
|
8
|
+
};
|
|
9
|
+
language?: string;
|
|
10
|
+
/** When true, mongooseRoleGuardPlugin allows role changes regardless of user permissions */
|
|
11
|
+
bypassRoleGuard?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Request-scoped context using AsyncLocalStorage.
|
|
16
|
+
* Provides access to the current user in Mongoose hooks and other
|
|
17
|
+
* places where NestJS request scope is not available.
|
|
18
|
+
*/
|
|
19
|
+
export class RequestContext {
|
|
20
|
+
private static storage = new AsyncLocalStorage<IRequestContext>();
|
|
21
|
+
|
|
22
|
+
static run<T>(context: IRequestContext, fn: () => T): T {
|
|
23
|
+
return this.storage.run(context, fn);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static get(): IRequestContext | undefined {
|
|
27
|
+
return this.storage.getStore();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static getCurrentUser(): IRequestContext['currentUser'] | undefined {
|
|
31
|
+
return this.storage.getStore()?.currentUser;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static getLanguage(): string | undefined {
|
|
35
|
+
return this.storage.getStore()?.language;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if the role guard bypass is active for the current context.
|
|
40
|
+
*/
|
|
41
|
+
static isBypassRoleGuard(): boolean {
|
|
42
|
+
return this.storage.getStore()?.bypassRoleGuard === true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run a function with the role guard bypass enabled.
|
|
47
|
+
* The current context (user, language) is preserved; only bypassRoleGuard is added.
|
|
48
|
+
*
|
|
49
|
+
* Use this when authorized code needs to set roles on users, e.g.:
|
|
50
|
+
* - signUp with default roles
|
|
51
|
+
* - Admin panel where a non-admin role (e.g. HR_MANAGER) creates users with roles
|
|
52
|
+
* - System setup creating initial admin
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* await RequestContext.runWithBypassRoleGuard(async () => {
|
|
57
|
+
* await this.mainDbModel.findByIdAndUpdate(userId, { roles: ['EMPLOYEE'] });
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
static runWithBypassRoleGuard<T>(fn: () => T): T {
|
|
62
|
+
const currentStore = this.storage.getStore();
|
|
63
|
+
const context: IRequestContext = {
|
|
64
|
+
...currentStore,
|
|
65
|
+
bypassRoleGuard: true,
|
|
66
|
+
};
|
|
67
|
+
return this.storage.run(context, fn);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -40,8 +40,8 @@ const createPassportContext =
|
|
|
40
40
|
try {
|
|
41
41
|
request.authInfo = info;
|
|
42
42
|
return resolve(callback(err, user, info));
|
|
43
|
-
} catch (
|
|
44
|
-
reject(
|
|
43
|
+
} catch (callbackError) {
|
|
44
|
+
reject(callbackError);
|
|
45
45
|
}
|
|
46
46
|
})(request, response, (err: any) => (err ? reject(err) : resolve(undefined))),
|
|
47
47
|
);
|
|
@@ -342,9 +342,11 @@ export class CoreBetterAuthResolver {
|
|
|
342
342
|
// 1. accessToken (JWT plugin enriched response)
|
|
343
343
|
// 2. token (top-level, some BetterAuth versions)
|
|
344
344
|
// 3. session.token (session-based fallback)
|
|
345
|
-
const
|
|
345
|
+
const tokenResponse = response as any;
|
|
346
346
|
const rawToken =
|
|
347
|
-
|
|
347
|
+
tokenResponse.accessToken ||
|
|
348
|
+
tokenResponse.token ||
|
|
349
|
+
(hasSession(response) ? response.session.token : undefined);
|
|
348
350
|
const token = await this.resolveJwtToken(rawToken);
|
|
349
351
|
|
|
350
352
|
return {
|
|
@@ -86,7 +86,7 @@ const config = {
|
|
|
86
86
|
};
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
**Done!** Your project errors are now available via `/
|
|
89
|
+
**Done!** Your project errors are now available via `/i18n/errors/:locale`.
|
|
90
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
@@ -221,14 +221,14 @@ After integration, verify:
|
|
|
221
221
|
|
|
222
222
|
- [ ] `npm run build` succeeds without errors
|
|
223
223
|
- [ ] `npm test` passes
|
|
224
|
-
- [ ] `GET /
|
|
225
|
-
- [ ] `GET /
|
|
224
|
+
- [ ] `GET /i18n/errors/de` returns your project error codes
|
|
225
|
+
- [ ] `GET /i18n/errors/en` returns English translations
|
|
226
226
|
- [ ] Error codes follow format `PREFIX_XXXX` (e.g., `PROJ_0001`)
|
|
227
227
|
- [ ] Translations include placeholders where needed (`{param}`)
|
|
228
228
|
|
|
229
229
|
### For Scenario C only:
|
|
230
230
|
|
|
231
|
-
- [ ] `GET /
|
|
231
|
+
- [ ] `GET /i18n/errors/codes` returns all error codes (if implemented)
|
|
232
232
|
|
|
233
233
|
---
|
|
234
234
|
|
|
@@ -269,10 +269,10 @@ const orderCode = ProjectErrorCode.ORDER_NOT_FOUND; // '#PROJ_0001: Order not fo
|
|
|
269
269
|
|
|
270
270
|
### REST Endpoints
|
|
271
271
|
|
|
272
|
-
| Endpoint
|
|
273
|
-
|
|
|
274
|
-
| `/
|
|
275
|
-
| `/
|
|
272
|
+
| Endpoint | Method | Description |
|
|
273
|
+
| ---------------------- | ------ | ----------------------------------------- |
|
|
274
|
+
| `/i18n/errors/:locale` | GET | Get translations for locale (de, en, ...) |
|
|
275
|
+
| `/i18n/errors/codes` | GET | Get all error codes (Scenario C only) |
|
|
276
276
|
|
|
277
277
|
### Response Format (Nuxt i18n compatible)
|
|
278
278
|
|
|
@@ -12,10 +12,10 @@ import { IErrorTranslationResponse, SupportedLocale } from './interfaces/error-c
|
|
|
12
12
|
* This controller is publicly accessible (no authentication required).
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
|
-
* GET /
|
|
16
|
-
* GET /
|
|
15
|
+
* GET /i18n/errors/de - Get German translations
|
|
16
|
+
* GET /i18n/errors/en - Get English translations
|
|
17
17
|
*/
|
|
18
|
-
@Controller('
|
|
18
|
+
@Controller('i18n/errors')
|
|
19
19
|
export class CoreErrorCodeController {
|
|
20
20
|
constructor(protected readonly errorCodeService: CoreErrorCodeService) {}
|
|
21
21
|
|
|
@@ -36,7 +36,7 @@ export interface IErrorCodeModuleConfig {
|
|
|
36
36
|
* @example
|
|
37
37
|
* ```typescript
|
|
38
38
|
* // Standalone controller (RECOMMENDED)
|
|
39
|
-
* @Controller('
|
|
39
|
+
* @Controller('i18n/errors')
|
|
40
40
|
* export class ErrorCodeController {
|
|
41
41
|
* constructor(protected readonly errorCodeService: ErrorCodeService) {}
|
|
42
42
|
*
|
|
@@ -68,7 +68,7 @@ Only needed if you want to add extra validation, logging, or custom fields.
|
|
|
68
68
|
import { Controller } from '@nestjs/common';
|
|
69
69
|
import { CoreSystemSetupController, Roles, RoleEnum } from '@lenne.tech/nest-server';
|
|
70
70
|
|
|
71
|
-
@Controller('
|
|
71
|
+
@Controller('system-setup')
|
|
72
72
|
@Roles(RoleEnum.ADMIN)
|
|
73
73
|
export class SystemSetupController extends CoreSystemSetupController {
|
|
74
74
|
// Override methods here for custom logic
|
|
@@ -83,10 +83,10 @@ export class SystemSetupController extends CoreSystemSetupController {
|
|
|
83
83
|
|
|
84
84
|
- [ ] `npm run build` succeeds
|
|
85
85
|
- [ ] `npm test` passes
|
|
86
|
-
- [ ] `GET /
|
|
87
|
-
- [ ] `POST /
|
|
88
|
-
- [ ] `GET /
|
|
89
|
-
- [ ] `POST /
|
|
86
|
+
- [ ] `GET /system-setup/status` returns `{ needsSetup: true }` on empty database
|
|
87
|
+
- [ ] `POST /system-setup/init` creates admin user with correct role
|
|
88
|
+
- [ ] `GET /system-setup/status` returns `{ needsSetup: false }` after init
|
|
89
|
+
- [ ] `POST /system-setup/init` returns 403 when users already exist
|
|
90
90
|
|
|
91
91
|
---
|
|
92
92
|
|
|
@@ -42,12 +42,12 @@ Once any user exists, the init endpoint is permanently locked (returns 403) and
|
|
|
42
42
|
|
|
43
43
|
## Endpoints
|
|
44
44
|
|
|
45
|
-
| Method | Endpoint
|
|
46
|
-
| ------ |
|
|
47
|
-
| GET | `/
|
|
48
|
-
| POST | `/
|
|
45
|
+
| Method | Endpoint | Description |
|
|
46
|
+
| ------ | ---------------------- | --------------------------- |
|
|
47
|
+
| GET | `/system-setup/status` | Check if system needs setup |
|
|
48
|
+
| POST | `/system-setup/init` | Create initial admin user |
|
|
49
49
|
|
|
50
|
-
### GET /
|
|
50
|
+
### GET /system-setup/status
|
|
51
51
|
|
|
52
52
|
Returns the current setup status.
|
|
53
53
|
|
|
@@ -60,7 +60,7 @@ Returns the current setup status.
|
|
|
60
60
|
}
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
### POST /
|
|
63
|
+
### POST /system-setup/init
|
|
64
64
|
|
|
65
65
|
Creates the initial admin user. Only works when zero users exist.
|
|
66
66
|
|
|
@@ -195,12 +195,12 @@ Typical frontend flow:
|
|
|
195
195
|
|
|
196
196
|
```typescript
|
|
197
197
|
// 1. Check if setup is needed
|
|
198
|
-
const status = await fetch('/
|
|
198
|
+
const status = await fetch('/system-setup/status');
|
|
199
199
|
const { needsSetup } = await status.json();
|
|
200
200
|
|
|
201
201
|
if (needsSetup) {
|
|
202
202
|
// 2. Show setup form and submit
|
|
203
|
-
const result = await fetch('/
|
|
203
|
+
const result = await fetch('/system-setup/init', {
|
|
204
204
|
method: 'POST',
|
|
205
205
|
headers: { 'Content-Type': 'application/json' },
|
|
206
206
|
body: JSON.stringify({
|
|
@@ -225,7 +225,7 @@ if (needsSetup) {
|
|
|
225
225
|
|
|
226
226
|
**Solutions:**
|
|
227
227
|
|
|
228
|
-
1. Check `GET /
|
|
228
|
+
1. Check `GET /system-setup/status` - `needsSetup` should be `true`
|
|
229
229
|
2. If this is a fresh deployment, verify the database is empty
|
|
230
230
|
|
|
231
231
|
### Init returns 403 "System setup requires BetterAuth"
|
|
@@ -35,7 +35,7 @@ export class SystemSetupInitDto {
|
|
|
35
35
|
* that no users exist before allowing creation.
|
|
36
36
|
*/
|
|
37
37
|
@ApiTags('System Setup')
|
|
38
|
-
@Controller('
|
|
38
|
+
@Controller('system-setup')
|
|
39
39
|
@Roles(RoleEnum.ADMIN)
|
|
40
40
|
export class CoreSystemSetupController {
|
|
41
41
|
constructor(protected readonly systemSetupService: CoreSystemSetupService) {}
|
package/src/core.module.ts
CHANGED
|
@@ -3,17 +3,23 @@ import { DynamicModule, Global, MiddlewareConsumer, Module, NestModule, Unauthor
|
|
|
3
3
|
import { APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
|
4
4
|
import { GraphQLModule } from '@nestjs/graphql';
|
|
5
5
|
import { MongooseModule } from '@nestjs/mongoose';
|
|
6
|
-
import { Context } from '
|
|
6
|
+
import type { Context } from 'graphql-ws';
|
|
7
7
|
import graphqlUploadExpress = require('graphql-upload/graphqlUploadExpress.js');
|
|
8
8
|
import mongoose from 'mongoose';
|
|
9
9
|
|
|
10
10
|
import { merge } from './core/common/helpers/config.helper';
|
|
11
11
|
import { CheckResponseInterceptor } from './core/common/interceptors/check-response.interceptor';
|
|
12
12
|
import { CheckSecurityInterceptor } from './core/common/interceptors/check-security.interceptor';
|
|
13
|
+
import { ResponseModelInterceptor } from './core/common/interceptors/response-model.interceptor';
|
|
14
|
+
import { TranslateResponseInterceptor } from './core/common/interceptors/translate-response.interceptor';
|
|
13
15
|
import { IServerOptions } from './core/common/interfaces/server-options.interface';
|
|
16
|
+
import { RequestContextMiddleware } from './core/common/middleware/request-context.middleware';
|
|
14
17
|
import { MapAndValidatePipe } from './core/common/pipes/map-and-validate.pipe';
|
|
15
18
|
import { ComplexityPlugin } from './core/common/plugins/complexity.plugin';
|
|
16
19
|
import { mongooseIdPlugin } from './core/common/plugins/mongoose-id.plugin';
|
|
20
|
+
import { mongooseAuditFieldsPlugin } from './core/common/plugins/mongoose-audit-fields.plugin';
|
|
21
|
+
import { mongoosePasswordPlugin } from './core/common/plugins/mongoose-password.plugin';
|
|
22
|
+
import { mongooseRoleGuardPlugin } from './core/common/plugins/mongoose-role-guard.plugin';
|
|
17
23
|
import { ConfigService } from './core/common/services/config.service';
|
|
18
24
|
import { EmailService } from './core/common/services/email.service';
|
|
19
25
|
import { MailjetService } from './core/common/services/mailjet.service';
|
|
@@ -49,6 +55,8 @@ export class CoreModule implements NestModule {
|
|
|
49
55
|
* Integrate middleware, e.g. GraphQL upload handing for express
|
|
50
56
|
*/
|
|
51
57
|
configure(consumer: MiddlewareConsumer) {
|
|
58
|
+
// RequestContext middleware must run for all routes to provide AsyncLocalStorage context
|
|
59
|
+
consumer.apply(RequestContextMiddleware).forRoutes('*');
|
|
52
60
|
if (CoreModule.graphQlEnabled) {
|
|
53
61
|
consumer.apply(graphqlUploadExpress()).forRoutes('graphql');
|
|
54
62
|
}
|
|
@@ -186,6 +194,29 @@ export class CoreModule implements NestModule {
|
|
|
186
194
|
options,
|
|
187
195
|
);
|
|
188
196
|
|
|
197
|
+
// Wrap connectionFactory to add security plugins (password hashing, role guard)
|
|
198
|
+
const originalConnectionFactory = config.mongoose?.options?.connectionFactory;
|
|
199
|
+
config.mongoose.options = config.mongoose.options || {};
|
|
200
|
+
config.mongoose.options.connectionFactory = (connection, name) => {
|
|
201
|
+
// Run original factory first (includes mongooseIdPlugin from defaults)
|
|
202
|
+
if (originalConnectionFactory) {
|
|
203
|
+
connection = originalConnectionFactory(connection, name);
|
|
204
|
+
}
|
|
205
|
+
// Add password hashing plugin (enabled by default, opt-out via config)
|
|
206
|
+
if (config.security?.mongoosePasswordPlugin !== false) {
|
|
207
|
+
connection.plugin(mongoosePasswordPlugin);
|
|
208
|
+
}
|
|
209
|
+
// Add role guard plugin (enabled by default, opt-out via config)
|
|
210
|
+
if (config.security?.mongooseRoleGuardPlugin !== false) {
|
|
211
|
+
connection.plugin(mongooseRoleGuardPlugin);
|
|
212
|
+
}
|
|
213
|
+
// Add audit fields plugin (enabled by default, opt-out via config)
|
|
214
|
+
if (config.security?.mongooseAuditFieldsPlugin !== false) {
|
|
215
|
+
connection.plugin(mongooseAuditFieldsPlugin);
|
|
216
|
+
}
|
|
217
|
+
return connection;
|
|
218
|
+
};
|
|
219
|
+
|
|
189
220
|
// Check secrets
|
|
190
221
|
const jwtConfig = config.jwt;
|
|
191
222
|
if (jwtConfig?.secret && jwtConfig.secret && jwtConfig.refresh && jwtConfig.refresh.secret === jwtConfig.secret) {
|
|
@@ -235,6 +266,26 @@ export class CoreModule implements NestModule {
|
|
|
235
266
|
});
|
|
236
267
|
}
|
|
237
268
|
|
|
269
|
+
// TranslateResponseInterceptor: Applies _translations based on Accept-Language header
|
|
270
|
+
// Registered after security interceptors → runs before them on response
|
|
271
|
+
// Translation happens before security checks strip restricted fields
|
|
272
|
+
if (config.security?.translateResponseInterceptor !== false) {
|
|
273
|
+
providers.push({
|
|
274
|
+
provide: APP_INTERCEPTOR,
|
|
275
|
+
useClass: TranslateResponseInterceptor,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ResponseModelInterceptor: Auto-converts plain objects to model instances
|
|
280
|
+
// Registered last → runs first on response (NestJS reverse order for interceptors)
|
|
281
|
+
// This ensures plain objects get securityCheck() and @Restricted metadata before other interceptors check them
|
|
282
|
+
if (config.security?.responseModelInterceptor !== false) {
|
|
283
|
+
providers.push({
|
|
284
|
+
provide: APP_INTERCEPTOR,
|
|
285
|
+
useClass: ResponseModelInterceptor,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
238
289
|
if (config.mongoose?.modelDocumentation) {
|
|
239
290
|
providers.push(ModelDocService);
|
|
240
291
|
}
|
|
@@ -375,7 +426,7 @@ export class CoreModule implements NestModule {
|
|
|
375
426
|
subscriptions: {
|
|
376
427
|
'graphql-ws': {
|
|
377
428
|
context: ({ extra }) => extra,
|
|
378
|
-
onConnect: async (context: Context<any>) => {
|
|
429
|
+
onConnect: async (context: Context<any, any>) => {
|
|
379
430
|
const { connectionParams, extra } = context;
|
|
380
431
|
const enableAuth = graphQlOpts?.enableSubscriptionAuth ?? true;
|
|
381
432
|
|
|
@@ -470,7 +521,7 @@ export class CoreModule implements NestModule {
|
|
|
470
521
|
subscriptions: {
|
|
471
522
|
'graphql-ws': {
|
|
472
523
|
context: ({ extra }) => extra,
|
|
473
|
-
onConnect: async (context: Context<any>) => {
|
|
524
|
+
onConnect: async (context: Context<any, any>) => {
|
|
474
525
|
const { connectionParams, extra } = context;
|
|
475
526
|
const enableAuth = graphQlOpts?.enableSubscriptionAuth ?? true;
|
|
476
527
|
|
|
@@ -582,7 +633,7 @@ export class CoreModule implements NestModule {
|
|
|
582
633
|
subscriptions: {
|
|
583
634
|
'graphql-ws': {
|
|
584
635
|
context: ({ extra }) => extra,
|
|
585
|
-
onConnect: async (context: Context<any>) => {
|
|
636
|
+
onConnect: async (context: Context<any, any>) => {
|
|
586
637
|
const { connectionParams, extra } = context;
|
|
587
638
|
if (enableSubscriptionAuth) {
|
|
588
639
|
// get authToken from authorization header
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from './core/common/decorators/graphql-service-options.decorator';
|
|
|
16
16
|
export * from './core/common/decorators/graphql-user.decorator';
|
|
17
17
|
export * from './core/common/decorators/rest-service-options.decorator';
|
|
18
18
|
export * from './core/common/decorators/rest-user.decorator';
|
|
19
|
+
export * from './core/common/decorators/response-model.decorator';
|
|
19
20
|
export * from './core/common/decorators/restricted.decorator';
|
|
20
21
|
export * from './core/common/decorators/roles.decorator';
|
|
21
22
|
export * from './core/common/decorators/translatable.decorator';
|
|
@@ -34,6 +35,7 @@ export * from './core/common/helpers/decorator.helper';
|
|
|
34
35
|
export * from './core/common/helpers/file.helper';
|
|
35
36
|
export * from './core/common/helpers/filter.helper';
|
|
36
37
|
export * from './core/common/helpers/graphql.helper';
|
|
38
|
+
export * from './core/common/helpers/interceptor.helper';
|
|
37
39
|
export * from './core/common/helpers/gridfs.helper';
|
|
38
40
|
export * from './core/common/helpers/input.helper';
|
|
39
41
|
export * from './core/common/helpers/logging.helper';
|
|
@@ -49,6 +51,8 @@ export * from './core/common/inputs/single-filter.input';
|
|
|
49
51
|
export * from './core/common/inputs/sort.input';
|
|
50
52
|
export * from './core/common/interceptors/check-response.interceptor';
|
|
51
53
|
export * from './core/common/interceptors/check-security.interceptor';
|
|
54
|
+
export * from './core/common/interceptors/response-model.interceptor';
|
|
55
|
+
export * from './core/common/interceptors/translate-response.interceptor';
|
|
52
56
|
export * from './core/common/interfaces/core-persistence-model.interface';
|
|
53
57
|
export * from './core/common/interfaces/cron-job-config-with-time-zone.interface';
|
|
54
58
|
export * from './core/common/interfaces/cron-job-config-with-utc-offset.interface';
|
|
@@ -59,6 +63,7 @@ export * from './core/common/interfaces/prepare-output-options.interface';
|
|
|
59
63
|
export * from './core/common/interfaces/resolve-selector.interface';
|
|
60
64
|
export * from './core/common/interfaces/server-options.interface';
|
|
61
65
|
export * from './core/common/interfaces/service-options.interface';
|
|
66
|
+
export * from './core/common/middleware/request-context.middleware';
|
|
62
67
|
export * from './core/common/middlewares/to-lower-case.middleware';
|
|
63
68
|
export * from './core/common/models/core-model.model';
|
|
64
69
|
export * from './core/common/models/core-persistence.model';
|
|
@@ -67,6 +72,9 @@ export * from './core/common/pipes/check-input.pipe';
|
|
|
67
72
|
export * from './core/common/pipes/map-and-validate.pipe';
|
|
68
73
|
export * from './core/common/plugins/complexity.plugin';
|
|
69
74
|
export * from './core/common/plugins/mongoose-id.plugin';
|
|
75
|
+
export * from './core/common/plugins/mongoose-audit-fields.plugin';
|
|
76
|
+
export * from './core/common/plugins/mongoose-password.plugin';
|
|
77
|
+
export * from './core/common/plugins/mongoose-role-guard.plugin';
|
|
70
78
|
export * from './core/common/scalars/any.scalar';
|
|
71
79
|
export * from './core/common/scalars/date-timestamp.scalar';
|
|
72
80
|
export * from './core/common/scalars/date.scalar';
|
|
@@ -78,7 +86,9 @@ export * from './core/common/services/crud.service';
|
|
|
78
86
|
export * from './core/common/services/email.service';
|
|
79
87
|
export * from './core/common/services/mailjet.service';
|
|
80
88
|
export * from './core/common/services/model-doc.service';
|
|
89
|
+
export * from './core/common/services/model-registry.service';
|
|
81
90
|
export * from './core/common/services/module.service';
|
|
91
|
+
export * from './core/common/services/request-context.service';
|
|
82
92
|
export * from './core/common/services/template.service';
|
|
83
93
|
export * from './core/common/types/array-element.type';
|
|
84
94
|
export * from './core/common/types/core-model-constructor.type';
|
|
@@ -33,9 +33,9 @@ Demonstrates **Scenario C: Custom Service + Controller via forRoot()** where a p
|
|
|
33
33
|
│ │ ErrorCodeService │ │ ErrorCodeController │ │
|
|
34
34
|
│ │ extends Core... │ │ (standalone - see below) │ │
|
|
35
35
|
│ │ │ │ │ │
|
|
36
|
-
│ │ - LTNS_* (core) │ │ GET /
|
|
37
|
-
│ │ - SRV_* (server) │ │ GET /
|
|
38
|
-
│ │ │ │ GET /
|
|
36
|
+
│ │ - LTNS_* (core) │ │ GET /i18n/errors/codes │ │
|
|
37
|
+
│ │ - SRV_* (server) │ │ GET /i18n/errors/de │ │
|
|
38
|
+
│ │ │ │ GET /i18n/errors/en │ │
|
|
39
39
|
│ └─────────────────────┘ └─────────────────────────────┘ │
|
|
40
40
|
└──────────────────────────────────────────────────────────────┘
|
|
41
41
|
```
|
|
@@ -107,7 +107,7 @@ static routes (`/codes`), even if you re-declare the methods.
|
|
|
107
107
|
|
|
108
108
|
```typescript
|
|
109
109
|
// DOES NOT WORK - parent route registered first!
|
|
110
|
-
@Controller('
|
|
110
|
+
@Controller('i18n/errors')
|
|
111
111
|
export class ErrorCodeController extends CoreErrorCodeController {
|
|
112
112
|
@Get('codes') // Registered AFTER parent's :locale
|
|
113
113
|
getAllCodes(): string[] {}
|
|
@@ -117,7 +117,7 @@ export class ErrorCodeController extends CoreErrorCodeController {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// WORKS - standalone ensures correct order
|
|
120
|
-
@Controller('
|
|
120
|
+
@Controller('i18n/errors')
|
|
121
121
|
export class ErrorCodeController {
|
|
122
122
|
@Get('codes') // Registered first
|
|
123
123
|
getAllCodes(): string[] {}
|
|
@@ -16,8 +16,8 @@ import { ErrorCodeService } from './error-code.service';
|
|
|
16
16
|
* correct route registration order.
|
|
17
17
|
*
|
|
18
18
|
* Endpoints:
|
|
19
|
-
* - GET /
|
|
20
|
-
* - GET /
|
|
19
|
+
* - GET /i18n/errors/codes - Get all available error codes (custom)
|
|
20
|
+
* - GET /i18n/errors/:locale - Get translations for a locale
|
|
21
21
|
*
|
|
22
22
|
* **WHY standalone instead of extending CoreErrorCodeController?**
|
|
23
23
|
* NestJS registers routes from parent classes first, regardless of method declaration
|
|
@@ -33,7 +33,7 @@ import { ErrorCodeService } from './error-code.service';
|
|
|
33
33
|
* })
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
|
-
@Controller('
|
|
36
|
+
@Controller('i18n/errors')
|
|
37
37
|
export class ErrorCodeController {
|
|
38
38
|
constructor(protected readonly errorCodeService: ErrorCodeService) {}
|
|
39
39
|
|