@rhino-dev/rhino-nestjs 4.0.0 → 4.2.1
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/README.md +52 -1
- package/dist/blueprint/blueprint-runner.js +32 -1
- package/dist/blueprint/blueprint-runner.js.map +1 -1
- package/dist/blueprint/blueprint-sorter.d.ts +54 -0
- package/dist/blueprint/blueprint-sorter.js +107 -0
- package/dist/blueprint/blueprint-sorter.js.map +1 -0
- package/dist/controllers/auth.controller.d.ts +34 -5
- package/dist/controllers/auth.controller.js +132 -12
- package/dist/controllers/auth.controller.js.map +1 -1
- package/dist/controllers/invitation.controller.d.ts +41 -2
- package/dist/controllers/invitation.controller.js +97 -12
- package/dist/controllers/invitation.controller.js.map +1 -1
- package/dist/errors/rhino-exception.d.ts +12 -1
- package/dist/errors/rhino-exception.js +19 -1
- package/dist/errors/rhino-exception.js.map +1 -1
- package/dist/guards/group-membership.guard.d.ts +25 -0
- package/dist/guards/group-membership.guard.js +70 -0
- package/dist/guards/group-membership.guard.js.map +1 -0
- package/dist/guards/jwt-auth.guard.js +23 -4
- package/dist/guards/jwt-auth.guard.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/dist/interfaces/rhino-config.interface.d.ts +71 -0
- package/dist/middleware/domain-route-resolver.d.ts +54 -0
- package/dist/middleware/domain-route-resolver.js +145 -0
- package/dist/middleware/domain-route-resolver.js.map +1 -0
- package/dist/middleware/route-group.middleware.d.ts +48 -4
- package/dist/middleware/route-group.middleware.js +274 -13
- package/dist/middleware/route-group.middleware.js.map +1 -1
- package/dist/rhino.config.d.ts +26 -1
- package/dist/rhino.config.js +56 -1
- package/dist/rhino.config.js.map +1 -1
- package/dist/rhino.module.d.ts +7 -0
- package/dist/rhino.module.js +23 -1
- package/dist/rhino.module.js.map +1 -1
- package/dist/services/auth-hooks.service.d.ts +27 -0
- package/dist/services/auth-hooks.service.js +85 -0
- package/dist/services/auth-hooks.service.js.map +1 -0
- package/dist/services/auth.service.d.ts +19 -0
- package/dist/services/auth.service.js +80 -5
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/invitation.service.d.ts +6 -0
- package/dist/services/invitation.service.js +45 -3
- package/dist/services/invitation.service.js.map +1 -1
- package/dist/services/membership.service.d.ts +56 -0
- package/dist/services/membership.service.js +113 -0
- package/dist/services/membership.service.js.map +1 -0
- package/dist/services/organization.service.js +8 -1
- package/dist/services/organization.service.js.map +1 -1
- package/dist/services/route-registration.service.d.ts +1 -0
- package/dist/services/route-registration.service.js +2 -0
- package/dist/services/route-registration.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/utils/domain-pattern.d.ts +36 -0
- package/dist/utils/domain-pattern.js +77 -0
- package/dist/utils/domain-pattern.js.map +1 -0
- package/dist/utils/permission-matcher.js +8 -0
- package/dist/utils/permission-matcher.js.map +1 -1
- package/dist/utils/route-group-validator.d.ts +14 -0
- package/dist/utils/route-group-validator.js +124 -0
- package/dist/utils/route-group-validator.js.map +1 -0
- package/package.json +1 -1
|
@@ -36,13 +36,74 @@ export interface ModelRegistration {
|
|
|
36
36
|
model: string;
|
|
37
37
|
}>;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Context handed to every lifecycle hook. `routeGroup` is the resolved group
|
|
41
|
+
* name (or `null`/`undefined` for the legacy/global auth path), `organization`
|
|
42
|
+
* is present only for tenant groups, `token` is the just-issued JWT for
|
|
43
|
+
* token-issuing actions (login/register), and `request` is the raw request.
|
|
44
|
+
*/
|
|
45
|
+
export interface AuthHookContext {
|
|
46
|
+
user: any;
|
|
47
|
+
routeGroup?: string | null;
|
|
48
|
+
organization?: any;
|
|
49
|
+
token?: string;
|
|
50
|
+
request?: any;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Per-group lifecycle hooks. Each method runs AFTER the corresponding auth
|
|
54
|
+
* action succeeds. A method may reject by throwing `RhinoAuthRejected` (or any
|
|
55
|
+
* error) — for token-issuing actions the controller revokes the issued token
|
|
56
|
+
* and returns the rejection's status (default 403). All methods are optional;
|
|
57
|
+
* an absent method is a no-op. Implementations are registered per group via
|
|
58
|
+
* `RouteGroupConfig.hooks` and resolved from the Nest DI container.
|
|
59
|
+
*/
|
|
60
|
+
export interface AuthLifecycleHooks {
|
|
61
|
+
afterLogin?(ctx: AuthHookContext): void | Promise<void>;
|
|
62
|
+
afterLogout?(ctx: AuthHookContext): void | Promise<void>;
|
|
63
|
+
afterRegister?(ctx: AuthHookContext): void | Promise<void>;
|
|
64
|
+
afterPasswordRecover?(ctx: AuthHookContext): void | Promise<void>;
|
|
65
|
+
afterPasswordReset?(ctx: AuthHookContext): void | Promise<void>;
|
|
66
|
+
}
|
|
39
67
|
export interface RouteGroupConfig {
|
|
40
68
|
prefix?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Constrain this group to a specific host. Two groups can then share the
|
|
71
|
+
* same URL prefix and be selected by host.
|
|
72
|
+
*
|
|
73
|
+
* - Omitted → the group matches any host (default; backward compatible).
|
|
74
|
+
* - Literal host, e.g. `'admin.example.com'` → requests to that host
|
|
75
|
+
* resolve to this group; requests from a non-matching host are rejected.
|
|
76
|
+
* - Parameterized host, e.g. `'{organization}.example.com'` → the captured
|
|
77
|
+
* `{organization}` subdomain feeds organization resolution, exactly like
|
|
78
|
+
* a path-prefix tenant param. Matches Laravel's `Route::domain(...)`.
|
|
79
|
+
*/
|
|
80
|
+
domain?: string;
|
|
41
81
|
middleware?: Type<NestMiddleware>[];
|
|
42
82
|
/** '*' = all registered models, array = subset by slug */
|
|
43
83
|
models: '*' | string[];
|
|
44
84
|
/** Skip the default JWT guard for this group (for public routes) */
|
|
45
85
|
skipAuth?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Register the full auth route set (login/logout/password/register) for this
|
|
88
|
+
* group, tagged with the group's name (Decision 9.A). The legacy unprefixed
|
|
89
|
+
* `/auth/*` set always remains for the default/global path. Opt-in; default
|
|
90
|
+
* `false`. The `public` group is never auth-enabled.
|
|
91
|
+
*/
|
|
92
|
+
auth?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Optional per-group lifecycle hooks. A class (resolved via Nest DI) or a
|
|
95
|
+
* plain object implementing {@link AuthLifecycleHooks}. Runs after each auth
|
|
96
|
+
* action for requests resolved to this group; may reject to revoke the token.
|
|
97
|
+
*/
|
|
98
|
+
hooks?: Type<AuthLifecycleHooks> | AuthLifecycleHooks;
|
|
99
|
+
/**
|
|
100
|
+
* Whether this group is org-scoped (a tenant group). Tenant-group membership
|
|
101
|
+
* rows require an organization; non-tenant groups (e.g. `admin`, `driver`)
|
|
102
|
+
* store a NULL org. When omitted, a group is treated as a tenant group iff
|
|
103
|
+
* multi-tenancy is enabled. Set `tenant: false` for org-less groups even when
|
|
104
|
+
* multi-tenancy is on.
|
|
105
|
+
*/
|
|
106
|
+
tenant?: boolean;
|
|
46
107
|
}
|
|
47
108
|
export interface MultiTenantConfig {
|
|
48
109
|
enabled?: boolean;
|
|
@@ -66,6 +127,16 @@ export interface AuthConfig {
|
|
|
66
127
|
userModel?: string;
|
|
67
128
|
passwordField?: string;
|
|
68
129
|
emailField?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Master flag (default `false`) gating group-membership enforcement. When
|
|
132
|
+
* off, behavior is byte-for-byte unchanged: no membership check, permissions
|
|
133
|
+
* resolve from the org-presence heuristic. When on, an authenticated user
|
|
134
|
+
* must hold a `user_roles` membership row matching the request's
|
|
135
|
+
* `route_group` (a NULL row is a wildcard — Decision 9.B) and, for tenant
|
|
136
|
+
* groups, the resolved org; no match → 403 (Decision 9.C). Permissions then
|
|
137
|
+
* resolve from the matched row.
|
|
138
|
+
*/
|
|
139
|
+
enforceGroupMembership?: boolean;
|
|
69
140
|
}
|
|
70
141
|
export interface PostmanConfig {
|
|
71
142
|
roleModel?: string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import type { RhinoConfig } from '../interfaces/rhino-config.interface';
|
|
3
|
+
import type { PrismaClientLike } from '../prisma/prisma.service';
|
|
4
|
+
export interface DomainRouteResolverOptions {
|
|
5
|
+
/**
|
|
6
|
+
* When `true` (default), a request whose host matches a parameterized
|
|
7
|
+
* domain group but whose captured subdomain resolves to no known
|
|
8
|
+
* organization → HTTP 404. When `false`, the request passes through
|
|
9
|
+
* unchanged (org simply isn't attached).
|
|
10
|
+
*/
|
|
11
|
+
strict?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* When `true` (default), after resolving the org the resolver verifies the
|
|
14
|
+
* authenticated user (if any) is a member. Non-members → 404 (matches the
|
|
15
|
+
* tenant-route-rewrite contract: cross-tenant = "not found", no
|
|
16
|
+
* enumeration). When `req.user` is absent the check is skipped (auth runs
|
|
17
|
+
* later).
|
|
18
|
+
*/
|
|
19
|
+
enforceMembership?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Factory for an Express-level middleware that implements host-based route
|
|
23
|
+
* group resolution — the subdomain-multitenancy analogue of
|
|
24
|
+
* `createTenantRouteRewrite`.
|
|
25
|
+
*
|
|
26
|
+
* Context: NestJS serves every model through a single GlobalController, and a
|
|
27
|
+
* route group is resolved at request time. For a parameterized domain group
|
|
28
|
+
* (`{organization}.example.com`), the captured subdomain must be turned into a
|
|
29
|
+
* resolved `req.organization` BEFORE the controller runs — exactly like the
|
|
30
|
+
* path-based `/api/<slug>/...` rewrite does for the `:organization` prefix.
|
|
31
|
+
*
|
|
32
|
+
* Behavior:
|
|
33
|
+
*
|
|
34
|
+
* 1. For each route group with a `domain`, compile its pattern.
|
|
35
|
+
* 2. On each request, match `req.hostname` against those patterns.
|
|
36
|
+
* - No domain group matches → next() (plain prefix routing applies).
|
|
37
|
+
* 3. When a parameterized domain group matches, take the captured
|
|
38
|
+
* `{organization}` (or `{org}`) subdomain and:
|
|
39
|
+
* - expose it on `req.params.organization`,
|
|
40
|
+
* - resolve the org by `organizationIdentifierColumn` (cached),
|
|
41
|
+
* strict-404 when unknown,
|
|
42
|
+
* - enforce membership when `req.user` is present,
|
|
43
|
+
* - attach `req.organization`.
|
|
44
|
+
* A literal domain group matches but captures no params, so it only marks
|
|
45
|
+
* the host as recognized (no org resolution).
|
|
46
|
+
*
|
|
47
|
+
* Intended for raw Express `app.use(createDomainRouteResolver(...))` BEFORE
|
|
48
|
+
* `applyRhinoRouting(...)`, alongside / instead of `createTenantRouteRewrite`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createDomainRouteResolver(args: {
|
|
51
|
+
prisma: PrismaClientLike;
|
|
52
|
+
config: RhinoConfig;
|
|
53
|
+
options?: DomainRouteResolverOptions;
|
|
54
|
+
}): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDomainRouteResolver = createDomainRouteResolver;
|
|
4
|
+
const domain_pattern_1 = require("../utils/domain-pattern");
|
|
5
|
+
/**
|
|
6
|
+
* Factory for an Express-level middleware that implements host-based route
|
|
7
|
+
* group resolution — the subdomain-multitenancy analogue of
|
|
8
|
+
* `createTenantRouteRewrite`.
|
|
9
|
+
*
|
|
10
|
+
* Context: NestJS serves every model through a single GlobalController, and a
|
|
11
|
+
* route group is resolved at request time. For a parameterized domain group
|
|
12
|
+
* (`{organization}.example.com`), the captured subdomain must be turned into a
|
|
13
|
+
* resolved `req.organization` BEFORE the controller runs — exactly like the
|
|
14
|
+
* path-based `/api/<slug>/...` rewrite does for the `:organization` prefix.
|
|
15
|
+
*
|
|
16
|
+
* Behavior:
|
|
17
|
+
*
|
|
18
|
+
* 1. For each route group with a `domain`, compile its pattern.
|
|
19
|
+
* 2. On each request, match `req.hostname` against those patterns.
|
|
20
|
+
* - No domain group matches → next() (plain prefix routing applies).
|
|
21
|
+
* 3. When a parameterized domain group matches, take the captured
|
|
22
|
+
* `{organization}` (or `{org}`) subdomain and:
|
|
23
|
+
* - expose it on `req.params.organization`,
|
|
24
|
+
* - resolve the org by `organizationIdentifierColumn` (cached),
|
|
25
|
+
* strict-404 when unknown,
|
|
26
|
+
* - enforce membership when `req.user` is present,
|
|
27
|
+
* - attach `req.organization`.
|
|
28
|
+
* A literal domain group matches but captures no params, so it only marks
|
|
29
|
+
* the host as recognized (no org resolution).
|
|
30
|
+
*
|
|
31
|
+
* Intended for raw Express `app.use(createDomainRouteResolver(...))` BEFORE
|
|
32
|
+
* `applyRhinoRouting(...)`, alongside / instead of `createTenantRouteRewrite`.
|
|
33
|
+
*/
|
|
34
|
+
function createDomainRouteResolver(args) {
|
|
35
|
+
const { strict = true, enforceMembership = true } = args.options ?? {};
|
|
36
|
+
// FIX 11.2: when group-membership enforcement is ON, an authenticated
|
|
37
|
+
// non-member must get a 403 from GroupMembershipGuard, which takes precedence
|
|
38
|
+
// over this resolver's cross-tenant 404. So we resolve & attach the org but
|
|
39
|
+
// skip the membership-404 here, deferring to the downstream guard. A
|
|
40
|
+
// genuinely unknown subdomain still 404s (strict). When enforcement is OFF
|
|
41
|
+
// (default), behavior is byte-for-byte unchanged.
|
|
42
|
+
const groupMembershipEnforced = args.config.auth?.enforceGroupMembership === true;
|
|
43
|
+
const orgModel = args.config.multiTenant?.organizationModel ?? 'organization';
|
|
44
|
+
const userOrgModel = args.config.multiTenant?.userOrganizationModel ?? 'userRole';
|
|
45
|
+
const idColumn = args.config.multiTenant?.organizationIdentifierColumn ?? 'slug';
|
|
46
|
+
const compiledGroups = [];
|
|
47
|
+
for (const [name, group] of Object.entries(args.config.routeGroups ?? {})) {
|
|
48
|
+
const compiled = (0, domain_pattern_1.compileDomain)(group.domain);
|
|
49
|
+
if (compiled)
|
|
50
|
+
compiledGroups.push({ name, group, compiled });
|
|
51
|
+
}
|
|
52
|
+
// Cache org lookups by captured subdomain value. `null` = probed, not found.
|
|
53
|
+
const cache = new Map();
|
|
54
|
+
const notFound = (res) => {
|
|
55
|
+
res.status(404).json({ code: 'NOT_FOUND', message: 'Organization not found' });
|
|
56
|
+
};
|
|
57
|
+
return async function domainRouteResolver(req, res, next) {
|
|
58
|
+
if (compiledGroups.length === 0)
|
|
59
|
+
return next();
|
|
60
|
+
try {
|
|
61
|
+
const host = resolveHost(req);
|
|
62
|
+
if (!host)
|
|
63
|
+
return next();
|
|
64
|
+
let matched = null;
|
|
65
|
+
for (const cg of compiledGroups) {
|
|
66
|
+
const result = (0, domain_pattern_1.matchDomain)(cg.compiled, host);
|
|
67
|
+
if (result) {
|
|
68
|
+
matched = { group: cg, params: result.params };
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!matched)
|
|
73
|
+
return next();
|
|
74
|
+
// Mark which group the host resolved to (parity with RouteGroupMiddleware).
|
|
75
|
+
req.__routeGroup = matched.group.name;
|
|
76
|
+
if (matched.group.group.skipAuth)
|
|
77
|
+
req.__skipAuth = true;
|
|
78
|
+
const rawSubdomain = matched.params.organization ?? matched.params.org;
|
|
79
|
+
if (rawSubdomain == null) {
|
|
80
|
+
// Literal domain group — no tenant to resolve.
|
|
81
|
+
return next();
|
|
82
|
+
}
|
|
83
|
+
// DNS hostnames are case-insensitive; orgs are slugged lowercase. Normalize
|
|
84
|
+
// the captured subdomain before the lookup, the cache key, and the param so
|
|
85
|
+
// `ACME.example.com` resolves the `acme` org consistently.
|
|
86
|
+
const subdomain = rawSubdomain.toLowerCase();
|
|
87
|
+
// Expose the captured subdomain like a path param.
|
|
88
|
+
req.params = req.params ?? {};
|
|
89
|
+
if (req.params.organization == null) {
|
|
90
|
+
req.params.organization = subdomain;
|
|
91
|
+
}
|
|
92
|
+
// Resolve the org from the subdomain (cached).
|
|
93
|
+
if (!cache.has(subdomain)) {
|
|
94
|
+
const delegate = args.prisma[camel(orgModel)] ?? args.prisma[orgModel];
|
|
95
|
+
if (!delegate?.findFirst) {
|
|
96
|
+
// No org delegate — can't resolve. Pass through.
|
|
97
|
+
return next();
|
|
98
|
+
}
|
|
99
|
+
const org = await delegate
|
|
100
|
+
.findFirst({ where: { [idColumn]: subdomain } })
|
|
101
|
+
.catch(() => null);
|
|
102
|
+
cache.set(subdomain, org ?? null);
|
|
103
|
+
}
|
|
104
|
+
const org = cache.get(subdomain);
|
|
105
|
+
if (!org) {
|
|
106
|
+
if (strict)
|
|
107
|
+
return notFound(res);
|
|
108
|
+
return next();
|
|
109
|
+
}
|
|
110
|
+
// Membership check (cross-tenant → 404, not 403). Skipped when group
|
|
111
|
+
// membership enforcement is ON — the GroupMembershipGuard returns 403 for
|
|
112
|
+
// an authenticated non-member, which takes precedence (FIX 11.2).
|
|
113
|
+
if (enforceMembership && !groupMembershipEnforced && req.user) {
|
|
114
|
+
const userId = req.user.id;
|
|
115
|
+
const urDelegate = args.prisma[camel(userOrgModel)] ?? args.prisma[userOrgModel];
|
|
116
|
+
if (urDelegate?.findFirst) {
|
|
117
|
+
const member = await urDelegate
|
|
118
|
+
.findFirst({ where: { userId, organizationId: org.id } })
|
|
119
|
+
.catch(() => null);
|
|
120
|
+
if (!member)
|
|
121
|
+
return notFound(res);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
req.organization = org;
|
|
125
|
+
req.__orgSubdomain = subdomain;
|
|
126
|
+
return next();
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
return next(err);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function resolveHost(req) {
|
|
134
|
+
const fromExpress = req.hostname;
|
|
135
|
+
if (fromExpress)
|
|
136
|
+
return String(fromExpress).split(':')[0];
|
|
137
|
+
const header = req.headers?.host ?? req.host;
|
|
138
|
+
return header ? String(header).split(':')[0] : undefined;
|
|
139
|
+
}
|
|
140
|
+
function camel(name) {
|
|
141
|
+
if (!name)
|
|
142
|
+
return name;
|
|
143
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=domain-route-resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-route-resolver.js","sourceRoot":"","sources":["../../src/middleware/domain-route-resolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,16 +2,60 @@ import { NestMiddleware } from '@nestjs/common';
|
|
|
2
2
|
import type { NextFunction, Request, Response } from 'express';
|
|
3
3
|
import { RhinoConfigService } from '../rhino.config';
|
|
4
4
|
/**
|
|
5
|
-
* Detects which Rhino route group the current request belongs to
|
|
6
|
-
* (based on URL prefix) and attaches:
|
|
5
|
+
* Detects which Rhino route group the current request belongs to and attaches:
|
|
7
6
|
* - `req.__routeGroup` — the group name
|
|
8
7
|
* - `req.__skipAuth` — true when the group has `skipAuth: true`
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Group selection considers BOTH:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Host** — when a group declares a `domain`, it is only eligible for
|
|
12
|
+
* requests whose `req.hostname` matches that domain. A literal domain
|
|
13
|
+
* (`admin.example.com`) must match exactly; a parameterized domain
|
|
14
|
+
* (`{organization}.example.com`) captures the subdomain and exposes it as
|
|
15
|
+
* `req.params.organization` so ResolveOrganizationMiddleware can resolve
|
|
16
|
+
* the tenant from the host — mirroring Laravel's `Route::domain(...)`.
|
|
17
|
+
*
|
|
18
|
+
* 2. **URL prefix** — the existing path-prefix matching, unchanged for groups
|
|
19
|
+
* without a `domain`.
|
|
20
|
+
*
|
|
21
|
+
* Host-scoped groups take precedence over plain prefix groups when both could
|
|
22
|
+
* match, so two groups can share the same prefix and be selected by host.
|
|
23
|
+
*
|
|
24
|
+
* Enforcement: if the request path targets a model that is ONLY served by
|
|
25
|
+
* domain-scoped group(s) and the host matches none of them, the request is
|
|
26
|
+
* rejected with 404 — a wrong-host request must not be served as that group.
|
|
27
|
+
*
|
|
28
|
+
* Install this middleware globally BEFORE `JwtAuthGuard` so the guard can read
|
|
29
|
+
* `req.__skipAuth`, and BEFORE `ResolveOrganizationMiddleware` so a captured
|
|
30
|
+
* subdomain param is available for org resolution.
|
|
12
31
|
*/
|
|
13
32
|
export declare class RouteGroupMiddleware implements NestMiddleware {
|
|
14
33
|
private readonly config;
|
|
34
|
+
/** Cache compiled domain patterns keyed by group name. */
|
|
35
|
+
private readonly compiledCache;
|
|
15
36
|
constructor(config: RhinoConfigService);
|
|
37
|
+
private compileFor;
|
|
16
38
|
use(req: Request & Record<string, any>, _res: Response, next: NextFunction): void;
|
|
39
|
+
/**
|
|
40
|
+
* Whether `url` targets one of the auth entry endpoints. These are the
|
|
41
|
+
* routes that must never be blocked by the JWT guard (login/register/recover)
|
|
42
|
+
* and that must resolve to the group they belong to. Matches the auth
|
|
43
|
+
* controller's `@Controller('auth')` action paths under any prefix.
|
|
44
|
+
*/
|
|
45
|
+
private isAuthEntryPath;
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the route group for an auth entry path (FIX 11.1). Precedence:
|
|
48
|
+
* 1. an auth-enabled DOMAIN group whose host matches (and prefix matches);
|
|
49
|
+
* 2. an auth-enabled PREFIX group whose prefix matches;
|
|
50
|
+
* 3. the empty-prefix / default group (auth-enabled preferred, else any);
|
|
51
|
+
* 4. otherwise leave `__routeGroup` unset (legacy/global auth path).
|
|
52
|
+
*
|
|
53
|
+
* `__skipAuth` is ALWAYS set on an auth entry path so the JWT guard never
|
|
54
|
+
* blocks login/register/recover, regardless of which group wins — even a
|
|
55
|
+
* host-claiming empty-prefix domain group cannot strip it.
|
|
56
|
+
*/
|
|
57
|
+
private resolveAuthEntry;
|
|
58
|
+
private resolveHost;
|
|
59
|
+
private prefixMatches;
|
|
60
|
+
private exposeDomainParams;
|
|
17
61
|
}
|