exguard-backend 1.1.1 → 1.1.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # ExGuard Backend SDK
2
2
 
3
- Simple RBAC/ABAC permission guard for NestJS.
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
- ### 1. Copy Guard Files
13
+ Run this command in your NestJS project root:
14
14
 
15
- Create `src/exguard/exguard.guard.ts`:
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 handler = context.getHandler();
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
- if (requireAll) {
52
- if (!permissions.every(p => userPermissions.includes(p))) {
53
- throw new ForbiddenException('Insufficient permissions');
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) : request.headers?.['x-access-token'] || null;
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 `src/exguard/exguard.module.ts`:
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
- ### 2. Configure AppModule
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
- ### 3. Use in Controllers
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
- @Get('drafts')
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) // ALL of these
146
+ @RequirePermissions(['item:delete', 'admin'], true)
148
147
  delete() { }
149
148
  }
150
149
  ```
151
150
 
152
- ## Token Format
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 in ms (5 min) |
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.1",
3
+ "version": "1.1.3",
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-nestjs.cjs"
38
+ "exguard-backend": "scripts/setup.cjs"
39
39
  },
40
40
  "keywords": [
41
41
  "exguard",
@@ -0,0 +1,209 @@
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(
48
+ EXGUARD_PERMISSIONS_KEY,
49
+ handler,
50
+ );
51
+
52
+ if (permMeta) {
53
+ const { permissions, requireAll } = permMeta;
54
+ const userPermissions = authResult.user.modules ? authResult.user.modules.flatMap(m => m.permissions) : [];
55
+
56
+ if (requireAll) {
57
+ if (!permissions.every(p => userPermissions.includes(p))) {
58
+ throw new ForbiddenException('Insufficient permissions');
59
+ }
60
+ } else {
61
+ if (!permissions.some(p => userPermissions.includes(p))) {
62
+ throw new ForbiddenException('Insufficient permissions');
63
+ }
64
+ }
65
+ }
66
+
67
+ const roleMeta = this.reflector.get(
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 => userRoles.includes(r))) {
78
+ throw new ForbiddenException('Insufficient roles');
79
+ }
80
+ } else {
81
+ if (!roles.some(r => 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) {
92
+ const auth = request.headers ? request.headers.authorization : null;
93
+ if (auth && auth.startsWith('Bearer ')) {
94
+ return auth.substring(7);
95
+ }
96
+ return request.headers ? request.headers['x-access-token'] : null;
97
+ }
98
+ }
99
+
100
+ export function RequirePermissions(permissions, requireAll = false) {
101
+ return function (target, propertyKey, descriptor) {
102
+ Reflect.defineMetadata(EXGUARD_PERMISSIONS_KEY, { permissions, requireAll }, descriptor.value);
103
+ return descriptor;
104
+ };
105
+ }
106
+
107
+ export function RequireRoles(roles, requireAll = false) {
108
+ return function (target, propertyKey, descriptor) {
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
+ @Global()
119
+ @Module({})
120
+ export class ExGuardModule {
121
+ static forRoot(options) {
122
+ const exGuard = new ExGuardBackend({
123
+ baseUrl: options.baseUrl,
124
+ apiKey: options.apiKey,
125
+ cache: options.cache || { enabled: true, ttl: 300000 },
126
+ });
127
+
128
+ return {
129
+ module: ExGuardModule,
130
+ providers: [
131
+ {
132
+ provide: 'EXGUARD_INSTANCE',
133
+ useValue: exGuard,
134
+ },
135
+ ],
136
+ exports: ['EXGUARD_INSTANCE'],
137
+ };
138
+ }
139
+ }
140
+ `;
141
+
142
+ function ensureDir(dirPath) {
143
+ if (!fs.existsSync(dirPath)) {
144
+ fs.mkdirSync(dirPath, { recursive: true });
145
+ }
146
+ }
147
+
148
+ function writeFile(filePath, content) {
149
+ const dir = path.dirname(filePath);
150
+ ensureDir(dir);
151
+ fs.writeFileSync(filePath, content);
152
+ console.log('Created: ' + filePath);
153
+ }
154
+
155
+ function setupNestJS() {
156
+ const srcDir = path.join(process.cwd(), 'src');
157
+ const exguardDir = path.join(srcDir, 'exguard');
158
+
159
+ if (!fs.existsSync(srcDir)) {
160
+ console.error('Error: Run this command in your NestJS project root (where src/ exists)');
161
+ process.exit(1);
162
+ }
163
+
164
+ ensureDir(exguardDir);
165
+
166
+ writeFile(path.join(exguardDir, 'exguard.guard.ts'), EXGUARD_GUARD_CONTENT);
167
+ writeFile(path.join(exguardDir, 'exguard.module.ts'), EXGUARD_MODULE_CONTENT);
168
+
169
+ console.log('');
170
+ console.log('ExGuard setup complete!');
171
+ console.log('');
172
+ console.log('Next steps:');
173
+ console.log('1. Add to app.module.ts:');
174
+ console.log('');
175
+ console.log(' import { ExGuardModule } from "./exguard/exguard.module";');
176
+ console.log('');
177
+ console.log(' @Module({');
178
+ console.log(' imports: [');
179
+ console.log(' ExGuardModule.forRoot({');
180
+ console.log(' baseUrl: "https://api.exguard.com",');
181
+ console.log(' apiKey: process.env.EXGUARD_API_KEY,');
182
+ console.log(' }),');
183
+ console.log(' ],');
184
+ console.log(' })');
185
+ console.log(' export class AppModule {}');
186
+ console.log('');
187
+ console.log('2. Use in controllers:');
188
+ console.log('');
189
+ console.log(' import { ExGuardPermissionGuard, RequirePermissions } from "@/exguard/exguard.guard";');
190
+ console.log('');
191
+ console.log(' @Controller("items")');
192
+ console.log(' @UseGuards(ExGuardPermissionGuard)');
193
+ console.log(' export class ItemsController {');
194
+ console.log(' @Get() @RequirePermissions(["item:read"]) findAll() {}');
195
+ console.log(' @Post() @RequirePermissions(["item:create"]) create() {}');
196
+ console.log(' }');
197
+ }
198
+
199
+ const args = process.argv.slice(2);
200
+ if (args[0] === 'setup') {
201
+ setupNestJS();
202
+ } else {
203
+ console.log('ExGuard NestJS Setup');
204
+ console.log('Usage: npx exguard-backend setup');
205
+ console.log('');
206
+ console.log('This will create:');
207
+ console.log(' - src/exguard/exguard.guard.ts');
208
+ console.log(' - src/exguard/exguard.module.ts');
209
+ }