@navios/core 0.4.0 → 0.5.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 +95 -2
- package/docs/README.md +310 -3
- package/docs/adapters.md +308 -0
- package/docs/application-setup.md +524 -0
- package/docs/attributes.md +689 -0
- package/docs/controllers.md +373 -0
- package/docs/endpoints.md +444 -0
- package/docs/exceptions.md +316 -0
- package/docs/guards.md +550 -0
- package/docs/modules.md +251 -0
- package/docs/quick-start.md +295 -0
- package/docs/services.md +428 -0
- package/docs/testing.md +704 -0
- package/lib/_tsup-dts-rollup.d.mts +300 -235
- package/lib/_tsup-dts-rollup.d.ts +300 -235
- package/lib/index.d.mts +47 -26
- package/lib/index.d.ts +47 -26
- package/lib/index.js +633 -1072
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +631 -1064
- package/lib/index.mjs.map +1 -1
- package/package.json +4 -7
- package/project.json +9 -1
- package/src/__tests__/config.service.spec.mts +11 -9
- package/src/__tests__/controller.spec.mts +0 -1
- package/src/config/config.service.mts +2 -2
- package/src/decorators/controller.decorator.mts +1 -1
- package/src/decorators/endpoint.decorator.mts +2 -2
- package/src/decorators/header.decorator.mts +1 -1
- package/src/decorators/multipart.decorator.mts +1 -1
- package/src/decorators/stream.decorator.mts +2 -3
- package/src/factories/endpoint-adapter.factory.mts +21 -0
- package/src/factories/http-adapter.factory.mts +20 -0
- package/src/factories/index.mts +6 -0
- package/src/factories/multipart-adapter.factory.mts +21 -0
- package/src/factories/reply.factory.mts +21 -0
- package/src/factories/request.factory.mts +21 -0
- package/src/factories/stream-adapter.factory.mts +20 -0
- package/src/index.mts +1 -1
- package/src/interfaces/abstract-execution-context.inteface.mts +13 -0
- package/src/interfaces/abstract-http-adapter.interface.mts +20 -0
- package/src/interfaces/abstract-http-cors-options.interface.mts +59 -0
- package/src/interfaces/abstract-http-handler-adapter.interface.mts +13 -0
- package/src/interfaces/abstract-http-listen-options.interface.mts +4 -0
- package/src/interfaces/can-activate.mts +4 -2
- package/src/interfaces/http-header.mts +18 -0
- package/src/interfaces/index.mts +6 -0
- package/src/logger/console-logger.service.mts +28 -44
- package/src/logger/index.mts +1 -2
- package/src/logger/logger.service.mts +9 -128
- package/src/logger/logger.tokens.mts +21 -0
- package/src/metadata/handler.metadata.mts +7 -5
- package/src/navios.application.mts +65 -172
- package/src/navios.environment.mts +30 -0
- package/src/navios.factory.mts +53 -12
- package/src/services/guard-runner.service.mts +19 -9
- package/src/services/index.mts +0 -2
- package/src/services/module-loader.service.mts +4 -3
- package/src/tokens/endpoint-adapter.token.mts +8 -0
- package/src/tokens/execution-context.token.mts +2 -2
- package/src/tokens/http-adapter.token.mts +8 -0
- package/src/tokens/index.mts +4 -1
- package/src/tokens/multipart-adapter.token.mts +8 -0
- package/src/tokens/reply.token.mts +1 -5
- package/src/tokens/request.token.mts +1 -7
- package/src/tokens/stream-adapter.token.mts +8 -0
- package/docs/recipes/prisma.md +0 -60
- package/e2e/endpoints/get.spec.mts +0 -97
- package/e2e/endpoints/post.spec.mts +0 -113
- package/examples/simple-test/api/index.mts +0 -64
- package/examples/simple-test/config/config.service.mts +0 -14
- package/examples/simple-test/config/configuration.mts +0 -7
- package/examples/simple-test/index.mts +0 -16
- package/examples/simple-test/src/acl/acl-modern.guard.mts +0 -15
- package/examples/simple-test/src/acl/acl.guard.mts +0 -14
- package/examples/simple-test/src/acl/app.guard.mts +0 -27
- package/examples/simple-test/src/acl/one-more.guard.mts +0 -15
- package/examples/simple-test/src/acl/public.attribute.mts +0 -21
- package/examples/simple-test/src/app.module.mts +0 -9
- package/examples/simple-test/src/user/user.controller.mts +0 -72
- package/examples/simple-test/src/user/user.module.mts +0 -14
- package/examples/simple-test/src/user/user.service.mts +0 -14
- package/src/adapters/endpoint-adapter.service.mts +0 -72
- package/src/adapters/handler-adapter.interface.mts +0 -21
- package/src/adapters/index.mts +0 -4
- package/src/adapters/multipart-adapter.service.mts +0 -135
- package/src/adapters/stream-adapter.service.mts +0 -91
- package/src/logger/logger.factory.mts +0 -36
- package/src/logger/pino-wrapper.mts +0 -64
- package/src/services/controller-adapter.service.mts +0 -124
- package/src/services/execution-context.mts +0 -54
- package/src/tokens/application.token.mts +0 -9
package/docs/guards.md
ADDED
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
# Guards
|
|
2
|
+
|
|
3
|
+
Guards in Navios are classes that determine whether a request should be allowed to proceed to the endpoint handler. They implement authentication, authorization, rate limiting, and other security or business logic checks.
|
|
4
|
+
|
|
5
|
+
## What are Guards?
|
|
6
|
+
|
|
7
|
+
Guards are executed before the endpoint handler and can:
|
|
8
|
+
|
|
9
|
+
- Allow or deny access to endpoints
|
|
10
|
+
- Perform authentication checks
|
|
11
|
+
- Validate user permissions
|
|
12
|
+
- Implement rate limiting
|
|
13
|
+
- Execute any custom logic before request processing
|
|
14
|
+
|
|
15
|
+
Guards receive an `AbstractExecutionContext` parameter that provides access to:
|
|
16
|
+
|
|
17
|
+
- Request and response objects via `getRequest()` and `getReply()`
|
|
18
|
+
- Module metadata via `getModule()`
|
|
19
|
+
- Controller metadata via `getController()`
|
|
20
|
+
- Handler/endpoint metadata via `getHandler()`
|
|
21
|
+
- All attributes defined on modules, controllers, and handlers
|
|
22
|
+
|
|
23
|
+
## Execution Context Interface
|
|
24
|
+
|
|
25
|
+
The `AbstractExecutionContext` interface provides the following methods:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
interface AbstractExecutionContext {
|
|
29
|
+
getModule(): ModuleMetadata // Module-level metadata and attributes
|
|
30
|
+
getController(): ControllerMetadata // Controller-level metadata and attributes
|
|
31
|
+
getHandler(): HandlerMetadata // Handler/endpoint-level metadata and attributes
|
|
32
|
+
getRequest(): any // Framework-specific request object
|
|
33
|
+
getReply(): any // Framework-specific response object
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Creating Guards
|
|
38
|
+
|
|
39
|
+
### Basic Guard
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import type { AbstractExecutionContext, CanActivate } from '@navios/core'
|
|
43
|
+
|
|
44
|
+
import { Injectable } from '@navios/di'
|
|
45
|
+
|
|
46
|
+
@Injectable()
|
|
47
|
+
export class AuthGuard implements CanActivate {
|
|
48
|
+
async canActivate(
|
|
49
|
+
executionContext: AbstractExecutionContext,
|
|
50
|
+
): Promise<boolean> {
|
|
51
|
+
const request = executionContext.getRequest()
|
|
52
|
+
const authHeader = request.headers?.authorization
|
|
53
|
+
|
|
54
|
+
if (!authHeader) {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Validate token
|
|
59
|
+
const token = authHeader.replace('Bearer ', '')
|
|
60
|
+
return this.validateToken(token)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async validateToken(token: string): Promise<boolean> {
|
|
64
|
+
// Token validation logic
|
|
65
|
+
return token === 'valid-token'
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Guard with Dependencies
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import type { AbstractExecutionContext, CanActivate } from '@navios/core'
|
|
74
|
+
|
|
75
|
+
import { inject, Injectable, Logger } from '@navios/di'
|
|
76
|
+
|
|
77
|
+
@Injectable()
|
|
78
|
+
export class JwtAuthGuard implements CanActivate {
|
|
79
|
+
private jwtService = inject(JwtService)
|
|
80
|
+
private userService = inject(UserService)
|
|
81
|
+
private logger = inject(Logger, { context: 'JwtAuthGuard' })
|
|
82
|
+
|
|
83
|
+
async canActivate(
|
|
84
|
+
executionContext: AbstractExecutionContext,
|
|
85
|
+
): Promise<boolean> {
|
|
86
|
+
try {
|
|
87
|
+
const request = executionContext.getRequest()
|
|
88
|
+
const token = this.extractTokenFromHeader(request.headers)
|
|
89
|
+
|
|
90
|
+
if (!token) {
|
|
91
|
+
this.logger.debug('No token provided')
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const payload = await this.jwtService.verify(token)
|
|
96
|
+
const user = await this.userService.findById(payload.sub)
|
|
97
|
+
|
|
98
|
+
if (!user || !user.isActive) {
|
|
99
|
+
this.logger.debug(`User not found or inactive: ${payload.sub}`)
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Attach user to request context
|
|
104
|
+
request.user = user
|
|
105
|
+
return true
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.logger.debug('Token validation failed', error.message)
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private extractTokenFromHeader(
|
|
113
|
+
headers: Record<string, string>,
|
|
114
|
+
): string | null {
|
|
115
|
+
const authHeader = headers?.authorization
|
|
116
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
return authHeader.substring(7)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Using Guards
|
|
125
|
+
|
|
126
|
+
### Controller-Level Guards
|
|
127
|
+
|
|
128
|
+
Guards applied to controllers affect all endpoints in that controller:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { Controller, UseGuards } from '@navios/core'
|
|
132
|
+
|
|
133
|
+
@Controller({
|
|
134
|
+
guards: [AuthGuard], // Applied to all endpoints
|
|
135
|
+
})
|
|
136
|
+
export class UserController {
|
|
137
|
+
// All endpoints here require authentication
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Endpoint-Level Guards
|
|
142
|
+
|
|
143
|
+
Guards applied to specific endpoints:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
@Controller()
|
|
147
|
+
export class UserController {
|
|
148
|
+
@Endpoint(profileEndpoint)
|
|
149
|
+
async getProfile() {
|
|
150
|
+
// No authentication required
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@UseGuards([AuthGuard, AdminGuard])
|
|
154
|
+
@Endpoint(deleteUserEndpoint)
|
|
155
|
+
async deleteUser() {
|
|
156
|
+
// Requires authentication AND admin role
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Module-Level Guards
|
|
162
|
+
|
|
163
|
+
Guards applied to modules affect all controllers in that module:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { Module } from '@navios/core'
|
|
167
|
+
|
|
168
|
+
@Module({
|
|
169
|
+
guards: [AuthGuard], // Applied to all controllers in module
|
|
170
|
+
controllers: [UserController, PostController],
|
|
171
|
+
})
|
|
172
|
+
export class ProtectedModule {}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Guard Execution Order
|
|
176
|
+
|
|
177
|
+
Guards are executed in the following order:
|
|
178
|
+
|
|
179
|
+
1. Module-level guards
|
|
180
|
+
2. Controller-level guards
|
|
181
|
+
3. Endpoint-level guards
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
@Module({
|
|
185
|
+
guards: [AuthGuard], // 1st
|
|
186
|
+
controllers: [UserController],
|
|
187
|
+
})
|
|
188
|
+
export class AppModule {}
|
|
189
|
+
|
|
190
|
+
@Controller({
|
|
191
|
+
guards: [RoleGuard], // 2nd
|
|
192
|
+
})
|
|
193
|
+
export class UserController {
|
|
194
|
+
@UseGuards([OwnershipGuard]) // 3rd
|
|
195
|
+
@Endpoint(deleteUserEndpoint)
|
|
196
|
+
async deleteUser() {
|
|
197
|
+
// Execution order: AuthGuard -> RoleGuard -> OwnershipGuard
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Common Guard Patterns
|
|
203
|
+
|
|
204
|
+
### Role-Based Authorization
|
|
205
|
+
|
|
206
|
+
Use AttributeFactory to define roles required for endpoints
|
|
207
|
+
|
|
208
|
+
### Resource Ownership Guard
|
|
209
|
+
|
|
210
|
+
### Rate Limiting Guard
|
|
211
|
+
|
|
212
|
+
### API Key Guard
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import type { AbstractExecutionContext, CanActivate } from '@navios/core'
|
|
216
|
+
|
|
217
|
+
import { inject, Injectable, Logger } from '@navios/di'
|
|
218
|
+
|
|
219
|
+
@Injectable()
|
|
220
|
+
export class ApiKeyGuard implements CanActivate {
|
|
221
|
+
private configService = inject(ConfigService)
|
|
222
|
+
private logger = inject(Logger, { context: 'ApiKeyGuard' })
|
|
223
|
+
|
|
224
|
+
async canActivate(
|
|
225
|
+
executionContext: AbstractExecutionContext,
|
|
226
|
+
): Promise<boolean> {
|
|
227
|
+
const request = executionContext.getRequest()
|
|
228
|
+
const apiKey = request.headers?.['x-api-key']
|
|
229
|
+
|
|
230
|
+
if (!apiKey) {
|
|
231
|
+
this.logger.debug('No API key provided')
|
|
232
|
+
return false
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const validApiKeys = this.configService.get<string[]>('VALID_API_KEYS')
|
|
236
|
+
const isValid = validApiKeys.includes(apiKey)
|
|
237
|
+
|
|
238
|
+
if (!isValid) {
|
|
239
|
+
this.logger.warn(`Invalid API key used: ${apiKey.substring(0, 8)}...`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return isValid
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
@Controller()
|
|
247
|
+
export class ApiController {
|
|
248
|
+
@UseGuards([ApiKeyGuard])
|
|
249
|
+
@Endpoint(publicDataEndpoint)
|
|
250
|
+
async getPublicData() {
|
|
251
|
+
// Requires valid API key
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Advanced Guard Patterns
|
|
257
|
+
|
|
258
|
+
### Conditional Guards
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import type { AbstractExecutionContext, CanActivate } from '@navios/core'
|
|
262
|
+
|
|
263
|
+
import { inject, Injectable } from '@navios/di'
|
|
264
|
+
|
|
265
|
+
@Injectable()
|
|
266
|
+
export class ConditionalAuthGuard implements CanActivate {
|
|
267
|
+
private configService = inject(ConfigService)
|
|
268
|
+
private authGuard = inject(AuthGuard)
|
|
269
|
+
|
|
270
|
+
async canActivate(
|
|
271
|
+
executionContext: AbstractExecutionContext,
|
|
272
|
+
): Promise<boolean> {
|
|
273
|
+
const authRequired = this.configService.get<boolean>('AUTH_REQUIRED')
|
|
274
|
+
|
|
275
|
+
if (!authRequired) {
|
|
276
|
+
return true
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return this.authGuard.canActivate(executionContext)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Guards with Attributes
|
|
285
|
+
|
|
286
|
+
Guards can read attributes from the execution context using `AttributeFactory`:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import type { AbstractExecutionContext, CanActivate } from '@navios/core'
|
|
290
|
+
|
|
291
|
+
import { AttributeFactory, inject, Injectable, Logger } from '@navios/di'
|
|
292
|
+
|
|
293
|
+
// Define attributes
|
|
294
|
+
const PublicSymbol = Symbol.for('Public')
|
|
295
|
+
export const Public = AttributeFactory.createAttribute(PublicSymbol)
|
|
296
|
+
|
|
297
|
+
const RolesSymbol = Symbol.for('Roles')
|
|
298
|
+
const RolesSchema = z.object({
|
|
299
|
+
roles: z.array(z.string()),
|
|
300
|
+
})
|
|
301
|
+
export const Roles = AttributeFactory.createAttribute(RolesSymbol, RolesSchema)
|
|
302
|
+
|
|
303
|
+
@Injectable()
|
|
304
|
+
export class AuthGuard implements CanActivate {
|
|
305
|
+
private logger = inject(Logger, { context: 'AuthGuard' })
|
|
306
|
+
|
|
307
|
+
async canActivate(
|
|
308
|
+
executionContext: AbstractExecutionContext,
|
|
309
|
+
): Promise<boolean> {
|
|
310
|
+
// Check if endpoint is marked as public using AttributeFactory.getLast
|
|
311
|
+
// This checks module, controller, and handler metadata in hierarchy order
|
|
312
|
+
const isPublic = AttributeFactory.getLast(Public, [
|
|
313
|
+
executionContext.getModule(),
|
|
314
|
+
executionContext.getController(),
|
|
315
|
+
executionContext.getHandler(),
|
|
316
|
+
])
|
|
317
|
+
|
|
318
|
+
if (isPublic) {
|
|
319
|
+
this.logger.debug('Public endpoint, skipping authentication')
|
|
320
|
+
return true
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Check required roles
|
|
324
|
+
const requiredRoles = AttributeFactory.getLast(Roles, [
|
|
325
|
+
executionContext.getModule(),
|
|
326
|
+
executionContext.getController(),
|
|
327
|
+
executionContext.getHandler(),
|
|
328
|
+
])
|
|
329
|
+
|
|
330
|
+
const request = executionContext.getRequest()
|
|
331
|
+
const user = request.user
|
|
332
|
+
|
|
333
|
+
if (!user) {
|
|
334
|
+
return false
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!requiredRoles) {
|
|
338
|
+
return true // No specific roles required, just authentication
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check if user has any of the required roles
|
|
342
|
+
return requiredRoles.roles.some((role) => user.roles.includes(role))
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Usage examples
|
|
347
|
+
@Controller()
|
|
348
|
+
export class UserController {
|
|
349
|
+
@Public() // This endpoint is public
|
|
350
|
+
@Endpoint(loginEndpoint)
|
|
351
|
+
async login() {
|
|
352
|
+
// Public endpoint
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
@Roles({ roles: ['admin', 'moderator'] }) // Requires admin or moderator role
|
|
356
|
+
@Endpoint(deleteUserEndpoint)
|
|
357
|
+
async deleteUser() {
|
|
358
|
+
// Requires authentication and admin/moderator role
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
You can also check attributes individually:
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
@Injectable()
|
|
367
|
+
export class RoleBasedGuard implements CanActivate {
|
|
368
|
+
async canActivate(
|
|
369
|
+
executionContext: AbstractExecutionContext,
|
|
370
|
+
): Promise<boolean> {
|
|
371
|
+
const handlerMetadata = executionContext.getHandler()
|
|
372
|
+
const controllerMetadata = executionContext.getController()
|
|
373
|
+
const moduleMetadata = executionContext.getModule()
|
|
374
|
+
|
|
375
|
+
// Check if handler has specific attribute
|
|
376
|
+
const handlerRoles = AttributeFactory.get(Roles, handlerMetadata)
|
|
377
|
+
if (handlerRoles) {
|
|
378
|
+
return this.checkRoles(handlerRoles.roles, executionContext)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Fall back to controller-level roles
|
|
382
|
+
const controllerRoles = AttributeFactory.get(Roles, controllerMetadata)
|
|
383
|
+
if (controllerRoles) {
|
|
384
|
+
return this.checkRoles(controllerRoles.roles, executionContext)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Fall back to module-level roles
|
|
388
|
+
const moduleRoles = AttributeFactory.get(Roles, moduleMetadata)
|
|
389
|
+
if (moduleRoles) {
|
|
390
|
+
return this.checkRoles(moduleRoles.roles, executionContext)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return true // No role restrictions
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private checkRoles(
|
|
397
|
+
requiredRoles: string[],
|
|
398
|
+
executionContext: AbstractExecutionContext,
|
|
399
|
+
): boolean {
|
|
400
|
+
const request = executionContext.getRequest()
|
|
401
|
+
const user = request.user
|
|
402
|
+
return user && requiredRoles.some((role) => user.roles.includes(role))
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Error Handling in Guards
|
|
408
|
+
|
|
409
|
+
Guards should handle errors gracefully:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import type { AbstractExecutionContext, CanActivate } from '@navios/core'
|
|
413
|
+
|
|
414
|
+
import { inject, Injectable, Logger } from '@navios/di'
|
|
415
|
+
|
|
416
|
+
@Injectable()
|
|
417
|
+
export class SafeAuthGuard implements CanActivate {
|
|
418
|
+
private jwtService = inject(JwtService)
|
|
419
|
+
private logger = inject(Logger, { context: 'SafeAuthGuard' })
|
|
420
|
+
|
|
421
|
+
async canActivate(
|
|
422
|
+
executionContext: AbstractExecutionContext,
|
|
423
|
+
): Promise<boolean> {
|
|
424
|
+
try {
|
|
425
|
+
const request = executionContext.getRequest()
|
|
426
|
+
const token = this.extractToken(request.headers)
|
|
427
|
+
|
|
428
|
+
if (!token) {
|
|
429
|
+
return false
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
await this.jwtService.verify(token)
|
|
433
|
+
return true
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (error.name === 'TokenExpiredError') {
|
|
436
|
+
this.logger.debug('Token expired')
|
|
437
|
+
} else if (error.name === 'JsonWebTokenError') {
|
|
438
|
+
this.logger.debug('Invalid token')
|
|
439
|
+
} else {
|
|
440
|
+
this.logger.error('Unexpected error in auth guard', error.stack)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return false
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private extractToken(headers: Record<string, string>): string | null {
|
|
448
|
+
const authHeader = headers?.authorization
|
|
449
|
+
return authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Best Practices
|
|
455
|
+
|
|
456
|
+
### 1. Keep Guards Focused
|
|
457
|
+
|
|
458
|
+
Each guard should have a single responsibility:
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
// ✅ Good - Single responsibility
|
|
462
|
+
@Injectable()
|
|
463
|
+
export class AuthenticationGuard {
|
|
464
|
+
async canActivate(context: GuardContext): Promise<boolean> {
|
|
465
|
+
// Only handles authentication
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
@Injectable()
|
|
470
|
+
export class AuthorizationGuard {
|
|
471
|
+
async canActivate(context: GuardContext): Promise<boolean> {
|
|
472
|
+
// Only handles authorization
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ❌ Avoid - Multiple responsibilities
|
|
477
|
+
@Injectable()
|
|
478
|
+
export class AuthGuard {
|
|
479
|
+
async canActivate(context: GuardContext): Promise<boolean> {
|
|
480
|
+
// Handles both authentication AND authorization
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### 2. Fail Securely
|
|
486
|
+
|
|
487
|
+
When in doubt, deny access:
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
@Injectable()
|
|
491
|
+
export class SecureGuard {
|
|
492
|
+
async canActivate(context: GuardContext): Promise<boolean> {
|
|
493
|
+
try {
|
|
494
|
+
// Validation logic
|
|
495
|
+
return this.validateAccess(context)
|
|
496
|
+
} catch (error) {
|
|
497
|
+
// Fail securely - deny access on errors
|
|
498
|
+
this.logger.error('Guard validation failed', error.stack)
|
|
499
|
+
return false
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### 3. Use Dependency Injection
|
|
506
|
+
|
|
507
|
+
Inject services rather than creating instances:
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
// ✅ Good - Use DI
|
|
511
|
+
@Injectable()
|
|
512
|
+
export class AuthGuard implements CanActivate {
|
|
513
|
+
private jwtService = inject(JwtService)
|
|
514
|
+
private userService = inject(UserService)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ❌ Avoid - Direct instantiation
|
|
518
|
+
@Injectable()
|
|
519
|
+
export class AuthGuard implements CanActivate {
|
|
520
|
+
private jwtService = new JwtService()
|
|
521
|
+
private userService = new UserService()
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### 4. Log Security Events
|
|
526
|
+
|
|
527
|
+
Log important security events for monitoring:
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
@Injectable()
|
|
531
|
+
export class AuthGuard implements CanActivate {
|
|
532
|
+
private logger = inject(Logger, { context: 'AuthGuard' })
|
|
533
|
+
|
|
534
|
+
async canActivate(
|
|
535
|
+
executionContext: AbstractExecutionContext,
|
|
536
|
+
): Promise<boolean> {
|
|
537
|
+
const request = executionContext.getRequest()
|
|
538
|
+
const result = await this.validateToken(request.headers?.authorization)
|
|
539
|
+
|
|
540
|
+
if (!result) {
|
|
541
|
+
this.logger.warn('Authentication failed', {
|
|
542
|
+
ip: request.ip,
|
|
543
|
+
userAgent: request.headers?.['user-agent'],
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return result
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
```
|