@rhino-dev/rhino-nestjs 4.0.0 → 4.2.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.
Files changed (58) hide show
  1. package/README.md +52 -1
  2. package/dist/controllers/auth.controller.d.ts +34 -5
  3. package/dist/controllers/auth.controller.js +132 -12
  4. package/dist/controllers/auth.controller.js.map +1 -1
  5. package/dist/controllers/invitation.controller.d.ts +41 -2
  6. package/dist/controllers/invitation.controller.js +97 -12
  7. package/dist/controllers/invitation.controller.js.map +1 -1
  8. package/dist/errors/rhino-exception.d.ts +12 -1
  9. package/dist/errors/rhino-exception.js +19 -1
  10. package/dist/errors/rhino-exception.js.map +1 -1
  11. package/dist/guards/group-membership.guard.d.ts +25 -0
  12. package/dist/guards/group-membership.guard.js +70 -0
  13. package/dist/guards/group-membership.guard.js.map +1 -0
  14. package/dist/guards/jwt-auth.guard.js +23 -4
  15. package/dist/guards/jwt-auth.guard.js.map +1 -1
  16. package/dist/index.d.ts +7 -1
  17. package/dist/index.js +17 -2
  18. package/dist/index.js.map +1 -1
  19. package/dist/interfaces/rhino-config.interface.d.ts +71 -0
  20. package/dist/middleware/domain-route-resolver.d.ts +54 -0
  21. package/dist/middleware/domain-route-resolver.js +145 -0
  22. package/dist/middleware/domain-route-resolver.js.map +1 -0
  23. package/dist/middleware/route-group.middleware.d.ts +48 -4
  24. package/dist/middleware/route-group.middleware.js +274 -13
  25. package/dist/middleware/route-group.middleware.js.map +1 -1
  26. package/dist/rhino.config.d.ts +26 -1
  27. package/dist/rhino.config.js +56 -1
  28. package/dist/rhino.config.js.map +1 -1
  29. package/dist/rhino.module.d.ts +7 -0
  30. package/dist/rhino.module.js +23 -1
  31. package/dist/rhino.module.js.map +1 -1
  32. package/dist/services/auth-hooks.service.d.ts +27 -0
  33. package/dist/services/auth-hooks.service.js +85 -0
  34. package/dist/services/auth-hooks.service.js.map +1 -0
  35. package/dist/services/auth.service.d.ts +19 -0
  36. package/dist/services/auth.service.js +80 -5
  37. package/dist/services/auth.service.js.map +1 -1
  38. package/dist/services/invitation.service.d.ts +6 -0
  39. package/dist/services/invitation.service.js +45 -3
  40. package/dist/services/invitation.service.js.map +1 -1
  41. package/dist/services/membership.service.d.ts +56 -0
  42. package/dist/services/membership.service.js +113 -0
  43. package/dist/services/membership.service.js.map +1 -0
  44. package/dist/services/organization.service.js +8 -1
  45. package/dist/services/organization.service.js.map +1 -1
  46. package/dist/services/route-registration.service.d.ts +1 -0
  47. package/dist/services/route-registration.service.js +2 -0
  48. package/dist/services/route-registration.service.js.map +1 -1
  49. package/dist/tsconfig.build.tsbuildinfo +1 -1
  50. package/dist/utils/domain-pattern.d.ts +36 -0
  51. package/dist/utils/domain-pattern.js +77 -0
  52. package/dist/utils/domain-pattern.js.map +1 -0
  53. package/dist/utils/permission-matcher.js +8 -0
  54. package/dist/utils/permission-matcher.js.map +1 -1
  55. package/dist/utils/route-group-validator.d.ts +14 -0
  56. package/dist/utils/route-group-validator.js +124 -0
  57. package/dist/utils/route-group-validator.js.map +1 -0
  58. package/package.json +1 -1
package/README.md CHANGED
@@ -169,7 +169,7 @@ That's it. You now have a full REST API for posts:
169
169
  | 13 | **Eager Loading** | `?include=author,comments` with nested dot-notation. |
170
170
  | 14 | **Multi-Tenancy** | Organization-based data isolation, auto-set `organizationId`, request scoping. |
171
171
  | 15 | **Nested Ownership** | Auto-scopes by `organizationId` on the registered model. |
172
- | 16 | **Route Groups** | Multiple URL prefixes with different middleware/auth (`tenant`, `public`, custom). |
172
+ | 16 | **Route Groups** | Multiple URL prefixes with different middleware/auth (`tenant`, `public`, custom). Optional per-group `domain` constrains a group to a host (literal or `{organization}.example.com` subdomain). |
173
173
  | 17 | **Soft Deletes** | Trash, restore, force-delete endpoints with individual permissions. |
174
174
  | 18 | **Audit Trail** | Logs all CRUD events with old/new values, user, IP, and org context. |
175
175
  | 19 | **Nested Operations** | `POST /nested` for atomic multi-model transactions with `$N.field` references. |
@@ -182,6 +182,37 @@ That's it. You now have a full REST API for posts:
182
182
  | 26 | **Generator CLI** | `rhino install`, `rhino generate`, `rhino blueprint`. |
183
183
  | 27 | **Postman Export** | Auto-generated Postman Collection v2.1 with all endpoints. |
184
184
  | 28 | **Blueprint System** | YAML-to-code generation for models, migrations, policies, tests, and seeders. |
185
+ | 29 | **Group Membership** | Opt-in `auth.enforceGroupMembership` makes a route group an access boundary. Memberships are keyed by `(user, route_group, organization, role)` on `user_roles`; a NULL `route_group` is a wildcard. No match → 403, enforced both at `/auth/login` (non-members can't sign in) and on every resource request. Permissions then resolve from the matched membership row. |
186
+ | 30 | **Group-Aware Auth & Lifecycle Hooks** | Per-group `auth: true` registers the auth route set under the group; per-group `hooks` (a provider implementing `AuthLifecycleHooks`) run `afterLogin/afterLogout/afterRegister/afterPasswordRecover/afterPasswordReset` and may reject (revoking the issued token). Invitations carry the `route_group`; accept populates the membership. See [Group-auth hooks & token revocation](#group-auth-hooks--token-revocation). |
187
+
188
+ ### Group-auth hooks & token revocation
189
+
190
+ When a lifecycle hook **rejects** a token-issuing action (`afterLogin` /
191
+ `afterRegister`), Rhino does two things:
192
+
193
+ 1. It **drops the issued token from the response** — the client never receives
194
+ it. This is the bounded, always-on guarantee.
195
+ 2. It **attempts to denylist the token** so a copy that already leaked can no
196
+ longer be used. This step requires a `RevokedToken` Prisma model
197
+ (`token`, `createdAt`). **If no `RevokedToken` model is configured, revoke is
198
+ advisory only** — Rhino logs a clear WARNING and the token simply isn't
199
+ denylisted. Because JWTs are stateless, you should therefore:
200
+
201
+ - Provision a `RevokedToken` model if you rely on hook rejection, **and**
202
+ - Use **short-TTL JWTs** (set `auth.jwtExpiresIn` to a small value) so an
203
+ un-denylisted token expires quickly.
204
+
205
+ **Controlling the HTTP status from a hook:** a hook must throw
206
+ `RhinoAuthRejected` (or any NestJS `HttpException`) to control the response
207
+ status (default 403; the hook may set 401/409/etc.). A plain `Error` is **not**
208
+ an `HttpException` and surfaces as a **500** after the token is revoked — so
209
+ always throw `RhinoAuthRejected`/`HttpException`, never a bare `Error`, to reject.
210
+
211
+ **Password recovery is never an enumeration oracle:** `afterPasswordRecover`
212
+ runs for side effects only — a rejection it throws is swallowed so the recovery
213
+ endpoint always returns the same uniform `{ success: true }` regardless of
214
+ whether the email exists. (Reject semantics are kept for login/register/logout/
215
+ reset.)
185
216
 
186
217
  ## Configuration Reference
187
218
 
@@ -238,9 +269,29 @@ routeGroups: {
238
269
  models: ['posts'],
239
270
  skipAuth: true,
240
271
  },
272
+ // Domain-aware groups: constrain a group to a specific host. Two groups can
273
+ // share the same prefix and be selected by host.
274
+ admin: {
275
+ domain: 'admin.example.com', // literal host — only this host serves this group
276
+ models: '*',
277
+ },
278
+ hostTenant: {
279
+ domain: '{organization}.example.com', // parameterized host — the captured
280
+ models: '*', // {organization} subdomain resolves the
281
+ }, // tenant, just like the :organization prefix
241
282
  }
242
283
  ```
243
284
 
285
+ **`domain` (per-group, optional):**
286
+
287
+ - Omitted → the group matches any host (default; backward compatible).
288
+ - Literal host (`'admin.example.com'`) → only requests to that host resolve to the group; a wrong-host request to the group's models is rejected with `404`.
289
+ - Parameterized host (`'{organization}.example.com'`) → the captured `{organization}` subdomain feeds organization resolution (subdomain multitenancy), exactly like the `:organization` path prefix. Mirrors Laravel's `Route::domain(...)`.
290
+
291
+ Host-based matching is performed by `RouteGroupMiddleware`. For subdomain → organization resolution at the Express layer (analogous to `createTenantRouteRewrite`), use `createDomainRouteResolver({ prisma, config })` in `main.ts` before `applyRhinoRouting(...)`.
292
+
293
+ > **Conflicting groups fail fast.** Two groups that share the same prefix **and** overlapping models **and** an intersecting host-set would silently shadow each other. `normalizeConfig` (run by `RhinoModule.forRoot`) throws `RouteGroupConflictError` in that case. Fix it with distinct prefixes, different `domain` values, or disjoint `models`. A group without a `domain` matches every host, so it intersects with all others.
294
+
244
295
  **Reserved group names:**
245
296
 
246
297
  | Name | Behavior |
@@ -2,6 +2,8 @@ import { AuthService } from '../services/auth.service';
2
2
  import { InvitationService } from '../services/invitation.service';
3
3
  import { PrismaService } from '../prisma/prisma.service';
4
4
  import { RhinoConfigService } from '../rhino.config';
5
+ import { AuthHooksService } from '../services/auth-hooks.service';
6
+ import { MembershipService } from '../services/membership.service';
5
7
  export interface LoginDto {
6
8
  email: string;
7
9
  password: string;
@@ -25,31 +27,58 @@ export declare class AuthController {
25
27
  private readonly invitationService;
26
28
  private readonly prisma;
27
29
  private readonly config;
28
- constructor(authService: AuthService, invitationService: InvitationService, prisma: PrismaService, config: RhinoConfigService);
29
- login(dto: LoginDto): Promise<{
30
+ private readonly hooks?;
31
+ private readonly membership?;
32
+ constructor(authService: AuthService, invitationService: InvitationService, prisma: PrismaService, config: RhinoConfigService, hooks?: AuthHooksService, membership?: MembershipService);
33
+ /** Group resolved for this auth request (set by RouteGroupMiddleware). */
34
+ private routeGroup;
35
+ /**
36
+ * Run a lifecycle hook for the resolved group. `revokes` flags token-issuing
37
+ * actions: when the hook rejects, the issued token is dropped (revoked) and
38
+ * the rejection's status is re-thrown. When no hooks service is wired, this
39
+ * is a no-op.
40
+ */
41
+ private runHook;
42
+ /**
43
+ * Enforce group membership at the auth boundary — parity with the
44
+ * GroupMembershipGuard (resource layer) and the Laravel/Rails AuthControllers.
45
+ *
46
+ * When `auth.enforceGroupMembership` is ON, a non-member must be rejected at
47
+ * `/auth/login` with **403** rather than being allowed to authenticate and
48
+ * only blocked later on the first resource request. The just-issued token is
49
+ * revoked (best-effort) and never returned. This is the COARSE gate, so it
50
+ * runs BEFORE the `afterLogin` hook.
51
+ *
52
+ * No-op when: enforcement is off (or the MembershipService isn't wired), the
53
+ * request skips auth, or the resolved group is `public`.
54
+ */
55
+ private assertGroupMembership;
56
+ login(dto: LoginDto, req?: any): Promise<{
30
57
  token: string;
31
58
  organization_slug: string;
32
59
  }>;
33
60
  logout(req: any): Promise<{
34
61
  success: boolean;
35
62
  }>;
36
- recover(dto: RecoverDto): Promise<{
63
+ recover(dto: RecoverDto, req?: any): Promise<{
37
64
  success: boolean;
38
65
  }>;
39
- reset(dto: ResetDto): Promise<{
66
+ reset(dto: ResetDto, req?: any): Promise<{
40
67
  success: boolean;
41
68
  }>;
42
- register(dto: RegisterDto): Promise<{
69
+ register(dto: RegisterDto, req?: any): Promise<{
43
70
  requiresRegistration: boolean;
44
71
  organization: any;
45
72
  role: any;
46
73
  email: any;
74
+ routeGroup: any;
47
75
  accepted?: undefined;
48
76
  token: string;
49
77
  user: any;
50
78
  } | {
51
79
  accepted: boolean;
52
80
  organization: any;
81
+ routeGroup: any;
53
82
  requiresRegistration?: undefined;
54
83
  role?: undefined;
55
84
  email?: undefined;
@@ -18,42 +18,140 @@ const auth_service_1 = require("../services/auth.service");
18
18
  const invitation_service_1 = require("../services/invitation.service");
19
19
  const prisma_service_1 = require("../prisma/prisma.service");
20
20
  const rhino_config_1 = require("../rhino.config");
21
+ const auth_hooks_service_1 = require("../services/auth-hooks.service");
22
+ const membership_service_1 = require("../services/membership.service");
23
+ const rhino_exception_1 = require("../errors/rhino-exception");
21
24
  let AuthController = class AuthController {
22
25
  authService;
23
26
  invitationService;
24
27
  prisma;
25
28
  config;
26
- constructor(authService, invitationService, prisma, config) {
29
+ hooks;
30
+ membership;
31
+ constructor(authService, invitationService, prisma, config,
32
+ // Optional so lightweight unit harnesses can omit it (no-op hooks then).
33
+ hooks,
34
+ // Optional for the same reason: when absent, membership enforcement is a
35
+ // no-op (equivalent to `auth.enforceGroupMembership` being off).
36
+ membership) {
27
37
  this.authService = authService;
28
38
  this.invitationService = invitationService;
29
39
  this.prisma = prisma;
30
40
  this.config = config;
41
+ this.hooks = hooks;
42
+ this.membership = membership;
31
43
  }
32
- async login(dto) {
44
+ /** Group resolved for this auth request (set by RouteGroupMiddleware). */
45
+ routeGroup(req) {
46
+ return req?.__routeGroup ?? null;
47
+ }
48
+ /**
49
+ * Run a lifecycle hook for the resolved group. `revokes` flags token-issuing
50
+ * actions: when the hook rejects, the issued token is dropped (revoked) and
51
+ * the rejection's status is re-thrown. When no hooks service is wired, this
52
+ * is a no-op.
53
+ */
54
+ async runHook(event, req, ctx) {
55
+ if (!this.hooks)
56
+ return;
57
+ await this.hooks.run(event, this.routeGroup(req), ctx);
58
+ }
59
+ /**
60
+ * Enforce group membership at the auth boundary — parity with the
61
+ * GroupMembershipGuard (resource layer) and the Laravel/Rails AuthControllers.
62
+ *
63
+ * When `auth.enforceGroupMembership` is ON, a non-member must be rejected at
64
+ * `/auth/login` with **403** rather than being allowed to authenticate and
65
+ * only blocked later on the first resource request. The just-issued token is
66
+ * revoked (best-effort) and never returned. This is the COARSE gate, so it
67
+ * runs BEFORE the `afterLogin` hook.
68
+ *
69
+ * No-op when: enforcement is off (or the MembershipService isn't wired), the
70
+ * request skips auth, or the resolved group is `public`.
71
+ */
72
+ async assertGroupMembership(req, user, token) {
73
+ if (!this.membership?.enabled())
74
+ return;
75
+ // NOTE: we deliberately do NOT bail on `req.__skipAuth` here. Consumers set
76
+ // that flag on auth entrypoints (login/register) to bypass the JWT guard
77
+ // (there's no token yet) — it does NOT mean "skip membership". The genuine
78
+ // skip signal is the `public` group, handled below (matches the guard).
79
+ const routeGroup = this.routeGroup(req);
80
+ if (routeGroup === 'public')
81
+ return;
82
+ const org = req?.organization ?? null;
83
+ const isTenant = this.config.isTenantGroup(routeGroup);
84
+ if (this.membership.isMember(user, routeGroup, org, isTenant))
85
+ return;
86
+ await this.authService.revokeToken(token);
87
+ throw rhino_exception_1.RhinoException.membershipDenied();
88
+ }
89
+ async login(dto, req) {
33
90
  if (!dto?.email || !dto?.password) {
34
91
  throw new common_1.BadRequestException('Email and password required');
35
92
  }
36
- const { token, organizationSlug } = await this.authService.login(dto.email, dto.password);
93
+ const { token, organizationSlug, user } = await this.authService.login(dto.email, dto.password);
94
+ // Coarse membership gate (before the hook): a non-member is rejected here
95
+ // with 403, matching the resource-layer guard and the Laravel/Rails stacks.
96
+ await this.assertGroupMembership(req, user, token);
97
+ const routeGroup = this.routeGroup(req);
98
+ try {
99
+ await this.runHook('afterLogin', req, {
100
+ user,
101
+ routeGroup,
102
+ token,
103
+ request: req,
104
+ });
105
+ }
106
+ catch (err) {
107
+ // Reject → revoke the just-issued token (never returned to the client).
108
+ await this.authService.revokeToken(token);
109
+ throw err;
110
+ }
37
111
  return { token, organization_slug: organizationSlug };
38
112
  }
39
113
  async logout(req) {
40
114
  await this.authService.logout(req.user);
115
+ await this.runHook('afterLogout', req, {
116
+ user: req.user,
117
+ routeGroup: this.routeGroup(req),
118
+ request: req,
119
+ });
41
120
  return { success: true };
42
121
  }
43
- async recover(dto) {
122
+ async recover(dto, req) {
44
123
  if (!dto?.email)
45
124
  throw new common_1.BadRequestException('Email required');
46
- await this.authService.requestPasswordRecovery(dto.email);
125
+ const result = await this.authService.requestPasswordRecovery(dto.email);
126
+ // FIX 6: password recovery MUST return a uniform response whether or not the
127
+ // email exists (no user-enumeration oracle). The hook runs for side effects
128
+ // only — a rejection is swallowed so it can never change the response and
129
+ // thereby reveal account existence. (Other actions keep reject semantics.)
130
+ try {
131
+ await this.runHook('afterPasswordRecover', req, {
132
+ user: result?.user ?? null,
133
+ routeGroup: this.routeGroup(req),
134
+ request: req,
135
+ });
136
+ }
137
+ catch {
138
+ /* swallow: recovery response is always uniform */
139
+ }
47
140
  return { success: true };
48
141
  }
49
- async reset(dto) {
142
+ async reset(dto, req) {
50
143
  if (!dto?.email || !dto?.token || !dto?.password) {
51
144
  throw new common_1.BadRequestException('Email, token and password required');
52
145
  }
53
146
  await this.authService.resetPassword(dto.email, dto.token, dto.password);
147
+ await this.runHook('afterPasswordReset', req, {
148
+ user: null,
149
+ routeGroup: this.routeGroup(req),
150
+ request: req,
151
+ });
54
152
  return { success: true };
55
153
  }
56
- async register(dto) {
154
+ async register(dto, req) {
57
155
  if (!dto?.email || !dto?.password || !dto?.invitationToken) {
58
156
  throw new common_1.BadRequestException('Email, password and invitationToken required');
59
157
  }
@@ -68,6 +166,22 @@ let AuthController = class AuthController {
68
166
  });
69
167
  const acceptance = await this.invitationService.accept(dto.invitationToken, user.id);
70
168
  const token = this.authService.signToken({ sub: user.id, email: dto.email });
169
+ // Hooks see the group the membership was created for, falling back to the
170
+ // request's resolved group.
171
+ const routeGroup = acceptance?.routeGroup ?? this.routeGroup(req);
172
+ try {
173
+ await this.runHook('afterRegister', req, {
174
+ user,
175
+ routeGroup,
176
+ organization: acceptance?.organization ?? null,
177
+ token,
178
+ request: req,
179
+ });
180
+ }
181
+ catch (err) {
182
+ await this.authService.revokeToken(token);
183
+ throw err;
184
+ }
71
185
  return {
72
186
  token,
73
187
  user,
@@ -79,8 +193,9 @@ exports.AuthController = AuthController;
79
193
  __decorate([
80
194
  (0, common_1.Post)('login'),
81
195
  __param(0, (0, common_1.Body)()),
196
+ __param(1, (0, common_1.Req)()),
82
197
  __metadata("design:type", Function),
83
- __metadata("design:paramtypes", [Object]),
198
+ __metadata("design:paramtypes", [Object, Object]),
84
199
  __metadata("design:returntype", Promise)
85
200
  ], AuthController.prototype, "login", null);
86
201
  __decorate([
@@ -93,22 +208,25 @@ __decorate([
93
208
  __decorate([
94
209
  (0, common_1.Post)('password/recover'),
95
210
  __param(0, (0, common_1.Body)()),
211
+ __param(1, (0, common_1.Req)()),
96
212
  __metadata("design:type", Function),
97
- __metadata("design:paramtypes", [Object]),
213
+ __metadata("design:paramtypes", [Object, Object]),
98
214
  __metadata("design:returntype", Promise)
99
215
  ], AuthController.prototype, "recover", null);
100
216
  __decorate([
101
217
  (0, common_1.Post)('password/reset'),
102
218
  __param(0, (0, common_1.Body)()),
219
+ __param(1, (0, common_1.Req)()),
103
220
  __metadata("design:type", Function),
104
- __metadata("design:paramtypes", [Object]),
221
+ __metadata("design:paramtypes", [Object, Object]),
105
222
  __metadata("design:returntype", Promise)
106
223
  ], AuthController.prototype, "reset", null);
107
224
  __decorate([
108
225
  (0, common_1.Post)('register'),
109
226
  __param(0, (0, common_1.Body)()),
227
+ __param(1, (0, common_1.Req)()),
110
228
  __metadata("design:type", Function),
111
- __metadata("design:paramtypes", [Object]),
229
+ __metadata("design:paramtypes", [Object, Object]),
112
230
  __metadata("design:returntype", Promise)
113
231
  ], AuthController.prototype, "register", null);
114
232
  exports.AuthController = AuthController = __decorate([
@@ -116,6 +234,8 @@ exports.AuthController = AuthController = __decorate([
116
234
  __metadata("design:paramtypes", [auth_service_1.AuthService,
117
235
  invitation_service_1.InvitationService,
118
236
  prisma_service_1.PrismaService,
119
- rhino_config_1.RhinoConfigService])
237
+ rhino_config_1.RhinoConfigService,
238
+ auth_hooks_service_1.AuthHooksService,
239
+ membership_service_1.MembershipService])
120
240
  ], AuthController);
121
241
  //# sourceMappingURL=auth.controller.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.controller.js","sourceRoot":"","sources":["../../src/controllers/auth.controller.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.controller.js","sourceRoot":"","sources":["../../src/controllers/auth.controller.ts"],"names":[],"mappings":"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"}
@@ -1,15 +1,52 @@
1
1
  import { InvitationService } from '../services/invitation.service';
2
+ import { RhinoConfigService } from '../rhino.config';
3
+ import { MembershipService } from '../services/membership.service';
2
4
  export declare class InvitationController {
3
5
  private readonly invitations;
4
- constructor(invitations: InvitationService);
5
- private assertInvitationPermission;
6
+ private readonly config;
7
+ private readonly membership;
8
+ constructor(invitations: InvitationService, config: RhinoConfigService, membership: MembershipService);
9
+ /**
10
+ * Authorize an invitation action.
11
+ *
12
+ * - Enforcement OFF (legacy): byte-for-byte the original check —
13
+ * `userHasPermission(user, invitations.{action}, org)`; 403 on failure.
14
+ * - Enforcement ON: a COARSE membership gate runs FIRST — the inviter must be
15
+ * a member of the target group (design §8 — "inviter must be a member of the
16
+ * group"). A non-member is a **403** for parity with Laravel/Rails
17
+ * (Decision 9.C), NOT a 400. A NULL/absent membership row is a wildcard and
18
+ * admits the inviter. The fine-grained permission check then runs (also 403
19
+ * on failure), resolving via the established heuristic (tenant groups → the
20
+ * org's user_roles permissions, non-tenant groups → top-level
21
+ * user.permissions). The two checks run in sequence and are never merged.
22
+ */
23
+ private assertCanInvite;
24
+ /** The `public` group (reserved name or any `skipAuth` group) has no auth, so it cannot be invited into. */
25
+ private isPublicGroup;
26
+ /**
27
+ * Reject a `routeGroup` that is neither a configured route group nor the
28
+ * reserved `public` name. A client could otherwise forge an arbitrary group
29
+ * value (overriding the resolved group) and create a dormant, unaudited grant
30
+ * that activates the day that group name is wired up. This guard runs
31
+ * regardless of enforcement. NULL (wildcard) is always allowed.
32
+ */
33
+ private assertKnownGroup;
6
34
  index(req: any, status?: string): Promise<{
7
35
  data: any;
8
36
  }>;
9
37
  store(req: any, body: {
10
38
  email: string;
11
39
  roleId: number;
40
+ routeGroup?: string | null;
12
41
  }): Promise<any>;
42
+ /**
43
+ * Whether this invite requires an organization context. We default to YES to
44
+ * preserve legacy behavior (store always required an org). An invite is only
45
+ * treated as non-tenant — and thus org-optional — when membership enforcement
46
+ * is on AND the resolved group is positively a non-tenant group. This keeps
47
+ * every flag-off code path byte-for-byte unchanged.
48
+ */
49
+ private isTenantInvite;
13
50
  resend(req: any, id: string): Promise<any>;
14
51
  cancel(req: any, id: string): Promise<any>;
15
52
  accept(body: {
@@ -19,10 +56,12 @@ export declare class InvitationController {
19
56
  organization: any;
20
57
  role: any;
21
58
  email: any;
59
+ routeGroup: any;
22
60
  accepted?: undefined;
23
61
  } | {
24
62
  accepted: boolean;
25
63
  organization: any;
64
+ routeGroup: any;
26
65
  requiresRegistration?: undefined;
27
66
  role?: undefined;
28
67
  email?: undefined;
@@ -15,46 +15,129 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.InvitationController = void 0;
16
16
  const common_1 = require("@nestjs/common");
17
17
  const invitation_service_1 = require("../services/invitation.service");
18
+ const rhino_config_1 = require("../rhino.config");
19
+ const membership_service_1 = require("../services/membership.service");
18
20
  const permission_matcher_1 = require("../utils/permission-matcher");
21
+ const rhino_exception_1 = require("../errors/rhino-exception");
19
22
  let InvitationController = class InvitationController {
20
23
  invitations;
21
- constructor(invitations) {
24
+ config;
25
+ membership;
26
+ constructor(invitations, config, membership) {
22
27
  this.invitations = invitations;
28
+ this.config = config;
29
+ this.membership = membership;
23
30
  }
24
- assertInvitationPermission(req, action) {
31
+ /**
32
+ * Authorize an invitation action.
33
+ *
34
+ * - Enforcement OFF (legacy): byte-for-byte the original check —
35
+ * `userHasPermission(user, invitations.{action}, org)`; 403 on failure.
36
+ * - Enforcement ON: a COARSE membership gate runs FIRST — the inviter must be
37
+ * a member of the target group (design §8 — "inviter must be a member of the
38
+ * group"). A non-member is a **403** for parity with Laravel/Rails
39
+ * (Decision 9.C), NOT a 400. A NULL/absent membership row is a wildcard and
40
+ * admits the inviter. The fine-grained permission check then runs (also 403
41
+ * on failure), resolving via the established heuristic (tenant groups → the
42
+ * org's user_roles permissions, non-tenant groups → top-level
43
+ * user.permissions). The two checks run in sequence and are never merged.
44
+ */
45
+ assertCanInvite(req, routeGroup, action) {
46
+ if (this.membership.enabled()) {
47
+ const isTenant = this.config.isTenantGroup(routeGroup ?? undefined);
48
+ if (!this.membership.isMember(req.user, routeGroup ?? null, req.organization, isTenant)) {
49
+ throw rhino_exception_1.RhinoException.membershipDenied(`Inviter is not a member of group '${routeGroup ?? '(any)'}'`);
50
+ }
51
+ }
25
52
  if (!(0, permission_matcher_1.userHasPermission)(req.user, `invitations.${action}`, req.organization)) {
26
- const err = new Error('Forbidden');
27
- err.status = 403;
28
- throw err;
53
+ throw rhino_exception_1.RhinoException.forbidden();
29
54
  }
30
55
  }
56
+ /** The `public` group (reserved name or any `skipAuth` group) has no auth, so it cannot be invited into. */
57
+ isPublicGroup(routeGroup) {
58
+ if (routeGroup == null)
59
+ return false;
60
+ if (routeGroup === 'public')
61
+ return true;
62
+ return this.config.routeGroups()?.[routeGroup]?.skipAuth === true;
63
+ }
64
+ /**
65
+ * Reject a `routeGroup` that is neither a configured route group nor the
66
+ * reserved `public` name. A client could otherwise forge an arbitrary group
67
+ * value (overriding the resolved group) and create a dormant, unaudited grant
68
+ * that activates the day that group name is wired up. This guard runs
69
+ * regardless of enforcement. NULL (wildcard) is always allowed.
70
+ */
71
+ assertKnownGroup(routeGroup) {
72
+ if (routeGroup == null)
73
+ return; // NULL wildcard
74
+ if (routeGroup === 'public')
75
+ return; // reserved; rejected later by isPublicGroup
76
+ if (this.config.routeGroups()?.[routeGroup])
77
+ return; // a configured group
78
+ throw rhino_exception_1.RhinoException.validationFailed({
79
+ routeGroup: [`Unknown route group '${routeGroup}'`],
80
+ });
81
+ }
31
82
  async index(req, status) {
32
- this.assertInvitationPermission(req, 'index');
83
+ this.assertCanInvite(req, req.__routeGroup ?? null, 'index');
33
84
  if (!req.organization)
34
85
  throw new common_1.BadRequestException('Organization context required');
35
86
  const items = await this.invitations.list(req.organization.id, status);
36
87
  return { data: items };
37
88
  }
38
89
  async store(req, body) {
39
- this.assertInvitationPermission(req, 'store');
40
- if (!req.organization)
41
- throw new common_1.BadRequestException('Organization context required');
42
90
  if (!body?.email || !body?.roleId) {
43
91
  throw new common_1.BadRequestException('email and roleId required');
44
92
  }
93
+ // The invite's group: an explicit body value, else the request's resolved
94
+ // group (set by RouteGroupMiddleware), else NULL (wildcard).
95
+ const routeGroup = body.routeGroup !== undefined
96
+ ? body.routeGroup
97
+ : req.__routeGroup ?? null;
98
+ // Reject a forged/unknown route group (422) before any other processing so
99
+ // a client can never seed a dormant grant for an unconfigured group.
100
+ this.assertKnownGroup(routeGroup);
101
+ // The public group has no auth and cannot be invited into.
102
+ if (this.isPublicGroup(routeGroup)) {
103
+ throw new common_1.BadRequestException('Cannot invite into the public group');
104
+ }
105
+ // Tenant-group invites still require an org context; non-tenant invites
106
+ // (an explicit NULL/non-tenant group) may proceed without one. This is
107
+ // checked BEFORE authorization so the legacy "org required" 400 is returned
108
+ // for org-less tenant invites rather than a permission 403.
109
+ if (this.isTenantInvite(routeGroup) && !req.organization) {
110
+ throw new common_1.BadRequestException('Organization context required');
111
+ }
112
+ this.assertCanInvite(req, routeGroup, 'store');
45
113
  return this.invitations.create({
46
114
  email: body.email,
47
115
  roleId: body.roleId,
48
116
  organization: req.organization,
49
117
  invitedBy: req.user,
118
+ routeGroup,
50
119
  });
51
120
  }
121
+ /**
122
+ * Whether this invite requires an organization context. We default to YES to
123
+ * preserve legacy behavior (store always required an org). An invite is only
124
+ * treated as non-tenant — and thus org-optional — when membership enforcement
125
+ * is on AND the resolved group is positively a non-tenant group. This keeps
126
+ * every flag-off code path byte-for-byte unchanged.
127
+ */
128
+ isTenantInvite(routeGroup) {
129
+ if (!this.config.enforceGroupMembership())
130
+ return true;
131
+ if (routeGroup == null)
132
+ return true;
133
+ return this.config.isTenantGroup(routeGroup);
134
+ }
52
135
  async resend(req, id) {
53
- this.assertInvitationPermission(req, 'resend');
136
+ this.assertCanInvite(req, req.__routeGroup ?? null, 'resend');
54
137
  return this.invitations.resend(parseInt(id, 10));
55
138
  }
56
139
  async cancel(req, id) {
57
- this.assertInvitationPermission(req, 'cancel');
140
+ this.assertCanInvite(req, req.__routeGroup ?? null, 'cancel');
58
141
  return this.invitations.cancel(parseInt(id, 10));
59
142
  }
60
143
  async accept(body, req) {
@@ -106,6 +189,8 @@ __decorate([
106
189
  ], InvitationController.prototype, "accept", null);
107
190
  exports.InvitationController = InvitationController = __decorate([
108
191
  (0, common_1.Controller)('invitations'),
109
- __metadata("design:paramtypes", [invitation_service_1.InvitationService])
192
+ __metadata("design:paramtypes", [invitation_service_1.InvitationService,
193
+ rhino_config_1.RhinoConfigService,
194
+ membership_service_1.MembershipService])
110
195
  ], InvitationController);
111
196
  //# sourceMappingURL=invitation.controller.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"invitation.controller.js","sourceRoot":"","sources":["../../src/controllers/invitation.controller.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"invitation.controller.js","sourceRoot":"","sources":["../../src/controllers/invitation.controller.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { HttpException } from '@nestjs/common';
2
- export type RhinoErrorCode = 'VALIDATION_FAILED' | 'FORBIDDEN_FIELDS' | 'CROSS_TENANT' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NOT_FOUND' | 'ACTION_DISABLED' | 'UNKNOWN_RESOURCE' | 'INVITATION_EXPIRED' | 'INVITATION_USED' | 'INVITATION_INVALID_ROLE' | 'NESTED_OPERATION_FAILED' | 'INCLUDE_NOT_AUTHORIZED';
2
+ export type RhinoErrorCode = 'VALIDATION_FAILED' | 'FORBIDDEN_FIELDS' | 'CROSS_TENANT' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NOT_FOUND' | 'ACTION_DISABLED' | 'UNKNOWN_RESOURCE' | 'INVITATION_EXPIRED' | 'INVITATION_USED' | 'INVITATION_INVALID_ROLE' | 'NESTED_OPERATION_FAILED' | 'INCLUDE_NOT_AUTHORIZED' | 'AUTH_REJECTED' | 'MEMBERSHIP_DENIED';
3
3
  export interface RhinoErrorBody {
4
4
  code: RhinoErrorCode;
5
5
  message: string;
@@ -21,4 +21,15 @@ export declare class RhinoException extends HttpException {
21
21
  static actionDisabled(action: string): RhinoException;
22
22
  static unknownResource(slug: string): RhinoException;
23
23
  static includeNotAuthorized(slug: string): RhinoException;
24
+ static authRejected(message?: string, status?: number): RhinoException;
25
+ static membershipDenied(message?: string): RhinoException;
26
+ }
27
+ /**
28
+ * Thrown by a lifecycle hook to reject an otherwise-successful auth action.
29
+ * The controller catches it: for token-issuing actions (login/register) the
30
+ * just-issued token is revoked; the configured `status` (default 403) is then
31
+ * returned to the client. A hook may also throw any other error.
32
+ */
33
+ export declare class RhinoAuthRejected extends RhinoException {
34
+ constructor(message?: string, status?: number);
24
35
  }