@vritti/api-sdk 0.0.2 → 0.0.5
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 +617 -43
- package/dist/index.cjs +1503 -312
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +963 -169
- package/dist/index.d.ts +963 -169
- package/dist/index.js +1484 -309
- package/dist/index.js.map +1 -1
- package/package.json +8 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import * as _nestjs_common from '@nestjs/common';
|
|
2
|
+
import { DynamicModule, OnModuleDestroy, OnModuleInit, Logger, CanActivate, ExecutionContext, ExceptionFilter, ArgumentsHost } from '@nestjs/common';
|
|
3
|
+
import { ConfigService } from '@nestjs/config';
|
|
4
|
+
import { Reflector } from '@nestjs/core';
|
|
5
|
+
import { JwtService } from '@nestjs/jwt';
|
|
6
|
+
import { FastifyRequest } from 'fastify';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Global authentication configuration module
|
|
10
|
+
*
|
|
11
|
+
* This module provides:
|
|
12
|
+
* - JWT token verification (JwtModule)
|
|
13
|
+
* - Global authentication guard (VrittiAuthGuard)
|
|
14
|
+
* - Support for @Public and @Onboarding decorators
|
|
15
|
+
*
|
|
16
|
+
* ## Features:
|
|
17
|
+
* - Automatically applies VrittiAuthGuard to all routes
|
|
18
|
+
* - Configures JwtModule with JWT_SECRET from environment
|
|
19
|
+
* - Exports JwtModule for token generation in services
|
|
20
|
+
*
|
|
21
|
+
* ## Usage in Application:
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // In app.module.ts
|
|
25
|
+
* @Module({
|
|
26
|
+
* imports: [
|
|
27
|
+
* ConfigModule.forRoot({ isGlobal: true }),
|
|
28
|
+
*
|
|
29
|
+
* // Auth configuration (global guard + JWT)
|
|
30
|
+
* AuthConfigModule.forRootAsync(),
|
|
31
|
+
*
|
|
32
|
+
* // Database configuration (Gateway mode)
|
|
33
|
+
* DatabaseModule.forServer({
|
|
34
|
+
* useFactory: (config: ConfigService) => ({
|
|
35
|
+
* primaryDb: {
|
|
36
|
+
* host: config.get('PRIMARY_DB_HOST'),
|
|
37
|
+
* // ... other config
|
|
38
|
+
* },
|
|
39
|
+
* prismaClientConstructor: PrismaClient,
|
|
40
|
+
* }),
|
|
41
|
+
* inject: [ConfigService],
|
|
42
|
+
* }),
|
|
43
|
+
* ],
|
|
44
|
+
* })
|
|
45
|
+
* export class AppModule {}
|
|
46
|
+
*
|
|
47
|
+
* ## Environment Variables Required:
|
|
48
|
+
* - JWT_SECRET: Secret key to verify access tokens (required)
|
|
49
|
+
* - JWT_REFRESH_SECRET: Secret key for refresh tokens (optional, falls back to JWT_SECRET)
|
|
50
|
+
*
|
|
51
|
+
* ## Bypass Authentication:
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* // Skip authentication on specific endpoints
|
|
55
|
+
* @Public()
|
|
56
|
+
* @Post('auth/login')
|
|
57
|
+
* async login() { ... }
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // Onboarding endpoints (only accept onboarding tokens)
|
|
61
|
+
* @Onboarding()
|
|
62
|
+
* @Post('onboarding/verify-email')
|
|
63
|
+
* async verifyEmail(@Request() req) {
|
|
64
|
+
* const userId = req.user.id; // Available from guard
|
|
65
|
+
* ...
|
|
66
|
+
* }
|
|
67
|
+
*/
|
|
68
|
+
declare class AuthConfigModule {
|
|
69
|
+
/**
|
|
70
|
+
* Register the auth module with async configuration
|
|
71
|
+
*
|
|
72
|
+
* This method:
|
|
73
|
+
* 1. Configures JwtModule with JWT_SECRET from ConfigService
|
|
74
|
+
* 2. Provides VrittiAuthGuard globally (applies to all routes)
|
|
75
|
+
* 3. Exports JwtModule for use in other modules (e.g., for signing tokens)
|
|
76
|
+
*
|
|
77
|
+
* @returns Dynamic module configuration
|
|
78
|
+
*/
|
|
79
|
+
static forRootAsync(): DynamicModule;
|
|
80
|
+
}
|
|
3
81
|
|
|
4
82
|
/**
|
|
5
83
|
* Primary database connection configuration
|
|
@@ -66,19 +144,22 @@ interface DatabaseModuleOptions {
|
|
|
66
144
|
/**
|
|
67
145
|
* Tenant configuration stored in cloud database
|
|
68
146
|
* This is the shape of data returned from the tenant registry
|
|
147
|
+
*
|
|
148
|
+
* Note: Database configuration is now stored in a separate TenantDatabaseConfig table
|
|
149
|
+
* but is flattened into this interface for convenience.
|
|
69
150
|
*/
|
|
70
151
|
interface TenantInfo {
|
|
71
152
|
/** Unique tenant identifier */
|
|
72
153
|
id: string;
|
|
73
154
|
/** Human-readable tenant slug */
|
|
74
155
|
subdomain: string;
|
|
75
|
-
/** Tenant type */
|
|
76
|
-
type: 'SHARED' | '
|
|
156
|
+
/** Tenant type - SHARED or DEDICATED */
|
|
157
|
+
type: 'SHARED' | 'DEDICATED';
|
|
77
158
|
/** Tenant status */
|
|
78
159
|
status: string;
|
|
79
|
-
/** For
|
|
160
|
+
/** For SHARED tenants: schema name within the shared database */
|
|
80
161
|
schemaName?: string;
|
|
81
|
-
/** For
|
|
162
|
+
/** For DEDICATED tenants: database configuration (from TenantDatabaseConfig table) */
|
|
82
163
|
databaseName?: string;
|
|
83
164
|
databaseHost?: string;
|
|
84
165
|
databasePort?: number;
|
|
@@ -98,20 +179,24 @@ interface TenantInfo {
|
|
|
98
179
|
* - Support for both gateway and microservice modes
|
|
99
180
|
*
|
|
100
181
|
* ## Gateway Mode (API Gateway)
|
|
182
|
+
* - Use DatabaseModule.forServer() method
|
|
101
183
|
* - Automatically extracts tenant from subdomain, falls back to x-tenant-id header
|
|
102
184
|
* - Provide primaryDb configuration and prismaClientConstructor
|
|
103
185
|
* - Automatically queries primary DB for tenant config
|
|
104
|
-
* -
|
|
186
|
+
* - Automatically registers TenantContextInterceptor globally
|
|
187
|
+
* - No manual interceptor registration needed
|
|
105
188
|
*
|
|
106
|
-
* ## Microservice Mode
|
|
189
|
+
* ## Microservice Mode (RabbitMQ Workers)
|
|
190
|
+
* - Use DatabaseModule.forMicroservice() method
|
|
107
191
|
* - Only provide prismaClientConstructor
|
|
108
192
|
* - Tenant context comes from RabbitMQ messages
|
|
109
|
-
* -
|
|
193
|
+
* - Automatically registers MessageTenantContextInterceptor globally
|
|
194
|
+
* - No manual interceptor registration needed
|
|
110
195
|
*
|
|
111
196
|
* @example
|
|
112
197
|
* // Gateway configuration
|
|
113
|
-
* DatabaseModule.
|
|
114
|
-
*
|
|
198
|
+
* DatabaseModule.forServer({
|
|
199
|
+
* inject: [ConfigService],
|
|
115
200
|
* useFactory: (config: ConfigService) => ({
|
|
116
201
|
* primaryDb: {
|
|
117
202
|
* host: config.get('PRIMARY_DB_HOST'),
|
|
@@ -122,127 +207,80 @@ interface TenantInfo {
|
|
|
122
207
|
* },
|
|
123
208
|
* prismaClientConstructor: PrismaClient,
|
|
124
209
|
* }),
|
|
125
|
-
* inject: [ConfigService],
|
|
126
210
|
* })
|
|
127
211
|
*
|
|
128
212
|
* @example
|
|
129
213
|
* // Microservice configuration
|
|
130
|
-
* DatabaseModule.
|
|
131
|
-
*
|
|
214
|
+
* DatabaseModule.forMicroservice({
|
|
215
|
+
* inject: [ConfigService],
|
|
216
|
+
* useFactory: (config: ConfigService) => ({
|
|
217
|
+
* prismaClientConstructor: PrismaClient,
|
|
218
|
+
* }),
|
|
132
219
|
* })
|
|
133
220
|
*/
|
|
134
221
|
declare class DatabaseModule {
|
|
135
222
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* @param options Module configuration options
|
|
139
|
-
* @returns Dynamic module configuration
|
|
140
|
-
*/
|
|
141
|
-
static forRoot(options: DatabaseModuleOptions): DynamicModule;
|
|
142
|
-
/**
|
|
143
|
-
* Asynchronous configuration (recommended)
|
|
223
|
+
* Configure DatabaseModule for Gateway/HTTP mode (multi-tenant web servers)
|
|
144
224
|
*
|
|
145
|
-
*
|
|
225
|
+
* This mode is for API Gateways that handle HTTP requests:
|
|
226
|
+
* - Automatically registers TenantContextInterceptor
|
|
227
|
+
* - Extracts tenant from subdomain or x-tenant-id header
|
|
228
|
+
* - Queries primary database for tenant configuration
|
|
229
|
+
* - Provides PrimaryDatabaseService for tenant lookup
|
|
146
230
|
*
|
|
147
231
|
* @param options Async configuration options
|
|
148
|
-
* @returns Dynamic module configuration
|
|
232
|
+
* @returns Dynamic module configuration with HTTP interceptor
|
|
149
233
|
*
|
|
150
234
|
* @example
|
|
151
|
-
* DatabaseModule.
|
|
152
|
-
*
|
|
153
|
-
* useFactory:
|
|
154
|
-
*
|
|
235
|
+
* DatabaseModule.forServer({
|
|
236
|
+
* inject: [ConfigService],
|
|
237
|
+
* useFactory: (config: ConfigService) => ({
|
|
238
|
+
* primaryDb: {
|
|
239
|
+
* host: config.get('PRIMARY_DB_HOST'),
|
|
240
|
+
* port: config.get('PRIMARY_DB_PORT'),
|
|
241
|
+
* username: config.get('PRIMARY_DB_USERNAME'),
|
|
242
|
+
* password: config.get('PRIMARY_DB_PASSWORD'),
|
|
243
|
+
* database: config.get('PRIMARY_DB_DATABASE'),
|
|
244
|
+
* },
|
|
155
245
|
* prismaClientConstructor: PrismaClient,
|
|
156
|
-
* tenantResolver: 'subdomain',
|
|
157
246
|
* }),
|
|
158
|
-
* inject: [ConfigService],
|
|
159
247
|
* })
|
|
160
248
|
*/
|
|
161
|
-
static
|
|
249
|
+
static forServer(options: {
|
|
162
250
|
useFactory: (...args: any[]) => Promise<DatabaseModuleOptions> | DatabaseModuleOptions;
|
|
163
251
|
inject?: any[];
|
|
164
252
|
}): DynamicModule;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Service responsible for querying the primary database to resolve tenant configurations
|
|
169
|
-
*
|
|
170
|
-
* This service:
|
|
171
|
-
* - Connects to the primary database (tenant registry)
|
|
172
|
-
* - Queries tenant metadata (database location, credentials, etc.)
|
|
173
|
-
* - Caches tenant configs in memory to reduce database load
|
|
174
|
-
* - Only used in GATEWAY MODE (microservices receive tenant config from messages)
|
|
175
|
-
*
|
|
176
|
-
* @example
|
|
177
|
-
* // In API Gateway
|
|
178
|
-
* const config = await primaryDatabase.getTenantConfig('acme');
|
|
179
|
-
* // Returns: { id, slug, type, databaseHost, databaseName, ... }
|
|
180
|
-
*/
|
|
181
|
-
declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
|
|
182
|
-
private readonly options;
|
|
183
|
-
private readonly logger;
|
|
184
|
-
/** Primary database client for querying tenant registry */
|
|
185
|
-
private primaryDbClient;
|
|
186
|
-
/** In-memory cache: Map<tenantIdentifier, TenantConfig> */
|
|
187
|
-
private readonly tenantConfigCache;
|
|
188
|
-
/** Cache TTL in milliseconds */
|
|
189
|
-
private readonly cacheTTL;
|
|
190
|
-
constructor(options: DatabaseModuleOptions);
|
|
191
|
-
onModuleInit(): Promise<void>;
|
|
192
253
|
/**
|
|
193
|
-
*
|
|
194
|
-
*/
|
|
195
|
-
private initializePrimaryDbClient;
|
|
196
|
-
/**
|
|
197
|
-
* Build connection URL from primary database properties
|
|
198
|
-
*/
|
|
199
|
-
private buildPrimaryDbUrl;
|
|
200
|
-
/**
|
|
201
|
-
* Mask password in connection URL for logging
|
|
202
|
-
*/
|
|
203
|
-
private maskPassword;
|
|
204
|
-
/**
|
|
205
|
-
* Get tenant configuration by identifier (ID or slug)
|
|
254
|
+
* Configure DatabaseModule for Microservice/Messaging mode (RabbitMQ workers)
|
|
206
255
|
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Cache tenant information with TTL
|
|
213
|
-
*/
|
|
214
|
-
private cacheInfo;
|
|
215
|
-
/**
|
|
216
|
-
* Clear cached tenant information
|
|
217
|
-
*
|
|
218
|
-
* Useful when tenant settings are updated and cache needs to be invalidated
|
|
219
|
-
*
|
|
220
|
-
* @param tenantIdentifier Tenant ID or slug
|
|
221
|
-
*/
|
|
222
|
-
clearTenantCache(tenantIdentifier: string): void;
|
|
223
|
-
/**
|
|
224
|
-
* Clear all cached tenant configurations
|
|
225
|
-
*/
|
|
226
|
-
clearAllCaches(): void;
|
|
227
|
-
/**
|
|
228
|
-
* Get primary database client for direct database access
|
|
256
|
+
* This mode is for microservices that process messages from queues:
|
|
257
|
+
* - Automatically registers MessageTenantContextInterceptor
|
|
258
|
+
* - Extracts tenant from RabbitMQ message patterns
|
|
259
|
+
* - No primary database needed (tenant comes from message context)
|
|
229
260
|
*
|
|
230
|
-
*
|
|
261
|
+
* @param options Async configuration options
|
|
262
|
+
* @returns Dynamic module configuration with message interceptor
|
|
231
263
|
*
|
|
232
|
-
* @
|
|
233
|
-
*
|
|
264
|
+
* @example
|
|
265
|
+
* DatabaseModule.forMicroservice({
|
|
266
|
+
* inject: [ConfigService],
|
|
267
|
+
* useFactory: (config: ConfigService) => ({
|
|
268
|
+
* prismaClientConstructor: PrismaClient,
|
|
269
|
+
* }),
|
|
270
|
+
* })
|
|
234
271
|
*/
|
|
235
|
-
|
|
272
|
+
static forMicroservice(options: {
|
|
273
|
+
useFactory: (...args: any[]) => Promise<DatabaseModuleOptions> | DatabaseModuleOptions;
|
|
274
|
+
inject?: any[];
|
|
275
|
+
}): DynamicModule;
|
|
236
276
|
/**
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
* Override this method to implement your encryption strategy
|
|
277
|
+
* Internal helper to create dynamic module with conditional interceptor registration
|
|
240
278
|
*
|
|
241
|
-
* @param
|
|
242
|
-
* @
|
|
279
|
+
* @param options Configuration options
|
|
280
|
+
* @param mode Mode of operation (gateway or microservice)
|
|
281
|
+
* @returns Dynamic module configuration
|
|
243
282
|
*/
|
|
244
|
-
private
|
|
245
|
-
onModuleDestroy(): Promise<void>;
|
|
283
|
+
private static createDynamicModule;
|
|
246
284
|
}
|
|
247
285
|
|
|
248
286
|
/**
|
|
@@ -338,6 +376,15 @@ declare class TenantDatabaseService implements OnModuleDestroy {
|
|
|
338
376
|
/** Cleanup interval timer */
|
|
339
377
|
private cleanupInterval?;
|
|
340
378
|
constructor(options: DatabaseModuleOptions, tenantContext: TenantContextService);
|
|
379
|
+
/**
|
|
380
|
+
* Get the Prisma client for the current tenant's database.
|
|
381
|
+
* This returns the tenant-scoped database client.
|
|
382
|
+
*
|
|
383
|
+
* @returns Tenant-scoped database client instance
|
|
384
|
+
* @throws UnauthorizedException if tenant context not set
|
|
385
|
+
* @throws InternalServerErrorException if connection fails
|
|
386
|
+
*/
|
|
387
|
+
get prismaClient(): any;
|
|
341
388
|
/**
|
|
342
389
|
* Get tenant-scoped database client for the current request/message
|
|
343
390
|
*
|
|
@@ -354,7 +401,7 @@ declare class TenantDatabaseService implements OnModuleDestroy {
|
|
|
354
401
|
* const dbClient = await tenantDatabase.getDbClient<PrismaClient>();
|
|
355
402
|
* const users = await dbClient.user.findMany();
|
|
356
403
|
*/
|
|
357
|
-
getDbClient
|
|
404
|
+
private getDbClient;
|
|
358
405
|
/**
|
|
359
406
|
* Create a new database client for the given tenant
|
|
360
407
|
*/
|
|
@@ -390,83 +437,705 @@ declare class TenantDatabaseService implements OnModuleDestroy {
|
|
|
390
437
|
}
|
|
391
438
|
|
|
392
439
|
/**
|
|
393
|
-
*
|
|
440
|
+
* Service responsible for querying the primary database to resolve tenant configurations
|
|
394
441
|
*
|
|
395
|
-
* This
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
442
|
+
* This service:
|
|
443
|
+
* - Connects to the primary database (tenant registry)
|
|
444
|
+
* - Queries tenant metadata (database location, credentials, etc.)
|
|
445
|
+
* - Caches tenant configs in memory to reduce database load
|
|
446
|
+
* - Only used in GATEWAY MODE (microservices receive tenant config from messages)
|
|
399
447
|
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
448
|
+
* @example
|
|
449
|
+
* // In API Gateway
|
|
450
|
+
* const config = await primaryDatabase.getTenantConfig('acme');
|
|
451
|
+
* // Returns: { id, slug, type, databaseHost, databaseName, ... }
|
|
452
|
+
*/
|
|
453
|
+
declare class PrimaryDatabaseService implements OnModuleInit, OnModuleDestroy {
|
|
454
|
+
private readonly options;
|
|
455
|
+
private readonly logger;
|
|
456
|
+
/** Primary database client for querying tenant registry */
|
|
457
|
+
private primaryDbClient;
|
|
458
|
+
/** In-memory cache: Map<tenantIdentifier, TenantConfig> */
|
|
459
|
+
private readonly tenantConfigCache;
|
|
460
|
+
/** Cache TTL in milliseconds */
|
|
461
|
+
private readonly cacheTTL;
|
|
462
|
+
constructor(options: DatabaseModuleOptions);
|
|
463
|
+
onModuleInit(): Promise<void>;
|
|
464
|
+
/**
|
|
465
|
+
* Initialize connection to primary database
|
|
466
|
+
*/
|
|
467
|
+
private initializePrimaryDbClient;
|
|
468
|
+
/**
|
|
469
|
+
* Build connection URL from primary database properties
|
|
470
|
+
*/
|
|
471
|
+
private buildPrimaryDbUrl;
|
|
472
|
+
/**
|
|
473
|
+
* Mask password in connection URL for logging
|
|
474
|
+
*/
|
|
475
|
+
private maskPassword;
|
|
476
|
+
/**
|
|
477
|
+
* Get tenant configuration by identifier (ID or slug)
|
|
478
|
+
*
|
|
479
|
+
* @param tenantIdentifier Tenant ID or slug
|
|
480
|
+
* @returns Tenant configuration or null if not found
|
|
481
|
+
*/
|
|
482
|
+
getTenantInfo(tenantIdentifier: string): Promise<TenantInfo | null>;
|
|
483
|
+
/**
|
|
484
|
+
* Cache tenant information with TTL
|
|
485
|
+
*/
|
|
486
|
+
private cacheInfo;
|
|
487
|
+
/**
|
|
488
|
+
* Clear cached tenant information
|
|
489
|
+
*
|
|
490
|
+
* Useful when tenant settings are updated and cache needs to be invalidated
|
|
491
|
+
*
|
|
492
|
+
* @param tenantIdentifier Tenant ID or slug
|
|
493
|
+
*/
|
|
494
|
+
clearTenantCache(tenantIdentifier: string): void;
|
|
495
|
+
/**
|
|
496
|
+
* Clear all cached tenant configurations
|
|
497
|
+
*/
|
|
498
|
+
clearAllCaches(): void;
|
|
499
|
+
/**
|
|
500
|
+
* Get the Prisma client for the primary database.
|
|
501
|
+
* This is a synchronous property that returns the initialized Prisma client.
|
|
502
|
+
*
|
|
503
|
+
* @returns Primary database client instance
|
|
504
|
+
* @throws Error if primary database client is not initialized
|
|
505
|
+
*/
|
|
506
|
+
get prismaClient(): any;
|
|
507
|
+
/**
|
|
508
|
+
* Decrypt database credentials
|
|
509
|
+
*
|
|
510
|
+
* Override this method to implement your encryption strategy
|
|
511
|
+
*
|
|
512
|
+
* @param encrypted Encrypted value
|
|
513
|
+
* @returns Decrypted value
|
|
514
|
+
*/
|
|
515
|
+
private decrypt;
|
|
516
|
+
onModuleDestroy(): Promise<void>;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Abstract base repository for primary database operations.
|
|
521
|
+
* Provides common CRUD operations with automatic logging.
|
|
522
|
+
*
|
|
523
|
+
* @template TModel - The Prisma model type
|
|
524
|
+
* @template TCreateDTO - DTO type for create operations
|
|
525
|
+
* @template TUpdateDTO - DTO type for update operations
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```typescript
|
|
529
|
+
* // Using the model delegate pattern (RECOMMENDED)
|
|
530
|
+
* // Type-safe, IDE autocomplete, refactor-friendly
|
|
531
|
+
* @Injectable()
|
|
532
|
+
* export class UserRepository extends PrimaryBaseRepository<
|
|
533
|
+
* User,
|
|
534
|
+
* CreateUserDto,
|
|
535
|
+
* UpdateUserDto
|
|
536
|
+
* > {
|
|
537
|
+
* constructor(database: PrimaryDatabaseService) {
|
|
538
|
+
* super(database, (prisma) => prisma.user); // ✅ Type-safe with autocomplete!
|
|
539
|
+
* }
|
|
540
|
+
*
|
|
541
|
+
* // Add custom methods as needed
|
|
542
|
+
* async findByEmail(email: string): Promise<User | null> {
|
|
543
|
+
* return this.model.findUnique({ where: { email } });
|
|
410
544
|
* }
|
|
411
545
|
* }
|
|
412
546
|
*
|
|
547
|
+
* // Short syntax is also supported
|
|
548
|
+
* @Injectable()
|
|
549
|
+
* export class TenantRepository extends PrimaryBaseRepository<Tenant> {
|
|
550
|
+
* constructor(database: PrimaryDatabaseService) {
|
|
551
|
+
* super(database, (p) => p.tenant); // ✅ Concise!
|
|
552
|
+
* }
|
|
553
|
+
* }
|
|
554
|
+
*
|
|
555
|
+
* // Works with complex model names
|
|
556
|
+
* @Injectable()
|
|
557
|
+
* export class EmailVerificationRepository extends PrimaryBaseRepository<EmailVerification> {
|
|
558
|
+
* constructor(database: PrimaryDatabaseService) {
|
|
559
|
+
* super(database, (p) => p.emailVerification); // ✅ Matches Prisma naming
|
|
560
|
+
* }
|
|
561
|
+
* }
|
|
562
|
+
* ```
|
|
563
|
+
*/
|
|
564
|
+
declare abstract class PrimaryBaseRepository<TModel, TCreateDTO = any, TUpdateDTO = any> {
|
|
565
|
+
protected readonly database: PrimaryDatabaseService;
|
|
566
|
+
protected readonly logger: Logger;
|
|
567
|
+
private readonly modelGetter;
|
|
568
|
+
/**
|
|
569
|
+
* Lazy getter for Prisma client.
|
|
570
|
+
* Accesses the client from the database service only when needed,
|
|
571
|
+
* avoiding initialization timing issues with NestJS lifecycle.
|
|
572
|
+
*/
|
|
573
|
+
protected get prisma(): any;
|
|
574
|
+
/**
|
|
575
|
+
* Lazy getter for the Prisma model delegate.
|
|
576
|
+
* Returns the specific model (e.g., prisma.user, prisma.tenant) for this repository.
|
|
577
|
+
*/
|
|
578
|
+
protected get model(): any;
|
|
579
|
+
/**
|
|
580
|
+
* Create a new repository instance
|
|
581
|
+
*
|
|
582
|
+
* @param database - The primary database service
|
|
583
|
+
* @param getModel - Function that returns the Prisma model delegate from the client
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```typescript
|
|
587
|
+
* // Standard usage with full parameter name
|
|
588
|
+
* constructor(database: PrimaryDatabaseService) {
|
|
589
|
+
* super(database, (prisma) => prisma.user);
|
|
590
|
+
* }
|
|
591
|
+
*
|
|
592
|
+
* // Short syntax
|
|
593
|
+
* constructor(database: PrimaryDatabaseService) {
|
|
594
|
+
* super(database, (p) => p.user);
|
|
595
|
+
* }
|
|
596
|
+
*
|
|
597
|
+
* // Complex model names
|
|
598
|
+
* constructor(database: PrimaryDatabaseService) {
|
|
599
|
+
* super(database, (p) => p.emailVerification);
|
|
600
|
+
* }
|
|
601
|
+
* ```
|
|
602
|
+
*/
|
|
603
|
+
constructor(database: PrimaryDatabaseService, getModel: (prisma: any) => any);
|
|
604
|
+
/**
|
|
605
|
+
* Create a new record
|
|
606
|
+
*
|
|
607
|
+
* @param data - The data to create the record with
|
|
608
|
+
* @returns Promise resolving to the created record
|
|
609
|
+
*
|
|
610
|
+
* @example
|
|
611
|
+
* ```typescript
|
|
612
|
+
* const user = await userRepository.create({
|
|
613
|
+
* email: 'user@example.com',
|
|
614
|
+
* name: 'John Doe'
|
|
615
|
+
* });
|
|
616
|
+
* ```
|
|
617
|
+
*/
|
|
618
|
+
create(data: TCreateDTO): Promise<TModel>;
|
|
619
|
+
/**
|
|
620
|
+
* Find a single record by ID
|
|
621
|
+
*
|
|
622
|
+
* @param id - The record ID
|
|
623
|
+
* @returns Promise resolving to the record or null if not found
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* ```typescript
|
|
627
|
+
* const user = await userRepository.findById('user-id-123');
|
|
628
|
+
* ```
|
|
629
|
+
*/
|
|
630
|
+
findById(id: string): Promise<TModel | null>;
|
|
631
|
+
/**
|
|
632
|
+
* Find a single record with custom where clause
|
|
633
|
+
*
|
|
634
|
+
* @param where - The where clause or findUnique args
|
|
635
|
+
* @returns Promise resolving to the record or null if not found
|
|
636
|
+
*
|
|
637
|
+
* @example
|
|
638
|
+
* ```typescript
|
|
639
|
+
* // Simple where clause
|
|
640
|
+
* const user = await userRepository.findOne({ email: 'user@example.com' });
|
|
641
|
+
*
|
|
642
|
+
* // With include
|
|
643
|
+
* const user = await userRepository.findOne({
|
|
644
|
+
* where: { email: 'user@example.com' },
|
|
645
|
+
* include: { posts: true }
|
|
646
|
+
* });
|
|
647
|
+
* ```
|
|
648
|
+
*/
|
|
649
|
+
findOne(where: any): Promise<TModel | null>;
|
|
650
|
+
/**
|
|
651
|
+
* Find multiple records
|
|
652
|
+
*
|
|
653
|
+
* @param args - Prisma findMany arguments (where, orderBy, take, skip, etc.)
|
|
654
|
+
* @returns Promise resolving to an array of records
|
|
655
|
+
*
|
|
656
|
+
* @example
|
|
657
|
+
* ```typescript
|
|
658
|
+
* // Find all users
|
|
659
|
+
* const users = await userRepository.findMany();
|
|
660
|
+
*
|
|
661
|
+
* // Find with filtering and pagination
|
|
662
|
+
* const users = await userRepository.findMany({
|
|
663
|
+
* where: { status: 'ACTIVE' },
|
|
664
|
+
* orderBy: { createdAt: 'desc' },
|
|
665
|
+
* take: 10,
|
|
666
|
+
* skip: 0
|
|
667
|
+
* });
|
|
668
|
+
* ```
|
|
669
|
+
*/
|
|
670
|
+
findMany(args?: any): Promise<TModel[]>;
|
|
671
|
+
/**
|
|
672
|
+
* Update a record by ID
|
|
673
|
+
*
|
|
674
|
+
* @param id - The record ID
|
|
675
|
+
* @param data - The data to update
|
|
676
|
+
* @returns Promise resolving to the updated record
|
|
677
|
+
*
|
|
678
|
+
* @example
|
|
679
|
+
* ```typescript
|
|
680
|
+
* const user = await userRepository.update('user-id-123', {
|
|
681
|
+
* name: 'Jane Doe'
|
|
682
|
+
* });
|
|
683
|
+
* ```
|
|
684
|
+
*/
|
|
685
|
+
update(id: string, data: TUpdateDTO): Promise<TModel>;
|
|
686
|
+
/**
|
|
687
|
+
* Update multiple records
|
|
688
|
+
*
|
|
689
|
+
* @param where - The where clause to match records
|
|
690
|
+
* @param data - The data to update
|
|
691
|
+
* @returns Promise resolving to the count of updated records
|
|
692
|
+
*
|
|
693
|
+
* @example
|
|
694
|
+
* ```typescript
|
|
695
|
+
* const result = await userRepository.updateMany(
|
|
696
|
+
* { status: 'PENDING' },
|
|
697
|
+
* { status: 'ACTIVE' }
|
|
698
|
+
* );
|
|
699
|
+
* console.log(`Updated ${result.count} users`);
|
|
700
|
+
* ```
|
|
701
|
+
*/
|
|
702
|
+
updateMany(where: any, data: TUpdateDTO): Promise<{
|
|
703
|
+
count: number;
|
|
704
|
+
}>;
|
|
705
|
+
/**
|
|
706
|
+
* Delete a record by ID
|
|
707
|
+
*
|
|
708
|
+
* @param id - The record ID
|
|
709
|
+
* @returns Promise resolving to the deleted record
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* ```typescript
|
|
713
|
+
* const user = await userRepository.delete('user-id-123');
|
|
714
|
+
* ```
|
|
715
|
+
*/
|
|
716
|
+
delete(id: string): Promise<TModel>;
|
|
717
|
+
/**
|
|
718
|
+
* Delete multiple records
|
|
719
|
+
*
|
|
720
|
+
* @param where - The where clause to match records
|
|
721
|
+
* @returns Promise resolving to the count of deleted records
|
|
722
|
+
*
|
|
723
|
+
* @example
|
|
724
|
+
* ```typescript
|
|
725
|
+
* const result = await userRepository.deleteMany({
|
|
726
|
+
* status: 'INACTIVE',
|
|
727
|
+
* createdAt: { lt: new Date('2020-01-01') }
|
|
728
|
+
* });
|
|
729
|
+
* console.log(`Deleted ${result.count} users`);
|
|
730
|
+
* ```
|
|
731
|
+
*/
|
|
732
|
+
deleteMany(where: any): Promise<{
|
|
733
|
+
count: number;
|
|
734
|
+
}>;
|
|
735
|
+
/**
|
|
736
|
+
* Count records
|
|
737
|
+
*
|
|
738
|
+
* @param where - Optional where clause to filter records
|
|
739
|
+
* @returns Promise resolving to the count of records
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* ```typescript
|
|
743
|
+
* // Count all users
|
|
744
|
+
* const total = await userRepository.count();
|
|
745
|
+
*
|
|
746
|
+
* // Count active users
|
|
747
|
+
* const activeCount = await userRepository.count({ status: 'ACTIVE' });
|
|
748
|
+
* ```
|
|
749
|
+
*/
|
|
750
|
+
count(where?: any): Promise<number>;
|
|
751
|
+
/**
|
|
752
|
+
* Check if a record exists
|
|
753
|
+
*
|
|
754
|
+
* @param where - The where clause to match records
|
|
755
|
+
* @returns Promise resolving to true if at least one record exists, false otherwise
|
|
756
|
+
*
|
|
757
|
+
* @example
|
|
758
|
+
* ```typescript
|
|
759
|
+
* const emailExists = await userRepository.exists({
|
|
760
|
+
* email: 'user@example.com'
|
|
761
|
+
* });
|
|
762
|
+
* ```
|
|
763
|
+
*/
|
|
764
|
+
exists(where: any): Promise<boolean>;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Abstract base repository for tenant-scoped database operations.
|
|
769
|
+
* All operations are automatically scoped to the current tenant.
|
|
770
|
+
*
|
|
771
|
+
* @template TModel - The Prisma model type
|
|
772
|
+
* @template TCreateDTO - DTO type for create operations
|
|
773
|
+
* @template TUpdateDTO - DTO type for update operations
|
|
774
|
+
*
|
|
413
775
|
* @example
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
776
|
+
* ```typescript
|
|
777
|
+
* // Using the model delegate pattern (RECOMMENDED)
|
|
778
|
+
* // Type-safe, IDE autocomplete, refactor-friendly
|
|
779
|
+
* @Injectable()
|
|
780
|
+
* export class ProductRepository extends TenantBaseRepository<
|
|
781
|
+
* Product,
|
|
782
|
+
* CreateProductDto,
|
|
783
|
+
* UpdateProductDto
|
|
784
|
+
* > {
|
|
785
|
+
* constructor(database: TenantDatabaseService) {
|
|
786
|
+
* super(database, (prisma) => prisma.product); // ✅ Type-safe with autocomplete!
|
|
787
|
+
* }
|
|
788
|
+
*
|
|
789
|
+
* // Add custom methods as needed
|
|
790
|
+
* async findBySku(sku: string): Promise<Product | null> {
|
|
791
|
+
* return this.model.findUnique({ where: { sku } });
|
|
792
|
+
* }
|
|
418
793
|
* }
|
|
794
|
+
*
|
|
795
|
+
* // Short syntax is also supported
|
|
796
|
+
* @Injectable()
|
|
797
|
+
* export class OrderRepository extends TenantBaseRepository<Order> {
|
|
798
|
+
* constructor(database: TenantDatabaseService) {
|
|
799
|
+
* super(database, (p) => p.order); // ✅ Concise!
|
|
800
|
+
* }
|
|
801
|
+
* }
|
|
802
|
+
*
|
|
803
|
+
* // Works with complex model names
|
|
804
|
+
* @Injectable()
|
|
805
|
+
* export class InventoryItemRepository extends TenantBaseRepository<InventoryItem> {
|
|
806
|
+
* constructor(database: TenantDatabaseService) {
|
|
807
|
+
* super(database, (p) => p.inventoryItem); // ✅ Matches Prisma naming
|
|
808
|
+
* }
|
|
809
|
+
* }
|
|
810
|
+
* ```
|
|
419
811
|
*/
|
|
420
|
-
declare class
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
812
|
+
declare abstract class TenantBaseRepository<TModel, TCreateDTO = any, TUpdateDTO = any> {
|
|
813
|
+
protected readonly database: TenantDatabaseService;
|
|
814
|
+
protected readonly logger: Logger;
|
|
815
|
+
private readonly modelGetter;
|
|
816
|
+
/**
|
|
817
|
+
* Lazy getter for Prisma client.
|
|
818
|
+
* Accesses the client from the database service only when needed,
|
|
819
|
+
* avoiding initialization timing issues with NestJS lifecycle.
|
|
820
|
+
*/
|
|
821
|
+
protected get prisma(): any;
|
|
822
|
+
/**
|
|
823
|
+
* Lazy getter for the Prisma model delegate.
|
|
824
|
+
* Returns the specific model (e.g., prisma.product, prisma.order) for this repository.
|
|
825
|
+
*/
|
|
826
|
+
protected get model(): any;
|
|
827
|
+
/**
|
|
828
|
+
* Create a new repository instance
|
|
829
|
+
*
|
|
830
|
+
* @param database - The tenant database service
|
|
831
|
+
* @param getModel - Function that returns the Prisma model delegate from the client
|
|
832
|
+
*
|
|
833
|
+
* @example
|
|
834
|
+
* ```typescript
|
|
835
|
+
* // Standard usage with full parameter name
|
|
836
|
+
* constructor(database: TenantDatabaseService) {
|
|
837
|
+
* super(database, (prisma) => prisma.product);
|
|
838
|
+
* }
|
|
839
|
+
*
|
|
840
|
+
* // Short syntax
|
|
841
|
+
* constructor(database: TenantDatabaseService) {
|
|
842
|
+
* super(database, (p) => p.product);
|
|
843
|
+
* }
|
|
844
|
+
*
|
|
845
|
+
* // Complex model names
|
|
846
|
+
* constructor(database: TenantDatabaseService) {
|
|
847
|
+
* super(database, (p) => p.inventoryItem);
|
|
848
|
+
* }
|
|
849
|
+
* ```
|
|
850
|
+
*/
|
|
851
|
+
constructor(database: TenantDatabaseService, getModel: (prisma: any) => any);
|
|
852
|
+
/**
|
|
853
|
+
* Create a new record
|
|
854
|
+
*
|
|
855
|
+
* @param data - The data to create the record with
|
|
856
|
+
* @returns Promise resolving to the created record
|
|
857
|
+
*
|
|
858
|
+
* @example
|
|
859
|
+
* ```typescript
|
|
860
|
+
* const product = await productRepository.create({
|
|
861
|
+
* name: 'Widget',
|
|
862
|
+
* sku: 'WDG-001',
|
|
863
|
+
* price: 9.99
|
|
864
|
+
* });
|
|
865
|
+
* ```
|
|
866
|
+
*/
|
|
867
|
+
create(data: TCreateDTO): Promise<TModel>;
|
|
868
|
+
/**
|
|
869
|
+
* Find a single record by ID
|
|
870
|
+
*
|
|
871
|
+
* @param id - The record ID
|
|
872
|
+
* @returns Promise resolving to the record or null if not found
|
|
873
|
+
*
|
|
874
|
+
* @example
|
|
875
|
+
* ```typescript
|
|
876
|
+
* const product = await productRepository.findById('product-id-123');
|
|
877
|
+
* ```
|
|
878
|
+
*/
|
|
879
|
+
findById(id: string): Promise<TModel | null>;
|
|
880
|
+
/**
|
|
881
|
+
* Find a single record with custom where clause
|
|
882
|
+
*
|
|
883
|
+
* @param where - The where clause or findUnique args
|
|
884
|
+
* @returns Promise resolving to the record or null if not found
|
|
885
|
+
*
|
|
886
|
+
* @example
|
|
887
|
+
* ```typescript
|
|
888
|
+
* // Simple where clause
|
|
889
|
+
* const product = await productRepository.findOne({ sku: 'WDG-001' });
|
|
890
|
+
*
|
|
891
|
+
* // With include
|
|
892
|
+
* const product = await productRepository.findOne({
|
|
893
|
+
* where: { sku: 'WDG-001' },
|
|
894
|
+
* include: { category: true }
|
|
895
|
+
* });
|
|
896
|
+
* ```
|
|
897
|
+
*/
|
|
898
|
+
findOne(where: any): Promise<TModel | null>;
|
|
899
|
+
/**
|
|
900
|
+
* Find multiple records
|
|
901
|
+
*
|
|
902
|
+
* @param args - Prisma findMany arguments (where, orderBy, take, skip, etc.)
|
|
903
|
+
* @returns Promise resolving to an array of records
|
|
904
|
+
*
|
|
905
|
+
* @example
|
|
906
|
+
* ```typescript
|
|
907
|
+
* // Find all products
|
|
908
|
+
* const products = await productRepository.findMany();
|
|
909
|
+
*
|
|
910
|
+
* // Find with filtering and pagination
|
|
911
|
+
* const products = await productRepository.findMany({
|
|
912
|
+
* where: { status: 'ACTIVE' },
|
|
913
|
+
* orderBy: { createdAt: 'desc' },
|
|
914
|
+
* take: 10,
|
|
915
|
+
* skip: 0
|
|
916
|
+
* });
|
|
917
|
+
* ```
|
|
918
|
+
*/
|
|
919
|
+
findMany(args?: any): Promise<TModel[]>;
|
|
920
|
+
/**
|
|
921
|
+
* Update a record by ID
|
|
922
|
+
*
|
|
923
|
+
* @param id - The record ID
|
|
924
|
+
* @param data - The data to update
|
|
925
|
+
* @returns Promise resolving to the updated record
|
|
926
|
+
*
|
|
927
|
+
* @example
|
|
928
|
+
* ```typescript
|
|
929
|
+
* const product = await productRepository.update('product-id-123', {
|
|
930
|
+
* price: 12.99
|
|
931
|
+
* });
|
|
932
|
+
* ```
|
|
933
|
+
*/
|
|
934
|
+
update(id: string, data: TUpdateDTO): Promise<TModel>;
|
|
935
|
+
/**
|
|
936
|
+
* Update multiple records
|
|
937
|
+
*
|
|
938
|
+
* @param where - The where clause to match records
|
|
939
|
+
* @param data - The data to update
|
|
940
|
+
* @returns Promise resolving to the count of updated records
|
|
941
|
+
*
|
|
942
|
+
* @example
|
|
943
|
+
* ```typescript
|
|
944
|
+
* const result = await productRepository.updateMany(
|
|
945
|
+
* { status: 'PENDING' },
|
|
946
|
+
* { status: 'ACTIVE' }
|
|
947
|
+
* );
|
|
948
|
+
* console.log(`Updated ${result.count} products`);
|
|
949
|
+
* ```
|
|
950
|
+
*/
|
|
951
|
+
updateMany(where: any, data: TUpdateDTO): Promise<{
|
|
952
|
+
count: number;
|
|
953
|
+
}>;
|
|
954
|
+
/**
|
|
955
|
+
* Delete a record by ID
|
|
956
|
+
*
|
|
957
|
+
* @param id - The record ID
|
|
958
|
+
* @returns Promise resolving to the deleted record
|
|
959
|
+
*
|
|
960
|
+
* @example
|
|
961
|
+
* ```typescript
|
|
962
|
+
* const product = await productRepository.delete('product-id-123');
|
|
963
|
+
* ```
|
|
964
|
+
*/
|
|
965
|
+
delete(id: string): Promise<TModel>;
|
|
966
|
+
/**
|
|
967
|
+
* Delete multiple records
|
|
968
|
+
*
|
|
969
|
+
* @param where - The where clause to match records
|
|
970
|
+
* @returns Promise resolving to the count of deleted records
|
|
971
|
+
*
|
|
972
|
+
* @example
|
|
973
|
+
* ```typescript
|
|
974
|
+
* const result = await productRepository.deleteMany({
|
|
975
|
+
* status: 'INACTIVE',
|
|
976
|
+
* createdAt: { lt: new Date('2020-01-01') }
|
|
977
|
+
* });
|
|
978
|
+
* console.log(`Deleted ${result.count} products`);
|
|
979
|
+
* ```
|
|
980
|
+
*/
|
|
981
|
+
deleteMany(where: any): Promise<{
|
|
982
|
+
count: number;
|
|
983
|
+
}>;
|
|
984
|
+
/**
|
|
985
|
+
* Count records
|
|
986
|
+
*
|
|
987
|
+
* @param where - Optional where clause to filter records
|
|
988
|
+
* @returns Promise resolving to the count of records
|
|
989
|
+
*
|
|
990
|
+
* @example
|
|
991
|
+
* ```typescript
|
|
992
|
+
* // Count all products
|
|
993
|
+
* const total = await productRepository.count();
|
|
994
|
+
*
|
|
995
|
+
* // Count active products
|
|
996
|
+
* const activeCount = await productRepository.count({ status: 'ACTIVE' });
|
|
997
|
+
* ```
|
|
998
|
+
*/
|
|
999
|
+
count(where?: any): Promise<number>;
|
|
425
1000
|
/**
|
|
426
|
-
*
|
|
1001
|
+
* Check if a record exists
|
|
1002
|
+
*
|
|
1003
|
+
* @param where - The where clause to match records
|
|
1004
|
+
* @returns Promise resolving to true if at least one record exists, false otherwise
|
|
1005
|
+
*
|
|
1006
|
+
* @example
|
|
1007
|
+
* ```typescript
|
|
1008
|
+
* const skuExists = await productRepository.exists({
|
|
1009
|
+
* sku: 'WDG-001'
|
|
1010
|
+
* });
|
|
1011
|
+
* ```
|
|
427
1012
|
*/
|
|
428
|
-
|
|
1013
|
+
exists(where: any): Promise<boolean>;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
declare class RequestService {
|
|
1017
|
+
private readonly request;
|
|
1018
|
+
constructor(request: FastifyRequest);
|
|
1019
|
+
/**
|
|
1020
|
+
* Extract tenant identifier from request headers
|
|
1021
|
+
* Priority: x-tenant-id > x-subdomain
|
|
1022
|
+
* @returns Tenant identifier or null if not found
|
|
1023
|
+
*/
|
|
1024
|
+
getTenantIdentifier(): string | null;
|
|
1025
|
+
/**
|
|
1026
|
+
* Extract access token from Authorization header
|
|
1027
|
+
* Format: "Bearer <token>"
|
|
1028
|
+
* @returns Access token or null if not found
|
|
1029
|
+
*/
|
|
1030
|
+
getAccessToken(): string | null;
|
|
1031
|
+
/**
|
|
1032
|
+
* Extract refresh token from session-id cookie
|
|
1033
|
+
* Cookie name: session-id
|
|
1034
|
+
* @returns Refresh token or null if not found
|
|
1035
|
+
*/
|
|
1036
|
+
getRefreshToken(): string | null;
|
|
1037
|
+
/**
|
|
1038
|
+
* Get a specific header value
|
|
1039
|
+
* @param key Header key
|
|
1040
|
+
* @returns Header value (string, array, or undefined)
|
|
1041
|
+
*/
|
|
1042
|
+
getHeader(key: string): string | string[] | undefined;
|
|
1043
|
+
/**
|
|
1044
|
+
* Get all headers
|
|
1045
|
+
* @returns Record of all headers
|
|
1046
|
+
*/
|
|
1047
|
+
getAllHeaders(): FastifyRequest['headers'];
|
|
429
1048
|
}
|
|
430
1049
|
|
|
431
1050
|
/**
|
|
432
|
-
*
|
|
1051
|
+
* Vritti Authentication Guard - Validates JWT tokens and tenant context
|
|
1052
|
+
*
|
|
1053
|
+
* This guard performs comprehensive validation and attaches user data to request.
|
|
1054
|
+
*
|
|
1055
|
+
* Validation Flow:
|
|
1056
|
+
* 1. Checks if endpoint is marked with @Public() decorator → skip all validation
|
|
1057
|
+
* 2. Checks if endpoint is marked with @Onboarding() decorator:
|
|
1058
|
+
* - Requires token type='onboarding'
|
|
1059
|
+
* - Validates JWT signature and expiry only
|
|
1060
|
+
* - Skips tenant and refresh token validation
|
|
1061
|
+
* - Attaches user data to request.user
|
|
1062
|
+
* 3. For regular endpoints (no decorator):
|
|
1063
|
+
* - Rejects tokens with type='onboarding'
|
|
1064
|
+
* - Validates access token (JWT signature, expiry, nbf)
|
|
1065
|
+
* - Validates refresh token from session-id cookie
|
|
1066
|
+
* - Validates tenant exists and is ACTIVE
|
|
1067
|
+
* - Attaches user data to request.user
|
|
433
1068
|
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
* 3. Stores tenant info in REQUEST-SCOPED TenantContextService
|
|
1069
|
+
* Token Format:
|
|
1070
|
+
* - Access Token: "Authorization: Bearer <jwt_token>"
|
|
1071
|
+
* - Refresh Token: "session-id" cookie
|
|
438
1072
|
*
|
|
439
|
-
*
|
|
440
|
-
* -
|
|
441
|
-
* -
|
|
1073
|
+
* Token Types:
|
|
1074
|
+
* - type='onboarding': Limited access during registration flow (@Onboarding endpoints only)
|
|
1075
|
+
* - type='access': Full access to authenticated endpoints
|
|
442
1076
|
*
|
|
443
|
-
*
|
|
1077
|
+
* Environment Variables Required:
|
|
1078
|
+
* - JWT_SECRET: Secret key to verify access tokens (required)
|
|
1079
|
+
* - JWT_REFRESH_SECRET: Secret key for refresh tokens (optional, falls back to JWT_SECRET)
|
|
1080
|
+
*
|
|
1081
|
+
* Error Responses:
|
|
1082
|
+
* - 401: Invalid/expired access token
|
|
1083
|
+
* - 401: Invalid/expired refresh token
|
|
1084
|
+
* - 401: Tenant not found or inactive
|
|
1085
|
+
* - 401: Tenant identifier not found
|
|
1086
|
+
* - 401: Token type mismatch (onboarding token on regular endpoint or vice versa)
|
|
1087
|
+
*
|
|
1088
|
+
* @example
|
|
1089
|
+
* // Automatically registered by AuthConfigModule.forRootAsync()
|
|
1090
|
+
* // No manual registration needed
|
|
1091
|
+
* //
|
|
1092
|
+
* // Internal registration uses useExisting pattern:
|
|
1093
|
+
* // providers: [
|
|
1094
|
+
* // VrittiAuthGuard,
|
|
1095
|
+
* // {
|
|
1096
|
+
* // provide: APP_GUARD,
|
|
1097
|
+
* // useExisting: VrittiAuthGuard,
|
|
1098
|
+
* // },
|
|
1099
|
+
* // ]
|
|
444
1100
|
*
|
|
445
1101
|
* @example
|
|
446
|
-
* //
|
|
447
|
-
*
|
|
1102
|
+
* // Bypass guard with @Public() decorator
|
|
1103
|
+
* @Public()
|
|
1104
|
+
* @Post('auth/login')
|
|
1105
|
+
* async login(@Body() dto: LoginDto) { ... }
|
|
1106
|
+
*
|
|
1107
|
+
* @example
|
|
1108
|
+
* // Restrict to onboarding tokens with @Onboarding() decorator
|
|
1109
|
+
* @Onboarding()
|
|
1110
|
+
* @Post('onboarding/verify-email')
|
|
1111
|
+
* async verifyEmail(@Request() req) {
|
|
1112
|
+
* const userId = req.user.id; // Available from guard
|
|
1113
|
+
* ...
|
|
1114
|
+
* }
|
|
448
1115
|
*/
|
|
449
|
-
declare class
|
|
450
|
-
private readonly
|
|
1116
|
+
declare class VrittiAuthGuard implements CanActivate {
|
|
1117
|
+
private readonly reflector;
|
|
1118
|
+
private readonly configService;
|
|
1119
|
+
private readonly jwtService;
|
|
451
1120
|
private readonly primaryDatabase;
|
|
452
|
-
private readonly
|
|
1121
|
+
private readonly requestService;
|
|
453
1122
|
private readonly logger;
|
|
454
|
-
constructor(
|
|
455
|
-
|
|
1123
|
+
constructor(reflector: Reflector, configService: ConfigService, jwtService: JwtService, primaryDatabase: PrimaryDatabaseService, requestService: RequestService);
|
|
1124
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
456
1125
|
/**
|
|
457
|
-
*
|
|
1126
|
+
* Validate access token with proper expiry checks
|
|
1127
|
+
* Throws UnauthorizedException if token is invalid or expired
|
|
458
1128
|
*/
|
|
459
|
-
private
|
|
1129
|
+
private validateAccessToken;
|
|
460
1130
|
/**
|
|
461
|
-
*
|
|
462
|
-
*
|
|
1131
|
+
* Validate refresh token with proper expiry checks
|
|
1132
|
+
* Throws UnauthorizedException if token is invalid or expired
|
|
463
1133
|
*/
|
|
464
|
-
private
|
|
1134
|
+
private validateRefreshToken;
|
|
465
1135
|
/**
|
|
466
|
-
*
|
|
467
|
-
* Checks x-tenant-id and x-subdomain headers
|
|
1136
|
+
* Helper to validate refresh token with specific secret
|
|
468
1137
|
*/
|
|
469
|
-
private
|
|
1138
|
+
private validateRefreshTokenWithSecret;
|
|
470
1139
|
}
|
|
471
1140
|
|
|
472
1141
|
/**
|
|
@@ -488,9 +1157,9 @@ declare class TenantContextInterceptor implements NestInterceptor {
|
|
|
488
1157
|
* @Get('info')
|
|
489
1158
|
* async getTenantInfo(@Tenant() tenant: TenantInfo) {
|
|
490
1159
|
* return {
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
1160
|
+
* id: tenant.id,
|
|
1161
|
+
* subdomain: tenant.subdomain,
|
|
1162
|
+
* type: tenant.type,
|
|
494
1163
|
* };
|
|
495
1164
|
* }
|
|
496
1165
|
*
|
|
@@ -501,7 +1170,7 @@ declare class TenantContextInterceptor implements NestInterceptor {
|
|
|
501
1170
|
* @Body() dto: CreateUserDto,
|
|
502
1171
|
* @Tenant() tenant: TenantInfo,
|
|
503
1172
|
* ) {
|
|
504
|
-
* this.logger.log(`Creating user for tenant: ${tenant.
|
|
1173
|
+
* this.logger.log(`Creating user for tenant: ${tenant.subdomain}`);
|
|
505
1174
|
* // ...
|
|
506
1175
|
* }
|
|
507
1176
|
*
|
|
@@ -509,7 +1178,7 @@ declare class TenantContextInterceptor implements NestInterceptor {
|
|
|
509
1178
|
* // Conditional business logic
|
|
510
1179
|
* @Get('features')
|
|
511
1180
|
* async getFeatures(@Tenant() tenant: TenantInfo) {
|
|
512
|
-
* if (tenant.
|
|
1181
|
+
* if (tenant.type === 'ENTERPRISE') {
|
|
513
1182
|
* return ['feature-a', 'feature-b', 'feature-c'];
|
|
514
1183
|
* }
|
|
515
1184
|
* return ['feature-a'];
|
|
@@ -518,17 +1187,142 @@ declare class TenantContextInterceptor implements NestInterceptor {
|
|
|
518
1187
|
declare const Tenant: (...dataOrPipes: unknown[]) => ParameterDecorator;
|
|
519
1188
|
|
|
520
1189
|
/**
|
|
521
|
-
*
|
|
1190
|
+
* Onboarding Decorator - Marks endpoints that require onboarding token
|
|
1191
|
+
*
|
|
1192
|
+
* Use this decorator on controllers or route handlers that should only be
|
|
1193
|
+
* accessible during the onboarding flow with JWT tokens containing type='onboarding'.
|
|
522
1194
|
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
1195
|
+
* These endpoints:
|
|
1196
|
+
* - Accept ONLY tokens with type='onboarding'
|
|
1197
|
+
* - Reject regular access tokens (type='access')
|
|
1198
|
+
* - Skip tenant validation and refresh token checks
|
|
1199
|
+
* - Only validate JWT signature and expiry
|
|
1200
|
+
*
|
|
1201
|
+
* Useful for:
|
|
1202
|
+
* - Email/phone verification during onboarding
|
|
1203
|
+
* - Onboarding status checks
|
|
1204
|
+
* - Resending OTPs during registration
|
|
1205
|
+
*
|
|
1206
|
+
* @example
|
|
1207
|
+
* // On a controller method
|
|
1208
|
+
* @Post('verify-email')
|
|
1209
|
+
* @Onboarding()
|
|
1210
|
+
* async verifyEmail(@Request() req, @Body() dto: VerifyEmailDto) {
|
|
1211
|
+
* const userId = req.user.id; // Available from VrittiAuthGuard
|
|
1212
|
+
* return this.service.verifyEmail(userId, dto.otp);
|
|
1213
|
+
* }
|
|
525
1214
|
*
|
|
526
1215
|
* @example
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
1216
|
+
* // Multiple onboarding endpoints
|
|
1217
|
+
* @Controller('onboarding')
|
|
1218
|
+
* export class OnboardingController {
|
|
1219
|
+
* @Post('verify-email')
|
|
1220
|
+
* @Onboarding()
|
|
1221
|
+
* async verifyEmail() { ... }
|
|
1222
|
+
*
|
|
1223
|
+
* @Post('resend-otp')
|
|
1224
|
+
* @Onboarding()
|
|
1225
|
+
* async resendOtp() { ... }
|
|
1226
|
+
* }
|
|
1227
|
+
*/
|
|
1228
|
+
declare const Onboarding: () => _nestjs_common.CustomDecorator<string>;
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Public Decorator - Marks endpoints that don't require authentication
|
|
1232
|
+
*
|
|
1233
|
+
* Use this decorator on controllers or route handlers to bypass VrittiAuthGuard
|
|
1234
|
+
* tenant validation. Useful for:
|
|
1235
|
+
* - Login/signup endpoints
|
|
1236
|
+
* - Health checks
|
|
1237
|
+
* - Public documentation endpoints
|
|
1238
|
+
* - Webhook endpoints that don't require tenant context
|
|
1239
|
+
*
|
|
1240
|
+
* @example
|
|
1241
|
+
* // On a controller method
|
|
1242
|
+
* @Public()
|
|
1243
|
+
* @Post('auth/login')
|
|
1244
|
+
* async login(@Body() dto: LoginDto) {
|
|
1245
|
+
* return this.authService.login(dto);
|
|
1246
|
+
* }
|
|
1247
|
+
*
|
|
1248
|
+
* @example
|
|
1249
|
+
* // On an entire controller
|
|
1250
|
+
* @Public()
|
|
1251
|
+
* @Controller('health')
|
|
1252
|
+
* export class HealthController {
|
|
1253
|
+
* @Get()
|
|
1254
|
+
* check() {
|
|
1255
|
+
* return { status: 'ok' };
|
|
1256
|
+
* }
|
|
1257
|
+
* }
|
|
531
1258
|
*/
|
|
532
|
-
declare
|
|
1259
|
+
declare const Public: () => _nestjs_common.CustomDecorator<string>;
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* HTTP Module
|
|
1263
|
+
*
|
|
1264
|
+
* Provides HTTP utilities including:
|
|
1265
|
+
* - CSRF Guard for request protection
|
|
1266
|
+
* - HTTP Exception Filter for standardized error responses
|
|
1267
|
+
*
|
|
1268
|
+
* Usage:
|
|
1269
|
+
* Import this module to access HTTP guards and filters.
|
|
1270
|
+
* Guards and filters are registered globally in the main application.
|
|
1271
|
+
*/
|
|
1272
|
+
declare class HttpModule {
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* CSRF Guard
|
|
1277
|
+
*
|
|
1278
|
+
* Global guard that automatically protects all state-changing requests (POST, PUT, PATCH, DELETE)
|
|
1279
|
+
* from CSRF attacks using Fastify's csrf-protection plugin.
|
|
1280
|
+
*
|
|
1281
|
+
* Flow:
|
|
1282
|
+
* 1. Skip safe methods (GET, HEAD, OPTIONS)
|
|
1283
|
+
* 2. Skip endpoints marked with @Public()
|
|
1284
|
+
* 3. Validate CSRF token for all other requests
|
|
1285
|
+
*
|
|
1286
|
+
* Token Sources (in priority order by @fastify/csrf-protection):
|
|
1287
|
+
* 1. req.headers['csrf-token']
|
|
1288
|
+
* 2. req.headers['xsrf-token']
|
|
1289
|
+
* 3. req.headers['x-csrf-token']
|
|
1290
|
+
* 4. req.headers['x-xsrf-token']
|
|
1291
|
+
* 5. req.body._csrf
|
|
1292
|
+
*
|
|
1293
|
+
* This guard should be registered globally in main.ts after CSRF plugin registration.
|
|
1294
|
+
*/
|
|
1295
|
+
declare class CsrfGuard implements CanActivate {
|
|
1296
|
+
private reflector;
|
|
1297
|
+
private readonly logger;
|
|
1298
|
+
constructor(reflector: Reflector);
|
|
1299
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* Global HTTP Exception Filter
|
|
1304
|
+
*
|
|
1305
|
+
* Standardizes all error responses in the format:
|
|
1306
|
+
* {
|
|
1307
|
+
* errors: [{ field: string, message: string }],
|
|
1308
|
+
* message?: string,
|
|
1309
|
+
* statusCode: number,
|
|
1310
|
+
* timestamp: string,
|
|
1311
|
+
* path: string
|
|
1312
|
+
* }
|
|
1313
|
+
*
|
|
1314
|
+
* Handles:
|
|
1315
|
+
* - Validation errors (class-validator) - Converts to field-specific errors
|
|
1316
|
+
* - HTTP exceptions - Maps to standardized format
|
|
1317
|
+
* - Unknown errors - Returns generic 500 error
|
|
1318
|
+
*/
|
|
1319
|
+
declare class HttpExceptionFilter implements ExceptionFilter {
|
|
1320
|
+
private readonly logger;
|
|
1321
|
+
catch(exception: unknown, host: ArgumentsHost): void;
|
|
1322
|
+
/**
|
|
1323
|
+
* Parse class-validator error messages into field-specific errors
|
|
1324
|
+
*/
|
|
1325
|
+
private parseValidationErrors;
|
|
1326
|
+
}
|
|
533
1327
|
|
|
534
|
-
export { DatabaseModule, type DatabaseModuleOptions,
|
|
1328
|
+
export { AuthConfigModule, CsrfGuard, DatabaseModule, type DatabaseModuleOptions, HttpExceptionFilter, HttpModule, Onboarding, PrimaryBaseRepository, PrimaryDatabaseService, type PrimaryDbConfig, Public, Tenant, TenantBaseRepository, TenantContextService, TenantDatabaseService, type TenantInfo, VrittiAuthGuard };
|