@hazeljs/auth 0.2.0-alpha.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/LICENSE +192 -0
- package/README.md +499 -0
- package/dist/auth.guard.d.ts +15 -0
- package/dist/auth.guard.d.ts.map +1 -0
- package/dist/auth.guard.js +96 -0
- package/dist/auth.service.d.ts +13 -0
- package/dist/auth.service.d.ts.map +1 -0
- package/dist/auth.service.js +38 -0
- package/dist/auth.test.d.ts +2 -0
- package/dist/auth.test.d.ts.map +1 -0
- package/dist/auth.test.js +682 -0
- package/dist/decorators/current-user.decorator.d.ts +26 -0
- package/dist/decorators/current-user.decorator.d.ts.map +1 -0
- package/dist/decorators/current-user.decorator.js +39 -0
- package/dist/guards/jwt-auth.guard.d.ts +24 -0
- package/dist/guards/jwt-auth.guard.d.ts.map +1 -0
- package/dist/guards/jwt-auth.guard.js +61 -0
- package/dist/guards/role.guard.d.ts +36 -0
- package/dist/guards/role.guard.d.ts.map +1 -0
- package/dist/guards/role.guard.js +66 -0
- package/dist/guards/tenant.guard.d.ts +54 -0
- package/dist/guards/tenant.guard.d.ts.map +1 -0
- package/dist/guards/tenant.guard.js +96 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/jwt/jwt.module.d.ts +5 -0
- package/dist/jwt/jwt.module.d.ts.map +1 -0
- package/dist/jwt/jwt.module.js +27 -0
- package/dist/jwt/jwt.service.d.ts +25 -0
- package/dist/jwt/jwt.service.d.ts.map +1 -0
- package/dist/jwt/jwt.service.js +61 -0
- package/dist/tenant/tenant-context.d.ts +81 -0
- package/dist/tenant/tenant-context.d.ts.map +1 -0
- package/dist/tenant/tenant-context.js +108 -0
- package/dist/utils/role-hierarchy.d.ts +42 -0
- package/dist/utils/role-hierarchy.d.ts.map +1 -0
- package/dist/utils/role-hierarchy.js +57 -0
- package/package.json +55 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
/**
|
|
3
|
+
* Parameter decorator that injects the authenticated user from the request
|
|
4
|
+
* context into a controller method parameter.
|
|
5
|
+
*
|
|
6
|
+
* Requires JwtAuthGuard (or any guard that sets `req.user`) to run before
|
|
7
|
+
* the route handler. The value injected is the `AuthUser` object attached
|
|
8
|
+
* by the guard.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* @UseGuards(JwtAuthGuard)
|
|
13
|
+
* @Get('/profile')
|
|
14
|
+
* getProfile(@CurrentUser() user: AuthUser) {
|
|
15
|
+
* return user;
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* // Access a specific field:
|
|
19
|
+
* @Get('/me')
|
|
20
|
+
* whoAmI(@CurrentUser('role') role: string) {
|
|
21
|
+
* return { role };
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function CurrentUser(field?: string): ParameterDecorator;
|
|
26
|
+
//# sourceMappingURL=current-user.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"current-user.decorator.d.ts","sourceRoot":"","sources":["../../src/decorators/current-user.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAI1B;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAe9D"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CurrentUser = CurrentUser;
|
|
4
|
+
require("reflect-metadata");
|
|
5
|
+
const INJECT_METADATA_KEY = 'hazel:inject';
|
|
6
|
+
/**
|
|
7
|
+
* Parameter decorator that injects the authenticated user from the request
|
|
8
|
+
* context into a controller method parameter.
|
|
9
|
+
*
|
|
10
|
+
* Requires JwtAuthGuard (or any guard that sets `req.user`) to run before
|
|
11
|
+
* the route handler. The value injected is the `AuthUser` object attached
|
|
12
|
+
* by the guard.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* @UseGuards(JwtAuthGuard)
|
|
17
|
+
* @Get('/profile')
|
|
18
|
+
* getProfile(@CurrentUser() user: AuthUser) {
|
|
19
|
+
* return user;
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* // Access a specific field:
|
|
23
|
+
* @Get('/me')
|
|
24
|
+
* whoAmI(@CurrentUser('role') role: string) {
|
|
25
|
+
* return { role };
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function CurrentUser(field) {
|
|
30
|
+
return (target, propertyKey, parameterIndex) => {
|
|
31
|
+
if (!propertyKey) {
|
|
32
|
+
throw new Error('@CurrentUser() must be used on a method parameter');
|
|
33
|
+
}
|
|
34
|
+
const constructor = target.constructor;
|
|
35
|
+
const injections = Reflect.getMetadata(INJECT_METADATA_KEY, constructor, propertyKey) ?? [];
|
|
36
|
+
injections[parameterIndex] = { type: 'user', field };
|
|
37
|
+
Reflect.defineMetadata(INJECT_METADATA_KEY, injections, constructor, propertyKey);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext } from '@hazeljs/core';
|
|
2
|
+
import { AuthService } from '../auth.service';
|
|
3
|
+
/**
|
|
4
|
+
* Guard that verifies a Bearer JWT token and attaches the decoded user to
|
|
5
|
+
* the request object. Use this with @UseGuards() on controllers or methods.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* @UseGuards(JwtAuthGuard)
|
|
10
|
+
* @Controller('/profile')
|
|
11
|
+
* export class ProfileController {
|
|
12
|
+
* @Get('/')
|
|
13
|
+
* getProfile(@CurrentUser() user: AuthUser) {
|
|
14
|
+
* return user;
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare class JwtAuthGuard implements CanActivate {
|
|
20
|
+
private readonly authService;
|
|
21
|
+
constructor(authService: AuthService);
|
|
22
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=jwt-auth.guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt-auth.guard.d.ts","sourceRoot":"","sources":["../../src/guards/jwt-auth.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C;;;;;;;;;;;;;;;GAeG;AACH,qBACa,YAAa,YAAW,WAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW;gBAAX,WAAW,EAAE,WAAW;IAE/C,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;CA2B/D"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.JwtAuthGuard = void 0;
|
|
13
|
+
const core_1 = require("@hazeljs/core");
|
|
14
|
+
const auth_service_1 = require("../auth.service");
|
|
15
|
+
/**
|
|
16
|
+
* Guard that verifies a Bearer JWT token and attaches the decoded user to
|
|
17
|
+
* the request object. Use this with @UseGuards() on controllers or methods.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* @UseGuards(JwtAuthGuard)
|
|
22
|
+
* @Controller('/profile')
|
|
23
|
+
* export class ProfileController {
|
|
24
|
+
* @Get('/')
|
|
25
|
+
* getProfile(@CurrentUser() user: AuthUser) {
|
|
26
|
+
* return user;
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
let JwtAuthGuard = class JwtAuthGuard {
|
|
32
|
+
constructor(authService) {
|
|
33
|
+
this.authService = authService;
|
|
34
|
+
}
|
|
35
|
+
async canActivate(context) {
|
|
36
|
+
const req = context.switchToHttp().getRequest();
|
|
37
|
+
const authHeader = req.headers?.['authorization'];
|
|
38
|
+
if (!authHeader) {
|
|
39
|
+
const err = Object.assign(new Error('No authorization header'), { status: 400 });
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
const [scheme, token] = authHeader.split(' ');
|
|
43
|
+
if (scheme?.toLowerCase() !== 'bearer' || !token) {
|
|
44
|
+
const err = Object.assign(new Error('Invalid authorization header format'), { status: 400 });
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
const user = await this.authService.verifyToken(token);
|
|
48
|
+
if (!user) {
|
|
49
|
+
const err = Object.assign(new Error('Invalid or expired token'), { status: 401 });
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
// Attach to req so the router propagates it to context.user and @CurrentUser() can read it.
|
|
53
|
+
req.user = user;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
exports.JwtAuthGuard = JwtAuthGuard;
|
|
58
|
+
exports.JwtAuthGuard = JwtAuthGuard = __decorate([
|
|
59
|
+
(0, core_1.Injectable)(),
|
|
60
|
+
__metadata("design:paramtypes", [auth_service_1.AuthService])
|
|
61
|
+
], JwtAuthGuard);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CanActivate, Type } from '@hazeljs/core';
|
|
2
|
+
import { RoleHierarchy, RoleHierarchyMap } from '../utils/role-hierarchy';
|
|
3
|
+
export interface RoleGuardOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Custom role hierarchy to use instead of DEFAULT_ROLE_HIERARCHY.
|
|
6
|
+
* Pass a plain map or a RoleHierarchy instance.
|
|
7
|
+
*/
|
|
8
|
+
hierarchy?: RoleHierarchyMap | RoleHierarchy;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Factory that returns a guard allowing only users whose role satisfies at
|
|
12
|
+
* least one of the provided roles — directly or via the role hierarchy.
|
|
13
|
+
*
|
|
14
|
+
* By default uses DEFAULT_ROLE_HIERARCHY so `admin` automatically passes a
|
|
15
|
+
* `manager` check, `manager` passes a `user` check, etc.
|
|
16
|
+
*
|
|
17
|
+
* Must be used after JwtAuthGuard (or any guard that attaches `user` to the
|
|
18
|
+
* request).
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* // admin, manager, AND user can access this:
|
|
23
|
+
* @UseGuards(JwtAuthGuard, RoleGuard('user'))
|
|
24
|
+
*
|
|
25
|
+
* // Only admin and above (superadmin) can access this:
|
|
26
|
+
* @UseGuards(JwtAuthGuard, RoleGuard('admin'))
|
|
27
|
+
*
|
|
28
|
+
* // Custom hierarchy (no inheritance):
|
|
29
|
+
* @UseGuards(JwtAuthGuard, RoleGuard('admin', { hierarchy: {} }))
|
|
30
|
+
*
|
|
31
|
+
* // Multiple accepted roles:
|
|
32
|
+
* @UseGuards(JwtAuthGuard, RoleGuard('admin', 'moderator'))
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function RoleGuard(...args: Array<string | RoleGuardOptions>): Type<CanActivate>;
|
|
36
|
+
//# sourceMappingURL=role.guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role.guard.d.ts","sourceRoot":"","sources":["../../src/guards/role.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,WAAW,EAAoB,IAAI,EAAE,MAAM,eAAe,CAAC;AAEhF,OAAO,EAAE,aAAa,EAA0B,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAElG,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,gBAAgB,GAAG,aAAa,CAAC;CAC9C;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,MAAM,GAAG,gBAAgB,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,CAwCtF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.RoleGuard = RoleGuard;
|
|
10
|
+
const core_1 = require("@hazeljs/core");
|
|
11
|
+
const role_hierarchy_1 = require("../utils/role-hierarchy");
|
|
12
|
+
/**
|
|
13
|
+
* Factory that returns a guard allowing only users whose role satisfies at
|
|
14
|
+
* least one of the provided roles — directly or via the role hierarchy.
|
|
15
|
+
*
|
|
16
|
+
* By default uses DEFAULT_ROLE_HIERARCHY so `admin` automatically passes a
|
|
17
|
+
* `manager` check, `manager` passes a `user` check, etc.
|
|
18
|
+
*
|
|
19
|
+
* Must be used after JwtAuthGuard (or any guard that attaches `user` to the
|
|
20
|
+
* request).
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // admin, manager, AND user can access this:
|
|
25
|
+
* @UseGuards(JwtAuthGuard, RoleGuard('user'))
|
|
26
|
+
*
|
|
27
|
+
* // Only admin and above (superadmin) can access this:
|
|
28
|
+
* @UseGuards(JwtAuthGuard, RoleGuard('admin'))
|
|
29
|
+
*
|
|
30
|
+
* // Custom hierarchy (no inheritance):
|
|
31
|
+
* @UseGuards(JwtAuthGuard, RoleGuard('admin', { hierarchy: {} }))
|
|
32
|
+
*
|
|
33
|
+
* // Multiple accepted roles:
|
|
34
|
+
* @UseGuards(JwtAuthGuard, RoleGuard('admin', 'moderator'))
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function RoleGuard(...args) {
|
|
38
|
+
// Separate trailing options object from role strings
|
|
39
|
+
const lastArg = args[args.length - 1];
|
|
40
|
+
const hasOptions = typeof lastArg === 'object' && lastArg !== null;
|
|
41
|
+
const roles = (hasOptions ? args.slice(0, -1) : args);
|
|
42
|
+
const options = (hasOptions ? lastArg : {});
|
|
43
|
+
const hierarchyInstance = options.hierarchy instanceof role_hierarchy_1.RoleHierarchy
|
|
44
|
+
? options.hierarchy
|
|
45
|
+
: new role_hierarchy_1.RoleHierarchy(options.hierarchy ?? role_hierarchy_1.DEFAULT_ROLE_HIERARCHY);
|
|
46
|
+
let RoleGuardMixin = class RoleGuardMixin {
|
|
47
|
+
canActivate(context) {
|
|
48
|
+
const req = context.switchToHttp().getRequest();
|
|
49
|
+
const user = req.user;
|
|
50
|
+
if (!user) {
|
|
51
|
+
const err = Object.assign(new Error('Unauthorized'), { status: 401 });
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
const permitted = roles.some((required) => hierarchyInstance.satisfies(user.role, required));
|
|
55
|
+
if (!permitted) {
|
|
56
|
+
const err = Object.assign(new Error(`Requires one of the following roles: ${roles.join(', ')}`), { status: 403 });
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
RoleGuardMixin = __decorate([
|
|
63
|
+
(0, core_1.Injectable)()
|
|
64
|
+
], RoleGuardMixin);
|
|
65
|
+
return RoleGuardMixin;
|
|
66
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { CanActivate, Type } from '@hazeljs/core';
|
|
2
|
+
export interface TenantGuardOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Where to read the expected tenant ID from the incoming request.
|
|
5
|
+
*
|
|
6
|
+
* - `'param'` — URL segment, e.g. `/orgs/:tenantId/products` (default)
|
|
7
|
+
* - `'header'` — HTTP header, e.g. `X-Tenant-ID`
|
|
8
|
+
* - `'query'` — Query string, e.g. `?tenantId=acme`
|
|
9
|
+
*
|
|
10
|
+
* @default 'param'
|
|
11
|
+
*/
|
|
12
|
+
source?: 'param' | 'header' | 'query';
|
|
13
|
+
/**
|
|
14
|
+
* Name of the param / header / query key that carries the tenant ID.
|
|
15
|
+
* @default 'tenantId'
|
|
16
|
+
*/
|
|
17
|
+
key?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Field on the authenticated user object that holds the user's tenant ID.
|
|
20
|
+
* @default 'tenantId'
|
|
21
|
+
*/
|
|
22
|
+
userField?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Factory that returns a guard enforcing tenant-level isolation.
|
|
26
|
+
*
|
|
27
|
+
* Compares the tenant ID carried by the request (from a URL param, header,
|
|
28
|
+
* or query param) against the tenant ID stored on the authenticated user
|
|
29
|
+
* (from the JWT payload). Returns 403 if they do not match.
|
|
30
|
+
*
|
|
31
|
+
* Must be used after JwtAuthGuard so that `req.user` is populated.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* // URL param: GET /orgs/:tenantId/invoices
|
|
36
|
+
* @UseGuards(JwtAuthGuard, TenantGuard())
|
|
37
|
+
* @Controller('/orgs/:tenantId/invoices')
|
|
38
|
+
* export class InvoicesController {}
|
|
39
|
+
*
|
|
40
|
+
* // Header: X-Org-ID
|
|
41
|
+
* @UseGuards(JwtAuthGuard, TenantGuard({ source: 'header', key: 'x-org-id' }))
|
|
42
|
+
* @Controller('/invoices')
|
|
43
|
+
* export class InvoicesController {}
|
|
44
|
+
*
|
|
45
|
+
* // Superadmins bypass tenant check:
|
|
46
|
+
* @UseGuards(JwtAuthGuard, TenantGuard({ bypassRoles: ['superadmin'] }))
|
|
47
|
+
* @Controller('/orgs/:tenantId/invoices')
|
|
48
|
+
* export class InvoicesController {}
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function TenantGuard(options?: TenantGuardOptions & {
|
|
52
|
+
bypassRoles?: string[];
|
|
53
|
+
}): Type<CanActivate>;
|
|
54
|
+
//# sourceMappingURL=tenant.guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant.guard.d.ts","sourceRoot":"","sources":["../../src/guards/tenant.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,WAAW,EAAoB,IAAI,EAAkB,MAAM,eAAe,CAAC;AAIhG,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;IAEtC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,WAAW,CACzB,OAAO,GAAE,kBAAkB,GAAG;IAAE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GAC5D,IAAI,CAAC,WAAW,CAAC,CAsEnB"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.TenantGuard = TenantGuard;
|
|
10
|
+
const core_1 = require("@hazeljs/core");
|
|
11
|
+
const tenant_context_1 = require("../tenant/tenant-context");
|
|
12
|
+
/**
|
|
13
|
+
* Factory that returns a guard enforcing tenant-level isolation.
|
|
14
|
+
*
|
|
15
|
+
* Compares the tenant ID carried by the request (from a URL param, header,
|
|
16
|
+
* or query param) against the tenant ID stored on the authenticated user
|
|
17
|
+
* (from the JWT payload). Returns 403 if they do not match.
|
|
18
|
+
*
|
|
19
|
+
* Must be used after JwtAuthGuard so that `req.user` is populated.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* // URL param: GET /orgs/:tenantId/invoices
|
|
24
|
+
* @UseGuards(JwtAuthGuard, TenantGuard())
|
|
25
|
+
* @Controller('/orgs/:tenantId/invoices')
|
|
26
|
+
* export class InvoicesController {}
|
|
27
|
+
*
|
|
28
|
+
* // Header: X-Org-ID
|
|
29
|
+
* @UseGuards(JwtAuthGuard, TenantGuard({ source: 'header', key: 'x-org-id' }))
|
|
30
|
+
* @Controller('/invoices')
|
|
31
|
+
* export class InvoicesController {}
|
|
32
|
+
*
|
|
33
|
+
* // Superadmins bypass tenant check:
|
|
34
|
+
* @UseGuards(JwtAuthGuard, TenantGuard({ bypassRoles: ['superadmin'] }))
|
|
35
|
+
* @Controller('/orgs/:tenantId/invoices')
|
|
36
|
+
* export class InvoicesController {}
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
function TenantGuard(options = {}) {
|
|
40
|
+
const { source = 'param', key = 'tenantId', userField = 'tenantId', bypassRoles = [] } = options;
|
|
41
|
+
let TenantGuardMixin = class TenantGuardMixin {
|
|
42
|
+
canActivate(context) {
|
|
43
|
+
const req = context.switchToHttp().getRequest();
|
|
44
|
+
const ctx = context.switchToHttp().getContext();
|
|
45
|
+
const user = req.user;
|
|
46
|
+
if (!user) {
|
|
47
|
+
const err = Object.assign(new Error('Unauthorized'), { status: 401 });
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
// Privileged roles can bypass the tenant check entirely.
|
|
51
|
+
// Still seed the context so their own queries are naturally scoped.
|
|
52
|
+
if (bypassRoles.includes(user.role)) {
|
|
53
|
+
const bypassTenantId = user[userField];
|
|
54
|
+
if (bypassTenantId)
|
|
55
|
+
tenant_context_1.TenantContext.enterWith(bypassTenantId);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const userTenantId = user[userField];
|
|
59
|
+
if (!userTenantId) {
|
|
60
|
+
const err = Object.assign(new Error(`User is not associated with any tenant (missing "${userField}")`), { status: 403 });
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
// Extract the request-level tenant ID from the configured source.
|
|
64
|
+
let requestTenantId;
|
|
65
|
+
switch (source) {
|
|
66
|
+
case 'param':
|
|
67
|
+
requestTenantId = ctx.params?.[key];
|
|
68
|
+
break;
|
|
69
|
+
case 'header':
|
|
70
|
+
requestTenantId = ctx.headers?.[key.toLowerCase()];
|
|
71
|
+
break;
|
|
72
|
+
case 'query':
|
|
73
|
+
requestTenantId = ctx.query?.[key];
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
if (!requestTenantId) {
|
|
77
|
+
const err = Object.assign(new Error(`Tenant ID not found in request ${source} "${key}"`), {
|
|
78
|
+
status: 400,
|
|
79
|
+
});
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
if (userTenantId !== requestTenantId) {
|
|
83
|
+
const err = Object.assign(new Error('Access denied: resource belongs to a different tenant'), { status: 403 });
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
// Seed AsyncLocalStorage so repositories can call tenantCtx.requireId()
|
|
87
|
+
// without needing tenantId passed through every function parameter.
|
|
88
|
+
tenant_context_1.TenantContext.enterWith(userTenantId);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
TenantGuardMixin = __decorate([
|
|
93
|
+
(0, core_1.Injectable)()
|
|
94
|
+
], TenantGuardMixin);
|
|
95
|
+
return TenantGuardMixin;
|
|
96
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hazeljs/auth - Authentication module for HazelJS
|
|
3
|
+
*/
|
|
4
|
+
export { AuthGuard, Auth } from './auth.guard';
|
|
5
|
+
export { AuthService } from './auth.service';
|
|
6
|
+
export type { AuthUser } from './auth.service';
|
|
7
|
+
export { JwtModule } from './jwt/jwt.module';
|
|
8
|
+
export { JwtService } from './jwt/jwt.service';
|
|
9
|
+
export { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|
10
|
+
export { RoleGuard } from './guards/role.guard';
|
|
11
|
+
export type { RoleGuardOptions } from './guards/role.guard';
|
|
12
|
+
export { TenantGuard } from './guards/tenant.guard';
|
|
13
|
+
export type { TenantGuardOptions } from './guards/tenant.guard';
|
|
14
|
+
export { TenantContext } from './tenant/tenant-context';
|
|
15
|
+
export { RoleHierarchy, DEFAULT_ROLE_HIERARCHY } from './utils/role-hierarchy';
|
|
16
|
+
export type { RoleHierarchyMap } from './utils/role-hierarchy';
|
|
17
|
+
export { CurrentUser } from './decorators/current-user.decorator';
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,YAAY,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,YAAY,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAC/E,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @hazeljs/auth - Authentication module for HazelJS
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CurrentUser = exports.DEFAULT_ROLE_HIERARCHY = exports.RoleHierarchy = exports.TenantContext = exports.TenantGuard = exports.RoleGuard = exports.JwtAuthGuard = exports.JwtService = exports.JwtModule = exports.AuthService = exports.Auth = exports.AuthGuard = void 0;
|
|
7
|
+
var auth_guard_1 = require("./auth.guard");
|
|
8
|
+
Object.defineProperty(exports, "AuthGuard", { enumerable: true, get: function () { return auth_guard_1.AuthGuard; } });
|
|
9
|
+
Object.defineProperty(exports, "Auth", { enumerable: true, get: function () { return auth_guard_1.Auth; } });
|
|
10
|
+
var auth_service_1 = require("./auth.service");
|
|
11
|
+
Object.defineProperty(exports, "AuthService", { enumerable: true, get: function () { return auth_service_1.AuthService; } });
|
|
12
|
+
var jwt_module_1 = require("./jwt/jwt.module");
|
|
13
|
+
Object.defineProperty(exports, "JwtModule", { enumerable: true, get: function () { return jwt_module_1.JwtModule; } });
|
|
14
|
+
var jwt_service_1 = require("./jwt/jwt.service");
|
|
15
|
+
Object.defineProperty(exports, "JwtService", { enumerable: true, get: function () { return jwt_service_1.JwtService; } });
|
|
16
|
+
var jwt_auth_guard_1 = require("./guards/jwt-auth.guard");
|
|
17
|
+
Object.defineProperty(exports, "JwtAuthGuard", { enumerable: true, get: function () { return jwt_auth_guard_1.JwtAuthGuard; } });
|
|
18
|
+
var role_guard_1 = require("./guards/role.guard");
|
|
19
|
+
Object.defineProperty(exports, "RoleGuard", { enumerable: true, get: function () { return role_guard_1.RoleGuard; } });
|
|
20
|
+
var tenant_guard_1 = require("./guards/tenant.guard");
|
|
21
|
+
Object.defineProperty(exports, "TenantGuard", { enumerable: true, get: function () { return tenant_guard_1.TenantGuard; } });
|
|
22
|
+
var tenant_context_1 = require("./tenant/tenant-context");
|
|
23
|
+
Object.defineProperty(exports, "TenantContext", { enumerable: true, get: function () { return tenant_context_1.TenantContext; } });
|
|
24
|
+
var role_hierarchy_1 = require("./utils/role-hierarchy");
|
|
25
|
+
Object.defineProperty(exports, "RoleHierarchy", { enumerable: true, get: function () { return role_hierarchy_1.RoleHierarchy; } });
|
|
26
|
+
Object.defineProperty(exports, "DEFAULT_ROLE_HIERARCHY", { enumerable: true, get: function () { return role_hierarchy_1.DEFAULT_ROLE_HIERARCHY; } });
|
|
27
|
+
var current_user_decorator_1 = require("./decorators/current-user.decorator");
|
|
28
|
+
Object.defineProperty(exports, "CurrentUser", { enumerable: true, get: function () { return current_user_decorator_1.CurrentUser; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.module.d.ts","sourceRoot":"","sources":["../../src/jwt/jwt.module.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAE9D,qBAIa,SAAS;IACpB,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,SAAS;CAM9D"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var JwtModule_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.JwtModule = void 0;
|
|
11
|
+
const core_1 = require("@hazeljs/core");
|
|
12
|
+
const jwt_service_1 = require("./jwt.service");
|
|
13
|
+
let JwtModule = JwtModule_1 = class JwtModule {
|
|
14
|
+
static forRoot(options) {
|
|
15
|
+
if (options) {
|
|
16
|
+
jwt_service_1.JwtService.configure(options);
|
|
17
|
+
}
|
|
18
|
+
return JwtModule_1;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
exports.JwtModule = JwtModule;
|
|
22
|
+
exports.JwtModule = JwtModule = JwtModule_1 = __decorate([
|
|
23
|
+
(0, core_1.HazelModule)({
|
|
24
|
+
providers: [jwt_service_1.JwtService],
|
|
25
|
+
exports: [jwt_service_1.JwtService],
|
|
26
|
+
})
|
|
27
|
+
], JwtModule);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface JwtPayload {
|
|
2
|
+
sub: string;
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
}
|
|
5
|
+
export interface JwtServiceOptions {
|
|
6
|
+
secret?: string;
|
|
7
|
+
expiresIn?: string | number;
|
|
8
|
+
issuer?: string;
|
|
9
|
+
audience?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class JwtService {
|
|
12
|
+
private readonly secret;
|
|
13
|
+
private readonly defaultExpiresIn;
|
|
14
|
+
private readonly issuer?;
|
|
15
|
+
private readonly audience?;
|
|
16
|
+
constructor();
|
|
17
|
+
private static moduleOptions;
|
|
18
|
+
static configure(options: JwtServiceOptions): void;
|
|
19
|
+
sign(payload: JwtPayload, options?: {
|
|
20
|
+
expiresIn?: string | number;
|
|
21
|
+
}): string;
|
|
22
|
+
verify(token: string): JwtPayload;
|
|
23
|
+
decode(token: string): JwtPayload | null;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=jwt.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.service.d.ts","sourceRoot":"","sources":["../../src/jwt/jwt.service.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBACa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAkB;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAS;;IAgBnC,OAAO,CAAC,MAAM,CAAC,aAAa,CAAyB;IAErD,MAAM,CAAC,SAAS,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAIlD,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,MAAM;IAU5E,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU;IAQjC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;CAIzC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
var JwtService_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.JwtService = void 0;
|
|
17
|
+
const core_1 = require("@hazeljs/core");
|
|
18
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
19
|
+
let JwtService = JwtService_1 = class JwtService {
|
|
20
|
+
constructor() {
|
|
21
|
+
const options = JwtService_1.moduleOptions;
|
|
22
|
+
this.secret = options.secret || process.env.JWT_SECRET || '';
|
|
23
|
+
this.defaultExpiresIn = options.expiresIn || process.env.JWT_EXPIRES_IN || '1h';
|
|
24
|
+
this.issuer = options.issuer || process.env.JWT_ISSUER;
|
|
25
|
+
this.audience = options.audience || process.env.JWT_AUDIENCE;
|
|
26
|
+
if (!this.secret) {
|
|
27
|
+
throw new Error('JWT secret is not configured. Set JWT_SECRET environment variable or pass secret via JwtModule.forRoot({ secret: "..." })');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
static configure(options) {
|
|
31
|
+
JwtService_1.moduleOptions = options;
|
|
32
|
+
}
|
|
33
|
+
sign(payload, options) {
|
|
34
|
+
const signOptions = {
|
|
35
|
+
expiresIn: (options?.expiresIn || this.defaultExpiresIn),
|
|
36
|
+
};
|
|
37
|
+
if (this.issuer)
|
|
38
|
+
signOptions.issuer = this.issuer;
|
|
39
|
+
if (this.audience)
|
|
40
|
+
signOptions.audience = this.audience;
|
|
41
|
+
return jsonwebtoken_1.default.sign(payload, this.secret, signOptions);
|
|
42
|
+
}
|
|
43
|
+
verify(token) {
|
|
44
|
+
const verifyOptions = {};
|
|
45
|
+
if (this.issuer)
|
|
46
|
+
verifyOptions.issuer = this.issuer;
|
|
47
|
+
if (this.audience)
|
|
48
|
+
verifyOptions.audience = this.audience;
|
|
49
|
+
return jsonwebtoken_1.default.verify(token, this.secret, verifyOptions);
|
|
50
|
+
}
|
|
51
|
+
decode(token) {
|
|
52
|
+
const decoded = jsonwebtoken_1.default.decode(token);
|
|
53
|
+
return decoded;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
exports.JwtService = JwtService;
|
|
57
|
+
JwtService.moduleOptions = {};
|
|
58
|
+
exports.JwtService = JwtService = JwtService_1 = __decorate([
|
|
59
|
+
(0, core_1.Service)(),
|
|
60
|
+
__metadata("design:paramtypes", [])
|
|
61
|
+
], JwtService);
|