@lenne.tech/nest-server 11.41.3 → 11.41.5
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 +1 -0
- package/.claude/rules/configurable-features.md +1 -0
- package/.claude/rules/role-system.md +15 -1
- package/.claude/rules/testing.md +16 -4
- package/CLAUDE.md +6 -3
- package/FRAMEWORK-API.md +4 -1
- package/dist/core/common/helpers/graceful-shutdown.helper.js.map +1 -1
- package/dist/core/common/helpers/logging.helper.js +2 -0
- package/dist/core/common/helpers/logging.helper.js.map +1 -1
- package/dist/core/common/helpers/process-diagnostics.helper.js.map +1 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +21 -1
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.d.ts +2 -2
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +46 -6
- package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +7 -1
- package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
- package/dist/core/modules/api-token/core-api-token.constants.d.ts +6 -0
- package/dist/core/modules/api-token/core-api-token.constants.js +11 -0
- package/dist/core/modules/api-token/core-api-token.constants.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.decorators.d.ts +1 -0
- package/dist/core/modules/api-token/core-api-token.decorators.js +8 -0
- package/dist/core/modules/api-token/core-api-token.decorators.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.helpers.d.ts +127 -0
- package/dist/core/modules/api-token/core-api-token.helpers.js +398 -0
- package/dist/core/modules/api-token/core-api-token.helpers.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.middleware.d.ts +10 -0
- package/dist/core/modules/api-token/core-api-token.middleware.js +58 -0
- package/dist/core/modules/api-token/core-api-token.middleware.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.model.d.ts +20 -0
- package/dist/core/modules/api-token/core-api-token.model.js +199 -0
- package/dist/core/modules/api-token/core-api-token.model.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.module.d.ts +11 -0
- package/dist/core/modules/api-token/core-api-token.module.js +39 -0
- package/dist/core/modules/api-token/core-api-token.module.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.registry.d.ts +9 -0
- package/dist/core/modules/api-token/core-api-token.registry.js +17 -0
- package/dist/core/modules/api-token/core-api-token.registry.js.map +1 -0
- package/dist/core/modules/api-token/core-api-token.service.d.ts +104 -0
- package/dist/core/modules/api-token/core-api-token.service.js +550 -0
- package/dist/core/modules/api-token/core-api-token.service.js.map +1 -0
- package/dist/core/modules/auth/guards/roles.guard.js +17 -1
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/better-auth-roles.guard.js +12 -2
- package/dist/core/modules/better-auth/better-auth-roles.guard.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.middleware.js +4 -0
- package/dist/core/modules/better-auth/core-better-auth.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth.module.d.ts +4 -0
- package/dist/core/modules/better-auth/core-better-auth.module.js +18 -0
- package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
- package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
- package/dist/core/modules/migrate/migration-runner.js +3 -0
- package/dist/core/modules/migrate/migration-runner.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant-guard.registry.d.ts +2 -0
- package/dist/core/modules/tenant/core-tenant-guard.registry.js +19 -0
- package/dist/core/modules/tenant/core-tenant-guard.registry.js.map +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +1 -0
- package/dist/core/modules/tenant/core-tenant.guard.js +28 -12
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.helpers.d.ts +1 -0
- package/dist/core/modules/tenant/core-tenant.helpers.js +19 -0
- package/dist/core/modules/tenant/core-tenant.helpers.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.module.d.ts +5 -2
- package/dist/core/modules/tenant/core-tenant.module.js +8 -0
- package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
- package/dist/core/modules/user/core-user.service.js +7 -0
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core.module.js +6 -0
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +49 -1
- package/docs/security-overrides.md +21 -13
- package/migration-guides/11.41.3-to-11.41.4.md +172 -0
- package/migration-guides/11.41.4-to-11.41.5.md +144 -0
- package/package.json +35 -34
- package/src/core/common/helpers/graceful-shutdown.helper.ts +9 -0
- package/src/core/common/helpers/logging.helper.ts +7 -0
- package/src/core/common/helpers/process-diagnostics.helper.ts +4 -0
- package/src/core/common/interfaces/server-options.interface.ts +122 -6
- package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +17 -10
- package/src/core/modules/ai/README.md +9 -1
- package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +74 -7
- package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +13 -1
- package/src/core/modules/api-token/INTEGRATION-CHECKLIST.md +121 -0
- package/src/core/modules/api-token/README.md +212 -0
- package/src/core/modules/api-token/core-api-token.constants.ts +27 -0
- package/src/core/modules/api-token/core-api-token.decorators.ts +29 -0
- package/src/core/modules/api-token/core-api-token.helpers.ts +711 -0
- package/src/core/modules/api-token/core-api-token.middleware.ts +57 -0
- package/src/core/modules/api-token/core-api-token.model.ts +193 -0
- package/src/core/modules/api-token/core-api-token.module.ts +48 -0
- package/src/core/modules/api-token/core-api-token.registry.ts +53 -0
- package/src/core/modules/api-token/core-api-token.service.ts +822 -0
- package/src/core/modules/auth/guards/roles.guard.ts +23 -2
- package/src/core/modules/better-auth/better-auth-roles.guard.ts +18 -4
- package/src/core/modules/better-auth/core-better-auth.middleware.ts +8 -0
- package/src/core/modules/better-auth/core-better-auth.module.ts +33 -0
- package/src/core/modules/migrate/README.md +15 -7
- package/src/core/modules/migrate/migration-runner.ts +9 -0
- package/src/core/modules/tenant/README.md +17 -0
- package/src/core/modules/tenant/core-tenant-guard.registry.ts +36 -0
- package/src/core/modules/tenant/core-tenant.guard.ts +52 -12
- package/src/core/modules/tenant/core-tenant.helpers.ts +30 -0
- package/src/core/modules/tenant/core-tenant.module.ts +19 -2
- package/src/core/modules/user/core-user.service.ts +14 -0
- package/src/core.module.ts +12 -0
- package/src/index.ts +12 -0
|
@@ -14,11 +14,13 @@ import { resolveGuardRequest } from '../../../common/helpers/execution-context-r
|
|
|
14
14
|
import { firstValueFrom, isObservable } from 'rxjs';
|
|
15
15
|
|
|
16
16
|
import { RoleEnum } from '../../../common/enums/role.enum';
|
|
17
|
+
import { ApiTokenKind } from '../../api-token/core-api-token.constants';
|
|
18
|
+
import { enforceApiTokenRoute } from '../../api-token/core-api-token.helpers';
|
|
17
19
|
import { BetterAuthTokenService } from '../../better-auth/better-auth-token.service';
|
|
18
20
|
import { BetterAuthenticatedUser } from '../../better-auth/better-auth.types';
|
|
19
21
|
import { CoreBetterAuthService } from '../../better-auth/core-better-auth.service';
|
|
20
22
|
import { ErrorCode } from '../../error-code';
|
|
21
|
-
import {
|
|
23
|
+
import { delegatesRolesToTenantGuard, isSystemRole, mergeRolesMetadata } from '../../tenant/core-tenant.helpers';
|
|
22
24
|
import { AuthGuardStrategy } from '../auth-guard-strategy.enum';
|
|
23
25
|
import { ExpiredTokenException } from '../exceptions/expired-token.exception';
|
|
24
26
|
import { InvalidTokenException } from '../exceptions/invalid-token.exception';
|
|
@@ -145,6 +147,25 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
|
|
|
145
147
|
throw new ForbiddenException(ErrorCode.ACCESS_DENIED);
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
// API tokens are decided BEFORE the public-route shortcut below: a token is denied on every route
|
|
151
|
+
// that does not declare @ApiTokenScopes(), public ones included (see core-api-token.helpers).
|
|
152
|
+
// A tenant token is fully decided there. A user token was authenticated by CoreApiTokenMiddleware,
|
|
153
|
+
// so Passport — which only knows JWTs — must not see it: its user goes straight to the role checks.
|
|
154
|
+
const apiTokenKind = enforceApiTokenRoute({
|
|
155
|
+
controllerClass: context.getClass(),
|
|
156
|
+
handler: context.getHandler(),
|
|
157
|
+
request: this.getRequest(context),
|
|
158
|
+
});
|
|
159
|
+
if (apiTokenKind === ApiTokenKind.TENANT) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (apiTokenKind === ApiTokenKind.USER) {
|
|
163
|
+
if (roles.some((value) => !!value) && !roles.includes(RoleEnum.S_EVERYONE)) {
|
|
164
|
+
this.handleRequest(null, this.getRequest(context).user, null, context);
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
148
169
|
// If no roles required, or S_EVERYONE is set, allow access without authentication
|
|
149
170
|
// This allows public endpoints (without @Roles decorator or with S_EVERYONE) to work
|
|
150
171
|
//
|
|
@@ -323,7 +344,7 @@ export class RolesGuard extends AuthGuard(AuthGuardStrategy.JWT) {
|
|
|
323
344
|
// When multiTenancy active: pass through ALL non-system roles to CoreTenantGuard.
|
|
324
345
|
// CoreTenantGuard handles hierarchy (level) and non-hierarchy (exact) checks
|
|
325
346
|
// against membership.role (tenant) or user.roles (no tenant).
|
|
326
|
-
if (user &&
|
|
347
|
+
if (user && delegatesRolesToTenantGuard() && roles.some((r) => !isSystemRole(r))) {
|
|
327
348
|
return user;
|
|
328
349
|
}
|
|
329
350
|
|
|
@@ -11,8 +11,10 @@ import { GqlExecutionContext } from '@nestjs/graphql';
|
|
|
11
11
|
import { resolveGuardRequest } from '../../common/helpers/execution-context-request.helper';
|
|
12
12
|
|
|
13
13
|
import { RoleEnum } from '../../common/enums/role.enum';
|
|
14
|
+
import { ApiTokenKind } from '../api-token/core-api-token.constants';
|
|
15
|
+
import { enforceApiTokenRoute } from '../api-token/core-api-token.helpers';
|
|
14
16
|
import { ErrorCode } from '../error-code';
|
|
15
|
-
import {
|
|
17
|
+
import { delegatesRolesToTenantGuard, isSystemRole, mergeRolesMetadata } from '../tenant/core-tenant.helpers';
|
|
16
18
|
import { BetterAuthTokenService } from './better-auth-token.service';
|
|
17
19
|
import { BetterAuthenticatedUser } from './better-auth.types';
|
|
18
20
|
import { getBetterAuthTokenService } from './core-better-auth.registry';
|
|
@@ -97,13 +99,25 @@ export class BetterAuthRolesGuard implements CanActivate {
|
|
|
97
99
|
throw new ForbiddenException(ErrorCode.ACCESS_DENIED);
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
// API tokens are decided BEFORE the public-route shortcut below: a token is denied on every route
|
|
103
|
+
// that does not declare @ApiTokenScopes(), public ones included. A tenant token is fully decided
|
|
104
|
+
// there; a user token continues through the ordinary checks as its user (see core-api-token.helpers).
|
|
105
|
+
const request = this.getRequest(context);
|
|
106
|
+
const apiTokenKind = enforceApiTokenRoute({
|
|
107
|
+
controllerClass: context.getClass(),
|
|
108
|
+
handler: context.getHandler(),
|
|
109
|
+
request,
|
|
110
|
+
});
|
|
111
|
+
if (apiTokenKind === ApiTokenKind.TENANT) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
100
115
|
// If no roles required, or S_EVERYONE is set, allow access without authentication
|
|
101
116
|
if (!roles || !roles.some((value) => !!value) || roles.includes(RoleEnum.S_EVERYONE)) {
|
|
102
117
|
return true;
|
|
103
118
|
}
|
|
104
119
|
|
|
105
|
-
//
|
|
106
|
-
const request = this.getRequest(context);
|
|
120
|
+
// Check for user (set by BetterAuth middleware or CoreApiTokenMiddleware)
|
|
107
121
|
let user = request?.user;
|
|
108
122
|
|
|
109
123
|
// If user isn't set (e.g., middleware didn't run in test environment),
|
|
@@ -129,7 +143,7 @@ export class BetterAuthRolesGuard implements CanActivate {
|
|
|
129
143
|
// When multiTenancy active: pass through ALL non-system roles to CoreTenantGuard.
|
|
130
144
|
// CoreTenantGuard handles hierarchy (level) and non-hierarchy (exact) checks
|
|
131
145
|
// against membership.role (tenant) or user.roles (no tenant).
|
|
132
|
-
if (
|
|
146
|
+
if (delegatesRolesToTenantGuard() && roles.some((r) => !isSystemRole(r))) {
|
|
133
147
|
return true;
|
|
134
148
|
}
|
|
135
149
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
|
2
2
|
import { NextFunction, Request, Response } from 'express';
|
|
3
3
|
|
|
4
|
+
import { hasApiTokenCredential } from '../api-token/core-api-token.helpers';
|
|
4
5
|
import { isLegacyJwt } from './core-better-auth-token.helper';
|
|
5
6
|
import { BetterAuthSessionUser, CoreBetterAuthUserMapper, MappedUser } from './core-better-auth-user.mapper';
|
|
6
7
|
import { convertExpressHeaders, extractSessionToken } from './core-better-auth-web.helper';
|
|
@@ -54,6 +55,13 @@ export class CoreBetterAuthMiddleware implements NestMiddleware {
|
|
|
54
55
|
return next();
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
// Leave API tokens to CoreApiTokenMiddleware (recognised by their configured prefix, `apiTokens`).
|
|
59
|
+
// Not even the cookie fallback may run: a request that presents a token is a token request, and a
|
|
60
|
+
// session cookie riding along must not turn it into a session request with the user's full rights.
|
|
61
|
+
if (hasApiTokenCredential(req)) {
|
|
62
|
+
return next();
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
// Strategy 1: Try Authorization header (Bearer token) - takes precedence
|
|
58
66
|
// The Authorization header is explicitly set by the client, so it should
|
|
59
67
|
// override cookies which are implicitly sent by the browser.
|
|
@@ -34,6 +34,7 @@ import { CoreBetterAuthChallengeService } from './core-better-auth-challenge.ser
|
|
|
34
34
|
import { CoreBetterAuthEmailVerificationService } from './core-better-auth-email-verification.service';
|
|
35
35
|
import { CoreBetterAuthRateLimitMiddleware } from './core-better-auth-rate-limit.middleware';
|
|
36
36
|
import { CoreBetterAuthRateLimiter } from './core-better-auth-rate-limiter.service';
|
|
37
|
+
import { revokeApiTokensOfUser } from '../api-token/core-api-token.registry';
|
|
37
38
|
import { getInFlightResetPassword } from './core-better-auth-password-reset.registry';
|
|
38
39
|
import { CoreBetterAuthSignUpValidatorService } from './core-better-auth-signup-validator.service';
|
|
39
40
|
import { CoreBetterAuthUserMapper } from './core-better-auth-user.mapper';
|
|
@@ -825,6 +826,33 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
825
826
|
* They access the service via static reference since Better-Auth hooks run outside DI context.
|
|
826
827
|
* @internal
|
|
827
828
|
*/
|
|
829
|
+
/**
|
|
830
|
+
* Revoke the user's API tokens after an IAM password reset — only when the reset also ends the
|
|
831
|
+
* user's sessions (`betterAuth.emailAndPassword.revokeSessionsOnPasswordReset`). A no-op without
|
|
832
|
+
* `apiTokens`. Never fails the reset: the credential is already written, so a failure is reported,
|
|
833
|
+
* not thrown. `protected` so a project can decide differently.
|
|
834
|
+
*/
|
|
835
|
+
protected static async revokeApiTokensAfterPasswordReset(
|
|
836
|
+
user: { email?: string; id?: string } | undefined,
|
|
837
|
+
): Promise<void> {
|
|
838
|
+
const revokeSessions =
|
|
839
|
+
this.configServiceInstance?.getFastButReadOnly('betterAuth.emailAndPassword.revokeSessionsOnPasswordReset') ===
|
|
840
|
+
true;
|
|
841
|
+
if (!revokeSessions || (!user?.email && !user?.id)) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
try {
|
|
845
|
+
const revoked = await revokeApiTokensOfUser({ email: user.email, iamId: user.id });
|
|
846
|
+
if (revoked) {
|
|
847
|
+
this.logger.log(`Revoked ${revoked} API token(s) after the password reset of ${maskEmail(user.email ?? '')}`);
|
|
848
|
+
}
|
|
849
|
+
} catch (error) {
|
|
850
|
+
this.logger.error(
|
|
851
|
+
`Could not revoke the API tokens after a password reset: ${error instanceof Error ? error.message : String(error)}`,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
828
856
|
/**
|
|
829
857
|
* `protected` rather than `private`: the callbacks it returns include `onPasswordReset`, which
|
|
830
858
|
* decides what mirroring a reset into the legacy store means for a deployment. A project with a
|
|
@@ -860,6 +888,11 @@ export class CoreBetterAuthModule implements NestModule, OnModuleInit {
|
|
|
860
888
|
}
|
|
861
889
|
},
|
|
862
890
|
onPasswordReset: async ({ user }) => {
|
|
891
|
+
// A reset that ends the user's sessions ends their API tokens too — a user token is a session
|
|
892
|
+
// in all but lifetime, and left alive it keeps the credential the reset was meant to retire
|
|
893
|
+
// working. Before the legacy mirror below, which returns early on IAM-only deployments.
|
|
894
|
+
await this.revokeApiTokensAfterPasswordReset(user);
|
|
895
|
+
|
|
863
896
|
// Mirror the new password into the legacy bcrypt store, so a deployment running
|
|
864
897
|
// Legacy Auth next to IAM does not keep the OLD password valid on the legacy path
|
|
865
898
|
// after a reset — including a reset performed BECAUSE the old one leaked.
|
|
@@ -240,9 +240,12 @@ How the lock works, regardless of which entry point uses it:
|
|
|
240
240
|
|
|
241
241
|
This ensures that in a cluster with multiple nodes, migrations run on only one machine at a time.
|
|
242
242
|
|
|
243
|
-
**Active by default for `migrate up`.** Stores built by `createMigrationStore()` use the
|
|
244
|
-
lock collection `migrations_lock` unless another name is given, and `MigrationRunner.up()`
|
|
245
|
-
(the CLI's `up`
|
|
243
|
+
**Active by default for `migrate up` and `migrate down`.** Stores built by `createMigrationStore()` use the
|
|
244
|
+
lock collection `migrations_lock` unless another name is given, and `MigrationRunner.up()` / `.down()`
|
|
245
|
+
(the CLI's `up` / `down` commands) acquire that lock around the whole run. `down` joined later (DEV-2728): a
|
|
246
|
+
rollback is typically run while a deploy is failing, i.e. while replicas restart and each boots into
|
|
247
|
+
`migrate up` — outside the lock the two rewrote the migration state concurrently. `migrate list` only
|
|
248
|
+
reads and stays unlocked. This matters because the
|
|
246
249
|
container entrypoint runs migrations on **every** boot: without the lock, N replicas
|
|
247
250
|
starting together each read the same empty state and apply the same pending migration N
|
|
248
251
|
times. A replica that waited re-reads the state inside the lock and finds nothing pending.
|
|
@@ -530,10 +533,15 @@ Enable it via any of:
|
|
|
530
533
|
| `NSC__MIGRATE__STRICT=1\|true\|yes` env var | CLI **and** programmatic runners (resolved in the `MigrationRunner` constructor) |
|
|
531
534
|
| `new MigrationRunner({ strict: true, ... })` | programmatic |
|
|
532
535
|
|
|
533
|
-
**
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
536
|
+
**Off by default, on only when a project explicitly wants it.** Deleting migration files that
|
|
537
|
+
ran everywhere is a normal practice — a new instance never needs them, and git history restores
|
|
538
|
+
them if ever needed — and under strict mode every such deletion would refuse the boot. Turn it on
|
|
539
|
+
for images that must carry the FULL migration history. Where the history is complete by
|
|
540
|
+
design, a recorded-but-missing file can only mean a broken build (empty/miscopied
|
|
541
|
+
`migrations/` directory) or a state-store mismatch (wrong database), and refusing to boot is
|
|
542
|
+
correct. This is independent of the entrypoint's failure policy: a migration that RAN AND
|
|
543
|
+
FAILED aborts the container start by default (`MIGRATIONS_ALLOW_FAILURE=true` opts out per
|
|
544
|
+
deploy), whatever `strict` says.
|
|
537
545
|
|
|
538
546
|
### Programmatic usage (MigrationRunner)
|
|
539
547
|
|
|
@@ -297,8 +297,17 @@ export class MigrationRunner {
|
|
|
297
297
|
|
|
298
298
|
/**
|
|
299
299
|
* Rollback the last migration (down)
|
|
300
|
+
*
|
|
301
|
+
* Serialized through the same lock as `up()`. A rollback is typically run while a deploy
|
|
302
|
+
* is failing, i.e. exactly while replicas restart and each boots into `migrate up`; outside
|
|
303
|
+
* the lock the two would read and rewrite the migration state concurrently, and one of them
|
|
304
|
+
* would save a state that no longer matches the database.
|
|
300
305
|
*/
|
|
301
306
|
async down(): Promise<void> {
|
|
307
|
+
await withMigrationLock(this.options.stateStore, () => this.runDown());
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
protected async runDown(): Promise<void> {
|
|
302
311
|
const { _endMigration, _startMigration } = await import('./helpers/migration.helper');
|
|
303
312
|
|
|
304
313
|
const state = await this.options.stateStore.loadAsync();
|
|
@@ -357,6 +357,23 @@ betterAuth: {
|
|
|
357
357
|
When `skipTenantCheck: false`, IAM endpoints will require a valid `X-Tenant-Id` header
|
|
358
358
|
and the user must be a member of that tenant for protected endpoints.
|
|
359
359
|
|
|
360
|
+
## API Tokens
|
|
361
|
+
|
|
362
|
+
`apiTokens` (see `src/core/modules/api-token/README.md`) adds bearer tokens that respect every rule in
|
|
363
|
+
this module:
|
|
364
|
+
|
|
365
|
+
- A **TENANT token** belongs to one tenant, is managed by members holding `apiTokens.manageRole`, and
|
|
366
|
+
acts with the LOWEST role of `roleHierarchy` inside that tenant only — an `X-Tenant-Id` naming another
|
|
367
|
+
tenant is refused, and `@SkipTenantCheck()` does not unbind it. The boot refuses a hierarchy in which
|
|
368
|
+
that lowest role would reach the manage role.
|
|
369
|
+
- A **USER token** acts as its user through the ordinary membership checks of `CoreTenantGuard`, with
|
|
370
|
+
global roles removed (no admin bypass), optionally restricted to one tenant and capped by
|
|
371
|
+
`maxTenantRole`.
|
|
372
|
+
|
|
373
|
+
Both are denied on routes without `@ApiTokenScopes()`. **Call `CoreApiTokenService.deleteAllForTenant()`
|
|
374
|
+
when you delete a tenant — nothing does it for you**: the core has no tenant model to notice the
|
|
375
|
+
deletion, and a tenant token does not depend on any member, so it keeps authenticating until then.
|
|
376
|
+
|
|
360
377
|
## Related
|
|
361
378
|
|
|
362
379
|
- [Integration Checklist](./INTEGRATION-CHECKLIST.md)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a tenant guard is actually REGISTERED in this process — as opposed to merely configured.
|
|
3
|
+
*
|
|
4
|
+
* `RolesGuard` and `BetterAuthRolesGuard` hand every non-system role over to the tenant guard when
|
|
5
|
+
* multi-tenancy is on, because that guard resolves them against the membership. Deciding that from
|
|
6
|
+
* CONFIGURATION alone meant: whenever configuration and registration were out of step, a role such
|
|
7
|
+
* as `RoleEnum.ADMIN` was handed to a guard that never ran, and every authenticated caller passed.
|
|
8
|
+
* The role guards therefore delegate only while this registry reports an active tenant guard.
|
|
9
|
+
*
|
|
10
|
+
* Counted, not flagged: `CoreTenantModule` registers (so a project's own guard class counts too), and
|
|
11
|
+
* so does `CoreTenantGuard` itself (for setups that list it as an APP_GUARD directly). Each disposer
|
|
12
|
+
* releases exactly its own registration.
|
|
13
|
+
*
|
|
14
|
+
* A true leaf: no imports (see `.claude/rules/architecture.md` → "DI Token Placement (SWC-Safe)").
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
let activeTenantGuards = 0;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Record an active tenant guard. Returns a disposer that releases this registration exactly once.
|
|
21
|
+
*/
|
|
22
|
+
export function registerActiveTenantGuard(): () => void {
|
|
23
|
+
activeTenantGuards++;
|
|
24
|
+
let released = false;
|
|
25
|
+
return () => {
|
|
26
|
+
if (!released) {
|
|
27
|
+
released = true;
|
|
28
|
+
activeTenantGuards = Math.max(0, activeTenantGuards - 1);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Is at least one tenant guard registered in this process? */
|
|
34
|
+
export function hasActiveTenantGuard(): boolean {
|
|
35
|
+
return activeTenantGuards > 0;
|
|
36
|
+
}
|
|
@@ -19,8 +19,16 @@ import { resolveGuardRequest } from '../../common/helpers/execution-context-requ
|
|
|
19
19
|
import { ConfigService } from '../../common/services/config.service';
|
|
20
20
|
import { ResolvedTenantContext, setTenantContextResolver } from '../../common/services/core-tenant-context.registry';
|
|
21
21
|
import { CoreRedisService } from '../../common/services/core-redis.service';
|
|
22
|
+
import { ApiTokenKind } from '../api-token/core-api-token.constants';
|
|
23
|
+
import {
|
|
24
|
+
capApiTokenTenantRole,
|
|
25
|
+
enforceApiTokenRoute,
|
|
26
|
+
getApiTokenContext,
|
|
27
|
+
getApiTokenTenantRestriction,
|
|
28
|
+
} from '../api-token/core-api-token.helpers';
|
|
22
29
|
import { ErrorCode } from '../error-code/error-codes';
|
|
23
30
|
import { CoreTenantMemberModel } from './core-tenant-member.model';
|
|
31
|
+
import { registerActiveTenantGuard } from './core-tenant-guard.registry';
|
|
24
32
|
import { SKIP_TENANT_CHECK_KEY } from './core-tenant.decorators';
|
|
25
33
|
import { TENANT_MEMBER_MODEL_TOKEN, TenantMemberStatus } from './core-tenant.enums';
|
|
26
34
|
import {
|
|
@@ -165,8 +173,14 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
|
|
|
165
173
|
setTenantContextResolver({
|
|
166
174
|
resolve: (user, headerTenantId) => this.resolveTenantContext(user, headerTenantId),
|
|
167
175
|
});
|
|
176
|
+
|
|
177
|
+
// Lets the role guards delegate non-system roles to this guard — only while it exists.
|
|
178
|
+
this.releaseTenantGuard = registerActiveTenantGuard();
|
|
168
179
|
}
|
|
169
180
|
|
|
181
|
+
/** Releases this guard's registration (see core-tenant-guard.registry.ts). */
|
|
182
|
+
private releaseTenantGuard?: () => void;
|
|
183
|
+
|
|
170
184
|
/**
|
|
171
185
|
* Answer "which tenant is this caller in?" WITHOUT an Express request.
|
|
172
186
|
*
|
|
@@ -253,6 +267,8 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
|
|
|
253
267
|
}
|
|
254
268
|
|
|
255
269
|
onModuleDestroy(): void {
|
|
270
|
+
this.releaseTenantGuard?.();
|
|
271
|
+
|
|
256
272
|
if (this.invalidationListener) {
|
|
257
273
|
try {
|
|
258
274
|
const subscriber = this.redisService?.getSubscriber();
|
|
@@ -324,11 +340,27 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
|
|
|
324
340
|
return true;
|
|
325
341
|
}
|
|
326
342
|
|
|
343
|
+
// API tokens (see core-api-token.helpers): a TENANT token is fully decided there — scopes, roles,
|
|
344
|
+
// binding to its own tenant. A USER token passed its scope check and continues below as its user,
|
|
345
|
+
// through the same membership validation as a session; only its restrictions are added on top.
|
|
346
|
+
const apiTokenKind = enforceApiTokenRoute({
|
|
347
|
+
controllerClass: context.getClass(),
|
|
348
|
+
handler: context.getHandler(),
|
|
349
|
+
request,
|
|
350
|
+
});
|
|
351
|
+
if (apiTokenKind === ApiTokenKind.TENANT) {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
// A user token restricted to one tenant is bound to it like a header naming it — the helper has
|
|
355
|
+
// already refused a header naming any other tenant.
|
|
356
|
+
const apiTokenTenant = getApiTokenTenantRestriction(request.user);
|
|
357
|
+
|
|
327
358
|
// Parse tenant header
|
|
328
359
|
const headerName = (config.headerName ?? 'x-tenant-id').toLowerCase();
|
|
329
360
|
const rawHeader = request.headers?.[headerName] as string | undefined;
|
|
330
361
|
const headerTenantId =
|
|
331
|
-
|
|
362
|
+
apiTokenTenant ??
|
|
363
|
+
(rawHeader && typeof rawHeader === 'string' && rawHeader.length <= 128 ? rawHeader.trim() : undefined);
|
|
332
364
|
|
|
333
365
|
// Two role sets for different purposes:
|
|
334
366
|
//
|
|
@@ -367,7 +399,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
|
|
|
367
399
|
const membership = await this.findMembershipCached(request.user.id, headerTenantId);
|
|
368
400
|
if (membership) {
|
|
369
401
|
request.tenantId = headerTenantId;
|
|
370
|
-
request.tenantRole = membership.role as string;
|
|
402
|
+
request.tenantRole = capApiTokenTenantRole(request.user, membership.role as string) ?? undefined;
|
|
371
403
|
}
|
|
372
404
|
}
|
|
373
405
|
return true;
|
|
@@ -380,10 +412,11 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
|
|
|
380
412
|
// Read @SkipTenantCheck early — it suppresses tenant membership validation for system roles too.
|
|
381
413
|
// When set, S_USER and S_VERIFIED still enforce authentication/verification, but no membership
|
|
382
414
|
// check is performed even when a tenant header is present.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
415
|
+
// A tenant-restricted user token stays bound to its tenant even on @SkipTenantCheck() routes:
|
|
416
|
+
// skipping the check would drop the binding, and the restriction is the whole point of the token.
|
|
417
|
+
const hasSkipDecorator =
|
|
418
|
+
!apiTokenTenant &&
|
|
419
|
+
this.reflector.getAllAndOverride<boolean>(SKIP_TENANT_CHECK_KEY, [context.getHandler(), context.getClass()]);
|
|
387
420
|
|
|
388
421
|
// S_USER check — any authenticated user satisfies this system role.
|
|
389
422
|
//
|
|
@@ -488,7 +521,9 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
|
|
|
488
521
|
throw new ForbiddenException('Not a member of this tenant');
|
|
489
522
|
}
|
|
490
523
|
|
|
491
|
-
|
|
524
|
+
// A user token with maxTenantRole acts with the lower of its cap and the membership role; `null`
|
|
525
|
+
// (a role the hierarchy cannot compare with the cap) satisfies no tenant role at all.
|
|
526
|
+
const memberRole = capApiTokenTenantRole(user, membership.role as string);
|
|
492
527
|
|
|
493
528
|
// Check role access if roles are required (hierarchy + normal, against membership.role).
|
|
494
529
|
//
|
|
@@ -509,7 +544,8 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
|
|
|
509
544
|
// The length guard is NOT redundant here (unlike the `.some()` above): checkRoleAccess
|
|
510
545
|
// returns TRUE for an empty required-roles list, so calling it with no tenant roles would
|
|
511
546
|
// grant access to a handler that only ever required a global role.
|
|
512
|
-
const satisfiedByTenant =
|
|
547
|
+
const satisfiedByTenant =
|
|
548
|
+
tenantRoles.length > 0 && memberRole !== null && checkRoleAccess(tenantRoles, undefined, memberRole);
|
|
513
549
|
|
|
514
550
|
if (!satisfiedGlobally && !satisfiedByTenant) {
|
|
515
551
|
throw new ForbiddenException('Insufficient tenant role');
|
|
@@ -519,7 +555,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
|
|
|
519
555
|
// Set validated tenant context on request (consumed by RequestContextMiddleware
|
|
520
556
|
// lazy getter → context.tenantId / context.tenantRole, and by @CurrentTenant() via RequestContext)
|
|
521
557
|
request.tenantId = headerTenantId;
|
|
522
|
-
request.tenantRole = memberRole;
|
|
558
|
+
request.tenantRole = memberRole ?? undefined;
|
|
523
559
|
return true;
|
|
524
560
|
}
|
|
525
561
|
|
|
@@ -573,7 +609,10 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
|
|
|
573
609
|
}
|
|
574
610
|
|
|
575
611
|
const userId = request.user.id;
|
|
576
|
-
|
|
612
|
+
// A user token's maxTenantRole changes which memberships reach minLevel, so its answer is computed
|
|
613
|
+
// per request instead of shared through the per-user cache.
|
|
614
|
+
const capped = !!getApiTokenContext(request.user)?.maxTenantRole;
|
|
615
|
+
const ttl = capped ? 0 : this.cacheTtlMs;
|
|
577
616
|
|
|
578
617
|
// When cache is enabled, check process-level cache
|
|
579
618
|
if (ttl > 0) {
|
|
@@ -600,7 +639,8 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
|
|
|
600
639
|
const hierarchy = getRoleHierarchy();
|
|
601
640
|
ids = memberships
|
|
602
641
|
.filter((m) => {
|
|
603
|
-
const
|
|
642
|
+
const role = capApiTokenTenantRole(request.user, m.role as string);
|
|
643
|
+
const level = role === null ? 0 : (hierarchy[role] ?? 0);
|
|
604
644
|
return level >= minLevel;
|
|
605
645
|
})
|
|
606
646
|
.map((m) => m.tenant as string);
|
|
@@ -808,7 +848,7 @@ export class CoreTenantGuard implements CanActivate, OnApplicationBootstrap, OnM
|
|
|
808
848
|
throw new ForbiddenException('Not a member of this tenant');
|
|
809
849
|
}
|
|
810
850
|
request.tenantId = headerTenantId;
|
|
811
|
-
request.tenantRole = membership.role as string;
|
|
851
|
+
request.tenantRole = capApiTokenTenantRole(user, membership.role as string) ?? undefined;
|
|
812
852
|
return true;
|
|
813
853
|
}
|
|
814
854
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { Logger } from '@nestjs/common';
|
|
2
|
+
|
|
1
3
|
import { isForbiddenMembershipRole, isGlobalOnlyRole, looksLikeSystemRole } from '../../common/enums/role.enum';
|
|
2
4
|
import { ConfigService } from '../../common/services/config.service';
|
|
3
5
|
import { RoleScope, roleScopeRegistry, RoleScopeSource } from './core-role-scope.registry';
|
|
6
|
+
import { hasActiveTenantGuard } from './core-tenant-guard.registry';
|
|
4
7
|
import { DEFAULT_ROLE_HIERARCHY } from './core-tenant.enums';
|
|
5
8
|
|
|
6
9
|
/**
|
|
@@ -172,6 +175,33 @@ export function isMultiTenancyActive(): boolean {
|
|
|
172
175
|
return !!config && config.enabled !== false;
|
|
173
176
|
}
|
|
174
177
|
|
|
178
|
+
let warnedAboutMissingTenantGuard = false;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Should a role guard hand non-system roles over to the tenant guard?
|
|
182
|
+
*
|
|
183
|
+
* Only when multi-tenancy is on AND a tenant guard is actually registered. Configured-but-absent is
|
|
184
|
+
* not "someone else checks it" — it is "nobody checks it", so the role guard then resolves the roles
|
|
185
|
+
* itself against `user.roles` (fail-closed) and says so once.
|
|
186
|
+
*/
|
|
187
|
+
export function delegatesRolesToTenantGuard(): boolean {
|
|
188
|
+
if (!isMultiTenancyActive()) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (hasActiveTenantGuard()) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
if (!warnedAboutMissingTenantGuard) {
|
|
195
|
+
warnedAboutMissingTenantGuard = true;
|
|
196
|
+
new Logger('CoreTenantModule').warn(
|
|
197
|
+
'multiTenancy is configured but no tenant guard is registered — the role guards check non-system roles ' +
|
|
198
|
+
'against user.roles themselves instead of delegating them. Register CoreTenantModule (CoreModule does so ' +
|
|
199
|
+
'automatically when multiTenancy is set).',
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
175
205
|
/**
|
|
176
206
|
* Check if a role is a hierarchy role (present in the configured role hierarchy).
|
|
177
207
|
* Returns false when multiTenancy is disabled to avoid false positives.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { CanActivate, DynamicModule, Global, Module, Type } from '@nestjs/common';
|
|
1
|
+
import { CanActivate, DynamicModule, Global, Module, OnModuleDestroy, OnModuleInit, Type } from '@nestjs/common';
|
|
2
2
|
import { APP_GUARD } from '@nestjs/core';
|
|
3
3
|
import { MongooseModule, SchemaFactory, getModelToken } from '@nestjs/mongoose';
|
|
4
4
|
import { Model } from 'mongoose';
|
|
5
5
|
|
|
6
6
|
import { roleScopeRegistry } from './core-role-scope.registry';
|
|
7
|
+
import { registerActiveTenantGuard } from './core-tenant-guard.registry';
|
|
7
8
|
import { CoreTenantMemberModel } from './core-tenant-member.model';
|
|
8
9
|
import { TENANT_MEMBER_MODEL_TOKEN } from './core-tenant.enums';
|
|
9
10
|
import { assertRoleVocabularyIsCoherent, configRoleScopeSource } from './core-tenant.helpers';
|
|
@@ -58,7 +59,23 @@ export interface CoreTenantModuleOptions {
|
|
|
58
59
|
*/
|
|
59
60
|
@Global()
|
|
60
61
|
@Module({})
|
|
61
|
-
export class CoreTenantModule {
|
|
62
|
+
export class CoreTenantModule implements OnModuleDestroy, OnModuleInit {
|
|
63
|
+
/** Releases this module's tenant-guard registration (see core-tenant-guard.registry.ts). */
|
|
64
|
+
private releaseTenantGuard?: () => void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Tell the role guards that a tenant guard really runs, so they may hand non-system roles to it.
|
|
68
|
+
* Registered by the MODULE, not only by CoreTenantGuard, because `forRoot({ guard })` may register
|
|
69
|
+
* a project's own guard class instead.
|
|
70
|
+
*/
|
|
71
|
+
onModuleInit(): void {
|
|
72
|
+
this.releaseTenantGuard = registerActiveTenantGuard();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onModuleDestroy(): void {
|
|
76
|
+
this.releaseTenantGuard?.();
|
|
77
|
+
}
|
|
78
|
+
|
|
62
79
|
static forRoot(options: CoreTenantModuleOptions = {}): DynamicModule {
|
|
63
80
|
// Teach the role-scope registry which roles are global and which are tenant-scoped, then
|
|
64
81
|
// refuse to boot on a vocabulary that cannot be enforced coherently (a tenant role named after
|
|
@@ -13,6 +13,7 @@ import { ConfigService } from '../../common/services/config.service';
|
|
|
13
13
|
import { CrudService } from '../../common/services/crud.service';
|
|
14
14
|
import { ErrorCode } from '../error-code/error-codes';
|
|
15
15
|
import { EmailService } from '../../common/services/email.service';
|
|
16
|
+
import { revokeApiTokensOfUser } from '../api-token/core-api-token.registry';
|
|
16
17
|
import { CoreModelConstructor } from '../../common/types/core-model-constructor.type';
|
|
17
18
|
import { CoreUserModel } from './core-user.model';
|
|
18
19
|
import { CoreUserCreateInput } from './inputs/core-user-create.input';
|
|
@@ -362,6 +363,19 @@ export abstract class CoreUserService<
|
|
|
362
363
|
}
|
|
363
364
|
}
|
|
364
365
|
|
|
366
|
+
// The legacy reset ends every legacy session above (refreshTokens), so it ends the user's
|
|
367
|
+
// API tokens too — left alive, they keep the credential the reset was meant to retire
|
|
368
|
+
// working. A no-op without `apiTokens`; never fails the reset.
|
|
369
|
+
try {
|
|
370
|
+
await revokeApiTokensOfUser({ email: dbObject.email, userId: dbObject.id });
|
|
371
|
+
} catch (error) {
|
|
372
|
+
this.userServiceLogger.error(
|
|
373
|
+
`Could not revoke the API tokens after the password reset of ${maskEmail(dbObject.email)}: ${
|
|
374
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
375
|
+
}`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
365
379
|
return updatedUser;
|
|
366
380
|
},
|
|
367
381
|
{ dbObject, serviceOptions },
|
package/src/core.module.ts
CHANGED
|
@@ -59,6 +59,7 @@ import { CoreHubModule } from './core/modules/hub/core-hub.module';
|
|
|
59
59
|
import { isHubEnabled, isHubQueriesEnabled } from './core/modules/hub/hub-config.helper';
|
|
60
60
|
import { CorePermissionsModule } from './core/modules/permissions/core-permissions.module';
|
|
61
61
|
import { CoreSystemSetupModule } from './core/modules/system-setup/core-system-setup.module';
|
|
62
|
+
import { CoreApiTokenModule } from './core/modules/api-token/core-api-token.module';
|
|
62
63
|
import { CoreTenantModule } from './core/modules/tenant/core-tenant.module';
|
|
63
64
|
|
|
64
65
|
/**
|
|
@@ -579,6 +580,17 @@ export class CoreModule implements NestModule {
|
|
|
579
580
|
imports.push(CoreTenantModule.forRoot({ modelName: membershipModelName }));
|
|
580
581
|
}
|
|
581
582
|
|
|
583
|
+
// Add CoreApiTokenModule when apiTokens is configured (boolean shorthand, presence implies enabled).
|
|
584
|
+
// Independent of multiTenancy: user tokens work without it; tenant tokens and tenant restrictions
|
|
585
|
+
// switch on by themselves when CoreTenantModule is registered as well.
|
|
586
|
+
const apiTokensConfig = config.apiTokens;
|
|
587
|
+
if (
|
|
588
|
+
apiTokensConfig === true ||
|
|
589
|
+
(typeof apiTokensConfig === 'object' && apiTokensConfig !== null && apiTokensConfig.enabled !== false)
|
|
590
|
+
) {
|
|
591
|
+
imports.push(CoreApiTokenModule.forRoot({ ...overrides?.apiToken }));
|
|
592
|
+
}
|
|
593
|
+
|
|
582
594
|
// Set exports
|
|
583
595
|
const exports: any[] = [
|
|
584
596
|
ConfigService,
|
package/src/index.ts
CHANGED
|
@@ -125,6 +125,18 @@ export * from './core/common/types/required-at-least-one.type';
|
|
|
125
125
|
export * from './core/common/types/string-or-object-id.type';
|
|
126
126
|
export * from './core/common/types/wrapper.type';
|
|
127
127
|
|
|
128
|
+
// =====================================================================================================================
|
|
129
|
+
// Core - Modules - API tokens
|
|
130
|
+
// =====================================================================================================================
|
|
131
|
+
|
|
132
|
+
export * from './core/modules/api-token/core-api-token.constants';
|
|
133
|
+
export * from './core/modules/api-token/core-api-token.decorators';
|
|
134
|
+
export * from './core/modules/api-token/core-api-token.helpers';
|
|
135
|
+
export * from './core/modules/api-token/core-api-token.middleware';
|
|
136
|
+
export * from './core/modules/api-token/core-api-token.model';
|
|
137
|
+
export * from './core/modules/api-token/core-api-token.module';
|
|
138
|
+
export * from './core/modules/api-token/core-api-token.service';
|
|
139
|
+
|
|
128
140
|
// =====================================================================================================================
|
|
129
141
|
// Core - Modules - Auth
|
|
130
142
|
// =====================================================================================================================
|