exguard-backend 1.1.0 → 1.1.2
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 +43 -38
- package/package.json +2 -2
- package/scripts/setup.cjs +213 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ExGuard Backend SDK
|
|
2
2
|
|
|
3
|
-
Simple RBAC
|
|
3
|
+
Simple RBAC permission guard for NestJS.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,11 +8,23 @@ Simple RBAC/ABAC permission guard for NestJS.
|
|
|
8
8
|
npm install exguard-backend
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
## Quick Setup
|
|
11
|
+
## Quick Setup (Auto)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Run this command in your NestJS project root:
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npx exguard-backend setup
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This creates:
|
|
20
|
+
- `src/exguard/exguard.guard.ts` - The permission guard
|
|
21
|
+
- `src/exguard/exguard.module.ts` - The module
|
|
22
|
+
|
|
23
|
+
## Manual Setup
|
|
24
|
+
|
|
25
|
+
### 1. Create Guard File
|
|
26
|
+
|
|
27
|
+
`src/exguard/exguard.guard.ts`:
|
|
16
28
|
|
|
17
29
|
```typescript
|
|
18
30
|
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, ForbiddenException, Inject, Optional } from '@nestjs/common';
|
|
@@ -37,26 +49,16 @@ export class ExGuardPermissionGuard implements CanActivate {
|
|
|
37
49
|
if (!this.exGuard) return true;
|
|
38
50
|
|
|
39
51
|
const authResult = await this.exGuard.authenticate({ token, request });
|
|
40
|
-
|
|
41
52
|
if (!authResult.allowed) throw new ForbiddenException(authResult.error || 'Access denied');
|
|
42
53
|
if (!authResult.user) throw new ForbiddenException('User not found');
|
|
43
54
|
|
|
44
|
-
const
|
|
45
|
-
const permMeta = this.reflector.get(EXGUARD_PERMISSIONS_KEY, handler);
|
|
46
|
-
|
|
55
|
+
const permMeta = this.reflector.get(EXGUARD_PERMISSIONS_KEY, context.getHandler());
|
|
47
56
|
if (permMeta) {
|
|
48
|
-
const { permissions, requireAll } = permMeta;
|
|
49
57
|
const userPermissions = authResult.user.modules?.flatMap(m => m.permissions) || [];
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
} else {
|
|
56
|
-
if (!permissions.some(p => userPermissions.includes(p))) {
|
|
57
|
-
throw new ForbiddenException('Insufficient permissions');
|
|
58
|
-
}
|
|
59
|
-
}
|
|
58
|
+
const hasPermission = permMeta.requireAll
|
|
59
|
+
? permMeta.permissions.every(p => userPermissions.includes(p))
|
|
60
|
+
: permMeta.permissions.some(p => userPermissions.includes(p));
|
|
61
|
+
if (!hasPermission) throw new ForbiddenException('Insufficient permissions');
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
request.user = authResult.user;
|
|
@@ -65,18 +67,21 @@ export class ExGuardPermissionGuard implements CanActivate {
|
|
|
65
67
|
|
|
66
68
|
private extractToken(request: any): string | null {
|
|
67
69
|
const auth = request.headers?.authorization;
|
|
68
|
-
return auth?.startsWith('Bearer ') ? auth.substring(7) :
|
|
70
|
+
return auth?.startsWith('Bearer ') ? auth.substring(7) : null;
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
export function RequirePermissions(permissions: string[], requireAll = false) {
|
|
73
|
-
return (target: any, propertyKey: string, descriptor: PropertyDescriptor)
|
|
74
|
+
export function RequirePermissions(permissions: string[], requireAll = false): any {
|
|
75
|
+
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
74
76
|
Reflect.defineMetadata(EXGUARD_PERMISSIONS_KEY, { permissions, requireAll }, descriptor.value);
|
|
77
|
+
return descriptor;
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
80
|
```
|
|
78
81
|
|
|
79
|
-
Create
|
|
82
|
+
### 2. Create Module File
|
|
83
|
+
|
|
84
|
+
`src/exguard/exguard.module.ts`:
|
|
80
85
|
|
|
81
86
|
```typescript
|
|
82
87
|
import { Module, Global, DynamicModule } from '@nestjs/common';
|
|
@@ -91,19 +96,16 @@ export class ExGuardModule {
|
|
|
91
96
|
apiKey: options.apiKey,
|
|
92
97
|
cache: options.cache || { enabled: true, ttl: 300000 },
|
|
93
98
|
});
|
|
94
|
-
|
|
95
99
|
return {
|
|
96
100
|
module: ExGuardModule,
|
|
97
|
-
providers: [
|
|
98
|
-
{ provide: 'EXGUARD_INSTANCE', useValue: exGuard },
|
|
99
|
-
],
|
|
101
|
+
providers: [{ provide: 'EXGUARD_INSTANCE', useValue: exGuard }],
|
|
100
102
|
exports: ['EXGUARD_INSTANCE'],
|
|
101
103
|
};
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
```
|
|
105
107
|
|
|
106
|
-
###
|
|
108
|
+
### 3. Configure AppModule
|
|
107
109
|
|
|
108
110
|
```typescript
|
|
109
111
|
import { Module } from '@nestjs/common';
|
|
@@ -114,14 +116,13 @@ import { ExGuardModule } from './exguard/exguard.module';
|
|
|
114
116
|
ExGuardModule.forRoot({
|
|
115
117
|
baseUrl: 'https://api.exguard.com',
|
|
116
118
|
apiKey: process.env.EXGUARD_API_KEY,
|
|
117
|
-
cache: { enabled: true, ttl: 300000 },
|
|
118
119
|
}),
|
|
119
120
|
],
|
|
120
121
|
})
|
|
121
122
|
export class AppModule {}
|
|
122
123
|
```
|
|
123
124
|
|
|
124
|
-
###
|
|
125
|
+
### 4. Use in Controllers
|
|
125
126
|
|
|
126
127
|
```typescript
|
|
127
128
|
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
|
|
@@ -131,6 +132,7 @@ import { ExGuardPermissionGuard, RequirePermissions } from '@/exguard/exguard.gu
|
|
|
131
132
|
@UseGuards(ExGuardPermissionGuard)
|
|
132
133
|
export class ItemsController {
|
|
133
134
|
|
|
135
|
+
// User needs ANY of these permissions
|
|
134
136
|
@Get()
|
|
135
137
|
@RequirePermissions(['item:read'])
|
|
136
138
|
findAll() { }
|
|
@@ -139,17 +141,21 @@ export class ItemsController {
|
|
|
139
141
|
@RequirePermissions(['item:create'])
|
|
140
142
|
create() { }
|
|
141
143
|
|
|
142
|
-
|
|
143
|
-
@RequirePermissions(['item:read_draft', 'item:admin']) // ANY of these
|
|
144
|
-
findDrafts() { }
|
|
145
|
-
|
|
144
|
+
// Multiple permissions - needs ALL
|
|
146
145
|
@Delete(':id')
|
|
147
|
-
@RequirePermissions(['item:delete', 'admin'], true)
|
|
146
|
+
@RequirePermissions(['item:delete', 'admin'], true)
|
|
148
147
|
delete() { }
|
|
149
148
|
}
|
|
150
149
|
```
|
|
151
150
|
|
|
152
|
-
##
|
|
151
|
+
## API Reference
|
|
152
|
+
|
|
153
|
+
| Decorator | Description |
|
|
154
|
+
|-----------|-------------|
|
|
155
|
+
| `@RequirePermissions(['perm1'])` | User needs ANY of the permissions |
|
|
156
|
+
| `@RequirePermissions(['perm1', 'perm2'], true)` | User needs ALL permissions |
|
|
157
|
+
|
|
158
|
+
## Token
|
|
153
159
|
|
|
154
160
|
The guard extracts token from:
|
|
155
161
|
- `Authorization: Bearer <token>` header
|
|
@@ -162,7 +168,7 @@ The guard extracts token from:
|
|
|
162
168
|
| baseUrl | string | required | ExGuard API URL |
|
|
163
169
|
| apiKey | string | required | Your API key |
|
|
164
170
|
| cache.enabled | boolean | true | Enable caching |
|
|
165
|
-
| cache.ttl | number | 300000 | Cache TTL
|
|
171
|
+
| cache.ttl | number | 300000 | Cache TTL (5 min) |
|
|
166
172
|
|
|
167
173
|
## Express/Fastify (Non-NestJS)
|
|
168
174
|
|
|
@@ -174,7 +180,6 @@ const guard = createExGuardExpress({
|
|
|
174
180
|
apiKey: process.env.EXGUARD_API_KEY,
|
|
175
181
|
});
|
|
176
182
|
|
|
177
|
-
// Use as middleware
|
|
178
183
|
app.use('/api', guard.requirePermissions(['item:read']));
|
|
179
184
|
```
|
|
180
185
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exguard-backend",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"create-example": "node scripts/create-example.cjs"
|
|
36
36
|
},
|
|
37
37
|
"bin": {
|
|
38
|
-
"exguard-backend": "scripts/setup
|
|
38
|
+
"exguard-backend": "scripts/setup.cjs"
|
|
39
39
|
},
|
|
40
40
|
"keywords": [
|
|
41
41
|
"exguard",
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExGuard NestJS Auto-Setup
|
|
3
|
+
* Run: npx exguard-backend setup
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const EXGUARD_GUARD_CONTENT = `import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, ForbiddenException, Inject, Optional } from '@nestjs/common';
|
|
10
|
+
import { Reflector } from '@nestjs/core';
|
|
11
|
+
import { ExGuardBackend } from 'exguard-backend';
|
|
12
|
+
|
|
13
|
+
export const EXGUARD_PERMISSIONS_KEY = 'exguard_permissions';
|
|
14
|
+
export const EXGUARD_ROLES_KEY = 'exguard_roles';
|
|
15
|
+
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class ExGuardPermissionGuard implements CanActivate {
|
|
18
|
+
constructor(
|
|
19
|
+
@Optional() @Inject('EXGUARD_INSTANCE') private exGuard: ExGuardBackend,
|
|
20
|
+
private reflector: Reflector,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
24
|
+
const request = context.switchToHttp().getRequest();
|
|
25
|
+
const token = this.extractToken(request);
|
|
26
|
+
|
|
27
|
+
if (!token) {
|
|
28
|
+
throw new UnauthorizedException('No token provided');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!this.exGuard) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const authResult = await this.exGuard.authenticate({ token, request });
|
|
36
|
+
|
|
37
|
+
if (!authResult.allowed) {
|
|
38
|
+
throw new ForbiddenException(authResult.error || 'Access denied');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!authResult.user) {
|
|
42
|
+
throw new ForbiddenException('User not found');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const handler = context.getHandler();
|
|
46
|
+
|
|
47
|
+
const permMeta = this.reflector.get<{ permissions: string[]; requireAll?: boolean }>(
|
|
48
|
+
EXGUARD_PERMISSIONS_KEY,
|
|
49
|
+
handler,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (permMeta) {
|
|
53
|
+
const { permissions, requireAll } = permMeta;
|
|
54
|
+
const userPermissions = authResult.user.modules?.flatMap((m: any) => m.permissions) || [];
|
|
55
|
+
|
|
56
|
+
if (requireAll) {
|
|
57
|
+
if (!permissions.every((p: string) => userPermissions.includes(p))) {
|
|
58
|
+
throw new ForbiddenException('Insufficient permissions');
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
if (!permissions.some((p: string) => userPermissions.includes(p))) {
|
|
62
|
+
throw new ForbiddenException('Insufficient permissions');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const roleMeta = this.reflector.get<{ roles: string[]; requireAll?: boolean }>(
|
|
68
|
+
EXGUARD_ROLES_KEY,
|
|
69
|
+
handler,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (roleMeta) {
|
|
73
|
+
const { roles, requireAll } = roleMeta;
|
|
74
|
+
const userRoles = authResult.user.roles || [];
|
|
75
|
+
|
|
76
|
+
if (requireAll) {
|
|
77
|
+
if (!roles.every((r: string) => userRoles.includes(r))) {
|
|
78
|
+
throw new ForbiddenException('Insufficient roles');
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
if (!roles.some((r: string) => userRoles.includes(r))) {
|
|
82
|
+
throw new ForbiddenException('Insufficient roles');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
request.user = authResult.user;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private extractToken(request: any): string | null {
|
|
92
|
+
const auth = request.headers?.authorization;
|
|
93
|
+
if (auth?.startsWith('Bearer ')) {
|
|
94
|
+
return auth.substring(7);
|
|
95
|
+
}
|
|
96
|
+
return request.headers?.['x-access-token'] || null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function RequirePermissions(permissions: string[], requireAll = false): any {
|
|
101
|
+
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
102
|
+
Reflect.defineMetadata(EXGUARD_PERMISSIONS_KEY, { permissions, requireAll }, descriptor.value);
|
|
103
|
+
return descriptor;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function RequireRoles(roles: string[], requireAll = false): any {
|
|
108
|
+
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
|
109
|
+
Reflect.defineMetadata(EXGUARD_ROLES_KEY, { roles, requireAll }, descriptor.value);
|
|
110
|
+
return descriptor;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
`;
|
|
114
|
+
|
|
115
|
+
const EXGUARD_MODULE_CONTENT = `import { Module, Global, DynamicModule } from '@nestjs/common';
|
|
116
|
+
import { ExGuardBackend } from 'exguard-backend';
|
|
117
|
+
|
|
118
|
+
export interface ExGuardModuleOptions {
|
|
119
|
+
baseUrl: string;
|
|
120
|
+
apiKey: string;
|
|
121
|
+
cache?: {
|
|
122
|
+
enabled?: boolean;
|
|
123
|
+
ttl?: number;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Global()
|
|
128
|
+
@Module({})
|
|
129
|
+
export class ExGuardModule {
|
|
130
|
+
static forRoot(options: ExGuardModuleOptions): DynamicModule {
|
|
131
|
+
const exGuard = new ExGuardBackend({
|
|
132
|
+
baseUrl: options.baseUrl,
|
|
133
|
+
apiKey: options.apiKey,
|
|
134
|
+
cache: options.cache || { enabled: true, ttl: 300000 },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
module: ExGuardModule,
|
|
139
|
+
providers: [
|
|
140
|
+
{
|
|
141
|
+
provide: 'EXGUARD_INSTANCE',
|
|
142
|
+
useValue: exGuard,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
exports: ['EXGUARD_INSTANCE'],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
function ensureDir(dirPath: string) {
|
|
152
|
+
if (!fs.existsSync(dirPath)) {
|
|
153
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function writeFile(filePath: string, content: string) {
|
|
158
|
+
const dir = path.dirname(filePath);
|
|
159
|
+
ensureDir(dir);
|
|
160
|
+
fs.writeFileSync(filePath, content);
|
|
161
|
+
console.log(\`Created: \${filePath}\`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function setupNestJS() {
|
|
165
|
+
const srcDir = path.join(process.cwd(), 'src');
|
|
166
|
+
const exguardDir = path.join(srcDir, 'exguard');
|
|
167
|
+
|
|
168
|
+
if (!fs.existsSync(srcDir)) {
|
|
169
|
+
console.error('Error: Run this command in your NestJS project root (where src/ exists)');
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ensureDir(exguardDir);
|
|
174
|
+
|
|
175
|
+
writeFile(path.join(exguardDir, 'exguard.guard.ts'), EXGUARD_GUARD_CONTENT);
|
|
176
|
+
writeFile(path.join(exguardDir, 'exguard.module.ts'), EXGUARD_MODULE_CONTENT);
|
|
177
|
+
|
|
178
|
+
console.log('\\n✅ ExGuard setup complete!');
|
|
179
|
+
console.log('\\nNext steps:');
|
|
180
|
+
console.log('1. Add to app.module.ts:');
|
|
181
|
+
console.log(' import { ExGuardModule } from "./exguard/exguard.module";');
|
|
182
|
+
console.log(' ');
|
|
183
|
+
console.log(' @Module({');
|
|
184
|
+
console.log(' imports: [');
|
|
185
|
+
console.log(' ExGuardModule.forRoot({');
|
|
186
|
+
console.log(' baseUrl: "https://api.exguard.com",');
|
|
187
|
+
console.log(' apiKey: process.env.EXGUARD_API_KEY,');
|
|
188
|
+
console.log(' }),');
|
|
189
|
+
console.log(' ],');
|
|
190
|
+
console.log(' })');
|
|
191
|
+
console.log(' export class AppModule {}');
|
|
192
|
+
console.log('\\n2. Use in controllers:');
|
|
193
|
+
console.log(' import { ExGuardPermissionGuard, RequirePermissions } from "@/exguard/exguard.guard";');
|
|
194
|
+
console.log(' ');
|
|
195
|
+
console.log(' @Controller("items")');
|
|
196
|
+
console.log(' @UseGuards(ExGuardPermissionGuard)');
|
|
197
|
+
console.log(' export class ItemsController {');
|
|
198
|
+
console.log(' @Get() @RequirePermissions(["item:read"]) findAll() {}');
|
|
199
|
+
console.log(' @Post() @RequirePermissions(["item:create"]) create() {}');
|
|
200
|
+
console.log(' }');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const args = process.argv.slice(2);
|
|
204
|
+
if (args[0] === 'setup') {
|
|
205
|
+
setupNestJS();
|
|
206
|
+
} else {
|
|
207
|
+
console.log('ExGuard NestJS Setup');
|
|
208
|
+
console.log('Usage: npx exguard-backend setup');
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log('This will create:');
|
|
211
|
+
console.log(' - src/exguard/exguard.guard.ts');
|
|
212
|
+
console.log(' - src/exguard/exguard.module.ts');
|
|
213
|
+
}
|