@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.
Files changed (63) hide show
  1. package/README.md +52 -1
  2. package/dist/blueprint/blueprint-runner.js +32 -1
  3. package/dist/blueprint/blueprint-runner.js.map +1 -1
  4. package/dist/blueprint/blueprint-sorter.d.ts +54 -0
  5. package/dist/blueprint/blueprint-sorter.js +107 -0
  6. package/dist/blueprint/blueprint-sorter.js.map +1 -0
  7. package/dist/controllers/auth.controller.d.ts +34 -5
  8. package/dist/controllers/auth.controller.js +132 -12
  9. package/dist/controllers/auth.controller.js.map +1 -1
  10. package/dist/controllers/invitation.controller.d.ts +41 -2
  11. package/dist/controllers/invitation.controller.js +97 -12
  12. package/dist/controllers/invitation.controller.js.map +1 -1
  13. package/dist/errors/rhino-exception.d.ts +12 -1
  14. package/dist/errors/rhino-exception.js +19 -1
  15. package/dist/errors/rhino-exception.js.map +1 -1
  16. package/dist/guards/group-membership.guard.d.ts +25 -0
  17. package/dist/guards/group-membership.guard.js +70 -0
  18. package/dist/guards/group-membership.guard.js.map +1 -0
  19. package/dist/guards/jwt-auth.guard.js +23 -4
  20. package/dist/guards/jwt-auth.guard.js.map +1 -1
  21. package/dist/index.d.ts +7 -1
  22. package/dist/index.js +17 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/interfaces/rhino-config.interface.d.ts +71 -0
  25. package/dist/middleware/domain-route-resolver.d.ts +54 -0
  26. package/dist/middleware/domain-route-resolver.js +145 -0
  27. package/dist/middleware/domain-route-resolver.js.map +1 -0
  28. package/dist/middleware/route-group.middleware.d.ts +48 -4
  29. package/dist/middleware/route-group.middleware.js +274 -13
  30. package/dist/middleware/route-group.middleware.js.map +1 -1
  31. package/dist/rhino.config.d.ts +26 -1
  32. package/dist/rhino.config.js +56 -1
  33. package/dist/rhino.config.js.map +1 -1
  34. package/dist/rhino.module.d.ts +7 -0
  35. package/dist/rhino.module.js +23 -1
  36. package/dist/rhino.module.js.map +1 -1
  37. package/dist/services/auth-hooks.service.d.ts +27 -0
  38. package/dist/services/auth-hooks.service.js +85 -0
  39. package/dist/services/auth-hooks.service.js.map +1 -0
  40. package/dist/services/auth.service.d.ts +19 -0
  41. package/dist/services/auth.service.js +80 -5
  42. package/dist/services/auth.service.js.map +1 -1
  43. package/dist/services/invitation.service.d.ts +6 -0
  44. package/dist/services/invitation.service.js +45 -3
  45. package/dist/services/invitation.service.js.map +1 -1
  46. package/dist/services/membership.service.d.ts +56 -0
  47. package/dist/services/membership.service.js +113 -0
  48. package/dist/services/membership.service.js.map +1 -0
  49. package/dist/services/organization.service.js +8 -1
  50. package/dist/services/organization.service.js.map +1 -1
  51. package/dist/services/route-registration.service.d.ts +1 -0
  52. package/dist/services/route-registration.service.js +2 -0
  53. package/dist/services/route-registration.service.js.map +1 -1
  54. package/dist/tsconfig.build.tsbuildinfo +1 -1
  55. package/dist/utils/domain-pattern.d.ts +36 -0
  56. package/dist/utils/domain-pattern.js +77 -0
  57. package/dist/utils/domain-pattern.js.map +1 -0
  58. package/dist/utils/permission-matcher.js +8 -0
  59. package/dist/utils/permission-matcher.js.map +1 -1
  60. package/dist/utils/route-group-validator.d.ts +14 -0
  61. package/dist/utils/route-group-validator.js +124 -0
  62. package/dist/utils/route-group-validator.js.map +1 -0
  63. 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
- * Install this middleware globally BEFORE `JwtAuthGuard` so the guard can
11
- * read `req.__skipAuth` and short-circuit.
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
  }