@rhino-dev/rhino-nestjs 0.2.5 → 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.
- package/README.md +52 -1
- package/dist/cli/index.js +0 -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 +6 -2
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 |
|
package/dist/cli/index.js
CHANGED
|
File without changes
|
|
@@ -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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":";;;;;;;;;;;;;;;AAAA,2CAMwB;AACxB,2DAAuD;AACvD,uEAAmE;AACnE,6DAAyD;AACzD,kDAAqD;
|
|
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
|
-
|
|
5
|
-
private
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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":";;;;;;;;;;;;;;;AAAA,2CAUwB;AACxB,uEAAmE;AACnE,oEAAgE;
|
|
1
|
+
{"version":3,"file":"invitation.controller.js","sourceRoot":"","sources":["../../src/controllers/invitation.controller.ts"],"names":[],"mappings":"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"}
|