@vritti/api-sdk 0.1.8 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2044 -1758
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +214 -2277
- package/dist/index.d.ts +214 -2277
- package/dist/index.js +1998 -1725
- package/dist/index.js.map +1 -1
- package/package.json +24 -6
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
3
3
|
|
|
4
4
|
// src/auth/auth-config.module.ts
|
|
5
5
|
import { Global as Global2, Module as Module2 } from "@nestjs/common";
|
|
6
|
-
import { ConfigModule, ConfigService as
|
|
6
|
+
import { ConfigModule, ConfigService as ConfigService3 } from "@nestjs/config";
|
|
7
7
|
import { APP_GUARD, Reflector as Reflector2 } from "@nestjs/core";
|
|
8
8
|
import { JwtModule } from "@nestjs/jwt";
|
|
9
9
|
|
|
@@ -27,8 +27,7 @@ var defaultConfig = {
|
|
|
27
27
|
jwt: {
|
|
28
28
|
accessTokenExpiry: "15m",
|
|
29
29
|
refreshTokenExpiry: "30d",
|
|
30
|
-
onboardingTokenExpiry: "24h"
|
|
31
|
-
validateTokenBinding: true
|
|
30
|
+
onboardingTokenExpiry: "24h"
|
|
32
31
|
},
|
|
33
32
|
guard: {
|
|
34
33
|
tenantHeaderName: "x-tenant-id",
|
|
@@ -119,11 +118,7 @@ var RequestService = class {
|
|
|
119
118
|
constructor(request) {
|
|
120
119
|
this.request = request;
|
|
121
120
|
}
|
|
122
|
-
|
|
123
|
-
* Extract tenant identifier from request headers
|
|
124
|
-
* Priority: x-tenant-id > x-subdomain
|
|
125
|
-
* @returns Tenant identifier or null if not found
|
|
126
|
-
*/
|
|
121
|
+
// Extracts tenant identifier from x-tenant-id or x-subdomain request header
|
|
127
122
|
getTenantIdentifier() {
|
|
128
123
|
const getHeader = /* @__PURE__ */ __name((key) => {
|
|
129
124
|
const value = this.request.headers?.[key];
|
|
@@ -131,11 +126,7 @@ var RequestService = class {
|
|
|
131
126
|
}, "getHeader");
|
|
132
127
|
return getHeader("x-tenant-id") || getHeader("x-subdomain") || null;
|
|
133
128
|
}
|
|
134
|
-
|
|
135
|
-
* Extract access token from Authorization header
|
|
136
|
-
* Format: "Bearer <token>"
|
|
137
|
-
* @returns Access token or null if not found
|
|
138
|
-
*/
|
|
129
|
+
// Extracts the bearer access token from the Authorization header
|
|
139
130
|
getAccessToken() {
|
|
140
131
|
const authHeader = this.request.headers?.authorization;
|
|
141
132
|
if (!authHeader) {
|
|
@@ -144,11 +135,7 @@ var RequestService = class {
|
|
|
144
135
|
const [type, token] = authHeader.split(" ") ?? [];
|
|
145
136
|
return type === "Bearer" && token ? token : null;
|
|
146
137
|
}
|
|
147
|
-
|
|
148
|
-
* Extract refresh token from httpOnly cookie
|
|
149
|
-
* Cookie name is configurable via api-sdk config
|
|
150
|
-
* @returns Refresh token or null if not found
|
|
151
|
-
*/
|
|
138
|
+
// Extracts the refresh token from the configured httpOnly cookie
|
|
152
139
|
getRefreshToken() {
|
|
153
140
|
try {
|
|
154
141
|
const cookies = this.request.cookies;
|
|
@@ -164,18 +151,11 @@ var RequestService = class {
|
|
|
164
151
|
return null;
|
|
165
152
|
}
|
|
166
153
|
}
|
|
167
|
-
|
|
168
|
-
* Get a specific header value
|
|
169
|
-
* @param key Header key
|
|
170
|
-
* @returns Header value (string, array, or undefined)
|
|
171
|
-
*/
|
|
154
|
+
// Returns the value of a specific request header by key
|
|
172
155
|
getHeader(key) {
|
|
173
156
|
return this.request.headers?.[key];
|
|
174
157
|
}
|
|
175
|
-
|
|
176
|
-
* Get all headers
|
|
177
|
-
* @returns Record of all headers
|
|
178
|
-
*/
|
|
158
|
+
// Returns all request headers
|
|
179
159
|
getAllHeaders() {
|
|
180
160
|
return this.request.headers || {};
|
|
181
161
|
}
|
|
@@ -217,252 +197,21 @@ RequestModule = _ts_decorate2([
|
|
|
217
197
|
], RequestModule);
|
|
218
198
|
|
|
219
199
|
// src/auth/guards/vritti-auth.guard.ts
|
|
220
|
-
import { ForbiddenException, Injectable as
|
|
200
|
+
import { ForbiddenException, Injectable as Injectable2, Logger as Logger2, Scope as Scope2, UnauthorizedException } from "@nestjs/common";
|
|
201
|
+
import { SSE_METADATA } from "@nestjs/common/constants";
|
|
221
202
|
import { ConfigService } from "@nestjs/config";
|
|
222
203
|
import { Reflector } from "@nestjs/core";
|
|
223
204
|
import { JwtService } from "@nestjs/jwt";
|
|
224
205
|
|
|
225
|
-
// src/auth/decorators/
|
|
206
|
+
// src/auth/decorators/reset.decorator.ts
|
|
226
207
|
import { SetMetadata } from "@nestjs/common";
|
|
227
|
-
var
|
|
228
|
-
var
|
|
229
|
-
|
|
230
|
-
// src/database/services/primary-database.service.ts
|
|
231
|
-
import { Inject as Inject2, Injectable as Injectable2, InternalServerErrorException, Logger as Logger2 } from "@nestjs/common";
|
|
232
|
-
import { eq, or } from "drizzle-orm";
|
|
233
|
-
import { drizzle } from "drizzle-orm/node-postgres";
|
|
234
|
-
import { Pool } from "pg";
|
|
235
|
-
|
|
236
|
-
// src/database/constants.ts
|
|
237
|
-
var DATABASE_MODULE_OPTIONS = Symbol("DATABASE_MODULE_OPTIONS");
|
|
208
|
+
var RESET_KEY = "isReset";
|
|
209
|
+
var Reset = /* @__PURE__ */ __name(() => SetMetadata(RESET_KEY, true), "Reset");
|
|
238
210
|
|
|
239
|
-
// src/
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
244
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
245
|
-
}
|
|
246
|
-
__name(_ts_decorate3, "_ts_decorate");
|
|
247
|
-
function _ts_metadata2(k, v) {
|
|
248
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
249
|
-
}
|
|
250
|
-
__name(_ts_metadata2, "_ts_metadata");
|
|
251
|
-
function _ts_param2(paramIndex, decorator) {
|
|
252
|
-
return function(target, key) {
|
|
253
|
-
decorator(target, key, paramIndex);
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
__name(_ts_param2, "_ts_param");
|
|
257
|
-
var PrimaryDatabaseService = class _PrimaryDatabaseService {
|
|
258
|
-
static {
|
|
259
|
-
__name(this, "PrimaryDatabaseService");
|
|
260
|
-
}
|
|
261
|
-
options;
|
|
262
|
-
logger = new Logger2(_PrimaryDatabaseService.name);
|
|
263
|
-
/** PostgreSQL connection pool */
|
|
264
|
-
pool = null;
|
|
265
|
-
/** Drizzle database instance */
|
|
266
|
-
db = null;
|
|
267
|
-
/** In-memory cache: Map<tenantIdentifier, TenantConfig> */
|
|
268
|
-
tenantConfigCache = /* @__PURE__ */ new Map();
|
|
269
|
-
/** Cache TTL in milliseconds */
|
|
270
|
-
cacheTTL;
|
|
271
|
-
constructor(options) {
|
|
272
|
-
this.options = options;
|
|
273
|
-
this.cacheTTL = options.connectionCacheTTL || 3e5;
|
|
274
|
-
}
|
|
275
|
-
async onModuleInit() {
|
|
276
|
-
if (this.options.primaryDb) {
|
|
277
|
-
await this.initializeDrizzleClient();
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Initialize connection to primary database using Drizzle
|
|
282
|
-
*/
|
|
283
|
-
async initializeDrizzleClient() {
|
|
284
|
-
try {
|
|
285
|
-
const databaseUrl = this.buildPrimaryDbUrl();
|
|
286
|
-
this.pool = new Pool({
|
|
287
|
-
connectionString: databaseUrl,
|
|
288
|
-
max: this.options.maxConnections || 10
|
|
289
|
-
});
|
|
290
|
-
this.logger.debug(`Schema keys passed to drizzle: [${Object.keys(this.options.drizzleSchema || {}).join(", ")}]`);
|
|
291
|
-
this.logger.debug(`Relations keys passed to drizzle: [${Object.keys(this.options.drizzleRelations || {}).join(", ")}]`);
|
|
292
|
-
this.db = drizzle({
|
|
293
|
-
client: this.pool,
|
|
294
|
-
schema: this.options.drizzleSchema,
|
|
295
|
-
relations: this.options.drizzleRelations
|
|
296
|
-
});
|
|
297
|
-
this.logger.debug(`Drizzle query keys after init: [${Object.keys(this.db.query || {}).join(", ")}]`);
|
|
298
|
-
await this.pool.query("SELECT 1");
|
|
299
|
-
this.logger.log("Connected to primary database (tenant registry)");
|
|
300
|
-
} catch (error) {
|
|
301
|
-
this.logger.error("Failed to connect to primary database", error);
|
|
302
|
-
throw new InternalServerErrorException("Failed to initialize tenant registry");
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
/**
|
|
306
|
-
* Build connection URL from primary database properties
|
|
307
|
-
*/
|
|
308
|
-
buildPrimaryDbUrl() {
|
|
309
|
-
if (!this.options.primaryDb) {
|
|
310
|
-
throw new Error("Primary database configuration not provided");
|
|
311
|
-
}
|
|
312
|
-
const { host, port = 5432, username, password, database, schema = "public", sslMode = "require" } = this.options.primaryDb;
|
|
313
|
-
let url = `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}`;
|
|
314
|
-
const params = new URLSearchParams();
|
|
315
|
-
if (schema) {
|
|
316
|
-
params.set("schema", schema);
|
|
317
|
-
}
|
|
318
|
-
params.set("sslmode", sslMode);
|
|
319
|
-
const queryString = params.toString();
|
|
320
|
-
if (queryString) {
|
|
321
|
-
url += `?${queryString}`;
|
|
322
|
-
}
|
|
323
|
-
this.logger.debug(`Primary DB connection URL: ${this.maskPassword(url)}`);
|
|
324
|
-
return url;
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Mask password in connection URL for logging
|
|
328
|
-
*/
|
|
329
|
-
maskPassword(url) {
|
|
330
|
-
return url.replace(/:([^@]+)@/, ":****@");
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Get tenant configuration by identifier (ID or subdomain)
|
|
334
|
-
*
|
|
335
|
-
* @param tenantIdentifier Tenant ID or subdomain
|
|
336
|
-
* @returns Tenant configuration or null if not found
|
|
337
|
-
*/
|
|
338
|
-
async getTenantInfo(tenantIdentifier) {
|
|
339
|
-
const cached = this.tenantConfigCache.get(tenantIdentifier);
|
|
340
|
-
if (cached) {
|
|
341
|
-
this.logger.debug(`Cache hit for tenant: ${tenantIdentifier}`);
|
|
342
|
-
return cached;
|
|
343
|
-
}
|
|
344
|
-
try {
|
|
345
|
-
if (!this.db) {
|
|
346
|
-
throw new Error("Primary database client not initialized");
|
|
347
|
-
}
|
|
348
|
-
this.logger.debug(`Querying primary database for tenant: ${tenantIdentifier}`);
|
|
349
|
-
const schema = this.options.drizzleSchema;
|
|
350
|
-
const { tenants, tenantDatabaseConfigs } = schema;
|
|
351
|
-
const result = await this.db.select().from(tenants).leftJoin(tenantDatabaseConfigs, eq(tenants.id, tenantDatabaseConfigs.tenantId)).where(or(eq(tenants.id, tenantIdentifier), eq(tenants.subdomain, tenantIdentifier))).limit(1);
|
|
352
|
-
if (!result.length) {
|
|
353
|
-
this.logger.warn(`Tenant not found: ${tenantIdentifier}`);
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
const row = result[0];
|
|
357
|
-
const tenant = row.tenants;
|
|
358
|
-
const config = row.tenant_database_configs;
|
|
359
|
-
if (tenant.status !== "ACTIVE") {
|
|
360
|
-
this.logger.warn(`Tenant not active: ${tenantIdentifier}`);
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
const info = {
|
|
364
|
-
id: tenant.id,
|
|
365
|
-
subdomain: tenant.subdomain,
|
|
366
|
-
type: tenant.dbType,
|
|
367
|
-
status: tenant.status,
|
|
368
|
-
// For SHARED tenants: schema name
|
|
369
|
-
schemaName: config?.dbSchema || void 0,
|
|
370
|
-
// For DEDICATED tenants: database configuration from TenantDatabaseConfig table
|
|
371
|
-
databaseName: config?.dbName || void 0,
|
|
372
|
-
databaseHost: config?.dbHost || void 0,
|
|
373
|
-
databasePort: config?.dbPort || void 0,
|
|
374
|
-
databaseUsername: config?.dbUsername ? this.decrypt(config.dbUsername) : void 0,
|
|
375
|
-
databasePassword: config?.dbPassword ? this.decrypt(config.dbPassword) : void 0,
|
|
376
|
-
databaseSslMode: config?.dbSslMode || void 0,
|
|
377
|
-
connectionPoolSize: config?.connectionPoolSize || void 0
|
|
378
|
-
};
|
|
379
|
-
this.cacheInfo(info);
|
|
380
|
-
return info;
|
|
381
|
-
} catch (error) {
|
|
382
|
-
this.logger.error(`Failed to fetch tenant info: ${tenantIdentifier}`, error);
|
|
383
|
-
throw new InternalServerErrorException("Failed to resolve tenant");
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
/**
|
|
387
|
-
* Cache tenant information with TTL
|
|
388
|
-
*/
|
|
389
|
-
cacheInfo(info) {
|
|
390
|
-
this.tenantConfigCache.set(info.id, info);
|
|
391
|
-
this.tenantConfigCache.set(info.subdomain, info);
|
|
392
|
-
setTimeout(() => {
|
|
393
|
-
this.tenantConfigCache.delete(info.id);
|
|
394
|
-
this.tenantConfigCache.delete(info.subdomain);
|
|
395
|
-
this.logger.debug(`Cache expired for tenant: ${info.subdomain}`);
|
|
396
|
-
}, this.cacheTTL);
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Clear cached tenant information
|
|
400
|
-
*
|
|
401
|
-
* Useful when tenant settings are updated and cache needs to be invalidated
|
|
402
|
-
*
|
|
403
|
-
* @param tenantIdentifier Tenant ID or subdomain
|
|
404
|
-
*/
|
|
405
|
-
clearTenantCache(tenantIdentifier) {
|
|
406
|
-
const config = this.tenantConfigCache.get(tenantIdentifier);
|
|
407
|
-
if (config) {
|
|
408
|
-
this.tenantConfigCache.delete(config.id);
|
|
409
|
-
this.tenantConfigCache.delete(config.subdomain);
|
|
410
|
-
this.logger.log(`Cleared cache for tenant: ${tenantIdentifier}`);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Clear all cached tenant configurations
|
|
415
|
-
*/
|
|
416
|
-
clearAllCaches() {
|
|
417
|
-
const size = this.tenantConfigCache.size;
|
|
418
|
-
this.tenantConfigCache.clear();
|
|
419
|
-
this.logger.log(`Cleared ${size} cached tenant configs`);
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Get the Drizzle database instance for the primary database.
|
|
423
|
-
* This is a synchronous property that returns the initialized Drizzle client.
|
|
424
|
-
*
|
|
425
|
-
* @returns Primary database Drizzle instance
|
|
426
|
-
* @throws Error if primary database client is not initialized
|
|
427
|
-
*/
|
|
428
|
-
get drizzleClient() {
|
|
429
|
-
if (!this.db) {
|
|
430
|
-
throw new Error("Primary database client not initialized");
|
|
431
|
-
}
|
|
432
|
-
return this.db;
|
|
433
|
-
}
|
|
434
|
-
/**
|
|
435
|
-
* Get the Drizzle schema
|
|
436
|
-
*/
|
|
437
|
-
get schema() {
|
|
438
|
-
return this.options.drizzleSchema;
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Decrypt database credentials
|
|
442
|
-
*
|
|
443
|
-
* Override this method to implement your encryption strategy
|
|
444
|
-
*
|
|
445
|
-
* @param encrypted Encrypted value
|
|
446
|
-
* @returns Decrypted value
|
|
447
|
-
*/
|
|
448
|
-
decrypt(encrypted) {
|
|
449
|
-
return encrypted;
|
|
450
|
-
}
|
|
451
|
-
async onModuleDestroy() {
|
|
452
|
-
if (this.pool) {
|
|
453
|
-
await this.pool.end();
|
|
454
|
-
this.logger.log("Disconnected from primary database");
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
};
|
|
458
|
-
PrimaryDatabaseService = _ts_decorate3([
|
|
459
|
-
Injectable2(),
|
|
460
|
-
_ts_param2(0, Inject2(DATABASE_MODULE_OPTIONS)),
|
|
461
|
-
_ts_metadata2("design:type", Function),
|
|
462
|
-
_ts_metadata2("design:paramtypes", [
|
|
463
|
-
typeof DatabaseModuleOptions === "undefined" ? Object : DatabaseModuleOptions
|
|
464
|
-
])
|
|
465
|
-
], PrimaryDatabaseService);
|
|
211
|
+
// src/auth/decorators/skip-csrf.decorator.ts
|
|
212
|
+
import { SetMetadata as SetMetadata2 } from "@nestjs/common";
|
|
213
|
+
var SKIP_CSRF_KEY = "skipCsrf";
|
|
214
|
+
var SkipCsrf = /* @__PURE__ */ __name(() => SetMetadata2(SKIP_CSRF_KEY, true), "SkipCsrf");
|
|
466
215
|
|
|
467
216
|
// src/auth/utils/token-hash.util.ts
|
|
468
217
|
import * as crypto from "crypto";
|
|
@@ -478,17 +227,17 @@ function verifyTokenHash(token, expectedHash) {
|
|
|
478
227
|
__name(verifyTokenHash, "verifyTokenHash");
|
|
479
228
|
|
|
480
229
|
// src/auth/guards/vritti-auth.guard.ts
|
|
481
|
-
function
|
|
230
|
+
function _ts_decorate3(decorators, target, key, desc) {
|
|
482
231
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
483
232
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
484
233
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
485
234
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
486
235
|
}
|
|
487
|
-
__name(
|
|
488
|
-
function
|
|
236
|
+
__name(_ts_decorate3, "_ts_decorate");
|
|
237
|
+
function _ts_metadata2(k, v) {
|
|
489
238
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
490
239
|
}
|
|
491
|
-
__name(
|
|
240
|
+
__name(_ts_metadata2, "_ts_metadata");
|
|
492
241
|
var VrittiAuthGuard = class _VrittiAuthGuard {
|
|
493
242
|
static {
|
|
494
243
|
__name(this, "VrittiAuthGuard");
|
|
@@ -496,14 +245,12 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
|
|
|
496
245
|
reflector;
|
|
497
246
|
_configService;
|
|
498
247
|
jwtService;
|
|
499
|
-
primaryDatabase;
|
|
500
248
|
requestService;
|
|
501
|
-
logger = new
|
|
502
|
-
constructor(reflector, _configService, jwtService,
|
|
249
|
+
logger = new Logger2(_VrittiAuthGuard.name);
|
|
250
|
+
constructor(reflector, _configService, jwtService, requestService) {
|
|
503
251
|
this.reflector = reflector;
|
|
504
252
|
this._configService = _configService;
|
|
505
253
|
this.jwtService = jwtService;
|
|
506
|
-
this.primaryDatabase = primaryDatabase;
|
|
507
254
|
this.requestService = requestService;
|
|
508
255
|
}
|
|
509
256
|
async canActivate(context) {
|
|
@@ -521,69 +268,44 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
|
|
|
521
268
|
context.getClass()
|
|
522
269
|
]);
|
|
523
270
|
if (isPublic) {
|
|
524
|
-
this.logger.debug("Public endpoint detected, skipping authentication");
|
|
525
271
|
return true;
|
|
526
272
|
}
|
|
527
273
|
const isOnboarding = this.reflector.getAllAndOverride("isOnboarding", [
|
|
528
274
|
context.getHandler(),
|
|
529
275
|
context.getClass()
|
|
530
276
|
]);
|
|
277
|
+
const isReset = this.reflector.getAllAndOverride(RESET_KEY, [
|
|
278
|
+
context.getHandler(),
|
|
279
|
+
context.getClass()
|
|
280
|
+
]);
|
|
281
|
+
const isSseEndpoint = this.reflector.get(SSE_METADATA, context.getHandler());
|
|
282
|
+
if (isSseEndpoint) {
|
|
283
|
+
return this.handleSseAuth(request, isOnboarding);
|
|
284
|
+
}
|
|
531
285
|
try {
|
|
532
286
|
const accessToken = this.requestService.getAccessToken();
|
|
533
287
|
if (!accessToken) {
|
|
534
|
-
this.logger.warn("Access token not found in Authorization header");
|
|
535
288
|
throw new UnauthorizedException("Access token not found");
|
|
536
289
|
}
|
|
537
|
-
const
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
throw new UnauthorizedException("Invalid token format");
|
|
541
|
-
}
|
|
542
|
-
if (isOnboarding) {
|
|
543
|
-
if (decodedToken.type !== "onboarding") {
|
|
544
|
-
this.logger.warn("Onboarding endpoint requires onboarding token");
|
|
545
|
-
throw new UnauthorizedException("This endpoint requires an onboarding token");
|
|
546
|
-
}
|
|
547
|
-
const validatedToken2 = this.validateAccessToken(accessToken);
|
|
548
|
-
this.logger.debug("Onboarding token validated successfully");
|
|
549
|
-
this.validateRefreshTokenBinding(context, validatedToken2);
|
|
550
|
-
const userId2 = validatedToken2.userId;
|
|
551
|
-
request.user = {
|
|
552
|
-
id: userId2
|
|
553
|
-
};
|
|
554
|
-
return true;
|
|
555
|
-
}
|
|
556
|
-
if (decodedToken.type === "onboarding") {
|
|
557
|
-
this.logger.warn("Regular endpoint accessed with onboarding token");
|
|
558
|
-
throw new UnauthorizedException("Onboarding tokens cannot access this endpoint");
|
|
290
|
+
const decodedAccessToken = this.validateAccessToken(accessToken);
|
|
291
|
+
if (decodedAccessToken.tokenType !== "access") {
|
|
292
|
+
throw new UnauthorizedException("Invalid token type");
|
|
559
293
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const userId = validatedToken.userId;
|
|
564
|
-
request.user = {
|
|
565
|
-
id: userId
|
|
566
|
-
};
|
|
567
|
-
const tenantIdentifier = this.requestService.getTenantIdentifier();
|
|
568
|
-
if (!tenantIdentifier) {
|
|
569
|
-
this.logger.warn("Tenant identifier not found in request");
|
|
570
|
-
throw new UnauthorizedException("Tenant identifier not found");
|
|
571
|
-
}
|
|
572
|
-
this.logger.debug(`Tenant identifier extracted: ${tenantIdentifier}`);
|
|
573
|
-
if (tenantIdentifier === "cloud") {
|
|
574
|
-
this.logger.debug("Platform admin access detected, skipping tenant database validation");
|
|
575
|
-
return true;
|
|
294
|
+
this.validateRefreshTokenBinding(decodedAccessToken);
|
|
295
|
+
if (isOnboarding && decodedAccessToken.sessionType !== "ONBOARDING") {
|
|
296
|
+
throw new UnauthorizedException("This endpoint requires an onboarding session");
|
|
576
297
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
this.logger.warn(`Invalid tenant: ${tenantIdentifier}`);
|
|
580
|
-
throw new UnauthorizedException("Invalid tenant");
|
|
298
|
+
if (isReset && decodedAccessToken.sessionType !== "RESET") {
|
|
299
|
+
throw new UnauthorizedException("This endpoint requires a reset session");
|
|
581
300
|
}
|
|
582
|
-
if (
|
|
583
|
-
|
|
584
|
-
throw new UnauthorizedException(`Tenant is ${tenantInfo.status}`);
|
|
301
|
+
if (!isOnboarding && !isReset && (decodedAccessToken.sessionType === "ONBOARDING" || decodedAccessToken.sessionType === "RESET")) {
|
|
302
|
+
throw new UnauthorizedException(`${decodedAccessToken.sessionType} sessions cannot access this endpoint`);
|
|
585
303
|
}
|
|
586
|
-
|
|
304
|
+
request.sessionInfo = {
|
|
305
|
+
userId: decodedAccessToken.userId,
|
|
306
|
+
sessionId: decodedAccessToken.sessionId,
|
|
307
|
+
sessionType: decodedAccessToken.sessionType
|
|
308
|
+
};
|
|
587
309
|
return true;
|
|
588
310
|
} catch (error) {
|
|
589
311
|
if (error instanceof UnauthorizedException) {
|
|
@@ -593,109 +315,91 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
|
|
|
593
315
|
throw new UnauthorizedException("Authentication failed");
|
|
594
316
|
}
|
|
595
317
|
}
|
|
596
|
-
|
|
597
|
-
* Validate access token with proper expiry checks
|
|
598
|
-
* Throws UnauthorizedException if token is invalid or expired
|
|
599
|
-
*/
|
|
318
|
+
// Validates JWT signature, expiry, and not-before claims
|
|
600
319
|
validateAccessToken(token) {
|
|
601
320
|
try {
|
|
602
|
-
|
|
603
|
-
this.logger.debug(`Access token decoded for user: ${decoded.userId}`);
|
|
604
|
-
if (decoded.exp) {
|
|
605
|
-
const expiryTime = decoded.exp * 1e3;
|
|
606
|
-
const currentTime = Date.now();
|
|
607
|
-
const timeRemaining = expiryTime - currentTime;
|
|
608
|
-
this.logger.debug(`Access token valid for ${Math.floor(timeRemaining / 1e3)} more seconds`);
|
|
609
|
-
}
|
|
610
|
-
return decoded;
|
|
321
|
+
return this.jwtService.verify(token);
|
|
611
322
|
} catch (error) {
|
|
612
|
-
if (error instanceof UnauthorizedException)
|
|
613
|
-
throw error;
|
|
614
|
-
}
|
|
323
|
+
if (error instanceof UnauthorizedException) throw error;
|
|
615
324
|
const jwtError = error;
|
|
616
325
|
if (jwtError?.name === "TokenExpiredError") {
|
|
617
|
-
this.logger.warn(`Access token expired at: ${jwtError?.expiredAt}`);
|
|
618
326
|
throw new UnauthorizedException("Access token has expired");
|
|
619
327
|
}
|
|
620
328
|
if (jwtError?.name === "JsonWebTokenError") {
|
|
621
|
-
this.logger.warn(`Access token verification failed: ${jwtError?.message}`);
|
|
622
329
|
throw new UnauthorizedException("Invalid access token");
|
|
623
330
|
}
|
|
624
331
|
if (jwtError?.name === "NotBeforeError") {
|
|
625
|
-
this.logger.warn("Access token used before valid (nbf claim)");
|
|
626
332
|
throw new UnauthorizedException("Access token not yet valid");
|
|
627
333
|
}
|
|
628
|
-
this.logger.error("Unexpected error validating access token", error);
|
|
629
334
|
throw new UnauthorizedException("Access token validation failed");
|
|
630
335
|
}
|
|
631
336
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
*
|
|
637
|
-
* @param context - The execution context containing the request
|
|
638
|
-
* @param validatedToken - The decoded and validated JWT token
|
|
639
|
-
* @throws UnauthorizedException if token binding validation fails
|
|
640
|
-
*/
|
|
641
|
-
validateRefreshTokenBinding(context, validatedToken) {
|
|
642
|
-
const config = getConfig();
|
|
643
|
-
if (!config.jwt.validateTokenBinding) {
|
|
644
|
-
this.logger.debug("Token binding validation is disabled");
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
if (!validatedToken.refreshTokenHash) {
|
|
648
|
-
this.logger.debug("Token does not contain refreshTokenHash, skipping binding validation");
|
|
649
|
-
return;
|
|
337
|
+
// Validates that the access token is bound to the refresh token in the cookie
|
|
338
|
+
validateRefreshTokenBinding(decodedAccessToken) {
|
|
339
|
+
if (!decodedAccessToken.refreshTokenHash) {
|
|
340
|
+
throw new UnauthorizedException("Token missing refresh token binding");
|
|
650
341
|
}
|
|
651
|
-
const
|
|
652
|
-
const cookies = request.cookies || {};
|
|
653
|
-
const refreshToken = cookies[config.cookie.refreshCookieName];
|
|
342
|
+
const refreshToken = this.requestService.getRefreshToken();
|
|
654
343
|
if (!refreshToken) {
|
|
655
|
-
this.logger.warn("Session validation failed - refresh token cookie not found");
|
|
656
344
|
throw new UnauthorizedException("Session validation failed");
|
|
657
345
|
}
|
|
658
|
-
if (!verifyTokenHash(refreshToken,
|
|
659
|
-
this.logger.warn("Session validation failed - token binding mismatch");
|
|
346
|
+
if (!verifyTokenHash(refreshToken, decodedAccessToken.refreshTokenHash)) {
|
|
660
347
|
throw new UnauthorizedException("Session validation failed");
|
|
661
348
|
}
|
|
662
|
-
this.logger.debug("Token binding validated successfully");
|
|
663
349
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
350
|
+
// Authenticates SSE connections using the refresh token httpOnly cookie
|
|
351
|
+
handleSseAuth(request, isOnboarding) {
|
|
352
|
+
const refreshToken = this.requestService.getRefreshToken();
|
|
353
|
+
if (!refreshToken) {
|
|
354
|
+
throw new UnauthorizedException("Authentication required");
|
|
355
|
+
}
|
|
356
|
+
let decoded;
|
|
357
|
+
try {
|
|
358
|
+
decoded = this.jwtService.verify(refreshToken);
|
|
359
|
+
} catch {
|
|
360
|
+
throw new UnauthorizedException("Invalid or expired session");
|
|
361
|
+
}
|
|
362
|
+
if (decoded.tokenType !== "refresh") {
|
|
363
|
+
throw new UnauthorizedException("Invalid token type");
|
|
364
|
+
}
|
|
365
|
+
if (isOnboarding && decoded.sessionType !== "ONBOARDING") {
|
|
366
|
+
throw new UnauthorizedException("This endpoint requires an onboarding session");
|
|
367
|
+
}
|
|
368
|
+
request.sessionInfo = {
|
|
369
|
+
userId: decoded.userId,
|
|
370
|
+
sessionId: decoded.sessionId,
|
|
371
|
+
sessionType: decoded.sessionType
|
|
372
|
+
};
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
// Validates CSRF token for state-changing requests
|
|
672
376
|
async validateCsrf(request, reply) {
|
|
673
377
|
const safeMethods = [
|
|
674
378
|
"GET",
|
|
675
379
|
"HEAD",
|
|
676
380
|
"OPTIONS"
|
|
677
381
|
];
|
|
678
|
-
if (safeMethods.includes(request.method))
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
382
|
+
if (safeMethods.includes(request.method)) return;
|
|
681
383
|
try {
|
|
682
384
|
const fastifyInstance = request.server;
|
|
683
|
-
|
|
684
|
-
|
|
385
|
+
const csrfProtection = fastifyInstance.csrfProtection;
|
|
386
|
+
if (!csrfProtection) {
|
|
685
387
|
throw new ForbiddenException("CSRF protection not configured");
|
|
686
388
|
}
|
|
687
389
|
await new Promise((resolve, reject) => {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
390
|
+
const originalSend = reply.send.bind(reply);
|
|
391
|
+
reply.send = () => {
|
|
392
|
+
reply.send = originalSend;
|
|
393
|
+
reject(new Error("CSRF validation failed"));
|
|
394
|
+
return reply;
|
|
395
|
+
};
|
|
396
|
+
csrfProtection(request, reply, (err) => {
|
|
397
|
+
reply.send = originalSend;
|
|
398
|
+
if (err) reject(err);
|
|
399
|
+
else resolve();
|
|
694
400
|
});
|
|
695
401
|
});
|
|
696
|
-
this.logger.debug(`CSRF validation successful for ${request.method} ${request.url}`);
|
|
697
402
|
} catch (error) {
|
|
698
|
-
this.logger.warn(`CSRF validation failed for ${request.method} ${request.url}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
699
403
|
throw new ForbiddenException({
|
|
700
404
|
errors: [
|
|
701
405
|
{
|
|
@@ -708,55 +412,167 @@ var VrittiAuthGuard = class _VrittiAuthGuard {
|
|
|
708
412
|
}
|
|
709
413
|
}
|
|
710
414
|
};
|
|
711
|
-
VrittiAuthGuard =
|
|
712
|
-
|
|
415
|
+
VrittiAuthGuard = _ts_decorate3([
|
|
416
|
+
Injectable2({
|
|
713
417
|
scope: Scope2.REQUEST
|
|
714
418
|
}),
|
|
715
|
-
|
|
716
|
-
|
|
419
|
+
_ts_metadata2("design:type", Function),
|
|
420
|
+
_ts_metadata2("design:paramtypes", [
|
|
717
421
|
typeof Reflector === "undefined" ? Object : Reflector,
|
|
718
422
|
typeof ConfigService === "undefined" ? Object : ConfigService,
|
|
719
423
|
typeof JwtService === "undefined" ? Object : JwtService,
|
|
720
|
-
typeof PrimaryDatabaseService === "undefined" ? Object : PrimaryDatabaseService,
|
|
721
424
|
typeof RequestService === "undefined" ? Object : RequestService
|
|
722
425
|
])
|
|
723
426
|
], VrittiAuthGuard);
|
|
724
427
|
|
|
725
|
-
// src/auth/auth
|
|
726
|
-
|
|
428
|
+
// src/auth/services/jwt-auth.service.ts
|
|
429
|
+
import { Injectable as Injectable3, Logger as Logger3 } from "@nestjs/common";
|
|
430
|
+
import { ConfigService as ConfigService2 } from "@nestjs/config";
|
|
431
|
+
import { JwtService as NestJwtService } from "@nestjs/jwt";
|
|
432
|
+
|
|
433
|
+
// src/utils/time.utils.ts
|
|
434
|
+
function parseExpiryToMs(expiry) {
|
|
435
|
+
const match = expiry.match(/^(\d+)([smhdwy])$/);
|
|
436
|
+
if (!match) throw new Error(`Invalid expiry format: ${expiry}`);
|
|
437
|
+
const value = Number.parseInt(match[1], 10);
|
|
438
|
+
const multipliers = {
|
|
439
|
+
s: 1e3,
|
|
440
|
+
m: 6e4,
|
|
441
|
+
h: 36e5,
|
|
442
|
+
d: 864e5,
|
|
443
|
+
w: 6048e5,
|
|
444
|
+
y: 31536e6
|
|
445
|
+
};
|
|
446
|
+
return value * multipliers[match[2]];
|
|
447
|
+
}
|
|
448
|
+
__name(parseExpiryToMs, "parseExpiryToMs");
|
|
449
|
+
|
|
450
|
+
// src/auth/jwt.config.ts
|
|
451
|
+
var jwtConfigFactory = /* @__PURE__ */ __name((configService) => ({
|
|
452
|
+
secret: configService.getOrThrow("JWT_SECRET"),
|
|
453
|
+
signOptions: {
|
|
454
|
+
issuer: "vritti-api"
|
|
455
|
+
}
|
|
456
|
+
}), "jwtConfigFactory");
|
|
457
|
+
var getTokenExpiry = /* @__PURE__ */ __name((configService) => ({
|
|
458
|
+
access: configService.getOrThrow("ACCESS_TOKEN_EXPIRY"),
|
|
459
|
+
refresh: configService.getOrThrow("REFRESH_TOKEN_EXPIRY")
|
|
460
|
+
}), "getTokenExpiry");
|
|
461
|
+
var TokenType = /* @__PURE__ */ (function(TokenType2) {
|
|
462
|
+
TokenType2["ACCESS"] = "access";
|
|
463
|
+
TokenType2["REFRESH"] = "refresh";
|
|
464
|
+
return TokenType2;
|
|
465
|
+
})({});
|
|
466
|
+
|
|
467
|
+
// src/auth/services/jwt-auth.service.ts
|
|
468
|
+
function _ts_decorate4(decorators, target, key, desc) {
|
|
727
469
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
728
470
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
729
471
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
730
472
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
731
473
|
}
|
|
732
|
-
__name(
|
|
733
|
-
|
|
474
|
+
__name(_ts_decorate4, "_ts_decorate");
|
|
475
|
+
function _ts_metadata3(k, v) {
|
|
476
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
477
|
+
}
|
|
478
|
+
__name(_ts_metadata3, "_ts_metadata");
|
|
479
|
+
var JwtAuthService = class _JwtAuthService {
|
|
734
480
|
static {
|
|
735
|
-
__name(this, "
|
|
481
|
+
__name(this, "JwtAuthService");
|
|
736
482
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
return {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
483
|
+
jwtService;
|
|
484
|
+
configService;
|
|
485
|
+
logger = new Logger3(_JwtAuthService.name);
|
|
486
|
+
tokenExpiry;
|
|
487
|
+
constructor(jwtService, configService) {
|
|
488
|
+
this.jwtService = jwtService;
|
|
489
|
+
this.configService = configService;
|
|
490
|
+
this.tokenExpiry = getTokenExpiry(configService);
|
|
491
|
+
}
|
|
492
|
+
// Generates an access token bound to the given refresh token
|
|
493
|
+
generateAccessToken(userId, sessionId, sessionType, refreshToken) {
|
|
494
|
+
return this.jwtService.sign({
|
|
495
|
+
sessionType,
|
|
496
|
+
tokenType: TokenType.ACCESS,
|
|
497
|
+
userId,
|
|
498
|
+
sessionId,
|
|
499
|
+
refreshTokenHash: hashToken(refreshToken)
|
|
500
|
+
}, {
|
|
501
|
+
expiresIn: this.tokenExpiry.access
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
// Generates a refresh token for session persistence
|
|
505
|
+
generateRefreshToken(userId, sessionId, sessionType) {
|
|
506
|
+
return this.jwtService.sign({
|
|
507
|
+
sessionType,
|
|
508
|
+
tokenType: TokenType.REFRESH,
|
|
509
|
+
userId,
|
|
510
|
+
sessionId
|
|
511
|
+
}, {
|
|
512
|
+
expiresIn: this.tokenExpiry.refresh
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
// Signs an arbitrary payload with optional JWT options
|
|
516
|
+
sign(payload, options) {
|
|
517
|
+
return this.jwtService.sign(payload, options);
|
|
518
|
+
}
|
|
519
|
+
// Verifies a token and ensures it matches the expected token type
|
|
520
|
+
verify(token, expectedType) {
|
|
521
|
+
try {
|
|
522
|
+
const payload = this.jwtService.verify(token);
|
|
523
|
+
if (payload.tokenType !== expectedType) {
|
|
524
|
+
throw new Error(`Expected ${expectedType} token, got ${payload.tokenType}`);
|
|
525
|
+
}
|
|
526
|
+
return payload;
|
|
527
|
+
} catch (error) {
|
|
528
|
+
this.logger.error(`Failed to verify ${expectedType} token`, error);
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Returns the expiry as a Date for the given token type
|
|
533
|
+
getExpiryTime(type) {
|
|
534
|
+
return new Date(Date.now() + parseExpiryToMs(this.tokenExpiry[type]));
|
|
535
|
+
}
|
|
536
|
+
// Returns the token lifetime in seconds for the given type
|
|
537
|
+
getExpiryInSeconds(type) {
|
|
538
|
+
return Math.floor(parseExpiryToMs(this.tokenExpiry[type]) / 1e3);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
JwtAuthService = _ts_decorate4([
|
|
542
|
+
Injectable3(),
|
|
543
|
+
_ts_metadata3("design:type", Function),
|
|
544
|
+
_ts_metadata3("design:paramtypes", [
|
|
545
|
+
typeof NestJwtService === "undefined" ? Object : NestJwtService,
|
|
546
|
+
typeof ConfigService2 === "undefined" ? Object : ConfigService2
|
|
547
|
+
])
|
|
548
|
+
], JwtAuthService);
|
|
549
|
+
|
|
550
|
+
// src/auth/auth-config.module.ts
|
|
551
|
+
function _ts_decorate5(decorators, target, key, desc) {
|
|
552
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
553
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
554
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
555
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
556
|
+
}
|
|
557
|
+
__name(_ts_decorate5, "_ts_decorate");
|
|
558
|
+
var AuthConfigModule = class _AuthConfigModule {
|
|
559
|
+
static {
|
|
560
|
+
__name(this, "AuthConfigModule");
|
|
561
|
+
}
|
|
562
|
+
// Registers JWT and global VrittiAuthGuard with async config
|
|
563
|
+
static forRootAsync() {
|
|
564
|
+
return {
|
|
565
|
+
module: _AuthConfigModule,
|
|
566
|
+
imports: [
|
|
567
|
+
ConfigModule,
|
|
568
|
+
RequestModule,
|
|
569
|
+
JwtModule.registerAsync({
|
|
570
|
+
imports: [
|
|
571
|
+
ConfigModule
|
|
572
|
+
],
|
|
573
|
+
inject: [
|
|
574
|
+
ConfigService3
|
|
575
|
+
],
|
|
760
576
|
useFactory: /* @__PURE__ */ __name((config) => ({
|
|
761
577
|
secret: config.get("JWT_SECRET"),
|
|
762
578
|
signOptions: {
|
|
@@ -774,10 +590,12 @@ var AuthConfigModule = class _AuthConfigModule {
|
|
|
774
590
|
{
|
|
775
591
|
provide: APP_GUARD,
|
|
776
592
|
useClass: VrittiAuthGuard
|
|
777
|
-
}
|
|
593
|
+
},
|
|
594
|
+
JwtAuthService
|
|
778
595
|
],
|
|
779
596
|
exports: [
|
|
780
|
-
JwtModule
|
|
597
|
+
JwtModule,
|
|
598
|
+
JwtAuthService
|
|
781
599
|
]
|
|
782
600
|
};
|
|
783
601
|
}
|
|
@@ -787,28 +605,69 @@ AuthConfigModule = _ts_decorate5([
|
|
|
787
605
|
Module2({})
|
|
788
606
|
], AuthConfigModule);
|
|
789
607
|
|
|
608
|
+
// src/auth/decorators/access-token.decorator.ts
|
|
609
|
+
import { createParamDecorator } from "@nestjs/common";
|
|
610
|
+
var AccessToken = createParamDecorator((_data, ctx) => {
|
|
611
|
+
const request = ctx.switchToHttp().getRequest();
|
|
612
|
+
const authHeader = request.headers.authorization;
|
|
613
|
+
return authHeader?.replace("Bearer ", "") || "";
|
|
614
|
+
});
|
|
615
|
+
|
|
790
616
|
// src/auth/decorators/onboarding.decorator.ts
|
|
791
|
-
import { SetMetadata as
|
|
792
|
-
var Onboarding = /* @__PURE__ */ __name(() =>
|
|
617
|
+
import { SetMetadata as SetMetadata3 } from "@nestjs/common";
|
|
618
|
+
var Onboarding = /* @__PURE__ */ __name(() => SetMetadata3("isOnboarding", true), "Onboarding");
|
|
793
619
|
|
|
794
620
|
// src/auth/decorators/public.decorator.ts
|
|
795
|
-
import { SetMetadata as
|
|
796
|
-
var Public = /* @__PURE__ */ __name(() =>
|
|
621
|
+
import { SetMetadata as SetMetadata4 } from "@nestjs/common";
|
|
622
|
+
var Public = /* @__PURE__ */ __name(() => SetMetadata4("isPublic", true), "Public");
|
|
623
|
+
|
|
624
|
+
// src/auth/decorators/refresh-token-cookie.decorator.ts
|
|
625
|
+
import { createParamDecorator as createParamDecorator2 } from "@nestjs/common";
|
|
626
|
+
var RefreshTokenCookie = createParamDecorator2((_data, ctx) => {
|
|
627
|
+
const request = ctx.switchToHttp().getRequest();
|
|
628
|
+
const cookies = request.cookies ?? {};
|
|
629
|
+
const config = getConfig();
|
|
630
|
+
return cookies[config.cookie.refreshCookieName];
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// src/auth/decorators/session-data.decorator.ts
|
|
634
|
+
import { createParamDecorator as createParamDecorator3 } from "@nestjs/common";
|
|
635
|
+
var SessionData = createParamDecorator3((_data, ctx) => {
|
|
636
|
+
const request = ctx.switchToHttp().getRequest();
|
|
637
|
+
const sessionInfo = request.sessionInfo;
|
|
638
|
+
if (!sessionInfo?.sessionId) {
|
|
639
|
+
throw new Error("Session info not found on request. Ensure route is protected by auth guard.");
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
userId: sessionInfo.userId,
|
|
643
|
+
sessionId: sessionInfo.sessionId,
|
|
644
|
+
sessionType: sessionInfo.sessionType
|
|
645
|
+
};
|
|
646
|
+
});
|
|
797
647
|
|
|
798
648
|
// src/auth/decorators/user-id.decorator.ts
|
|
799
|
-
import { createParamDecorator } from "@nestjs/common";
|
|
800
|
-
var UserId =
|
|
649
|
+
import { createParamDecorator as createParamDecorator4 } from "@nestjs/common";
|
|
650
|
+
var UserId = createParamDecorator4((_data, ctx) => {
|
|
801
651
|
const request = ctx.switchToHttp().getRequest();
|
|
802
|
-
const
|
|
803
|
-
if (!
|
|
652
|
+
const sessionInfo = request.sessionInfo;
|
|
653
|
+
if (!sessionInfo?.userId) {
|
|
804
654
|
throw new Error("User ID not found on request. Ensure route is protected by auth guard.");
|
|
805
655
|
}
|
|
806
|
-
return
|
|
656
|
+
return sessionInfo.userId;
|
|
807
657
|
});
|
|
808
658
|
|
|
809
|
-
// src/
|
|
810
|
-
import {
|
|
811
|
-
import {
|
|
659
|
+
// src/database/database.module.ts
|
|
660
|
+
import { Global as Global3, Module as Module3 } from "@nestjs/common";
|
|
661
|
+
import { Reflector as Reflector3 } from "@nestjs/core";
|
|
662
|
+
|
|
663
|
+
// src/database/constants.ts
|
|
664
|
+
var DATABASE_MODULE_OPTIONS = Symbol("DATABASE_MODULE_OPTIONS");
|
|
665
|
+
|
|
666
|
+
// src/database/services/primary-database.service.ts
|
|
667
|
+
import { Inject as Inject2, Injectable as Injectable4, InternalServerErrorException, Logger as Logger4 } from "@nestjs/common";
|
|
668
|
+
import { eq, or } from "drizzle-orm";
|
|
669
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
670
|
+
import { Pool } from "pg";
|
|
812
671
|
function _ts_decorate6(decorators, target, key, desc) {
|
|
813
672
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
814
673
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -820,102 +679,184 @@ function _ts_metadata4(k, v) {
|
|
|
820
679
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
821
680
|
}
|
|
822
681
|
__name(_ts_metadata4, "_ts_metadata");
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
"https://local.vrittiai.com:3012",
|
|
831
|
-
"https://cloud.local.vrittiai.com:3012"
|
|
832
|
-
];
|
|
833
|
-
var SseAuthGuard = class _SseAuthGuard {
|
|
682
|
+
function _ts_param2(paramIndex, decorator) {
|
|
683
|
+
return function(target, key) {
|
|
684
|
+
decorator(target, key, paramIndex);
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
__name(_ts_param2, "_ts_param");
|
|
688
|
+
var PrimaryDatabaseService = class _PrimaryDatabaseService {
|
|
834
689
|
static {
|
|
835
|
-
__name(this, "
|
|
690
|
+
__name(this, "PrimaryDatabaseService");
|
|
836
691
|
}
|
|
837
|
-
|
|
838
|
-
logger = new Logger4(
|
|
839
|
-
|
|
840
|
-
|
|
692
|
+
options;
|
|
693
|
+
logger = new Logger4(_PrimaryDatabaseService.name);
|
|
694
|
+
pool = null;
|
|
695
|
+
db = null;
|
|
696
|
+
tenantConfigCache = /* @__PURE__ */ new Map();
|
|
697
|
+
cacheTTL;
|
|
698
|
+
constructor(options) {
|
|
699
|
+
this.options = options;
|
|
700
|
+
this.cacheTTL = options.connectionCacheTTL || 3e5;
|
|
841
701
|
}
|
|
842
|
-
async
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
this.setCorsHeaders(request, response);
|
|
846
|
-
const token = request.query?.token;
|
|
847
|
-
if (!token) {
|
|
848
|
-
this.logger.warn("SSE authentication failed: token not found in query params");
|
|
849
|
-
throw new UnauthorizedException2("Authentication required");
|
|
702
|
+
async onModuleInit() {
|
|
703
|
+
if (this.options.primaryDb) {
|
|
704
|
+
await this.initializeDrizzleClient();
|
|
850
705
|
}
|
|
706
|
+
}
|
|
707
|
+
// Initializes connection to primary database using Drizzle
|
|
708
|
+
async initializeDrizzleClient() {
|
|
851
709
|
try {
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
710
|
+
const databaseUrl = this.buildPrimaryDbUrl();
|
|
711
|
+
this.pool = new Pool({
|
|
712
|
+
connectionString: databaseUrl,
|
|
713
|
+
max: this.options.maxConnections || 10
|
|
714
|
+
});
|
|
715
|
+
this.logger.debug(`Schema keys passed to drizzle: [${Object.keys(this.options.drizzleSchema || {}).join(", ")}]`);
|
|
716
|
+
this.logger.debug(`Relations keys passed to drizzle: [${Object.keys(this.options.drizzleRelations || {}).join(", ")}]`);
|
|
717
|
+
this.db = drizzle({
|
|
718
|
+
client: this.pool,
|
|
719
|
+
schema: this.options.drizzleSchema,
|
|
720
|
+
relations: this.options.drizzleRelations
|
|
721
|
+
});
|
|
722
|
+
this.logger.debug(`Drizzle query keys after init: [${Object.keys(this.db.query || {}).join(", ")}]`);
|
|
723
|
+
await this.pool.query("SELECT 1");
|
|
724
|
+
this.logger.log("Connected to primary database (tenant registry)");
|
|
867
725
|
} catch (error) {
|
|
868
|
-
|
|
869
|
-
|
|
726
|
+
this.logger.error("Failed to connect to primary database", error);
|
|
727
|
+
throw new InternalServerErrorException("Failed to initialize tenant registry");
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
// Builds the PostgreSQL connection URL from primary database config properties
|
|
731
|
+
buildPrimaryDbUrl() {
|
|
732
|
+
if (!this.options.primaryDb) {
|
|
733
|
+
throw new Error("Primary database configuration not provided");
|
|
734
|
+
}
|
|
735
|
+
const { host, port = 5432, username, password, database, schema = "public", sslMode = "require" } = this.options.primaryDb;
|
|
736
|
+
let url = `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}`;
|
|
737
|
+
const params = new URLSearchParams();
|
|
738
|
+
if (schema) {
|
|
739
|
+
params.set("schema", schema);
|
|
740
|
+
}
|
|
741
|
+
params.set("sslmode", sslMode);
|
|
742
|
+
const queryString = params.toString();
|
|
743
|
+
if (queryString) {
|
|
744
|
+
url += `?${queryString}`;
|
|
745
|
+
}
|
|
746
|
+
this.logger.debug(`Primary DB connection URL: ${this.maskPassword(url)}`);
|
|
747
|
+
return url;
|
|
748
|
+
}
|
|
749
|
+
// Masks password in connection URL for safe logging
|
|
750
|
+
maskPassword(url) {
|
|
751
|
+
return url.replace(/:([^@]+)@/, ":****@");
|
|
752
|
+
}
|
|
753
|
+
// Retrieves tenant configuration by ID or subdomain, with in-memory caching
|
|
754
|
+
async getTenantInfo(tenantIdentifier) {
|
|
755
|
+
const cached = this.tenantConfigCache.get(tenantIdentifier);
|
|
756
|
+
if (cached) {
|
|
757
|
+
this.logger.debug(`Cache hit for tenant: ${tenantIdentifier}`);
|
|
758
|
+
return cached;
|
|
759
|
+
}
|
|
760
|
+
try {
|
|
761
|
+
if (!this.db) {
|
|
762
|
+
throw new Error("Primary database client not initialized");
|
|
870
763
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
764
|
+
this.logger.debug(`Querying primary database for tenant: ${tenantIdentifier}`);
|
|
765
|
+
const schema = this.options.drizzleSchema;
|
|
766
|
+
const { tenants, tenantDatabaseConfigs } = schema;
|
|
767
|
+
const result = await this.db.select().from(tenants).leftJoin(tenantDatabaseConfigs, eq(tenants.id, tenantDatabaseConfigs.tenantId)).where(or(eq(tenants.id, tenantIdentifier), eq(tenants.subdomain, tenantIdentifier))).limit(1);
|
|
768
|
+
if (!result.length) {
|
|
769
|
+
this.logger.warn(`Tenant not found: ${tenantIdentifier}`);
|
|
770
|
+
return null;
|
|
875
771
|
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
772
|
+
const row = result[0];
|
|
773
|
+
const tenant = row.tenants;
|
|
774
|
+
const config = row.tenant_database_configs;
|
|
775
|
+
if (tenant.status !== "ACTIVE") {
|
|
776
|
+
this.logger.warn(`Tenant not active: ${tenantIdentifier}`);
|
|
777
|
+
return null;
|
|
879
778
|
}
|
|
880
|
-
|
|
881
|
-
|
|
779
|
+
const info = {
|
|
780
|
+
id: tenant.id,
|
|
781
|
+
subdomain: tenant.subdomain,
|
|
782
|
+
type: tenant.dbType,
|
|
783
|
+
status: tenant.status,
|
|
784
|
+
// For SHARED tenants: schema name
|
|
785
|
+
schemaName: config?.dbSchema || void 0,
|
|
786
|
+
// For DEDICATED tenants: database configuration from TenantDatabaseConfig table
|
|
787
|
+
databaseName: config?.dbName || void 0,
|
|
788
|
+
databaseHost: config?.dbHost || void 0,
|
|
789
|
+
databasePort: config?.dbPort || void 0,
|
|
790
|
+
databaseUsername: config?.dbUsername ? this.decrypt(config.dbUsername) : void 0,
|
|
791
|
+
databasePassword: config?.dbPassword ? this.decrypt(config.dbPassword) : void 0,
|
|
792
|
+
databaseSslMode: config?.dbSslMode || void 0,
|
|
793
|
+
connectionPoolSize: config?.connectionPoolSize || void 0
|
|
794
|
+
};
|
|
795
|
+
this.cacheInfo(info);
|
|
796
|
+
return info;
|
|
797
|
+
} catch (error) {
|
|
798
|
+
this.logger.error(`Failed to fetch tenant info: ${tenantIdentifier}`, error);
|
|
799
|
+
throw new InternalServerErrorException("Failed to resolve tenant");
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// Caches tenant info by both ID and subdomain with TTL expiration
|
|
803
|
+
cacheInfo(info) {
|
|
804
|
+
this.tenantConfigCache.set(info.id, info);
|
|
805
|
+
this.tenantConfigCache.set(info.subdomain, info);
|
|
806
|
+
setTimeout(() => {
|
|
807
|
+
this.tenantConfigCache.delete(info.id);
|
|
808
|
+
this.tenantConfigCache.delete(info.subdomain);
|
|
809
|
+
this.logger.debug(`Cache expired for tenant: ${info.subdomain}`);
|
|
810
|
+
}, this.cacheTTL);
|
|
811
|
+
}
|
|
812
|
+
// Clears cached tenant info for the given ID or subdomain
|
|
813
|
+
clearTenantCache(tenantIdentifier) {
|
|
814
|
+
const config = this.tenantConfigCache.get(tenantIdentifier);
|
|
815
|
+
if (config) {
|
|
816
|
+
this.tenantConfigCache.delete(config.id);
|
|
817
|
+
this.tenantConfigCache.delete(config.subdomain);
|
|
818
|
+
this.logger.log(`Cleared cache for tenant: ${tenantIdentifier}`);
|
|
882
819
|
}
|
|
883
820
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
|
|
821
|
+
// Clears all cached tenant configurations
|
|
822
|
+
clearAllCaches() {
|
|
823
|
+
const size = this.tenantConfigCache.size;
|
|
824
|
+
this.tenantConfigCache.clear();
|
|
825
|
+
this.logger.log(`Cleared ${size} cached tenant configs`);
|
|
826
|
+
}
|
|
827
|
+
// Returns the initialized Drizzle client, throwing if not yet initialized
|
|
828
|
+
get drizzleClient() {
|
|
829
|
+
if (!this.db) {
|
|
830
|
+
throw new Error("Primary database client not initialized");
|
|
831
|
+
}
|
|
832
|
+
return this.db;
|
|
833
|
+
}
|
|
834
|
+
// Returns the Drizzle schema passed in module options
|
|
835
|
+
get schema() {
|
|
836
|
+
return this.options.drizzleSchema;
|
|
837
|
+
}
|
|
838
|
+
// Decrypts a database credential value (placeholder for actual decryption)
|
|
839
|
+
decrypt(encrypted) {
|
|
840
|
+
return encrypted;
|
|
841
|
+
}
|
|
842
|
+
async onModuleDestroy() {
|
|
843
|
+
if (this.pool) {
|
|
844
|
+
await this.pool.end();
|
|
845
|
+
this.logger.log("Disconnected from primary database");
|
|
896
846
|
}
|
|
897
847
|
}
|
|
898
848
|
};
|
|
899
|
-
|
|
900
|
-
Injectable4(
|
|
901
|
-
|
|
902
|
-
}),
|
|
849
|
+
PrimaryDatabaseService = _ts_decorate6([
|
|
850
|
+
Injectable4(),
|
|
851
|
+
_ts_param2(0, Inject2(DATABASE_MODULE_OPTIONS)),
|
|
903
852
|
_ts_metadata4("design:type", Function),
|
|
904
853
|
_ts_metadata4("design:paramtypes", [
|
|
905
|
-
typeof
|
|
854
|
+
typeof DatabaseModuleOptions === "undefined" ? Object : DatabaseModuleOptions
|
|
906
855
|
])
|
|
907
|
-
],
|
|
908
|
-
|
|
909
|
-
// src/database/database.module.ts
|
|
910
|
-
import { Global as Global3, Module as Module3 } from "@nestjs/common";
|
|
911
|
-
import { APP_INTERCEPTOR, Reflector as Reflector4 } from "@nestjs/core";
|
|
912
|
-
|
|
913
|
-
// src/database/interceptors/message-tenant-context.interceptor.ts
|
|
914
|
-
import { Injectable as Injectable6, Logger as Logger5, Scope as Scope5 } from "@nestjs/common";
|
|
915
|
-
import { tap } from "rxjs/operators";
|
|
856
|
+
], PrimaryDatabaseService);
|
|
916
857
|
|
|
917
858
|
// src/database/services/tenant-context.service.ts
|
|
918
|
-
import { Injectable as Injectable5, Scope as
|
|
859
|
+
import { Injectable as Injectable5, Scope as Scope3, UnauthorizedException as UnauthorizedException2 } from "@nestjs/common";
|
|
919
860
|
function _ts_decorate7(decorators, target, key, desc) {
|
|
920
861
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
921
862
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -928,79 +869,47 @@ var TenantContextService = class {
|
|
|
928
869
|
__name(this, "TenantContextService");
|
|
929
870
|
}
|
|
930
871
|
tenantInfo = null;
|
|
931
|
-
|
|
932
|
-
* Set tenant information for this request/message
|
|
933
|
-
*
|
|
934
|
-
* This is typically called by:
|
|
935
|
-
* - TenantContextInterceptor (for HTTP requests in gateway)
|
|
936
|
-
* - MessageTenantContextInterceptor (for RabbitMQ messages in microservices)
|
|
937
|
-
* - Manual context setup in message handlers
|
|
938
|
-
*
|
|
939
|
-
* @param tenantInfo Complete tenant information
|
|
940
|
-
* @throws Error if tenant context is already set (prevents accidental overwrites)
|
|
941
|
-
*/
|
|
872
|
+
// Sets tenant info for this request, throwing if already set to prevent overwrites
|
|
942
873
|
setTenant(tenantInfo) {
|
|
943
874
|
if (this.tenantInfo) {
|
|
944
875
|
throw new Error("Tenant context already set for this request");
|
|
945
876
|
}
|
|
946
877
|
this.tenantInfo = tenantInfo;
|
|
947
878
|
}
|
|
948
|
-
|
|
949
|
-
* Get tenant information for this request/message
|
|
950
|
-
*
|
|
951
|
-
* @returns Tenant information
|
|
952
|
-
* @throws UnauthorizedException if tenant context hasn't been set
|
|
953
|
-
*/
|
|
879
|
+
// Returns the tenant info for this request, throwing if context is not set
|
|
954
880
|
getTenant() {
|
|
955
881
|
if (!this.tenantInfo) {
|
|
956
|
-
throw new
|
|
882
|
+
throw new UnauthorizedException2("Tenant context not set");
|
|
957
883
|
}
|
|
958
884
|
return this.tenantInfo;
|
|
959
885
|
}
|
|
960
|
-
|
|
961
|
-
* Check if tenant context has been set
|
|
962
|
-
*
|
|
963
|
-
* @returns true if tenant context is available
|
|
964
|
-
*/
|
|
886
|
+
// Returns true if tenant context has been set for this request
|
|
965
887
|
hasTenant() {
|
|
966
888
|
return this.tenantInfo !== null;
|
|
967
889
|
}
|
|
968
|
-
|
|
969
|
-
* Clear tenant context
|
|
970
|
-
*
|
|
971
|
-
* This is useful for cleanup in RabbitMQ message handlers
|
|
972
|
-
* after the message has been processed.
|
|
973
|
-
*
|
|
974
|
-
* HTTP requests don't need manual cleanup as the service
|
|
975
|
-
* instance is destroyed when the request ends.
|
|
976
|
-
*/
|
|
890
|
+
// Clears the tenant context (useful for RabbitMQ message handler cleanup)
|
|
977
891
|
clearTenant() {
|
|
978
892
|
this.tenantInfo = null;
|
|
979
893
|
}
|
|
980
|
-
|
|
981
|
-
* Get tenant ID safely (returns null if not set)
|
|
982
|
-
*
|
|
983
|
-
* @returns Tenant ID or null
|
|
984
|
-
*/
|
|
894
|
+
// Returns the tenant ID or null if context is not set
|
|
985
895
|
getTenantIdSafe() {
|
|
986
896
|
return this.tenantInfo?.id ?? null;
|
|
987
897
|
}
|
|
988
|
-
|
|
989
|
-
* Get tenant subdomain safely (returns null if not set)
|
|
990
|
-
*
|
|
991
|
-
* @returns Tenant subdomain or null
|
|
992
|
-
*/
|
|
898
|
+
// Returns the tenant subdomain or null if context is not set
|
|
993
899
|
getTenantSubdomainSafe() {
|
|
994
900
|
return this.tenantInfo?.subdomain ?? null;
|
|
995
901
|
}
|
|
996
902
|
};
|
|
997
903
|
TenantContextService = _ts_decorate7([
|
|
998
904
|
Injectable5({
|
|
999
|
-
scope:
|
|
905
|
+
scope: Scope3.REQUEST
|
|
1000
906
|
})
|
|
1001
907
|
], TenantContextService);
|
|
1002
908
|
|
|
1003
|
-
// src/database/
|
|
909
|
+
// src/database/services/tenant-database.service.ts
|
|
910
|
+
import { Inject as Inject3, Injectable as Injectable6, InternalServerErrorException as InternalServerErrorException2, Logger as Logger5 } from "@nestjs/common";
|
|
911
|
+
import { drizzle as drizzle2 } from "drizzle-orm/node-postgres";
|
|
912
|
+
import { Pool as Pool2 } from "pg";
|
|
1004
913
|
function _ts_decorate8(decorators, target, key, desc) {
|
|
1005
914
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1006
915
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1012,217 +921,36 @@ function _ts_metadata5(k, v) {
|
|
|
1012
921
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1013
922
|
}
|
|
1014
923
|
__name(_ts_metadata5, "_ts_metadata");
|
|
1015
|
-
|
|
924
|
+
function _ts_param3(paramIndex, decorator) {
|
|
925
|
+
return function(target, key) {
|
|
926
|
+
decorator(target, key, paramIndex);
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
__name(_ts_param3, "_ts_param");
|
|
930
|
+
var TenantDatabaseService = class _TenantDatabaseService {
|
|
1016
931
|
static {
|
|
1017
|
-
__name(this, "
|
|
932
|
+
__name(this, "TenantDatabaseService");
|
|
1018
933
|
}
|
|
934
|
+
options;
|
|
1019
935
|
tenantContext;
|
|
1020
|
-
logger = new Logger5(
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
if (contextType === "rpc") {
|
|
1027
|
-
const rpcContext = context.switchToRpc();
|
|
1028
|
-
const payload = rpcContext.getData();
|
|
1029
|
-
if (payload?.tenant) {
|
|
1030
|
-
const tenant = payload.tenant;
|
|
1031
|
-
this.logger.debug(`Setting tenant context from message: ${tenant.subdomain}`);
|
|
1032
|
-
try {
|
|
1033
|
-
this.tenantContext.setTenant(tenant);
|
|
1034
|
-
this.logger.log(`Tenant context set: ${tenant.subdomain} (${tenant.type})`);
|
|
1035
|
-
} catch (error) {
|
|
1036
|
-
this.logger.error("Failed to set tenant context from message", error);
|
|
1037
|
-
}
|
|
1038
|
-
} else {
|
|
1039
|
-
this.logger.warn("Message payload missing tenant information");
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
return next.handle().pipe(tap({
|
|
1043
|
-
next: /* @__PURE__ */ __name(() => {
|
|
1044
|
-
this.cleanupContext();
|
|
1045
|
-
}, "next"),
|
|
1046
|
-
error: /* @__PURE__ */ __name(() => {
|
|
1047
|
-
this.cleanupContext();
|
|
1048
|
-
}, "error"),
|
|
1049
|
-
complete: /* @__PURE__ */ __name(() => {
|
|
1050
|
-
this.cleanupContext();
|
|
1051
|
-
}, "complete")
|
|
1052
|
-
}));
|
|
1053
|
-
}
|
|
1054
|
-
/**
|
|
1055
|
-
* Clean up tenant context after message is processed
|
|
1056
|
-
*/
|
|
1057
|
-
cleanupContext() {
|
|
1058
|
-
if (this.tenantContext.hasTenant()) {
|
|
1059
|
-
const tenant = this.tenantContext.getTenantIdSafe();
|
|
1060
|
-
this.tenantContext.clearTenant();
|
|
1061
|
-
this.logger.debug(`Cleaned up tenant context: ${tenant}`);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
};
|
|
1065
|
-
MessageTenantContextInterceptor = _ts_decorate8([
|
|
1066
|
-
Injectable6({
|
|
1067
|
-
scope: Scope5.REQUEST
|
|
1068
|
-
}),
|
|
1069
|
-
_ts_metadata5("design:type", Function),
|
|
1070
|
-
_ts_metadata5("design:paramtypes", [
|
|
1071
|
-
typeof TenantContextService === "undefined" ? Object : TenantContextService
|
|
1072
|
-
])
|
|
1073
|
-
], MessageTenantContextInterceptor);
|
|
1074
|
-
|
|
1075
|
-
// src/database/interceptors/tenant-context.interceptor.ts
|
|
1076
|
-
import { Injectable as Injectable7, Logger as Logger6, Scope as Scope6, UnauthorizedException as UnauthorizedException4 } from "@nestjs/common";
|
|
1077
|
-
import { Reflector as Reflector3 } from "@nestjs/core";
|
|
1078
|
-
function _ts_decorate9(decorators, target, key, desc) {
|
|
1079
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1080
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1081
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1082
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1083
|
-
}
|
|
1084
|
-
__name(_ts_decorate9, "_ts_decorate");
|
|
1085
|
-
function _ts_metadata6(k, v) {
|
|
1086
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1087
|
-
}
|
|
1088
|
-
__name(_ts_metadata6, "_ts_metadata");
|
|
1089
|
-
var TenantContextInterceptor = class _TenantContextInterceptor {
|
|
1090
|
-
static {
|
|
1091
|
-
__name(this, "TenantContextInterceptor");
|
|
1092
|
-
}
|
|
1093
|
-
reflector;
|
|
1094
|
-
tenantContext;
|
|
1095
|
-
primaryDatabase;
|
|
1096
|
-
requestService;
|
|
1097
|
-
logger = new Logger6(_TenantContextInterceptor.name);
|
|
1098
|
-
constructor(reflector, tenantContext, primaryDatabase, requestService) {
|
|
1099
|
-
this.reflector = reflector;
|
|
1100
|
-
this.tenantContext = tenantContext;
|
|
1101
|
-
this.primaryDatabase = primaryDatabase;
|
|
1102
|
-
this.requestService = requestService;
|
|
1103
|
-
}
|
|
1104
|
-
async intercept(context, next) {
|
|
1105
|
-
const request = context.switchToHttp().getRequest();
|
|
1106
|
-
this.logger.debug(`Processing request: ${request.method} ${request.url}`);
|
|
1107
|
-
const isPublic = this.reflector.getAllAndOverride("isPublic", [
|
|
1108
|
-
context.getHandler(),
|
|
1109
|
-
context.getClass()
|
|
1110
|
-
]);
|
|
1111
|
-
try {
|
|
1112
|
-
const tenantIdentifier = this.requestService.getTenantIdentifier();
|
|
1113
|
-
if (isPublic && !tenantIdentifier) {
|
|
1114
|
-
this.logger.debug("Public endpoint without tenant identifier, skipping tenant context setup");
|
|
1115
|
-
return next.handle();
|
|
1116
|
-
}
|
|
1117
|
-
if (!tenantIdentifier) {
|
|
1118
|
-
throw new UnauthorizedException4("Tenant identifier not found in request");
|
|
1119
|
-
}
|
|
1120
|
-
this.logger.debug(`Tenant identifier extracted: ${tenantIdentifier}`);
|
|
1121
|
-
if (tenantIdentifier === "cloud") {
|
|
1122
|
-
this.logger.log("Cloud platform access detected, skipping tenant context setup");
|
|
1123
|
-
return next.handle();
|
|
1124
|
-
}
|
|
1125
|
-
const tenantInfo = await this.primaryDatabase.getTenantInfo(tenantIdentifier);
|
|
1126
|
-
if (!tenantInfo) {
|
|
1127
|
-
this.logger.warn(`Invalid tenant: ${tenantIdentifier}`);
|
|
1128
|
-
throw new UnauthorizedException4("Invalid tenant");
|
|
1129
|
-
}
|
|
1130
|
-
if (tenantInfo.status !== "ACTIVE") {
|
|
1131
|
-
this.logger.warn(`Tenant ${tenantIdentifier} has status: ${tenantInfo.status}`);
|
|
1132
|
-
throw new UnauthorizedException4(`Tenant is ${tenantInfo.status}`);
|
|
1133
|
-
}
|
|
1134
|
-
this.logger.debug(`Tenant config loaded: ${tenantInfo.subdomain} (${tenantInfo.type})`);
|
|
1135
|
-
this.tenantContext.setTenant(tenantInfo);
|
|
1136
|
-
request.tenant = tenantInfo;
|
|
1137
|
-
this.logger.log(`Tenant context set: ${tenantInfo.subdomain}`);
|
|
1138
|
-
} catch (error) {
|
|
1139
|
-
this.logger.error("Failed to set tenant context", error);
|
|
1140
|
-
throw error;
|
|
1141
|
-
}
|
|
1142
|
-
return next.handle();
|
|
1143
|
-
}
|
|
1144
|
-
};
|
|
1145
|
-
TenantContextInterceptor = _ts_decorate9([
|
|
1146
|
-
Injectable7({
|
|
1147
|
-
scope: Scope6.REQUEST
|
|
1148
|
-
}),
|
|
1149
|
-
_ts_metadata6("design:type", Function),
|
|
1150
|
-
_ts_metadata6("design:paramtypes", [
|
|
1151
|
-
typeof Reflector3 === "undefined" ? Object : Reflector3,
|
|
1152
|
-
typeof TenantContextService === "undefined" ? Object : TenantContextService,
|
|
1153
|
-
typeof PrimaryDatabaseService === "undefined" ? Object : PrimaryDatabaseService,
|
|
1154
|
-
typeof RequestService === "undefined" ? Object : RequestService
|
|
1155
|
-
])
|
|
1156
|
-
], TenantContextInterceptor);
|
|
1157
|
-
|
|
1158
|
-
// src/database/services/tenant-database.service.ts
|
|
1159
|
-
import { Inject as Inject3, Injectable as Injectable8, InternalServerErrorException as InternalServerErrorException2, Logger as Logger7 } from "@nestjs/common";
|
|
1160
|
-
import { drizzle as drizzle2 } from "drizzle-orm/node-postgres";
|
|
1161
|
-
import { Pool as Pool2 } from "pg";
|
|
1162
|
-
function _ts_decorate10(decorators, target, key, desc) {
|
|
1163
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1164
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1165
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1166
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1167
|
-
}
|
|
1168
|
-
__name(_ts_decorate10, "_ts_decorate");
|
|
1169
|
-
function _ts_metadata7(k, v) {
|
|
1170
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1171
|
-
}
|
|
1172
|
-
__name(_ts_metadata7, "_ts_metadata");
|
|
1173
|
-
function _ts_param3(paramIndex, decorator) {
|
|
1174
|
-
return function(target, key) {
|
|
1175
|
-
decorator(target, key, paramIndex);
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
__name(_ts_param3, "_ts_param");
|
|
1179
|
-
var TenantDatabaseService = class _TenantDatabaseService {
|
|
1180
|
-
static {
|
|
1181
|
-
__name(this, "TenantDatabaseService");
|
|
1182
|
-
}
|
|
1183
|
-
options;
|
|
1184
|
-
tenantContext;
|
|
1185
|
-
logger = new Logger7(_TenantDatabaseService.name);
|
|
1186
|
-
/** Connection pool: Map<cacheKey, TenantConnection> */
|
|
1187
|
-
clients = /* @__PURE__ */ new Map();
|
|
1188
|
-
/** Track last usage time for idle connection cleanup */
|
|
1189
|
-
clientLastUsed = /* @__PURE__ */ new Map();
|
|
1190
|
-
/** Cleanup interval timer */
|
|
1191
|
-
cleanupInterval;
|
|
1192
|
-
constructor(options, tenantContext) {
|
|
1193
|
-
this.options = options;
|
|
936
|
+
logger = new Logger5(_TenantDatabaseService.name);
|
|
937
|
+
clients = /* @__PURE__ */ new Map();
|
|
938
|
+
clientLastUsed = /* @__PURE__ */ new Map();
|
|
939
|
+
cleanupInterval;
|
|
940
|
+
constructor(options, tenantContext) {
|
|
941
|
+
this.options = options;
|
|
1194
942
|
this.tenantContext = tenantContext;
|
|
1195
943
|
this.startConnectionCleaner();
|
|
1196
944
|
}
|
|
1197
|
-
|
|
1198
|
-
* Get the Drizzle client for the current tenant's database.
|
|
1199
|
-
* This returns the tenant-scoped database client.
|
|
1200
|
-
*
|
|
1201
|
-
* @returns Tenant-scoped Drizzle database instance
|
|
1202
|
-
* @throws UnauthorizedException if tenant context not set
|
|
1203
|
-
* @throws InternalServerErrorException if connection fails
|
|
1204
|
-
*/
|
|
945
|
+
// Returns the Drizzle client scoped to the current tenant's database
|
|
1205
946
|
get drizzleClient() {
|
|
1206
947
|
return this.getDbClient();
|
|
1207
948
|
}
|
|
1208
|
-
|
|
1209
|
-
* Get the Drizzle schema
|
|
1210
|
-
*/
|
|
949
|
+
// Returns the Drizzle schema passed in module options
|
|
1211
950
|
get schema() {
|
|
1212
951
|
return this.options.drizzleSchema;
|
|
1213
952
|
}
|
|
1214
|
-
|
|
1215
|
-
* Get tenant-scoped database client for the current request/message
|
|
1216
|
-
*
|
|
1217
|
-
* This method:
|
|
1218
|
-
* 1. Gets tenant info from TenantContextService
|
|
1219
|
-
* 2. Builds a connection URL based on tenant type
|
|
1220
|
-
* 3. Returns cached client if exists, otherwise creates new one
|
|
1221
|
-
*
|
|
1222
|
-
* @returns Drizzle database instance
|
|
1223
|
-
* @throws UnauthorizedException if tenant context not set
|
|
1224
|
-
* @throws InternalServerErrorException if connection fails
|
|
1225
|
-
*/
|
|
953
|
+
// Returns a cached or new Drizzle client for the current tenant context
|
|
1226
954
|
getDbClient() {
|
|
1227
955
|
const tenant = this.tenantContext.getTenant();
|
|
1228
956
|
const cacheKey = this.buildCacheKey(tenant);
|
|
@@ -1238,9 +966,7 @@ var TenantDatabaseService = class _TenantDatabaseService {
|
|
|
1238
966
|
this.clientLastUsed.set(cacheKey, Date.now());
|
|
1239
967
|
return connection.db;
|
|
1240
968
|
}
|
|
1241
|
-
|
|
1242
|
-
* Create a new database client for the given tenant (synchronous)
|
|
1243
|
-
*/
|
|
969
|
+
// Creates a new pool and Drizzle client for the given tenant
|
|
1244
970
|
createDbClientSync(tenant) {
|
|
1245
971
|
try {
|
|
1246
972
|
const databaseUrl = this.buildTenantDbUrl(tenant);
|
|
@@ -1262,9 +988,7 @@ var TenantDatabaseService = class _TenantDatabaseService {
|
|
|
1262
988
|
throw new InternalServerErrorException2("Failed to connect to tenant database");
|
|
1263
989
|
}
|
|
1264
990
|
}
|
|
1265
|
-
|
|
1266
|
-
* Build connection URL for tenant (dedicated database)
|
|
1267
|
-
*/
|
|
991
|
+
// Builds the PostgreSQL connection URL for a dedicated tenant database
|
|
1268
992
|
buildTenantDbUrl(tenant) {
|
|
1269
993
|
const { databaseHost, databasePort, databaseName, databaseUsername, databasePassword, databaseSslMode } = tenant;
|
|
1270
994
|
if (!databaseHost || !databaseName || !databaseUsername) {
|
|
@@ -1276,15 +1000,11 @@ var TenantDatabaseService = class _TenantDatabaseService {
|
|
|
1276
1000
|
this.logger.debug(`Tenant connection URL: ${this.maskPassword(connectionUrl)}`);
|
|
1277
1001
|
return connectionUrl;
|
|
1278
1002
|
}
|
|
1279
|
-
|
|
1280
|
-
* Build cache key for connection pooling
|
|
1281
|
-
*/
|
|
1003
|
+
// Builds a cache key for connection pooling from tenant database coordinates
|
|
1282
1004
|
buildCacheKey(tenant) {
|
|
1283
1005
|
return `${tenant.type}:${tenant.databaseName}@${tenant.databaseHost}`;
|
|
1284
1006
|
}
|
|
1285
|
-
|
|
1286
|
-
* Start periodic cleanup of idle connections
|
|
1287
|
-
*/
|
|
1007
|
+
// Starts a periodic interval to close idle database connections
|
|
1288
1008
|
startConnectionCleaner() {
|
|
1289
1009
|
const interval = this.options.connectionCacheTTL || 3e5;
|
|
1290
1010
|
this.cleanupInterval = setInterval(() => {
|
|
@@ -1292,9 +1012,7 @@ var TenantDatabaseService = class _TenantDatabaseService {
|
|
|
1292
1012
|
}, interval);
|
|
1293
1013
|
this.logger.log(`Connection cleanup scheduled every ${interval / 1e3} seconds`);
|
|
1294
1014
|
}
|
|
1295
|
-
|
|
1296
|
-
* Clean up idle connections that haven't been used recently
|
|
1297
|
-
*/
|
|
1015
|
+
// Closes and removes connections that have been idle beyond the TTL
|
|
1298
1016
|
async cleanupIdleConnections() {
|
|
1299
1017
|
const now = Date.now();
|
|
1300
1018
|
const maxIdle = this.options.connectionCacheTTL || 3e5;
|
|
@@ -1319,18 +1037,14 @@ var TenantDatabaseService = class _TenantDatabaseService {
|
|
|
1319
1037
|
this.logger.log(`Cleaned up ${cleaned} idle connections`);
|
|
1320
1038
|
}
|
|
1321
1039
|
}
|
|
1322
|
-
|
|
1323
|
-
* Get current connection pool statistics
|
|
1324
|
-
*/
|
|
1040
|
+
// Returns the current number of active pooled connections and their tenant keys
|
|
1325
1041
|
getPoolStats() {
|
|
1326
1042
|
return {
|
|
1327
1043
|
activeConnections: this.clients.size,
|
|
1328
1044
|
tenants: Array.from(this.clients.keys())
|
|
1329
1045
|
};
|
|
1330
1046
|
}
|
|
1331
|
-
|
|
1332
|
-
* Mask password in connection URL for logging
|
|
1333
|
-
*/
|
|
1047
|
+
// Masks password in connection URL for safe logging
|
|
1334
1048
|
maskPassword(url) {
|
|
1335
1049
|
return url.replace(/:([^@]+)@/, ":****@");
|
|
1336
1050
|
}
|
|
@@ -1351,87 +1065,37 @@ var TenantDatabaseService = class _TenantDatabaseService {
|
|
|
1351
1065
|
this.logger.log("All database connections closed");
|
|
1352
1066
|
}
|
|
1353
1067
|
};
|
|
1354
|
-
TenantDatabaseService =
|
|
1355
|
-
|
|
1068
|
+
TenantDatabaseService = _ts_decorate8([
|
|
1069
|
+
Injectable6(),
|
|
1356
1070
|
_ts_param3(0, Inject3(DATABASE_MODULE_OPTIONS)),
|
|
1357
|
-
|
|
1358
|
-
|
|
1071
|
+
_ts_metadata5("design:type", Function),
|
|
1072
|
+
_ts_metadata5("design:paramtypes", [
|
|
1359
1073
|
typeof DatabaseModuleOptions === "undefined" ? Object : DatabaseModuleOptions,
|
|
1360
1074
|
typeof TenantContextService === "undefined" ? Object : TenantContextService
|
|
1361
1075
|
])
|
|
1362
1076
|
], TenantDatabaseService);
|
|
1363
1077
|
|
|
1364
1078
|
// src/database/database.module.ts
|
|
1365
|
-
function
|
|
1079
|
+
function _ts_decorate9(decorators, target, key, desc) {
|
|
1366
1080
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1367
1081
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1368
1082
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1369
1083
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1370
1084
|
}
|
|
1371
|
-
__name(
|
|
1085
|
+
__name(_ts_decorate9, "_ts_decorate");
|
|
1372
1086
|
var DatabaseModule = class _DatabaseModule {
|
|
1373
1087
|
static {
|
|
1374
1088
|
__name(this, "DatabaseModule");
|
|
1375
1089
|
}
|
|
1376
|
-
|
|
1377
|
-
* Configure DatabaseModule for Gateway/HTTP mode (multi-tenant web servers)
|
|
1378
|
-
*
|
|
1379
|
-
* This mode is for API Gateways that handle HTTP requests:
|
|
1380
|
-
* - Automatically registers TenantContextInterceptor
|
|
1381
|
-
* - Extracts tenant from subdomain or x-tenant-id header
|
|
1382
|
-
* - Queries primary database for tenant configuration
|
|
1383
|
-
* - Provides PrimaryDatabaseService for tenant lookup
|
|
1384
|
-
*
|
|
1385
|
-
* @param options Async configuration options
|
|
1386
|
-
* @returns Dynamic module configuration with HTTP interceptor
|
|
1387
|
-
*
|
|
1388
|
-
* @example
|
|
1389
|
-
* DatabaseModule.forServer({
|
|
1390
|
-
* inject: [ConfigService],
|
|
1391
|
-
* useFactory: (config: ConfigService) => ({
|
|
1392
|
-
* primaryDb: {
|
|
1393
|
-
* host: config.get('PRIMARY_DB_HOST'),
|
|
1394
|
-
* port: config.get('PRIMARY_DB_PORT'),
|
|
1395
|
-
* username: config.get('PRIMARY_DB_USERNAME'),
|
|
1396
|
-
* password: config.get('PRIMARY_DB_PASSWORD'),
|
|
1397
|
-
* database: config.get('PRIMARY_DB_DATABASE'),
|
|
1398
|
-
* },
|
|
1399
|
-
* prismaClientConstructor: PrismaClient,
|
|
1400
|
-
* }),
|
|
1401
|
-
* })
|
|
1402
|
-
*/
|
|
1090
|
+
// Configures the module for gateway/HTTP mode with TenantContextInterceptor
|
|
1403
1091
|
static forServer(options) {
|
|
1404
1092
|
return _DatabaseModule.createDynamicModule(options, "server");
|
|
1405
1093
|
}
|
|
1406
|
-
|
|
1407
|
-
* Configure DatabaseModule for Microservice/Messaging mode (RabbitMQ workers)
|
|
1408
|
-
*
|
|
1409
|
-
* This mode is for microservices that process messages from queues:
|
|
1410
|
-
* - Automatically registers MessageTenantContextInterceptor
|
|
1411
|
-
* - Extracts tenant from RabbitMQ message patterns
|
|
1412
|
-
* - No primary database needed (tenant comes from message context)
|
|
1413
|
-
*
|
|
1414
|
-
* @param options Async configuration options
|
|
1415
|
-
* @returns Dynamic module configuration with message interceptor
|
|
1416
|
-
*
|
|
1417
|
-
* @example
|
|
1418
|
-
* DatabaseModule.forMicroservice({
|
|
1419
|
-
* inject: [ConfigService],
|
|
1420
|
-
* useFactory: (config: ConfigService) => ({
|
|
1421
|
-
* prismaClientConstructor: PrismaClient,
|
|
1422
|
-
* }),
|
|
1423
|
-
* })
|
|
1424
|
-
*/
|
|
1094
|
+
// Configures the module for microservice mode with MessageTenantContextInterceptor
|
|
1425
1095
|
static forMicroservice(options) {
|
|
1426
1096
|
return _DatabaseModule.createDynamicModule(options, "microservice");
|
|
1427
1097
|
}
|
|
1428
|
-
|
|
1429
|
-
* Internal helper to create dynamic module with conditional interceptor registration
|
|
1430
|
-
*
|
|
1431
|
-
* @param options Configuration options
|
|
1432
|
-
* @param mode Mode of operation (gateway or microservice)
|
|
1433
|
-
* @returns Dynamic module configuration
|
|
1434
|
-
*/
|
|
1098
|
+
// Creates the dynamic module configuration with the appropriate interceptor for the given mode
|
|
1435
1099
|
static createDynamicModule(options, mode) {
|
|
1436
1100
|
const asyncProvider = {
|
|
1437
1101
|
provide: DATABASE_MODULE_OPTIONS,
|
|
@@ -1441,25 +1105,14 @@ var DatabaseModule = class _DatabaseModule {
|
|
|
1441
1105
|
const providers = [
|
|
1442
1106
|
// Required for external packages - NestJS global Reflector not available
|
|
1443
1107
|
{
|
|
1444
|
-
provide:
|
|
1445
|
-
useClass:
|
|
1108
|
+
provide: Reflector3,
|
|
1109
|
+
useClass: Reflector3
|
|
1446
1110
|
},
|
|
1447
1111
|
asyncProvider,
|
|
1448
1112
|
TenantContextService,
|
|
1449
1113
|
PrimaryDatabaseService,
|
|
1450
1114
|
TenantDatabaseService
|
|
1451
1115
|
];
|
|
1452
|
-
if (mode === "server") {
|
|
1453
|
-
providers.push({
|
|
1454
|
-
provide: APP_INTERCEPTOR,
|
|
1455
|
-
useClass: TenantContextInterceptor
|
|
1456
|
-
});
|
|
1457
|
-
} else {
|
|
1458
|
-
providers.push({
|
|
1459
|
-
provide: APP_INTERCEPTOR,
|
|
1460
|
-
useClass: MessageTenantContextInterceptor
|
|
1461
|
-
});
|
|
1462
|
-
}
|
|
1463
1116
|
return {
|
|
1464
1117
|
module: _DatabaseModule,
|
|
1465
1118
|
imports: [
|
|
@@ -1475,14 +1128,14 @@ var DatabaseModule = class _DatabaseModule {
|
|
|
1475
1128
|
};
|
|
1476
1129
|
}
|
|
1477
1130
|
};
|
|
1478
|
-
DatabaseModule =
|
|
1131
|
+
DatabaseModule = _ts_decorate9([
|
|
1479
1132
|
Global3(),
|
|
1480
1133
|
Module3({})
|
|
1481
1134
|
], DatabaseModule);
|
|
1482
1135
|
|
|
1483
1136
|
// src/database/decorators/tenant.decorator.ts
|
|
1484
|
-
import { createParamDecorator as
|
|
1485
|
-
var Tenant =
|
|
1137
|
+
import { createParamDecorator as createParamDecorator5 } from "@nestjs/common";
|
|
1138
|
+
var Tenant = createParamDecorator5((_data, ctx) => {
|
|
1486
1139
|
const request = ctx.switchToHttp().getRequest();
|
|
1487
1140
|
const tenantContext = request.app?.get?.(TenantContextService);
|
|
1488
1141
|
if (!tenantContext) {
|
|
@@ -1491,9 +1144,118 @@ var Tenant = createParamDecorator2((_data, ctx) => {
|
|
|
1491
1144
|
return tenantContext.getTenant();
|
|
1492
1145
|
});
|
|
1493
1146
|
|
|
1147
|
+
// src/database/dto/select-options-query.dto.ts
|
|
1148
|
+
import { ApiPropertyOptional } from "@nestjs/swagger";
|
|
1149
|
+
import { Type } from "class-transformer";
|
|
1150
|
+
import { IsInt, IsOptional, IsString, Min } from "class-validator";
|
|
1151
|
+
function _ts_decorate10(decorators, target, key, desc) {
|
|
1152
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1153
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1154
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1155
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1156
|
+
}
|
|
1157
|
+
__name(_ts_decorate10, "_ts_decorate");
|
|
1158
|
+
function _ts_metadata6(k, v) {
|
|
1159
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1160
|
+
}
|
|
1161
|
+
__name(_ts_metadata6, "_ts_metadata");
|
|
1162
|
+
var SelectOptionsQueryDto = class {
|
|
1163
|
+
static {
|
|
1164
|
+
__name(this, "SelectOptionsQueryDto");
|
|
1165
|
+
}
|
|
1166
|
+
search;
|
|
1167
|
+
limit;
|
|
1168
|
+
offset;
|
|
1169
|
+
values;
|
|
1170
|
+
excludeIds;
|
|
1171
|
+
valueKey;
|
|
1172
|
+
labelKey;
|
|
1173
|
+
groupIdKey;
|
|
1174
|
+
};
|
|
1175
|
+
_ts_decorate10([
|
|
1176
|
+
ApiPropertyOptional({
|
|
1177
|
+
description: "Search term to filter by label",
|
|
1178
|
+
example: "united"
|
|
1179
|
+
}),
|
|
1180
|
+
IsOptional(),
|
|
1181
|
+
IsString(),
|
|
1182
|
+
_ts_metadata6("design:type", String)
|
|
1183
|
+
], SelectOptionsQueryDto.prototype, "search", void 0);
|
|
1184
|
+
_ts_decorate10([
|
|
1185
|
+
ApiPropertyOptional({
|
|
1186
|
+
description: "Maximum number of results",
|
|
1187
|
+
example: 20,
|
|
1188
|
+
default: 20
|
|
1189
|
+
}),
|
|
1190
|
+
IsOptional(),
|
|
1191
|
+
Type(() => Number),
|
|
1192
|
+
IsInt(),
|
|
1193
|
+
Min(1),
|
|
1194
|
+
_ts_metadata6("design:type", Number)
|
|
1195
|
+
], SelectOptionsQueryDto.prototype, "limit", void 0);
|
|
1196
|
+
_ts_decorate10([
|
|
1197
|
+
ApiPropertyOptional({
|
|
1198
|
+
description: "Number of results to skip",
|
|
1199
|
+
example: 0,
|
|
1200
|
+
default: 0
|
|
1201
|
+
}),
|
|
1202
|
+
IsOptional(),
|
|
1203
|
+
Type(() => Number),
|
|
1204
|
+
IsInt(),
|
|
1205
|
+
Min(0),
|
|
1206
|
+
_ts_metadata6("design:type", Number)
|
|
1207
|
+
], SelectOptionsQueryDto.prototype, "offset", void 0);
|
|
1208
|
+
_ts_decorate10([
|
|
1209
|
+
ApiPropertyOptional({
|
|
1210
|
+
description: "Comma-separated values to fetch specific options",
|
|
1211
|
+
example: "1,2,3"
|
|
1212
|
+
}),
|
|
1213
|
+
IsOptional(),
|
|
1214
|
+
IsString(),
|
|
1215
|
+
_ts_metadata6("design:type", String)
|
|
1216
|
+
], SelectOptionsQueryDto.prototype, "values", void 0);
|
|
1217
|
+
_ts_decorate10([
|
|
1218
|
+
ApiPropertyOptional({
|
|
1219
|
+
description: "Comma-separated IDs to exclude from results (already selected)",
|
|
1220
|
+
example: "5,10"
|
|
1221
|
+
}),
|
|
1222
|
+
IsOptional(),
|
|
1223
|
+
IsString(),
|
|
1224
|
+
_ts_metadata6("design:type", String)
|
|
1225
|
+
], SelectOptionsQueryDto.prototype, "excludeIds", void 0);
|
|
1226
|
+
_ts_decorate10([
|
|
1227
|
+
ApiPropertyOptional({
|
|
1228
|
+
description: "Column name for option value",
|
|
1229
|
+
example: "id",
|
|
1230
|
+
default: "id"
|
|
1231
|
+
}),
|
|
1232
|
+
IsOptional(),
|
|
1233
|
+
IsString(),
|
|
1234
|
+
_ts_metadata6("design:type", String)
|
|
1235
|
+
], SelectOptionsQueryDto.prototype, "valueKey", void 0);
|
|
1236
|
+
_ts_decorate10([
|
|
1237
|
+
ApiPropertyOptional({
|
|
1238
|
+
description: "Column name for option label",
|
|
1239
|
+
example: "name",
|
|
1240
|
+
default: "name"
|
|
1241
|
+
}),
|
|
1242
|
+
IsOptional(),
|
|
1243
|
+
IsString(),
|
|
1244
|
+
_ts_metadata6("design:type", String)
|
|
1245
|
+
], SelectOptionsQueryDto.prototype, "labelKey", void 0);
|
|
1246
|
+
_ts_decorate10([
|
|
1247
|
+
ApiPropertyOptional({
|
|
1248
|
+
description: "Column name for group ID",
|
|
1249
|
+
example: "regionId"
|
|
1250
|
+
}),
|
|
1251
|
+
IsOptional(),
|
|
1252
|
+
IsString(),
|
|
1253
|
+
_ts_metadata6("design:type", String)
|
|
1254
|
+
], SelectOptionsQueryDto.prototype, "groupIdKey", void 0);
|
|
1255
|
+
|
|
1494
1256
|
// src/database/repositories/primary-base.repository.ts
|
|
1495
|
-
import { Logger as
|
|
1496
|
-
import { eq as eq2, getTableName, sql } from "drizzle-orm";
|
|
1257
|
+
import { Logger as Logger6 } from "@nestjs/common";
|
|
1258
|
+
import { and, asc, eq as eq2, getTableName, ilike, inArray, notInArray, sql } from "drizzle-orm";
|
|
1497
1259
|
function snakeToCamel(str) {
|
|
1498
1260
|
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1499
1261
|
}
|
|
@@ -1505,34 +1267,10 @@ var PrimaryBaseRepository = class {
|
|
|
1505
1267
|
database;
|
|
1506
1268
|
table;
|
|
1507
1269
|
logger;
|
|
1508
|
-
/**
|
|
1509
|
-
* The table name extracted from the Drizzle table at runtime.
|
|
1510
|
-
* Stored in camelCase to match Drizzle's query object keys.
|
|
1511
|
-
* Example: 'email_verifications' -> 'emailVerifications'
|
|
1512
|
-
*/
|
|
1513
1270
|
tableName;
|
|
1514
|
-
/**
|
|
1515
|
-
* Lazy getter for Drizzle client.
|
|
1516
|
-
* Accesses the client from the database service only when needed,
|
|
1517
|
-
* avoiding initialization timing issues with NestJS lifecycle.
|
|
1518
|
-
*/
|
|
1519
1271
|
get db() {
|
|
1520
1272
|
return this.database.drizzleClient;
|
|
1521
1273
|
}
|
|
1522
|
-
/**
|
|
1523
|
-
* Model query API for THIS repository's table (Drizzle v2 relational queries)
|
|
1524
|
-
* Scoped to only the table this repository manages.
|
|
1525
|
-
* Returns a type-safe wrapper around Drizzle's RelationalQueryBuilder.
|
|
1526
|
-
*
|
|
1527
|
-
* @example
|
|
1528
|
-
* ```typescript
|
|
1529
|
-
* // Use relational queries with v2 object-based where syntax
|
|
1530
|
-
* const user = await this.model.findFirst({
|
|
1531
|
-
* where: { id },
|
|
1532
|
-
* with: { posts: true, profile: true }
|
|
1533
|
-
* });
|
|
1534
|
-
* ```
|
|
1535
|
-
*/
|
|
1536
1274
|
get model() {
|
|
1537
1275
|
const query = this.database.drizzleClient.query;
|
|
1538
1276
|
const queryKeys = Object.keys(query || {});
|
|
@@ -1543,60 +1281,24 @@ var PrimaryBaseRepository = class {
|
|
|
1543
1281
|
}
|
|
1544
1282
|
return model;
|
|
1545
1283
|
}
|
|
1546
|
-
/**
|
|
1547
|
-
* Create a new repository instance
|
|
1548
|
-
*
|
|
1549
|
-
* @param database - The primary database service
|
|
1550
|
-
* @param table - The Drizzle table schema object
|
|
1551
|
-
*
|
|
1552
|
-
* @example
|
|
1553
|
-
* ```typescript
|
|
1554
|
-
* import { users } from '@/db/schema';
|
|
1555
|
-
*
|
|
1556
|
-
* constructor(database: PrimaryDatabaseService) {
|
|
1557
|
-
* super(database, users);
|
|
1558
|
-
* }
|
|
1559
|
-
* ```
|
|
1560
|
-
*/
|
|
1561
1284
|
constructor(database, table) {
|
|
1562
1285
|
this.database = database;
|
|
1563
1286
|
this.table = table;
|
|
1564
1287
|
const dbTableName = getTableName(table);
|
|
1565
1288
|
this.tableName = snakeToCamel(dbTableName);
|
|
1566
|
-
this.logger = new
|
|
1289
|
+
this.logger = new Logger6(this.constructor.name);
|
|
1567
1290
|
this.logger.debug(`Initialized ${this.constructor.name}`);
|
|
1568
1291
|
this.logger.debug(`Table name: '${dbTableName}' -> query key: '${this.tableName}'`);
|
|
1569
1292
|
}
|
|
1570
|
-
|
|
1571
|
-
* Create a new record
|
|
1572
|
-
*
|
|
1573
|
-
* @param data - The data to create the record with
|
|
1574
|
-
* @returns Promise resolving to the created record
|
|
1575
|
-
*
|
|
1576
|
-
* @example
|
|
1577
|
-
* ```typescript
|
|
1578
|
-
* const user = await userRepository.create({
|
|
1579
|
-
* email: 'user@example.com',
|
|
1580
|
-
* firstName: 'John'
|
|
1581
|
-
* });
|
|
1582
|
-
* ```
|
|
1583
|
-
*/
|
|
1293
|
+
// Creates a new record and returns it
|
|
1584
1294
|
async create(data) {
|
|
1585
1295
|
this.logger.log("Creating record");
|
|
1586
1296
|
const results = await this.db.insert(this.table).values(data).returning();
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
* @param id - The record ID
|
|
1593
|
-
* @returns Promise resolving to the record or undefined if not found
|
|
1594
|
-
*
|
|
1595
|
-
* @example
|
|
1596
|
-
* ```typescript
|
|
1597
|
-
* const user = await userRepository.findById('user-id-123');
|
|
1598
|
-
* ```
|
|
1599
|
-
*/
|
|
1297
|
+
const record = results[0];
|
|
1298
|
+
if (!record) throw new Error(`${this.tableName}: database operation returned no record`);
|
|
1299
|
+
return record;
|
|
1300
|
+
}
|
|
1301
|
+
// Finds a single record by primary key ID
|
|
1600
1302
|
async findById(id) {
|
|
1601
1303
|
this.logger.debug(`Finding record by ID: ${id}`);
|
|
1602
1304
|
return this.model.findFirst({
|
|
@@ -1605,105 +1307,29 @@ var PrimaryBaseRepository = class {
|
|
|
1605
1307
|
}
|
|
1606
1308
|
});
|
|
1607
1309
|
}
|
|
1608
|
-
|
|
1609
|
-
* Find a single record with custom where clause (Drizzle v2 object-based syntax)
|
|
1610
|
-
*
|
|
1611
|
-
* @param where - Object-based filter condition
|
|
1612
|
-
* @returns Promise resolving to the record or undefined if not found
|
|
1613
|
-
*
|
|
1614
|
-
* @example
|
|
1615
|
-
* ```typescript
|
|
1616
|
-
* // Simple equality
|
|
1617
|
-
* const user = await userRepository.findOne({ email: 'user@example.com' });
|
|
1618
|
-
*
|
|
1619
|
-
* // With operators
|
|
1620
|
-
* const user = await userRepository.findOne({ age: { gte: 18 } });
|
|
1621
|
-
*
|
|
1622
|
-
* // Multiple conditions (AND)
|
|
1623
|
-
* const user = await userRepository.findOne({
|
|
1624
|
-
* email: 'user@example.com',
|
|
1625
|
-
* status: 'ACTIVE'
|
|
1626
|
-
* });
|
|
1627
|
-
* ```
|
|
1628
|
-
*/
|
|
1310
|
+
// Finds a single record matching the given where filter
|
|
1629
1311
|
async findOne(where) {
|
|
1630
1312
|
this.logger.debug("Finding record with custom query");
|
|
1631
1313
|
return this.model.findFirst({
|
|
1632
1314
|
where
|
|
1633
1315
|
});
|
|
1634
1316
|
}
|
|
1635
|
-
|
|
1636
|
-
* Find multiple records (Drizzle v2 object-based syntax)
|
|
1637
|
-
*
|
|
1638
|
-
* @param options - Query options (where, orderBy, limit, offset)
|
|
1639
|
-
* @returns Promise resolving to an array of records
|
|
1640
|
-
*
|
|
1641
|
-
* @example
|
|
1642
|
-
* ```typescript
|
|
1643
|
-
* // Find all users
|
|
1644
|
-
* const users = await userRepository.findMany();
|
|
1645
|
-
*
|
|
1646
|
-
* // Find with filtering and pagination (v2 object syntax)
|
|
1647
|
-
* const users = await userRepository.findMany({
|
|
1648
|
-
* where: { accountStatus: 'ACTIVE' },
|
|
1649
|
-
* orderBy: { createdAt: 'desc' },
|
|
1650
|
-
* limit: 10,
|
|
1651
|
-
* offset: 0
|
|
1652
|
-
* });
|
|
1653
|
-
*
|
|
1654
|
-
* // Multiple conditions
|
|
1655
|
-
* const users = await userRepository.findMany({
|
|
1656
|
-
* where: {
|
|
1657
|
-
* AND: [
|
|
1658
|
-
* { status: 'ACTIVE' },
|
|
1659
|
-
* { age: { gte: 18 } }
|
|
1660
|
-
* ]
|
|
1661
|
-
* }
|
|
1662
|
-
* });
|
|
1663
|
-
* ```
|
|
1664
|
-
*/
|
|
1317
|
+
// Finds multiple records with optional filtering, ordering, and pagination
|
|
1665
1318
|
async findMany(options) {
|
|
1666
1319
|
this.logger.debug("Finding multiple records");
|
|
1667
1320
|
return this.model.findMany(options);
|
|
1668
1321
|
}
|
|
1669
|
-
|
|
1670
|
-
* Update a record by ID
|
|
1671
|
-
*
|
|
1672
|
-
* @param id - The record ID
|
|
1673
|
-
* @param data - The data to update
|
|
1674
|
-
* @returns Promise resolving to the updated record
|
|
1675
|
-
*
|
|
1676
|
-
* @example
|
|
1677
|
-
* ```typescript
|
|
1678
|
-
* const user = await userRepository.update('user-id-123', {
|
|
1679
|
-
* firstName: 'Jane'
|
|
1680
|
-
* });
|
|
1681
|
-
* ```
|
|
1682
|
-
*/
|
|
1322
|
+
// Updates a record by ID and returns the updated record
|
|
1683
1323
|
async update(id, data) {
|
|
1684
1324
|
this.logger.log(`Updating record with ID: ${id}`);
|
|
1685
1325
|
const idColumn = this.table.id;
|
|
1326
|
+
if (!idColumn) throw new Error(`Table '${this.tableName}' has no 'id' column`);
|
|
1686
1327
|
const results = await this.db.update(this.table).set(data).where(eq2(idColumn, id)).returning();
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
* @param where - SQL condition to match records
|
|
1693
|
-
* @param data - The data to update
|
|
1694
|
-
* @returns Promise resolving to the count of updated records
|
|
1695
|
-
*
|
|
1696
|
-
* @example
|
|
1697
|
-
* ```typescript
|
|
1698
|
-
* import { eq } from 'drizzle-orm';
|
|
1699
|
-
*
|
|
1700
|
-
* const result = await userRepository.updateMany(
|
|
1701
|
-
* eq(users.accountStatus, 'PENDING'),
|
|
1702
|
-
* { accountStatus: 'ACTIVE' }
|
|
1703
|
-
* );
|
|
1704
|
-
* console.log(`Updated ${result.count} users`);
|
|
1705
|
-
* ```
|
|
1706
|
-
*/
|
|
1328
|
+
const record = results[0];
|
|
1329
|
+
if (!record) throw new Error(`${this.tableName}: database operation returned no record`);
|
|
1330
|
+
return record;
|
|
1331
|
+
}
|
|
1332
|
+
// Updates all records matching the SQL condition and returns the affected count
|
|
1707
1333
|
async updateMany(where, data) {
|
|
1708
1334
|
this.logger.log("Updating multiple records");
|
|
1709
1335
|
const result = await this.db.update(this.table).set(data).where(where);
|
|
@@ -1711,39 +1337,17 @@ var PrimaryBaseRepository = class {
|
|
|
1711
1337
|
count: result.rowCount ?? 0
|
|
1712
1338
|
};
|
|
1713
1339
|
}
|
|
1714
|
-
|
|
1715
|
-
* Delete a record by ID
|
|
1716
|
-
*
|
|
1717
|
-
* @param id - The record ID
|
|
1718
|
-
* @returns Promise resolving to the deleted record
|
|
1719
|
-
*
|
|
1720
|
-
* @example
|
|
1721
|
-
* ```typescript
|
|
1722
|
-
* const user = await userRepository.delete('user-id-123');
|
|
1723
|
-
* ```
|
|
1724
|
-
*/
|
|
1340
|
+
// Deletes a record by ID and returns the deleted record
|
|
1725
1341
|
async delete(id) {
|
|
1726
1342
|
this.logger.log(`Deleting record with ID: ${id}`);
|
|
1727
1343
|
const idColumn = this.table.id;
|
|
1344
|
+
if (!idColumn) throw new Error(`Table '${this.tableName}' has no 'id' column`);
|
|
1728
1345
|
const results = await this.db.delete(this.table).where(eq2(idColumn, id)).returning();
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
* @param where - SQL condition to match records
|
|
1735
|
-
* @returns Promise resolving to the count of deleted records
|
|
1736
|
-
*
|
|
1737
|
-
* @example
|
|
1738
|
-
* ```typescript
|
|
1739
|
-
* import { lt } from 'drizzle-orm';
|
|
1740
|
-
*
|
|
1741
|
-
* const result = await userRepository.deleteMany(
|
|
1742
|
-
* lt(users.createdAt, new Date('2020-01-01'))
|
|
1743
|
-
* );
|
|
1744
|
-
* console.log(`Deleted ${result.count} users`);
|
|
1745
|
-
* ```
|
|
1746
|
-
*/
|
|
1346
|
+
const record = results[0];
|
|
1347
|
+
if (!record) throw new Error(`${this.tableName}: database operation returned no record`);
|
|
1348
|
+
return record;
|
|
1349
|
+
}
|
|
1350
|
+
// Deletes all records matching the SQL condition and returns the affected count
|
|
1747
1351
|
async deleteMany(where) {
|
|
1748
1352
|
this.logger.log("Deleting multiple records");
|
|
1749
1353
|
const result = await this.db.delete(this.table).where(where);
|
|
@@ -1751,25 +1355,7 @@ var PrimaryBaseRepository = class {
|
|
|
1751
1355
|
count: result.rowCount ?? 0
|
|
1752
1356
|
};
|
|
1753
1357
|
}
|
|
1754
|
-
|
|
1755
|
-
* Count records
|
|
1756
|
-
*
|
|
1757
|
-
* @param where - Optional SQL condition to filter records
|
|
1758
|
-
* @returns Promise resolving to the count of records
|
|
1759
|
-
*
|
|
1760
|
-
* @example
|
|
1761
|
-
* ```typescript
|
|
1762
|
-
* import { eq } from 'drizzle-orm';
|
|
1763
|
-
*
|
|
1764
|
-
* // Count all users
|
|
1765
|
-
* const total = await userRepository.count();
|
|
1766
|
-
*
|
|
1767
|
-
* // Count active users
|
|
1768
|
-
* const activeCount = await userRepository.count(
|
|
1769
|
-
* eq(users.accountStatus, 'ACTIVE')
|
|
1770
|
-
* );
|
|
1771
|
-
* ```
|
|
1772
|
-
*/
|
|
1358
|
+
// Counts records matching the optional SQL condition
|
|
1773
1359
|
async count(where) {
|
|
1774
1360
|
this.logger.debug("Counting records");
|
|
1775
1361
|
let query = this.db.select({
|
|
@@ -1781,30 +1367,125 @@ var PrimaryBaseRepository = class {
|
|
|
1781
1367
|
const results = await query;
|
|
1782
1368
|
return results[0].count;
|
|
1783
1369
|
}
|
|
1784
|
-
|
|
1785
|
-
* Check if a record exists
|
|
1786
|
-
*
|
|
1787
|
-
* @param where - SQL condition to match records
|
|
1788
|
-
* @returns Promise resolving to true if at least one record exists, false otherwise
|
|
1789
|
-
*
|
|
1790
|
-
* @example
|
|
1791
|
-
* ```typescript
|
|
1792
|
-
* import { eq } from 'drizzle-orm';
|
|
1793
|
-
*
|
|
1794
|
-
* const emailExists = await userRepository.exists(
|
|
1795
|
-
* eq(users.email, 'user@example.com')
|
|
1796
|
-
* );
|
|
1797
|
-
* ```
|
|
1798
|
-
*/
|
|
1370
|
+
// Returns true if at least one record matches the SQL condition
|
|
1799
1371
|
async exists(where) {
|
|
1800
1372
|
const count = await this.count(where);
|
|
1801
1373
|
return count > 0;
|
|
1802
1374
|
}
|
|
1375
|
+
// Finds records formatted as select dropdown options with optional search, pagination, and grouping
|
|
1376
|
+
async findForSelect(config) {
|
|
1377
|
+
this.logger.debug("Finding records for select dropdown");
|
|
1378
|
+
const parsedValues = typeof config.values === "string" ? config.values.split(",").map((v) => v.trim()).filter(Boolean) : config.values;
|
|
1379
|
+
const parsedExcludeIds = typeof config.excludeIds === "string" ? config.excludeIds.split(",").map((v) => v.trim()).filter(Boolean) : config.excludeIds ?? [];
|
|
1380
|
+
const tableColumns = this.table;
|
|
1381
|
+
const valueCol = tableColumns[config.value];
|
|
1382
|
+
if (!valueCol) throw new Error(`Column '${config.value}' not found in table '${this.tableName}'`);
|
|
1383
|
+
const labelCol = tableColumns[config.label];
|
|
1384
|
+
if (!labelCol) throw new Error(`Column '${config.label}' not found in table '${this.tableName}'`);
|
|
1385
|
+
if (parsedValues && parsedValues.length > 0) {
|
|
1386
|
+
const selectCols = {
|
|
1387
|
+
value: valueCol,
|
|
1388
|
+
label: labelCol
|
|
1389
|
+
};
|
|
1390
|
+
if (config.groupId) {
|
|
1391
|
+
const groupIdCol = tableColumns[config.groupId];
|
|
1392
|
+
if (groupIdCol) selectCols.groupId = groupIdCol;
|
|
1393
|
+
}
|
|
1394
|
+
const rows2 = await this.db.select(selectCols).from(this.table).where(inArray(valueCol, parsedValues));
|
|
1395
|
+
return {
|
|
1396
|
+
options: rows2.map((row) => ({
|
|
1397
|
+
value: row.value,
|
|
1398
|
+
label: String(row.label),
|
|
1399
|
+
...config.groupId && row.groupId != null ? {
|
|
1400
|
+
groupId: row.groupId
|
|
1401
|
+
} : {}
|
|
1402
|
+
})),
|
|
1403
|
+
hasMore: false,
|
|
1404
|
+
...config.groups ? {
|
|
1405
|
+
groups: config.groups
|
|
1406
|
+
} : {}
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
const selectFields = {
|
|
1410
|
+
value: valueCol,
|
|
1411
|
+
label: labelCol,
|
|
1412
|
+
totalCount: sql`count(*) over()`.mapWith(Number)
|
|
1413
|
+
};
|
|
1414
|
+
if (config.groupId) {
|
|
1415
|
+
const groupIdCol = tableColumns[config.groupId];
|
|
1416
|
+
if (groupIdCol) selectFields.groupId = groupIdCol;
|
|
1417
|
+
}
|
|
1418
|
+
const conditions = [];
|
|
1419
|
+
if (config.search) {
|
|
1420
|
+
conditions.push(ilike(labelCol, `%${config.search}%`));
|
|
1421
|
+
}
|
|
1422
|
+
if (parsedExcludeIds.length > 0) {
|
|
1423
|
+
conditions.push(notInArray(valueCol, parsedExcludeIds));
|
|
1424
|
+
}
|
|
1425
|
+
if (config.where) {
|
|
1426
|
+
for (const [field, val] of Object.entries(config.where)) {
|
|
1427
|
+
const column = tableColumns[field];
|
|
1428
|
+
if (column) {
|
|
1429
|
+
conditions.push(eq2(column, val));
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
const orderByKey = config.orderBy ? Object.keys(config.orderBy)[0] : void 0;
|
|
1434
|
+
const orderByCol = orderByKey ? tableColumns[orderByKey] ?? labelCol : labelCol;
|
|
1435
|
+
const limit = Number(config.limit) || 20;
|
|
1436
|
+
const offset = Number(config.offset) || 0;
|
|
1437
|
+
let query = this.db.select(selectFields).from(this.table).$dynamic();
|
|
1438
|
+
if (conditions.length > 0) {
|
|
1439
|
+
query = query.where(conditions.length === 1 ? conditions[0] : and(...conditions));
|
|
1440
|
+
}
|
|
1441
|
+
const orderClauses = [];
|
|
1442
|
+
if (config.groupId) {
|
|
1443
|
+
const groupIdCol = tableColumns[config.groupId];
|
|
1444
|
+
if (groupIdCol) orderClauses.push(asc(groupIdCol));
|
|
1445
|
+
}
|
|
1446
|
+
orderClauses.push(asc(orderByCol));
|
|
1447
|
+
query = query.orderBy(...orderClauses).limit(limit).offset(offset);
|
|
1448
|
+
const rows = await query;
|
|
1449
|
+
const totalCount = rows.length > 0 ? rows[0].totalCount : 0;
|
|
1450
|
+
const options = rows.map((row) => ({
|
|
1451
|
+
value: row.value,
|
|
1452
|
+
label: String(row.label),
|
|
1453
|
+
...config.groupId && row.groupId != null ? {
|
|
1454
|
+
groupId: row.groupId
|
|
1455
|
+
} : {}
|
|
1456
|
+
}));
|
|
1457
|
+
let resolvedGroups = config.groups;
|
|
1458
|
+
if (config.groupTable && config.groupId) {
|
|
1459
|
+
const groupTableColumns = config.groupTable;
|
|
1460
|
+
const groupIdKey = config.groupIdKey ?? "id";
|
|
1461
|
+
const groupNameKey = config.groupLabelKey ?? "name";
|
|
1462
|
+
const groupIdCol = groupTableColumns[groupIdKey];
|
|
1463
|
+
if (!groupIdCol) throw new Error(`Column '${groupIdKey}' not found in group table`);
|
|
1464
|
+
const groupNameCol = groupTableColumns[groupNameKey];
|
|
1465
|
+
if (!groupNameCol) throw new Error(`Column '${groupNameKey}' not found in group table`);
|
|
1466
|
+
const groupRows = await this.db.select({
|
|
1467
|
+
id: groupIdCol,
|
|
1468
|
+
name: groupNameCol
|
|
1469
|
+
}).from(config.groupTable).orderBy(asc(groupNameCol));
|
|
1470
|
+
resolvedGroups = groupRows.map((r) => ({
|
|
1471
|
+
id: r.id,
|
|
1472
|
+
name: String(r.name)
|
|
1473
|
+
}));
|
|
1474
|
+
}
|
|
1475
|
+
return {
|
|
1476
|
+
options,
|
|
1477
|
+
hasMore: offset + limit < totalCount,
|
|
1478
|
+
totalCount,
|
|
1479
|
+
...resolvedGroups ? {
|
|
1480
|
+
groups: resolvedGroups
|
|
1481
|
+
} : {}
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1803
1484
|
};
|
|
1804
1485
|
|
|
1805
1486
|
// src/database/repositories/tenant-base.repository.ts
|
|
1806
|
-
import { Logger as
|
|
1807
|
-
import { eq as eq3, getTableName as getTableName2, sql as sql2 } from "drizzle-orm";
|
|
1487
|
+
import { Logger as Logger7 } from "@nestjs/common";
|
|
1488
|
+
import { and as and2, asc as asc2, eq as eq3, getTableName as getTableName2, ilike as ilike2, inArray as inArray2, notInArray as notInArray2, sql as sql2 } from "drizzle-orm";
|
|
1808
1489
|
var TenantBaseRepository = class {
|
|
1809
1490
|
static {
|
|
1810
1491
|
__name(this, "TenantBaseRepository");
|
|
@@ -1812,133 +1493,43 @@ var TenantBaseRepository = class {
|
|
|
1812
1493
|
database;
|
|
1813
1494
|
table;
|
|
1814
1495
|
logger;
|
|
1815
|
-
/**
|
|
1816
|
-
* The table name extracted from the Drizzle table at runtime.
|
|
1817
|
-
* Used to access the query API for this repository's table.
|
|
1818
|
-
*/
|
|
1819
1496
|
tableName;
|
|
1820
|
-
/**
|
|
1821
|
-
* Lazy getter for Drizzle client.
|
|
1822
|
-
* Accesses the client from the database service only when needed,
|
|
1823
|
-
* avoiding initialization timing issues with NestJS lifecycle.
|
|
1824
|
-
*/
|
|
1825
1497
|
get db() {
|
|
1826
1498
|
return this.database.drizzleClient;
|
|
1827
1499
|
}
|
|
1828
|
-
/**
|
|
1829
|
-
* Model query API for THIS repository's table (Prisma-like syntax)
|
|
1830
|
-
* Scoped to only the table this repository manages
|
|
1831
|
-
*
|
|
1832
|
-
* @example
|
|
1833
|
-
* ```typescript
|
|
1834
|
-
* // Use relational queries with type safety
|
|
1835
|
-
* const product = await this.model.findFirst({
|
|
1836
|
-
* where: eq(products.id, id),
|
|
1837
|
-
* with: { category: true, variants: true }
|
|
1838
|
-
* });
|
|
1839
|
-
* ```
|
|
1840
|
-
*/
|
|
1841
1500
|
get model() {
|
|
1842
1501
|
return this.database.drizzleClient.query[this.tableName];
|
|
1843
1502
|
}
|
|
1844
|
-
/**
|
|
1845
|
-
* Create a new repository instance
|
|
1846
|
-
*
|
|
1847
|
-
* @param database - The tenant database service
|
|
1848
|
-
* @param table - The Drizzle table schema object
|
|
1849
|
-
*
|
|
1850
|
-
* @example
|
|
1851
|
-
* ```typescript
|
|
1852
|
-
* import { products } from '@/db/schema';
|
|
1853
|
-
*
|
|
1854
|
-
* constructor(database: TenantDatabaseService) {
|
|
1855
|
-
* super(database, products);
|
|
1856
|
-
* }
|
|
1857
|
-
* ```
|
|
1858
|
-
*/
|
|
1859
1503
|
constructor(database, table) {
|
|
1860
1504
|
this.database = database;
|
|
1861
1505
|
this.table = table;
|
|
1862
1506
|
this.tableName = getTableName2(table);
|
|
1863
|
-
this.logger = new
|
|
1507
|
+
this.logger = new Logger7(this.constructor.name);
|
|
1864
1508
|
this.logger.debug(`Initialized ${this.constructor.name}`);
|
|
1865
1509
|
}
|
|
1866
|
-
|
|
1867
|
-
* Create a new record
|
|
1868
|
-
*
|
|
1869
|
-
* @param data - The data to create the record with
|
|
1870
|
-
* @returns Promise resolving to the created record
|
|
1871
|
-
*
|
|
1872
|
-
* @example
|
|
1873
|
-
* ```typescript
|
|
1874
|
-
* const product = await productRepository.create({
|
|
1875
|
-
* name: 'Widget',
|
|
1876
|
-
* sku: 'WDG-001',
|
|
1877
|
-
* price: 9.99
|
|
1878
|
-
* });
|
|
1879
|
-
* ```
|
|
1880
|
-
*/
|
|
1510
|
+
// Creates a new record and returns it
|
|
1881
1511
|
async create(data) {
|
|
1882
1512
|
this.logger.log("Creating record");
|
|
1883
1513
|
const results = await this.db.insert(this.table).values(data).returning();
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
* @param id - The record ID
|
|
1890
|
-
* @returns Promise resolving to the record or null if not found
|
|
1891
|
-
*
|
|
1892
|
-
* @example
|
|
1893
|
-
* ```typescript
|
|
1894
|
-
* const product = await productRepository.findById('product-id-123');
|
|
1895
|
-
* ```
|
|
1896
|
-
*/
|
|
1514
|
+
const record = results[0];
|
|
1515
|
+
if (!record) throw new Error(`${this.tableName}: database operation returned no record`);
|
|
1516
|
+
return record;
|
|
1517
|
+
}
|
|
1518
|
+
// Finds a single record by primary key ID
|
|
1897
1519
|
async findById(id) {
|
|
1898
1520
|
this.logger.debug(`Finding record by ID: ${id}`);
|
|
1899
1521
|
const idColumn = this.table.id;
|
|
1522
|
+
if (!idColumn) throw new Error(`Table '${this.tableName}' has no 'id' column`);
|
|
1900
1523
|
const results = await this.db.select().from(this.table).where(eq3(idColumn, id)).limit(1);
|
|
1901
1524
|
return results[0] ?? null;
|
|
1902
1525
|
}
|
|
1903
|
-
|
|
1904
|
-
* Find a single record with custom where clause
|
|
1905
|
-
*
|
|
1906
|
-
* @param where - SQL condition
|
|
1907
|
-
* @returns Promise resolving to the record or null if not found
|
|
1908
|
-
*
|
|
1909
|
-
* @example
|
|
1910
|
-
* ```typescript
|
|
1911
|
-
* import { eq } from 'drizzle-orm';
|
|
1912
|
-
* const product = await productRepository.findOne(eq(products.sku, 'WDG-001'));
|
|
1913
|
-
* ```
|
|
1914
|
-
*/
|
|
1526
|
+
// Finds a single record matching the given SQL condition
|
|
1915
1527
|
async findOne(where) {
|
|
1916
1528
|
this.logger.debug("Finding record with custom query");
|
|
1917
1529
|
const results = await this.db.select().from(this.table).where(where).limit(1);
|
|
1918
1530
|
return results[0] ?? null;
|
|
1919
1531
|
}
|
|
1920
|
-
|
|
1921
|
-
* Find multiple records
|
|
1922
|
-
*
|
|
1923
|
-
* @param options - Query options (where, orderBy, limit, offset)
|
|
1924
|
-
* @returns Promise resolving to an array of records
|
|
1925
|
-
*
|
|
1926
|
-
* @example
|
|
1927
|
-
* ```typescript
|
|
1928
|
-
* import { eq, desc } from 'drizzle-orm';
|
|
1929
|
-
*
|
|
1930
|
-
* // Find all products
|
|
1931
|
-
* const products = await productRepository.findMany();
|
|
1932
|
-
*
|
|
1933
|
-
* // Find with filtering and pagination
|
|
1934
|
-
* const products = await productRepository.findMany({
|
|
1935
|
-
* where: eq(products.status, 'ACTIVE'),
|
|
1936
|
-
* orderBy: desc(products.createdAt),
|
|
1937
|
-
* limit: 10,
|
|
1938
|
-
* offset: 0
|
|
1939
|
-
* });
|
|
1940
|
-
* ```
|
|
1941
|
-
*/
|
|
1532
|
+
// Finds multiple records with optional SQL filtering, ordering, and pagination
|
|
1942
1533
|
async findMany(options) {
|
|
1943
1534
|
this.logger.debug("Finding multiple records");
|
|
1944
1535
|
let query = this.db.select().from(this.table).$dynamic();
|
|
@@ -1956,44 +1547,17 @@ var TenantBaseRepository = class {
|
|
|
1956
1547
|
}
|
|
1957
1548
|
return await query;
|
|
1958
1549
|
}
|
|
1959
|
-
|
|
1960
|
-
* Update a record by ID
|
|
1961
|
-
*
|
|
1962
|
-
* @param id - The record ID
|
|
1963
|
-
* @param data - The data to update
|
|
1964
|
-
* @returns Promise resolving to the updated record
|
|
1965
|
-
*
|
|
1966
|
-
* @example
|
|
1967
|
-
* ```typescript
|
|
1968
|
-
* const product = await productRepository.update('product-id-123', {
|
|
1969
|
-
* price: 12.99
|
|
1970
|
-
* });
|
|
1971
|
-
* ```
|
|
1972
|
-
*/
|
|
1550
|
+
// Updates a record by ID and returns the updated record
|
|
1973
1551
|
async update(id, data) {
|
|
1974
1552
|
this.logger.log(`Updating record with ID: ${id}`);
|
|
1975
1553
|
const idColumn = this.table.id;
|
|
1554
|
+
if (!idColumn) throw new Error(`Table '${this.tableName}' has no 'id' column`);
|
|
1976
1555
|
const results = await this.db.update(this.table).set(data).where(eq3(idColumn, id)).returning();
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
* @param where - SQL condition to match records
|
|
1983
|
-
* @param data - The data to update
|
|
1984
|
-
* @returns Promise resolving to the count of updated records
|
|
1985
|
-
*
|
|
1986
|
-
* @example
|
|
1987
|
-
* ```typescript
|
|
1988
|
-
* import { eq } from 'drizzle-orm';
|
|
1989
|
-
*
|
|
1990
|
-
* const result = await productRepository.updateMany(
|
|
1991
|
-
* eq(products.status, 'PENDING'),
|
|
1992
|
-
* { status: 'ACTIVE' }
|
|
1993
|
-
* );
|
|
1994
|
-
* console.log(`Updated ${result.count} products`);
|
|
1995
|
-
* ```
|
|
1996
|
-
*/
|
|
1556
|
+
const record = results[0];
|
|
1557
|
+
if (!record) throw new Error(`${this.tableName}: database operation returned no record`);
|
|
1558
|
+
return record;
|
|
1559
|
+
}
|
|
1560
|
+
// Updates all records matching the SQL condition and returns the affected count
|
|
1997
1561
|
async updateMany(where, data) {
|
|
1998
1562
|
this.logger.log("Updating multiple records");
|
|
1999
1563
|
const result = await this.db.update(this.table).set(data).where(where);
|
|
@@ -2001,39 +1565,17 @@ var TenantBaseRepository = class {
|
|
|
2001
1565
|
count: result.rowCount ?? 0
|
|
2002
1566
|
};
|
|
2003
1567
|
}
|
|
2004
|
-
|
|
2005
|
-
* Delete a record by ID
|
|
2006
|
-
*
|
|
2007
|
-
* @param id - The record ID
|
|
2008
|
-
* @returns Promise resolving to the deleted record
|
|
2009
|
-
*
|
|
2010
|
-
* @example
|
|
2011
|
-
* ```typescript
|
|
2012
|
-
* const product = await productRepository.delete('product-id-123');
|
|
2013
|
-
* ```
|
|
2014
|
-
*/
|
|
1568
|
+
// Deletes a record by ID and returns the deleted record
|
|
2015
1569
|
async delete(id) {
|
|
2016
1570
|
this.logger.log(`Deleting record with ID: ${id}`);
|
|
2017
1571
|
const idColumn = this.table.id;
|
|
1572
|
+
if (!idColumn) throw new Error(`Table '${this.tableName}' has no 'id' column`);
|
|
2018
1573
|
const results = await this.db.delete(this.table).where(eq3(idColumn, id)).returning();
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
* @param where - SQL condition to match records
|
|
2025
|
-
* @returns Promise resolving to the count of deleted records
|
|
2026
|
-
*
|
|
2027
|
-
* @example
|
|
2028
|
-
* ```typescript
|
|
2029
|
-
* import { lt } from 'drizzle-orm';
|
|
2030
|
-
*
|
|
2031
|
-
* const result = await productRepository.deleteMany(
|
|
2032
|
-
* lt(products.createdAt, new Date('2020-01-01'))
|
|
2033
|
-
* );
|
|
2034
|
-
* console.log(`Deleted ${result.count} products`);
|
|
2035
|
-
* ```
|
|
2036
|
-
*/
|
|
1574
|
+
const record = results[0];
|
|
1575
|
+
if (!record) throw new Error(`${this.tableName}: database operation returned no record`);
|
|
1576
|
+
return record;
|
|
1577
|
+
}
|
|
1578
|
+
// Deletes all records matching the SQL condition and returns the affected count
|
|
2037
1579
|
async deleteMany(where) {
|
|
2038
1580
|
this.logger.log("Deleting multiple records");
|
|
2039
1581
|
const result = await this.db.delete(this.table).where(where);
|
|
@@ -2041,25 +1583,7 @@ var TenantBaseRepository = class {
|
|
|
2041
1583
|
count: result.rowCount ?? 0
|
|
2042
1584
|
};
|
|
2043
1585
|
}
|
|
2044
|
-
|
|
2045
|
-
* Count records
|
|
2046
|
-
*
|
|
2047
|
-
* @param where - Optional SQL condition to filter records
|
|
2048
|
-
* @returns Promise resolving to the count of records
|
|
2049
|
-
*
|
|
2050
|
-
* @example
|
|
2051
|
-
* ```typescript
|
|
2052
|
-
* import { eq } from 'drizzle-orm';
|
|
2053
|
-
*
|
|
2054
|
-
* // Count all products
|
|
2055
|
-
* const total = await productRepository.count();
|
|
2056
|
-
*
|
|
2057
|
-
* // Count active products
|
|
2058
|
-
* const activeCount = await productRepository.count(
|
|
2059
|
-
* eq(products.status, 'ACTIVE')
|
|
2060
|
-
* );
|
|
2061
|
-
* ```
|
|
2062
|
-
*/
|
|
1586
|
+
// Counts records matching the optional SQL condition
|
|
2063
1587
|
async count(where) {
|
|
2064
1588
|
this.logger.debug("Counting records");
|
|
2065
1589
|
let query = this.db.select({
|
|
@@ -2071,117 +1595,769 @@ var TenantBaseRepository = class {
|
|
|
2071
1595
|
const results = await query;
|
|
2072
1596
|
return results[0].count;
|
|
2073
1597
|
}
|
|
2074
|
-
|
|
2075
|
-
* Check if a record exists
|
|
2076
|
-
*
|
|
2077
|
-
* @param where - SQL condition to match records
|
|
2078
|
-
* @returns Promise resolving to true if at least one record exists, false otherwise
|
|
2079
|
-
*
|
|
2080
|
-
* @example
|
|
2081
|
-
* ```typescript
|
|
2082
|
-
* import { eq } from 'drizzle-orm';
|
|
2083
|
-
*
|
|
2084
|
-
* const skuExists = await productRepository.exists(
|
|
2085
|
-
* eq(products.sku, 'WDG-001')
|
|
2086
|
-
* );
|
|
2087
|
-
* ```
|
|
2088
|
-
*/
|
|
1598
|
+
// Returns true if at least one record matches the SQL condition
|
|
2089
1599
|
async exists(where) {
|
|
2090
1600
|
const count = await this.count(where);
|
|
2091
1601
|
return count > 0;
|
|
2092
1602
|
}
|
|
1603
|
+
// Finds records formatted as select dropdown options with optional search, pagination, and grouping
|
|
1604
|
+
async findForSelect(config) {
|
|
1605
|
+
this.logger.debug("Finding records for select dropdown");
|
|
1606
|
+
const parsedValues = typeof config.values === "string" ? config.values.split(",").map((v) => v.trim()).filter(Boolean) : config.values;
|
|
1607
|
+
const parsedExcludeIds = typeof config.excludeIds === "string" ? config.excludeIds.split(",").map((v) => v.trim()).filter(Boolean) : config.excludeIds ?? [];
|
|
1608
|
+
const tableColumns = this.table;
|
|
1609
|
+
const valueCol = tableColumns[config.value];
|
|
1610
|
+
if (!valueCol) throw new Error(`Column '${config.value}' not found in table '${this.tableName}'`);
|
|
1611
|
+
const labelCol = tableColumns[config.label];
|
|
1612
|
+
if (!labelCol) throw new Error(`Column '${config.label}' not found in table '${this.tableName}'`);
|
|
1613
|
+
if (parsedValues && parsedValues.length > 0) {
|
|
1614
|
+
const selectCols = {
|
|
1615
|
+
value: valueCol,
|
|
1616
|
+
label: labelCol
|
|
1617
|
+
};
|
|
1618
|
+
if (config.groupId) {
|
|
1619
|
+
const groupIdCol = tableColumns[config.groupId];
|
|
1620
|
+
if (groupIdCol) selectCols.groupId = groupIdCol;
|
|
1621
|
+
}
|
|
1622
|
+
const rows2 = await this.db.select(selectCols).from(this.table).where(inArray2(valueCol, parsedValues));
|
|
1623
|
+
return {
|
|
1624
|
+
options: rows2.map((row) => ({
|
|
1625
|
+
value: row.value,
|
|
1626
|
+
label: String(row.label),
|
|
1627
|
+
...config.groupId && row.groupId != null ? {
|
|
1628
|
+
groupId: row.groupId
|
|
1629
|
+
} : {}
|
|
1630
|
+
})),
|
|
1631
|
+
hasMore: false,
|
|
1632
|
+
...config.groups ? {
|
|
1633
|
+
groups: config.groups
|
|
1634
|
+
} : {}
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
const selectFields = {
|
|
1638
|
+
value: valueCol,
|
|
1639
|
+
label: labelCol,
|
|
1640
|
+
totalCount: sql2`count(*) over()`.mapWith(Number)
|
|
1641
|
+
};
|
|
1642
|
+
if (config.groupId) {
|
|
1643
|
+
const groupIdCol = tableColumns[config.groupId];
|
|
1644
|
+
if (groupIdCol) selectFields.groupId = groupIdCol;
|
|
1645
|
+
}
|
|
1646
|
+
const conditions = [];
|
|
1647
|
+
if (config.search) {
|
|
1648
|
+
conditions.push(ilike2(labelCol, `%${config.search}%`));
|
|
1649
|
+
}
|
|
1650
|
+
if (parsedExcludeIds.length > 0) {
|
|
1651
|
+
conditions.push(notInArray2(valueCol, parsedExcludeIds));
|
|
1652
|
+
}
|
|
1653
|
+
if (config.where) {
|
|
1654
|
+
for (const [field, val] of Object.entries(config.where)) {
|
|
1655
|
+
const column = tableColumns[field];
|
|
1656
|
+
if (column) {
|
|
1657
|
+
conditions.push(eq3(column, val));
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const orderByKey = config.orderBy ? Object.keys(config.orderBy)[0] : void 0;
|
|
1662
|
+
const orderByCol = orderByKey ? tableColumns[orderByKey] ?? labelCol : labelCol;
|
|
1663
|
+
const limit = Number(config.limit) || 20;
|
|
1664
|
+
const offset = Number(config.offset) || 0;
|
|
1665
|
+
let query = this.db.select(selectFields).from(this.table).$dynamic();
|
|
1666
|
+
if (conditions.length > 0) {
|
|
1667
|
+
query = query.where(conditions.length === 1 ? conditions[0] : and2(...conditions));
|
|
1668
|
+
}
|
|
1669
|
+
const orderClauses = [];
|
|
1670
|
+
if (config.groupId) {
|
|
1671
|
+
const groupIdCol = tableColumns[config.groupId];
|
|
1672
|
+
if (groupIdCol) orderClauses.push(asc2(groupIdCol));
|
|
1673
|
+
}
|
|
1674
|
+
orderClauses.push(asc2(orderByCol));
|
|
1675
|
+
query = query.orderBy(...orderClauses).limit(limit).offset(offset);
|
|
1676
|
+
const rows = await query;
|
|
1677
|
+
const totalCount = rows.length > 0 ? rows[0].totalCount : 0;
|
|
1678
|
+
const options = rows.map((row) => ({
|
|
1679
|
+
value: row.value,
|
|
1680
|
+
label: String(row.label),
|
|
1681
|
+
...config.groupId && row.groupId != null ? {
|
|
1682
|
+
groupId: row.groupId
|
|
1683
|
+
} : {}
|
|
1684
|
+
}));
|
|
1685
|
+
let resolvedGroups = config.groups;
|
|
1686
|
+
if (config.groupTable && config.groupId) {
|
|
1687
|
+
const groupTableColumns = config.groupTable;
|
|
1688
|
+
const groupIdKey = config.groupIdKey ?? "id";
|
|
1689
|
+
const groupNameKey = config.groupLabelKey ?? "name";
|
|
1690
|
+
const groupIdCol = groupTableColumns[groupIdKey];
|
|
1691
|
+
if (!groupIdCol) throw new Error(`Column '${groupIdKey}' not found in group table`);
|
|
1692
|
+
const groupNameCol = groupTableColumns[groupNameKey];
|
|
1693
|
+
if (!groupNameCol) throw new Error(`Column '${groupNameKey}' not found in group table`);
|
|
1694
|
+
const groupRows = await this.db.select({
|
|
1695
|
+
id: groupIdCol,
|
|
1696
|
+
name: groupNameCol
|
|
1697
|
+
}).from(config.groupTable).orderBy(asc2(groupNameCol));
|
|
1698
|
+
resolvedGroups = groupRows.map((r) => ({
|
|
1699
|
+
id: r.id,
|
|
1700
|
+
name: String(r.name)
|
|
1701
|
+
}));
|
|
1702
|
+
}
|
|
1703
|
+
return {
|
|
1704
|
+
options,
|
|
1705
|
+
hasMore: offset + limit < totalCount,
|
|
1706
|
+
totalCount,
|
|
1707
|
+
...resolvedGroups ? {
|
|
1708
|
+
groups: resolvedGroups
|
|
1709
|
+
} : {}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
2093
1712
|
};
|
|
2094
1713
|
|
|
2095
|
-
// src/
|
|
2096
|
-
import {
|
|
1714
|
+
// src/email/email.module.ts
|
|
1715
|
+
import { Global as Global4, Module as Module4 } from "@nestjs/common";
|
|
1716
|
+
import { ConfigModule as ConfigModule2 } from "@nestjs/config";
|
|
2097
1717
|
|
|
2098
|
-
// src/
|
|
2099
|
-
import {
|
|
2100
|
-
|
|
1718
|
+
// src/email/email.service.ts
|
|
1719
|
+
import { BrevoClient, BrevoError, BrevoTimeoutError } from "@getbrevo/brevo";
|
|
1720
|
+
import { Injectable as Injectable7, Logger as Logger8 } from "@nestjs/common";
|
|
1721
|
+
import { ConfigService as ConfigService4 } from "@nestjs/config";
|
|
1722
|
+
function _ts_decorate11(decorators, target, key, desc) {
|
|
1723
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1724
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1725
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1726
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1727
|
+
}
|
|
1728
|
+
__name(_ts_decorate11, "_ts_decorate");
|
|
1729
|
+
function _ts_metadata7(k, v) {
|
|
1730
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
1731
|
+
}
|
|
1732
|
+
__name(_ts_metadata7, "_ts_metadata");
|
|
1733
|
+
var EmailService = class _EmailService {
|
|
2101
1734
|
static {
|
|
2102
|
-
__name(this, "
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
}
|
|
1735
|
+
__name(this, "EmailService");
|
|
1736
|
+
}
|
|
1737
|
+
configService;
|
|
1738
|
+
logger = new Logger8(_EmailService.name);
|
|
1739
|
+
brevoClient;
|
|
1740
|
+
senderEmail;
|
|
1741
|
+
senderName;
|
|
1742
|
+
constructor(configService) {
|
|
1743
|
+
this.configService = configService;
|
|
1744
|
+
const apiKey = this.configService.get("BREVO_API_KEY");
|
|
1745
|
+
if (!apiKey) {
|
|
1746
|
+
this.logger.error("BREVO_API_KEY is not configured. Email sending will fail.");
|
|
1747
|
+
throw new Error("Email service configuration error: Missing BREVO_API_KEY");
|
|
1748
|
+
}
|
|
1749
|
+
this.brevoClient = new BrevoClient({
|
|
1750
|
+
apiKey,
|
|
1751
|
+
maxRetries: 3
|
|
1752
|
+
});
|
|
1753
|
+
const senderEmail = this.configService.get("SENDER_EMAIL");
|
|
1754
|
+
const senderName = this.configService.get("SENDER_NAME");
|
|
1755
|
+
if (!senderEmail || !senderName) {
|
|
1756
|
+
this.logger.error("Sender email or name is not configured.");
|
|
1757
|
+
throw new Error("Email service configuration error: Missing SENDER_EMAIL or SENDER_NAME");
|
|
1758
|
+
}
|
|
1759
|
+
this.senderEmail = senderEmail;
|
|
1760
|
+
this.senderName = senderName;
|
|
1761
|
+
this.logger.log("Brevo email service initialized successfully");
|
|
1762
|
+
}
|
|
1763
|
+
// Sends an email verification OTP to the given recipient
|
|
1764
|
+
async sendVerificationEmail(email, otp, expiresAt, displayName) {
|
|
1765
|
+
const name = displayName || "there";
|
|
1766
|
+
const expiryMinutes = Math.ceil((expiresAt.getTime() - Date.now()) / 6e4);
|
|
1767
|
+
const subject = "Verify Your Email - Vritti AI Cloud";
|
|
1768
|
+
const htmlContent = `
|
|
1769
|
+
<!DOCTYPE html>
|
|
1770
|
+
<html>
|
|
1771
|
+
<head>
|
|
1772
|
+
<meta charset="UTF-8">
|
|
1773
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1774
|
+
</head>
|
|
1775
|
+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
|
1776
|
+
<table role="presentation" style="width: 100%; border-collapse: collapse;">
|
|
1777
|
+
<tr>
|
|
1778
|
+
<td style="padding: 40px 20px;">
|
|
1779
|
+
<table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
|
1780
|
+
<!-- Header -->
|
|
1781
|
+
<tr>
|
|
1782
|
+
<td style="padding: 40px 40px 20px; text-align: center; border-bottom: 1px solid #e0e0e0;">
|
|
1783
|
+
<h1 style="margin: 0; color: #1a1a1a; font-size: 24px; font-weight: 600;">Email Verification</h1>
|
|
1784
|
+
</td>
|
|
1785
|
+
</tr>
|
|
2116
1786
|
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
1787
|
+
<!-- Content -->
|
|
1788
|
+
<tr>
|
|
1789
|
+
<td style="padding: 40px;">
|
|
1790
|
+
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;">
|
|
1791
|
+
Hello <strong>${name}</strong>,
|
|
1792
|
+
</p>
|
|
1793
|
+
<p style="margin: 0 0 30px; color: #333333; font-size: 16px; line-height: 1.6;">
|
|
1794
|
+
Thank you for signing up with Vritti AI Cloud. Please use the following verification code to complete your registration:
|
|
1795
|
+
</p>
|
|
2126
1796
|
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
constructor(detailOrOptions) {
|
|
2134
|
-
super(detailOrOptions ?? "Bad Request", HttpStatus2.BAD_REQUEST);
|
|
2135
|
-
}
|
|
2136
|
-
};
|
|
1797
|
+
<!-- OTP Box -->
|
|
1798
|
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 8px; text-align: center; margin: 30px 0;">
|
|
1799
|
+
<div style="color: #ffffff; font-size: 36px; font-weight: bold; letter-spacing: 10px; font-family: 'Courier New', monospace;">
|
|
1800
|
+
${otp}
|
|
1801
|
+
</div>
|
|
1802
|
+
</div>
|
|
2137
1803
|
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
}
|
|
2147
|
-
};
|
|
1804
|
+
<p style="margin: 30px 0 20px; color: #666666; font-size: 14px; line-height: 1.6;">
|
|
1805
|
+
<strong>Important:</strong> This code will expire in <strong>${expiryMinutes} minute${expiryMinutes === 1 ? "" : "s"}</strong>.
|
|
1806
|
+
</p>
|
|
1807
|
+
<p style="margin: 0; color: #666666; font-size: 14px; line-height: 1.6;">
|
|
1808
|
+
If you didn't request this verification, please ignore this email.
|
|
1809
|
+
</p>
|
|
1810
|
+
</td>
|
|
1811
|
+
</tr>
|
|
2148
1812
|
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
1813
|
+
<!-- Footer -->
|
|
1814
|
+
<tr>
|
|
1815
|
+
<td style="padding: 30px 40px; border-top: 1px solid #e0e0e0; text-align: center;">
|
|
1816
|
+
<p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
|
|
1817
|
+
Vritti AI Cloud - Cloud Management Platform
|
|
1818
|
+
</p>
|
|
1819
|
+
<p style="margin: 8px 0 0; color: #999999; font-size: 12px; line-height: 1.5;">
|
|
1820
|
+
This is an automated message, please do not reply.
|
|
1821
|
+
</p>
|
|
1822
|
+
</td>
|
|
1823
|
+
</tr>
|
|
1824
|
+
</table>
|
|
1825
|
+
</td>
|
|
1826
|
+
</tr>
|
|
1827
|
+
</table>
|
|
1828
|
+
</body>
|
|
1829
|
+
</html>
|
|
1830
|
+
`;
|
|
1831
|
+
const textContent = `
|
|
1832
|
+
Hello ${name},
|
|
2159
1833
|
|
|
2160
|
-
|
|
2161
|
-
import { HttpStatus as HttpStatus5 } from "@nestjs/common";
|
|
2162
|
-
var GoneException = class extends HttpProblemException {
|
|
2163
|
-
static {
|
|
2164
|
-
__name(this, "GoneException");
|
|
2165
|
-
}
|
|
2166
|
-
constructor(detailOrOptions) {
|
|
2167
|
-
super(detailOrOptions ?? "Gone", HttpStatus5.GONE);
|
|
2168
|
-
}
|
|
2169
|
-
};
|
|
1834
|
+
Thank you for signing up with Vritti AI Cloud. Please use the following verification code to complete your registration:
|
|
2170
1835
|
|
|
2171
|
-
|
|
2172
|
-
import { HttpStatus as HttpStatus6 } from "@nestjs/common";
|
|
2173
|
-
var InternalServerErrorException3 = class extends HttpProblemException {
|
|
2174
|
-
static {
|
|
2175
|
-
__name(this, "InternalServerErrorException");
|
|
2176
|
-
}
|
|
2177
|
-
constructor(detailOrOptions) {
|
|
2178
|
-
super(detailOrOptions ?? "Internal Server Error", HttpStatus6.INTERNAL_SERVER_ERROR);
|
|
2179
|
-
}
|
|
2180
|
-
};
|
|
1836
|
+
Verification Code: ${otp}
|
|
2181
1837
|
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
1838
|
+
This code will expire in ${expiryMinutes} minute${expiryMinutes === 1 ? "" : "s"}.
|
|
1839
|
+
|
|
1840
|
+
If you didn't request this verification, please ignore this email.
|
|
1841
|
+
|
|
1842
|
+
---
|
|
1843
|
+
Vritti AI Cloud - Cloud Management Platform
|
|
1844
|
+
This is an automated message, please do not reply.
|
|
1845
|
+
`.trim();
|
|
1846
|
+
await this.sendEmail({
|
|
1847
|
+
to: [
|
|
1848
|
+
{
|
|
1849
|
+
email,
|
|
1850
|
+
name
|
|
1851
|
+
}
|
|
1852
|
+
],
|
|
1853
|
+
subject,
|
|
1854
|
+
htmlContent,
|
|
1855
|
+
textContent
|
|
1856
|
+
});
|
|
1857
|
+
this.logger.log(`Verification email sent to ${email}`);
|
|
1858
|
+
}
|
|
1859
|
+
// Sends a password reset OTP to the given recipient
|
|
1860
|
+
async sendPasswordResetEmail(email, otp, expiresAt, displayName) {
|
|
1861
|
+
const name = displayName || "there";
|
|
1862
|
+
const expiryMinutes = Math.ceil((expiresAt.getTime() - Date.now()) / 6e4);
|
|
1863
|
+
const subject = "Reset Your Password - Vritti AI Cloud";
|
|
1864
|
+
const htmlContent = `
|
|
1865
|
+
<!DOCTYPE html>
|
|
1866
|
+
<html>
|
|
1867
|
+
<head>
|
|
1868
|
+
<meta charset="UTF-8">
|
|
1869
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1870
|
+
</head>
|
|
1871
|
+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
|
1872
|
+
<table role="presentation" style="width: 100%; border-collapse: collapse;">
|
|
1873
|
+
<tr>
|
|
1874
|
+
<td style="padding: 40px 20px;">
|
|
1875
|
+
<table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
|
1876
|
+
<!-- Header -->
|
|
1877
|
+
<tr>
|
|
1878
|
+
<td style="padding: 40px 40px 20px; text-align: center; border-bottom: 1px solid #e0e0e0;">
|
|
1879
|
+
<h1 style="margin: 0; color: #1a1a1a; font-size: 24px; font-weight: 600;">Password Reset</h1>
|
|
1880
|
+
</td>
|
|
1881
|
+
</tr>
|
|
1882
|
+
|
|
1883
|
+
<!-- Content -->
|
|
1884
|
+
<tr>
|
|
1885
|
+
<td style="padding: 40px;">
|
|
1886
|
+
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;">
|
|
1887
|
+
Hello <strong>${name}</strong>,
|
|
1888
|
+
</p>
|
|
1889
|
+
<p style="margin: 0 0 30px; color: #333333; font-size: 16px; line-height: 1.6;">
|
|
1890
|
+
We received a request to reset your password. Use the following code to complete the process:
|
|
1891
|
+
</p>
|
|
1892
|
+
|
|
1893
|
+
<!-- OTP Box -->
|
|
1894
|
+
<div style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); padding: 30px; border-radius: 8px; text-align: center; margin: 30px 0;">
|
|
1895
|
+
<div style="color: #ffffff; font-size: 36px; font-weight: bold; letter-spacing: 10px; font-family: 'Courier New', monospace;">
|
|
1896
|
+
${otp}
|
|
1897
|
+
</div>
|
|
1898
|
+
</div>
|
|
1899
|
+
|
|
1900
|
+
<p style="margin: 30px 0 20px; color: #666666; font-size: 14px; line-height: 1.6;">
|
|
1901
|
+
<strong>Important:</strong> This code will expire in <strong>${expiryMinutes} minute${expiryMinutes === 1 ? "" : "s"}</strong>.
|
|
1902
|
+
</p>
|
|
1903
|
+
<p style="margin: 0 0 20px; color: #666666; font-size: 14px; line-height: 1.6;">
|
|
1904
|
+
If you didn't request a password reset, please ignore this email and your password will remain unchanged.
|
|
1905
|
+
</p>
|
|
1906
|
+
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin-top: 20px; border-radius: 4px;">
|
|
1907
|
+
<p style="margin: 0; color: #856404; font-size: 13px; line-height: 1.5;">
|
|
1908
|
+
<strong>Security Tip:</strong> Never share this code with anyone. Vritti will never ask for your verification code.
|
|
1909
|
+
</p>
|
|
1910
|
+
</div>
|
|
1911
|
+
</td>
|
|
1912
|
+
</tr>
|
|
1913
|
+
|
|
1914
|
+
<!-- Footer -->
|
|
1915
|
+
<tr>
|
|
1916
|
+
<td style="padding: 30px 40px; border-top: 1px solid #e0e0e0; text-align: center;">
|
|
1917
|
+
<p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
|
|
1918
|
+
Vritti AI Cloud - Cloud Management Platform
|
|
1919
|
+
</p>
|
|
1920
|
+
<p style="margin: 8px 0 0; color: #999999; font-size: 12px; line-height: 1.5;">
|
|
1921
|
+
This is an automated message, please do not reply.
|
|
1922
|
+
</p>
|
|
1923
|
+
</td>
|
|
1924
|
+
</tr>
|
|
1925
|
+
</table>
|
|
1926
|
+
</td>
|
|
1927
|
+
</tr>
|
|
1928
|
+
</table>
|
|
1929
|
+
</body>
|
|
1930
|
+
</html>
|
|
1931
|
+
`;
|
|
1932
|
+
const textContent = `
|
|
1933
|
+
Hello ${name},
|
|
1934
|
+
|
|
1935
|
+
We received a request to reset your password. Use the following code to complete the process:
|
|
1936
|
+
|
|
1937
|
+
Reset Code: ${otp}
|
|
1938
|
+
|
|
1939
|
+
This code will expire in ${expiryMinutes} minute${expiryMinutes === 1 ? "" : "s"}.
|
|
1940
|
+
|
|
1941
|
+
If you didn't request a password reset, please ignore this email and your password will remain unchanged.
|
|
1942
|
+
|
|
1943
|
+
SECURITY TIP: Never share this code with anyone. Vritti will never ask for your verification code.
|
|
1944
|
+
|
|
1945
|
+
---
|
|
1946
|
+
Vritti AI Cloud - Cloud Management Platform
|
|
1947
|
+
This is an automated message, please do not reply.
|
|
1948
|
+
`.trim();
|
|
1949
|
+
await this.sendEmail({
|
|
1950
|
+
to: [
|
|
1951
|
+
{
|
|
1952
|
+
email,
|
|
1953
|
+
name
|
|
1954
|
+
}
|
|
1955
|
+
],
|
|
1956
|
+
subject,
|
|
1957
|
+
htmlContent,
|
|
1958
|
+
textContent
|
|
1959
|
+
});
|
|
1960
|
+
this.logger.log(`Password reset email sent to ${email}`);
|
|
1961
|
+
}
|
|
1962
|
+
// Sends an email change notification to the old address with a revert link
|
|
1963
|
+
async sendEmailChangeNotification(oldEmail, newEmail, revertToken, revertExpiresAt, displayName) {
|
|
1964
|
+
const name = displayName || "there";
|
|
1965
|
+
const subject = "Your Email Address Has Been Changed - Vritti AI Cloud";
|
|
1966
|
+
const hoursUntilExpiry = Math.floor((revertExpiresAt.getTime() - Date.now()) / (1e3 * 60 * 60));
|
|
1967
|
+
const revertLink = `https://local.vrittiai.com:3012/settings/profile/revert-email?token=${revertToken}`;
|
|
1968
|
+
const htmlContent = `
|
|
1969
|
+
<!DOCTYPE html>
|
|
1970
|
+
<html>
|
|
1971
|
+
<head>
|
|
1972
|
+
<meta charset="UTF-8">
|
|
1973
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1974
|
+
</head>
|
|
1975
|
+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
|
1976
|
+
<table role="presentation" style="width: 100%; border-collapse: collapse;">
|
|
1977
|
+
<tr>
|
|
1978
|
+
<td style="padding: 40px 20px;">
|
|
1979
|
+
<table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
|
1980
|
+
<!-- Header -->
|
|
1981
|
+
<tr>
|
|
1982
|
+
<td style="padding: 40px 40px 20px; text-align: center; border-bottom: 1px solid #e0e0e0;">
|
|
1983
|
+
<h1 style="margin: 0; color: #1a1a1a; font-size: 24px; font-weight: 600;">Email Address Changed</h1>
|
|
1984
|
+
</td>
|
|
1985
|
+
</tr>
|
|
1986
|
+
|
|
1987
|
+
<!-- Content -->
|
|
1988
|
+
<tr>
|
|
1989
|
+
<td style="padding: 40px;">
|
|
1990
|
+
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;">
|
|
1991
|
+
Hello <strong>${name}</strong>,
|
|
1992
|
+
</p>
|
|
1993
|
+
<p style="margin: 0 0 30px; color: #333333; font-size: 16px; line-height: 1.6;">
|
|
1994
|
+
We're writing to inform you that your Vritti AI Cloud email address has been successfully changed.
|
|
1995
|
+
</p>
|
|
1996
|
+
|
|
1997
|
+
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 30px 0;">
|
|
1998
|
+
<p style="margin: 0 0 10px; color: #666666; font-size: 14px;">
|
|
1999
|
+
<strong>Previous Email:</strong>
|
|
2000
|
+
</p>
|
|
2001
|
+
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; font-family: monospace;">
|
|
2002
|
+
${oldEmail}
|
|
2003
|
+
</p>
|
|
2004
|
+
<p style="margin: 0 0 10px; color: #666666; font-size: 14px;">
|
|
2005
|
+
<strong>New Email:</strong>
|
|
2006
|
+
</p>
|
|
2007
|
+
<p style="margin: 0; color: #333333; font-size: 16px; font-family: monospace;">
|
|
2008
|
+
${newEmail}
|
|
2009
|
+
</p>
|
|
2010
|
+
</div>
|
|
2011
|
+
|
|
2012
|
+
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 20px; margin: 30px 0; border-radius: 4px;">
|
|
2013
|
+
<p style="margin: 0 0 15px; color: #856404; font-size: 14px; line-height: 1.6;">
|
|
2014
|
+
<strong>Didn't make this change?</strong>
|
|
2015
|
+
</p>
|
|
2016
|
+
<p style="margin: 0 0 20px; color: #856404; font-size: 14px; line-height: 1.6;">
|
|
2017
|
+
If you did not authorize this change, you can revert it within the next <strong>${hoursUntilExpiry} hours</strong> by clicking the button below:
|
|
2018
|
+
</p>
|
|
2019
|
+
<div style="text-align: center;">
|
|
2020
|
+
<a href="${revertLink}" style="display: inline-block; padding: 12px 30px; background-color: #dc3545; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 600; font-size: 14px;">
|
|
2021
|
+
Revert Email Change
|
|
2022
|
+
</a>
|
|
2023
|
+
</div>
|
|
2024
|
+
</div>
|
|
2025
|
+
|
|
2026
|
+
<p style="margin: 30px 0 0; color: #666666; font-size: 14px; line-height: 1.6;">
|
|
2027
|
+
If you made this change, you can safely ignore this email.
|
|
2028
|
+
</p>
|
|
2029
|
+
</td>
|
|
2030
|
+
</tr>
|
|
2031
|
+
|
|
2032
|
+
<!-- Footer -->
|
|
2033
|
+
<tr>
|
|
2034
|
+
<td style="padding: 30px 40px; border-top: 1px solid #e0e0e0; text-align: center;">
|
|
2035
|
+
<p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
|
|
2036
|
+
Vritti AI Cloud - Cloud Management Platform
|
|
2037
|
+
</p>
|
|
2038
|
+
<p style="margin: 8px 0 0; color: #999999; font-size: 12px; line-height: 1.5;">
|
|
2039
|
+
This is an automated message, please do not reply.
|
|
2040
|
+
</p>
|
|
2041
|
+
</td>
|
|
2042
|
+
</tr>
|
|
2043
|
+
</table>
|
|
2044
|
+
</td>
|
|
2045
|
+
</tr>
|
|
2046
|
+
</table>
|
|
2047
|
+
</body>
|
|
2048
|
+
</html>
|
|
2049
|
+
`;
|
|
2050
|
+
const textContent = `
|
|
2051
|
+
Hello ${name},
|
|
2052
|
+
|
|
2053
|
+
We're writing to inform you that your Vritti AI Cloud email address has been successfully changed.
|
|
2054
|
+
|
|
2055
|
+
Previous Email: ${oldEmail}
|
|
2056
|
+
New Email: ${newEmail}
|
|
2057
|
+
|
|
2058
|
+
DIDN'T MAKE THIS CHANGE?
|
|
2059
|
+
|
|
2060
|
+
If you did not authorize this change, you can revert it within the next ${hoursUntilExpiry} hours by visiting:
|
|
2061
|
+
${revertLink}
|
|
2062
|
+
|
|
2063
|
+
If you made this change, you can safely ignore this email.
|
|
2064
|
+
|
|
2065
|
+
---
|
|
2066
|
+
Vritti AI Cloud - Cloud Management Platform
|
|
2067
|
+
This is an automated message, please do not reply.
|
|
2068
|
+
`.trim();
|
|
2069
|
+
await this.sendEmail({
|
|
2070
|
+
to: [
|
|
2071
|
+
{
|
|
2072
|
+
email: oldEmail,
|
|
2073
|
+
name
|
|
2074
|
+
}
|
|
2075
|
+
],
|
|
2076
|
+
subject,
|
|
2077
|
+
htmlContent,
|
|
2078
|
+
textContent
|
|
2079
|
+
});
|
|
2080
|
+
this.logger.log(`Email change notification sent to ${oldEmail}`);
|
|
2081
|
+
}
|
|
2082
|
+
// Sends a confirmation to the restored email address after a revert
|
|
2083
|
+
async sendEmailRevertConfirmation(email, displayName) {
|
|
2084
|
+
const name = displayName || "there";
|
|
2085
|
+
const subject = "Email Address Change Reverted - Vritti AI Cloud";
|
|
2086
|
+
const htmlContent = `
|
|
2087
|
+
<!DOCTYPE html>
|
|
2088
|
+
<html>
|
|
2089
|
+
<head>
|
|
2090
|
+
<meta charset="UTF-8">
|
|
2091
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2092
|
+
</head>
|
|
2093
|
+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f5f5f5;">
|
|
2094
|
+
<table role="presentation" style="width: 100%; border-collapse: collapse;">
|
|
2095
|
+
<tr>
|
|
2096
|
+
<td style="padding: 40px 20px;">
|
|
2097
|
+
<table role="presentation" style="max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
|
2098
|
+
<!-- Header -->
|
|
2099
|
+
<tr>
|
|
2100
|
+
<td style="padding: 40px 40px 20px; text-align: center; border-bottom: 1px solid #e0e0e0;">
|
|
2101
|
+
<h1 style="margin: 0; color: #1a1a1a; font-size: 24px; font-weight: 600;">Email Change Reverted</h1>
|
|
2102
|
+
</td>
|
|
2103
|
+
</tr>
|
|
2104
|
+
|
|
2105
|
+
<!-- Content -->
|
|
2106
|
+
<tr>
|
|
2107
|
+
<td style="padding: 40px;">
|
|
2108
|
+
<p style="margin: 0 0 20px; color: #333333; font-size: 16px; line-height: 1.6;">
|
|
2109
|
+
Hello <strong>${name}</strong>,
|
|
2110
|
+
</p>
|
|
2111
|
+
<p style="margin: 0 0 30px; color: #333333; font-size: 16px; line-height: 1.6;">
|
|
2112
|
+
Your recent email address change has been successfully reverted. Your email is now:
|
|
2113
|
+
</p>
|
|
2114
|
+
|
|
2115
|
+
<div style="background-color: #d4edda; padding: 20px; border-radius: 8px; margin: 30px 0; text-align: center;">
|
|
2116
|
+
<p style="margin: 0; color: #155724; font-size: 18px; font-weight: 600; font-family: monospace;">
|
|
2117
|
+
${email}
|
|
2118
|
+
</p>
|
|
2119
|
+
</div>
|
|
2120
|
+
|
|
2121
|
+
<p style="margin: 30px 0 20px; color: #666666; font-size: 14px; line-height: 1.6;">
|
|
2122
|
+
If you did not request this revert, please contact our support team immediately.
|
|
2123
|
+
</p>
|
|
2124
|
+
</td>
|
|
2125
|
+
</tr>
|
|
2126
|
+
|
|
2127
|
+
<!-- Footer -->
|
|
2128
|
+
<tr>
|
|
2129
|
+
<td style="padding: 30px 40px; border-top: 1px solid #e0e0e0; text-align: center;">
|
|
2130
|
+
<p style="margin: 0; color: #999999; font-size: 12px; line-height: 1.5;">
|
|
2131
|
+
Vritti AI Cloud - Cloud Management Platform
|
|
2132
|
+
</p>
|
|
2133
|
+
<p style="margin: 8px 0 0; color: #999999; font-size: 12px; line-height: 1.5;">
|
|
2134
|
+
This is an automated message, please do not reply.
|
|
2135
|
+
</p>
|
|
2136
|
+
</td>
|
|
2137
|
+
</tr>
|
|
2138
|
+
</table>
|
|
2139
|
+
</td>
|
|
2140
|
+
</tr>
|
|
2141
|
+
</table>
|
|
2142
|
+
</body>
|
|
2143
|
+
</html>
|
|
2144
|
+
`;
|
|
2145
|
+
const textContent = `
|
|
2146
|
+
Hello ${name},
|
|
2147
|
+
|
|
2148
|
+
Your recent email address change has been successfully reverted. Your email is now:
|
|
2149
|
+
|
|
2150
|
+
${email}
|
|
2151
|
+
|
|
2152
|
+
If you did not request this revert, please contact our support team immediately.
|
|
2153
|
+
|
|
2154
|
+
---
|
|
2155
|
+
Vritti AI Cloud - Cloud Management Platform
|
|
2156
|
+
This is an automated message, please do not reply.
|
|
2157
|
+
`.trim();
|
|
2158
|
+
await this.sendEmail({
|
|
2159
|
+
to: [
|
|
2160
|
+
{
|
|
2161
|
+
email,
|
|
2162
|
+
name
|
|
2163
|
+
}
|
|
2164
|
+
],
|
|
2165
|
+
subject,
|
|
2166
|
+
htmlContent,
|
|
2167
|
+
textContent
|
|
2168
|
+
});
|
|
2169
|
+
this.logger.log(`Email revert confirmation sent to ${email}`);
|
|
2170
|
+
}
|
|
2171
|
+
// Verifies Brevo API connectivity — a 400 response means the API is reachable
|
|
2172
|
+
async verifyConnection() {
|
|
2173
|
+
try {
|
|
2174
|
+
await this.brevoClient.transactionalEmails.sendTransacEmail({
|
|
2175
|
+
sender: {
|
|
2176
|
+
email: this.senderEmail,
|
|
2177
|
+
name: this.senderName
|
|
2178
|
+
},
|
|
2179
|
+
to: [
|
|
2180
|
+
{
|
|
2181
|
+
email: this.senderEmail
|
|
2182
|
+
}
|
|
2183
|
+
],
|
|
2184
|
+
subject: "Connection Test",
|
|
2185
|
+
htmlContent: "<p>Test</p>"
|
|
2186
|
+
});
|
|
2187
|
+
return true;
|
|
2188
|
+
} catch (err) {
|
|
2189
|
+
if (err instanceof BrevoError && err.statusCode === 400) {
|
|
2190
|
+
return true;
|
|
2191
|
+
}
|
|
2192
|
+
this.logger.error("Brevo connection verification failed:", err);
|
|
2193
|
+
return false;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
// Sends a transactional email via Brevo — retries handled internally by BrevoClient
|
|
2197
|
+
async sendEmail(emailData) {
|
|
2198
|
+
try {
|
|
2199
|
+
const result = await this.brevoClient.transactionalEmails.sendTransacEmail({
|
|
2200
|
+
sender: {
|
|
2201
|
+
email: this.senderEmail,
|
|
2202
|
+
name: this.senderName
|
|
2203
|
+
},
|
|
2204
|
+
to: emailData.to,
|
|
2205
|
+
subject: emailData.subject,
|
|
2206
|
+
htmlContent: emailData.htmlContent,
|
|
2207
|
+
textContent: emailData.textContent
|
|
2208
|
+
});
|
|
2209
|
+
this.logger.debug(`Email sent successfully. Message ID: ${result.messageId}`);
|
|
2210
|
+
} catch (err) {
|
|
2211
|
+
if (err instanceof BrevoTimeoutError) {
|
|
2212
|
+
this.logger.error("Brevo request timed out after retries.");
|
|
2213
|
+
throw new Error("Email sending failed: timeout");
|
|
2214
|
+
}
|
|
2215
|
+
if (err instanceof BrevoError) {
|
|
2216
|
+
if (err.statusCode === 429) {
|
|
2217
|
+
this.logger.error("Brevo rate limit exceeded after retries.");
|
|
2218
|
+
throw new Error("Email sending failed: rate limit exceeded");
|
|
2219
|
+
}
|
|
2220
|
+
if (err.statusCode === 401) {
|
|
2221
|
+
this.logger.error("Brevo authentication failed. Check your API key.");
|
|
2222
|
+
throw new Error("Email service authentication failed");
|
|
2223
|
+
}
|
|
2224
|
+
if (err.statusCode === 400) {
|
|
2225
|
+
this.logger.error("Bad request to Brevo API:", err.message);
|
|
2226
|
+
throw new Error(`Invalid email parameters: ${err.message}`);
|
|
2227
|
+
}
|
|
2228
|
+
this.logger.error(`Brevo API error ${err.statusCode}:`, err.message);
|
|
2229
|
+
throw new Error(`Email sending failed: ${err.message}`);
|
|
2230
|
+
}
|
|
2231
|
+
throw err;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
};
|
|
2235
|
+
EmailService = _ts_decorate11([
|
|
2236
|
+
Injectable7(),
|
|
2237
|
+
_ts_metadata7("design:type", Function),
|
|
2238
|
+
_ts_metadata7("design:paramtypes", [
|
|
2239
|
+
typeof ConfigService4 === "undefined" ? Object : ConfigService4
|
|
2240
|
+
])
|
|
2241
|
+
], EmailService);
|
|
2242
|
+
|
|
2243
|
+
// src/email/email.module.ts
|
|
2244
|
+
function _ts_decorate12(decorators, target, key, desc) {
|
|
2245
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2246
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2247
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2248
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2249
|
+
}
|
|
2250
|
+
__name(_ts_decorate12, "_ts_decorate");
|
|
2251
|
+
var EmailModule = class {
|
|
2252
|
+
static {
|
|
2253
|
+
__name(this, "EmailModule");
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
EmailModule = _ts_decorate12([
|
|
2257
|
+
Global4(),
|
|
2258
|
+
Module4({
|
|
2259
|
+
imports: [
|
|
2260
|
+
ConfigModule2
|
|
2261
|
+
],
|
|
2262
|
+
providers: [
|
|
2263
|
+
EmailService
|
|
2264
|
+
],
|
|
2265
|
+
exports: [
|
|
2266
|
+
EmailService
|
|
2267
|
+
]
|
|
2268
|
+
})
|
|
2269
|
+
], EmailModule);
|
|
2270
|
+
|
|
2271
|
+
// src/exceptions/bad-gateway.exception.ts
|
|
2272
|
+
import { HttpStatus } from "@nestjs/common";
|
|
2273
|
+
|
|
2274
|
+
// src/exceptions/base-field.exception.ts
|
|
2275
|
+
import { HttpException } from "@nestjs/common";
|
|
2276
|
+
var HttpProblemException = class extends HttpException {
|
|
2277
|
+
static {
|
|
2278
|
+
__name(this, "HttpProblemException");
|
|
2279
|
+
}
|
|
2280
|
+
constructor(detailOrOptions, httpStatus) {
|
|
2281
|
+
const options = typeof detailOrOptions === "string" ? {
|
|
2282
|
+
detail: detailOrOptions
|
|
2283
|
+
} : detailOrOptions;
|
|
2284
|
+
super({
|
|
2285
|
+
type: options.type ?? "about:blank",
|
|
2286
|
+
label: options.label,
|
|
2287
|
+
detail: options.detail,
|
|
2288
|
+
errors: options.errors ?? []
|
|
2289
|
+
}, httpStatus);
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
|
|
2293
|
+
// src/exceptions/bad-gateway.exception.ts
|
|
2294
|
+
var BadGatewayException = class extends HttpProblemException {
|
|
2295
|
+
static {
|
|
2296
|
+
__name(this, "BadGatewayException");
|
|
2297
|
+
}
|
|
2298
|
+
constructor(detailOrOptions) {
|
|
2299
|
+
super(detailOrOptions ?? "Bad Gateway", HttpStatus.BAD_GATEWAY);
|
|
2300
|
+
}
|
|
2301
|
+
};
|
|
2302
|
+
|
|
2303
|
+
// src/exceptions/bad-request.exception.ts
|
|
2304
|
+
import { HttpStatus as HttpStatus2 } from "@nestjs/common";
|
|
2305
|
+
var BadRequestException = class extends HttpProblemException {
|
|
2306
|
+
static {
|
|
2307
|
+
__name(this, "BadRequestException");
|
|
2308
|
+
}
|
|
2309
|
+
constructor(detailOrOptions) {
|
|
2310
|
+
super(detailOrOptions ?? "Bad Request", HttpStatus2.BAD_REQUEST);
|
|
2311
|
+
}
|
|
2312
|
+
};
|
|
2313
|
+
|
|
2314
|
+
// src/exceptions/conflict.exception.ts
|
|
2315
|
+
import { HttpStatus as HttpStatus3 } from "@nestjs/common";
|
|
2316
|
+
var ConflictException = class extends HttpProblemException {
|
|
2317
|
+
static {
|
|
2318
|
+
__name(this, "ConflictException");
|
|
2319
|
+
}
|
|
2320
|
+
constructor(detailOrOptions) {
|
|
2321
|
+
super(detailOrOptions ?? "Conflict", HttpStatus3.CONFLICT);
|
|
2322
|
+
}
|
|
2323
|
+
};
|
|
2324
|
+
|
|
2325
|
+
// src/exceptions/forbidden.exception.ts
|
|
2326
|
+
import { HttpStatus as HttpStatus4 } from "@nestjs/common";
|
|
2327
|
+
var ForbiddenException2 = class extends HttpProblemException {
|
|
2328
|
+
static {
|
|
2329
|
+
__name(this, "ForbiddenException");
|
|
2330
|
+
}
|
|
2331
|
+
constructor(detailOrOptions) {
|
|
2332
|
+
super(detailOrOptions ?? "Forbidden", HttpStatus4.FORBIDDEN);
|
|
2333
|
+
}
|
|
2334
|
+
};
|
|
2335
|
+
|
|
2336
|
+
// src/exceptions/gone.exception.ts
|
|
2337
|
+
import { HttpStatus as HttpStatus5 } from "@nestjs/common";
|
|
2338
|
+
var GoneException = class extends HttpProblemException {
|
|
2339
|
+
static {
|
|
2340
|
+
__name(this, "GoneException");
|
|
2341
|
+
}
|
|
2342
|
+
constructor(detailOrOptions) {
|
|
2343
|
+
super(detailOrOptions ?? "Gone", HttpStatus5.GONE);
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
|
|
2347
|
+
// src/exceptions/internal-server-error.exception.ts
|
|
2348
|
+
import { HttpStatus as HttpStatus6 } from "@nestjs/common";
|
|
2349
|
+
var InternalServerErrorException3 = class extends HttpProblemException {
|
|
2350
|
+
static {
|
|
2351
|
+
__name(this, "InternalServerErrorException");
|
|
2352
|
+
}
|
|
2353
|
+
constructor(detailOrOptions) {
|
|
2354
|
+
super(detailOrOptions ?? "Internal Server Error", HttpStatus6.INTERNAL_SERVER_ERROR);
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
|
|
2358
|
+
// src/exceptions/method-not-allowed.exception.ts
|
|
2359
|
+
import { HttpStatus as HttpStatus7 } from "@nestjs/common";
|
|
2360
|
+
var MethodNotAllowedException = class extends HttpProblemException {
|
|
2185
2361
|
static {
|
|
2186
2362
|
__name(this, "MethodNotAllowedException");
|
|
2187
2363
|
}
|
|
@@ -2269,7 +2445,7 @@ var TooManyRequestsException = class extends HttpProblemException {
|
|
|
2269
2445
|
|
|
2270
2446
|
// src/exceptions/unauthorized.exception.ts
|
|
2271
2447
|
import { HttpStatus as HttpStatus15 } from "@nestjs/common";
|
|
2272
|
-
var
|
|
2448
|
+
var UnauthorizedException3 = class extends HttpProblemException {
|
|
2273
2449
|
static {
|
|
2274
2450
|
__name(this, "UnauthorizedException");
|
|
2275
2451
|
}
|
|
@@ -2312,14 +2488,14 @@ var ValidationException = class extends HttpProblemException {
|
|
|
2312
2488
|
};
|
|
2313
2489
|
|
|
2314
2490
|
// src/filters/http-exception.filter.ts
|
|
2315
|
-
import { Catch, HttpException as HttpException2, HttpStatus as HttpStatus19, Logger as
|
|
2316
|
-
function
|
|
2491
|
+
import { Catch, HttpException as HttpException2, HttpStatus as HttpStatus19, Logger as Logger9 } from "@nestjs/common";
|
|
2492
|
+
function _ts_decorate13(decorators, target, key, desc) {
|
|
2317
2493
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2318
2494
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2319
2495
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2320
2496
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2321
2497
|
}
|
|
2322
|
-
__name(
|
|
2498
|
+
__name(_ts_decorate13, "_ts_decorate");
|
|
2323
2499
|
function getHttpStatusTitle(status) {
|
|
2324
2500
|
const enumKey = Object.entries(HttpStatus19).find(([key, value]) => value === status && Number.isNaN(Number(key)))?.[0];
|
|
2325
2501
|
if (!enumKey) {
|
|
@@ -2332,7 +2508,7 @@ var HttpExceptionFilter = class _HttpExceptionFilter {
|
|
|
2332
2508
|
static {
|
|
2333
2509
|
__name(this, "HttpExceptionFilter");
|
|
2334
2510
|
}
|
|
2335
|
-
logger = new
|
|
2511
|
+
logger = new Logger9(_HttpExceptionFilter.name);
|
|
2336
2512
|
catch(exception, host) {
|
|
2337
2513
|
const ctx = host.switchToHttp();
|
|
2338
2514
|
const response = ctx.getResponse();
|
|
@@ -2392,226 +2568,16 @@ var HttpExceptionFilter = class _HttpExceptionFilter {
|
|
|
2392
2568
|
response.header("Content-Type", "application/problem+json").status(status).send(problemDetails);
|
|
2393
2569
|
}
|
|
2394
2570
|
};
|
|
2395
|
-
HttpExceptionFilter =
|
|
2571
|
+
HttpExceptionFilter = _ts_decorate13([
|
|
2396
2572
|
Catch()
|
|
2397
2573
|
], HttpExceptionFilter);
|
|
2398
2574
|
|
|
2399
|
-
// src/utils/phone.utils.ts
|
|
2400
|
-
var CALLING_CODE_TO_COUNTRY = {
|
|
2401
|
-
// 3-digit codes
|
|
2402
|
-
"355": "AL",
|
|
2403
|
-
"213": "DZ",
|
|
2404
|
-
"376": "AD",
|
|
2405
|
-
"244": "AO",
|
|
2406
|
-
"672": "AQ",
|
|
2407
|
-
"374": "AM",
|
|
2408
|
-
"297": "AW",
|
|
2409
|
-
"994": "AZ",
|
|
2410
|
-
"973": "BH",
|
|
2411
|
-
"880": "BD",
|
|
2412
|
-
"375": "BY",
|
|
2413
|
-
"501": "BZ",
|
|
2414
|
-
"229": "BJ",
|
|
2415
|
-
"975": "BT",
|
|
2416
|
-
"591": "BO",
|
|
2417
|
-
"387": "BA",
|
|
2418
|
-
"267": "BW",
|
|
2419
|
-
"673": "BN",
|
|
2420
|
-
"359": "BG",
|
|
2421
|
-
"226": "BF",
|
|
2422
|
-
"257": "BI",
|
|
2423
|
-
"855": "KH",
|
|
2424
|
-
"237": "CM",
|
|
2425
|
-
"238": "CV",
|
|
2426
|
-
"236": "CF",
|
|
2427
|
-
"235": "TD",
|
|
2428
|
-
"269": "KM",
|
|
2429
|
-
"242": "CG",
|
|
2430
|
-
"243": "CD",
|
|
2431
|
-
"506": "CR",
|
|
2432
|
-
"385": "HR",
|
|
2433
|
-
"357": "CY",
|
|
2434
|
-
"420": "CZ",
|
|
2435
|
-
"253": "DJ",
|
|
2436
|
-
"593": "EC",
|
|
2437
|
-
"503": "SV",
|
|
2438
|
-
"240": "GQ",
|
|
2439
|
-
"291": "ER",
|
|
2440
|
-
"372": "EE",
|
|
2441
|
-
"251": "ET",
|
|
2442
|
-
"679": "FJ",
|
|
2443
|
-
"358": "FI",
|
|
2444
|
-
"241": "GA",
|
|
2445
|
-
"220": "GM",
|
|
2446
|
-
"995": "GE",
|
|
2447
|
-
"233": "GH",
|
|
2448
|
-
"350": "GI",
|
|
2449
|
-
"299": "GL",
|
|
2450
|
-
"502": "GT",
|
|
2451
|
-
"224": "GN",
|
|
2452
|
-
"245": "GW",
|
|
2453
|
-
"592": "GY",
|
|
2454
|
-
"509": "HT",
|
|
2455
|
-
"504": "HN",
|
|
2456
|
-
"354": "IS",
|
|
2457
|
-
"964": "IQ",
|
|
2458
|
-
"353": "IE",
|
|
2459
|
-
"972": "IL",
|
|
2460
|
-
"225": "CI",
|
|
2461
|
-
"962": "JO",
|
|
2462
|
-
"254": "KE",
|
|
2463
|
-
"686": "KI",
|
|
2464
|
-
"965": "KW",
|
|
2465
|
-
"996": "KG",
|
|
2466
|
-
"856": "LA",
|
|
2467
|
-
"371": "LV",
|
|
2468
|
-
"961": "LB",
|
|
2469
|
-
"266": "LS",
|
|
2470
|
-
"231": "LR",
|
|
2471
|
-
"218": "LY",
|
|
2472
|
-
"423": "LI",
|
|
2473
|
-
"370": "LT",
|
|
2474
|
-
"352": "LU",
|
|
2475
|
-
"389": "MK",
|
|
2476
|
-
"261": "MG",
|
|
2477
|
-
"265": "MW",
|
|
2478
|
-
"960": "MV",
|
|
2479
|
-
"223": "ML",
|
|
2480
|
-
"356": "MT",
|
|
2481
|
-
"692": "MH",
|
|
2482
|
-
"222": "MR",
|
|
2483
|
-
"230": "MU",
|
|
2484
|
-
"262": "YT",
|
|
2485
|
-
"691": "FM",
|
|
2486
|
-
"373": "MD",
|
|
2487
|
-
"377": "MC",
|
|
2488
|
-
"976": "MN",
|
|
2489
|
-
"382": "ME",
|
|
2490
|
-
"258": "MZ",
|
|
2491
|
-
"264": "NA",
|
|
2492
|
-
"674": "NR",
|
|
2493
|
-
"977": "NP",
|
|
2494
|
-
"505": "NI",
|
|
2495
|
-
"227": "NE",
|
|
2496
|
-
"234": "NG",
|
|
2497
|
-
"683": "NU",
|
|
2498
|
-
"968": "OM",
|
|
2499
|
-
"680": "PW",
|
|
2500
|
-
"970": "PS",
|
|
2501
|
-
"507": "PA",
|
|
2502
|
-
"675": "PG",
|
|
2503
|
-
"595": "PY",
|
|
2504
|
-
"351": "PT",
|
|
2505
|
-
"974": "QA",
|
|
2506
|
-
"250": "RW",
|
|
2507
|
-
"685": "WS",
|
|
2508
|
-
"378": "SM",
|
|
2509
|
-
"239": "ST",
|
|
2510
|
-
"966": "SA",
|
|
2511
|
-
"221": "SN",
|
|
2512
|
-
"381": "RS",
|
|
2513
|
-
"248": "SC",
|
|
2514
|
-
"232": "SL",
|
|
2515
|
-
"421": "SK",
|
|
2516
|
-
"386": "SI",
|
|
2517
|
-
"677": "SB",
|
|
2518
|
-
"252": "SO",
|
|
2519
|
-
"211": "SS",
|
|
2520
|
-
"249": "SD",
|
|
2521
|
-
"597": "SR",
|
|
2522
|
-
"268": "SZ",
|
|
2523
|
-
"963": "SY",
|
|
2524
|
-
"992": "TJ",
|
|
2525
|
-
"255": "TZ",
|
|
2526
|
-
"228": "TG",
|
|
2527
|
-
"676": "TO",
|
|
2528
|
-
"216": "TN",
|
|
2529
|
-
"993": "TM",
|
|
2530
|
-
"688": "TV",
|
|
2531
|
-
"256": "UG",
|
|
2532
|
-
"380": "UA",
|
|
2533
|
-
"971": "AE",
|
|
2534
|
-
"598": "UY",
|
|
2535
|
-
"998": "UZ",
|
|
2536
|
-
"678": "VU",
|
|
2537
|
-
"379": "VA",
|
|
2538
|
-
"967": "YE",
|
|
2539
|
-
"260": "ZM",
|
|
2540
|
-
"263": "ZW",
|
|
2541
|
-
// 2-digit codes
|
|
2542
|
-
"93": "AF",
|
|
2543
|
-
"54": "AR",
|
|
2544
|
-
"61": "AU",
|
|
2545
|
-
"43": "AT",
|
|
2546
|
-
"32": "BE",
|
|
2547
|
-
"55": "BR",
|
|
2548
|
-
"56": "CL",
|
|
2549
|
-
"86": "CN",
|
|
2550
|
-
"57": "CO",
|
|
2551
|
-
"53": "CU",
|
|
2552
|
-
"45": "DK",
|
|
2553
|
-
"20": "EG",
|
|
2554
|
-
"33": "FR",
|
|
2555
|
-
"49": "DE",
|
|
2556
|
-
"30": "GR",
|
|
2557
|
-
"36": "HU",
|
|
2558
|
-
"91": "IN",
|
|
2559
|
-
"62": "ID",
|
|
2560
|
-
"98": "IR",
|
|
2561
|
-
"39": "IT",
|
|
2562
|
-
"81": "JP",
|
|
2563
|
-
"82": "KR",
|
|
2564
|
-
"60": "MY",
|
|
2565
|
-
"52": "MX",
|
|
2566
|
-
"31": "NL",
|
|
2567
|
-
"64": "NZ",
|
|
2568
|
-
"47": "NO",
|
|
2569
|
-
"92": "PK",
|
|
2570
|
-
"51": "PE",
|
|
2571
|
-
"63": "PH",
|
|
2572
|
-
"48": "PL",
|
|
2573
|
-
"40": "RO",
|
|
2574
|
-
"65": "SG",
|
|
2575
|
-
"27": "ZA",
|
|
2576
|
-
"34": "ES",
|
|
2577
|
-
"94": "LK",
|
|
2578
|
-
"46": "SE",
|
|
2579
|
-
"41": "CH",
|
|
2580
|
-
"66": "TH",
|
|
2581
|
-
"90": "TR",
|
|
2582
|
-
"44": "GB",
|
|
2583
|
-
"58": "VE",
|
|
2584
|
-
"84": "VN",
|
|
2585
|
-
// 1-digit codes (shared codes default to most common country)
|
|
2586
|
-
"1": "US",
|
|
2587
|
-
"7": "RU"
|
|
2588
|
-
};
|
|
2589
|
-
function extractCountryFromPhone(phone) {
|
|
2590
|
-
const digits = phone.startsWith("+") ? phone.slice(1) : phone;
|
|
2591
|
-
for (const length of [
|
|
2592
|
-
3,
|
|
2593
|
-
2,
|
|
2594
|
-
1
|
|
2595
|
-
]) {
|
|
2596
|
-
const prefix = digits.slice(0, length);
|
|
2597
|
-
if (CALLING_CODE_TO_COUNTRY[prefix]) {
|
|
2598
|
-
return CALLING_CODE_TO_COUNTRY[prefix];
|
|
2599
|
-
}
|
|
2600
|
-
}
|
|
2601
|
-
return void 0;
|
|
2602
|
-
}
|
|
2603
|
-
__name(extractCountryFromPhone, "extractCountryFromPhone");
|
|
2604
|
-
function normalizePhoneNumber(phone) {
|
|
2605
|
-
return phone.startsWith("+") ? phone : `+${phone}`;
|
|
2606
|
-
}
|
|
2607
|
-
__name(normalizePhoneNumber, "normalizePhoneNumber");
|
|
2608
|
-
|
|
2609
2575
|
// src/logger/interceptors/http-logger.interceptor.ts
|
|
2610
|
-
import { Injectable as
|
|
2611
|
-
import { catchError, tap
|
|
2576
|
+
import { Injectable as Injectable9, Optional as Optional2 } from "@nestjs/common";
|
|
2577
|
+
import { catchError, tap } from "rxjs/operators";
|
|
2612
2578
|
|
|
2613
2579
|
// src/logger/services/logger.service.ts
|
|
2614
|
-
import { Injectable as
|
|
2580
|
+
import { Injectable as Injectable8, Optional } from "@nestjs/common";
|
|
2615
2581
|
import { createLogger, format, transports } from "winston";
|
|
2616
2582
|
import DailyRotateFile from "winston-daily-rotate-file";
|
|
2617
2583
|
|
|
@@ -2649,13 +2615,13 @@ function addCorrelationIdToResponse(reply, correlationId, headerName = DEFAULT_C
|
|
|
2649
2615
|
__name(addCorrelationIdToResponse, "addCorrelationIdToResponse");
|
|
2650
2616
|
|
|
2651
2617
|
// src/logger/services/logger.service.ts
|
|
2652
|
-
function
|
|
2618
|
+
function _ts_decorate14(decorators, target, key, desc) {
|
|
2653
2619
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2654
2620
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2655
2621
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2656
2622
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2657
2623
|
}
|
|
2658
|
-
__name(
|
|
2624
|
+
__name(_ts_decorate14, "_ts_decorate");
|
|
2659
2625
|
function _ts_metadata8(k, v) {
|
|
2660
2626
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
2661
2627
|
}
|
|
@@ -2687,10 +2653,7 @@ var LoggerService = class _LoggerService {
|
|
|
2687
2653
|
this.activeLogger = this.createWinstonLogger(options);
|
|
2688
2654
|
}
|
|
2689
2655
|
}
|
|
2690
|
-
|
|
2691
|
-
* Creates a Winston logger instance with inline configuration.
|
|
2692
|
-
* Consolidates winston-config.factory.ts logic.
|
|
2693
|
-
*/
|
|
2656
|
+
// Creates a Winston logger instance with inline transports and format configuration
|
|
2694
2657
|
createWinstonLogger(opts) {
|
|
2695
2658
|
const level = opts.level ?? "debug";
|
|
2696
2659
|
const logFormat = opts.format ?? "text";
|
|
@@ -2781,9 +2744,7 @@ ${trace}`;
|
|
|
2781
2744
|
setContext(context) {
|
|
2782
2745
|
this.context = context;
|
|
2783
2746
|
}
|
|
2784
|
-
|
|
2785
|
-
* Unified internal logging method that handles both Winston and NestJS Logger.
|
|
2786
|
-
*/
|
|
2747
|
+
// Dispatches a log entry to either the Winston or NestJS logger implementation
|
|
2787
2748
|
_log(level, message, context, trace) {
|
|
2788
2749
|
const ctx = context ?? this.context;
|
|
2789
2750
|
if ("format" in this.activeLogger && "transports" in this.activeLogger) {
|
|
@@ -2811,9 +2772,7 @@ ${trace}`;
|
|
|
2811
2772
|
}
|
|
2812
2773
|
}
|
|
2813
2774
|
}
|
|
2814
|
-
|
|
2815
|
-
* Logs with custom metadata (Winston only).
|
|
2816
|
-
*/
|
|
2775
|
+
// Logs a message with custom metadata fields (Winston only)
|
|
2817
2776
|
logWithMetadata(level, message, metadata, context) {
|
|
2818
2777
|
const ctx = context ?? this.context;
|
|
2819
2778
|
if ("format" in this.activeLogger && "transports" in this.activeLogger) {
|
|
@@ -2840,10 +2799,7 @@ ${trace}`;
|
|
|
2840
2799
|
}
|
|
2841
2800
|
return String(message);
|
|
2842
2801
|
}
|
|
2843
|
-
|
|
2844
|
-
* Enriches metadata with correlation context from AsyncLocalStorage.
|
|
2845
|
-
* Inline from winston-logger.service.ts
|
|
2846
|
-
*/
|
|
2802
|
+
// Enriches metadata with correlation context from AsyncLocalStorage
|
|
2847
2803
|
enrichMetadata(metadata = {}, context, trace) {
|
|
2848
2804
|
const enriched = {
|
|
2849
2805
|
...metadata
|
|
@@ -2867,8 +2823,8 @@ ${trace}`;
|
|
|
2867
2823
|
return childLogger;
|
|
2868
2824
|
}
|
|
2869
2825
|
};
|
|
2870
|
-
LoggerService =
|
|
2871
|
-
|
|
2826
|
+
LoggerService = _ts_decorate14([
|
|
2827
|
+
Injectable8(),
|
|
2872
2828
|
_ts_param4(0, Optional()),
|
|
2873
2829
|
_ts_param4(1, Optional()),
|
|
2874
2830
|
_ts_metadata8("design:type", Function),
|
|
@@ -2879,13 +2835,13 @@ LoggerService = _ts_decorate13([
|
|
|
2879
2835
|
], LoggerService);
|
|
2880
2836
|
|
|
2881
2837
|
// src/logger/interceptors/http-logger.interceptor.ts
|
|
2882
|
-
function
|
|
2838
|
+
function _ts_decorate15(decorators, target, key, desc) {
|
|
2883
2839
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2884
2840
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2885
2841
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2886
2842
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2887
2843
|
}
|
|
2888
|
-
__name(
|
|
2844
|
+
__name(_ts_decorate15, "_ts_decorate");
|
|
2889
2845
|
function _ts_metadata9(k, v) {
|
|
2890
2846
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
2891
2847
|
}
|
|
@@ -2921,7 +2877,7 @@ var HttpLoggerInterceptor = class {
|
|
|
2921
2877
|
if (this.enableRequestLog) {
|
|
2922
2878
|
this.logRequest(request);
|
|
2923
2879
|
}
|
|
2924
|
-
return next.handle().pipe(
|
|
2880
|
+
return next.handle().pipe(tap(() => {
|
|
2925
2881
|
if (this.enableResponseLog) {
|
|
2926
2882
|
const duration = Date.now() - startTime;
|
|
2927
2883
|
this.logResponse(request, response, duration);
|
|
@@ -2974,6 +2930,7 @@ var HttpLoggerInterceptor = class {
|
|
|
2974
2930
|
try {
|
|
2975
2931
|
const correlationContext = getCorrelationContext();
|
|
2976
2932
|
const statusCode = response.statusCode || 500;
|
|
2933
|
+
const err = error;
|
|
2977
2934
|
const metadata = {
|
|
2978
2935
|
type: "http_error",
|
|
2979
2936
|
method: request.method,
|
|
@@ -2981,24 +2938,24 @@ var HttpLoggerInterceptor = class {
|
|
|
2981
2938
|
statusCode,
|
|
2982
2939
|
duration,
|
|
2983
2940
|
correlationId: correlationContext?.correlationId,
|
|
2984
|
-
errorName:
|
|
2985
|
-
errorMessage:
|
|
2941
|
+
errorName: err.name || "Error",
|
|
2942
|
+
errorMessage: err.message || "Unknown error"
|
|
2986
2943
|
};
|
|
2987
|
-
if (
|
|
2988
|
-
metadata.trace =
|
|
2944
|
+
if (err.stack) {
|
|
2945
|
+
metadata.trace = err.stack;
|
|
2989
2946
|
}
|
|
2990
|
-
if (
|
|
2991
|
-
metadata.errorDetails =
|
|
2947
|
+
if (err.response) {
|
|
2948
|
+
metadata.errorDetails = err.response;
|
|
2992
2949
|
}
|
|
2993
|
-
const message = `ERROR ${request.method} ${request.url} ${statusCode} - ${
|
|
2950
|
+
const message = `ERROR ${request.method} ${request.url} ${statusCode} - ${err.message || "Unknown error"}`;
|
|
2994
2951
|
this.logger.logWithMetadata("error", message, metadata);
|
|
2995
2952
|
} catch (loggingError) {
|
|
2996
2953
|
this.logger.error("Failed to log HTTP error", loggingError.stack);
|
|
2997
2954
|
}
|
|
2998
2955
|
}
|
|
2999
2956
|
};
|
|
3000
|
-
HttpLoggerInterceptor =
|
|
3001
|
-
|
|
2957
|
+
HttpLoggerInterceptor = _ts_decorate15([
|
|
2958
|
+
Injectable9(),
|
|
3002
2959
|
_ts_param5(1, Optional2()),
|
|
3003
2960
|
_ts_metadata9("design:type", Function),
|
|
3004
2961
|
_ts_metadata9("design:paramtypes", [
|
|
@@ -3008,17 +2965,17 @@ HttpLoggerInterceptor = _ts_decorate14([
|
|
|
3008
2965
|
], HttpLoggerInterceptor);
|
|
3009
2966
|
|
|
3010
2967
|
// src/logger/logger.module.ts
|
|
3011
|
-
import { Global as
|
|
2968
|
+
import { Global as Global5, Logger as Logger10, Module as Module5 } from "@nestjs/common";
|
|
3012
2969
|
|
|
3013
2970
|
// src/logger/middleware/correlation-id.middleware.ts
|
|
3014
|
-
import { Injectable as
|
|
3015
|
-
function
|
|
2971
|
+
import { Injectable as Injectable10 } from "@nestjs/common";
|
|
2972
|
+
function _ts_decorate16(decorators, target, key, desc) {
|
|
3016
2973
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3017
2974
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
3018
2975
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
3019
2976
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
3020
2977
|
}
|
|
3021
|
-
__name(
|
|
2978
|
+
__name(_ts_decorate16, "_ts_decorate");
|
|
3022
2979
|
function _ts_metadata10(k, v) {
|
|
3023
2980
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
3024
2981
|
}
|
|
@@ -3033,9 +2990,7 @@ var CorrelationIdMiddleware = class {
|
|
|
3033
2990
|
this.includeInResponse = options.includeInResponse ?? true;
|
|
3034
2991
|
this.responseHeader = options.responseHeader ?? DEFAULT_CORRELATION_HEADER;
|
|
3035
2992
|
}
|
|
3036
|
-
|
|
3037
|
-
* Middleware handler for processing requests.
|
|
3038
|
-
*/
|
|
2993
|
+
// Generates and stores a correlation ID for the incoming request
|
|
3039
2994
|
use(_req, reply, next) {
|
|
3040
2995
|
const correlationId = generateCorrelationId();
|
|
3041
2996
|
if (this.includeInResponse) {
|
|
@@ -3047,11 +3002,7 @@ var CorrelationIdMiddleware = class {
|
|
|
3047
3002
|
next();
|
|
3048
3003
|
});
|
|
3049
3004
|
}
|
|
3050
|
-
|
|
3051
|
-
* Fastify hook handler for onRequest.
|
|
3052
|
-
* This is an async function that returns a Promise, ensuring the AsyncLocalStorage
|
|
3053
|
-
* context persists throughout the entire request lifecycle.
|
|
3054
|
-
*/
|
|
3005
|
+
// Fastify onRequest hook that initializes correlation context in AsyncLocalStorage
|
|
3055
3006
|
async onRequest(_req, reply) {
|
|
3056
3007
|
const correlationId = generateCorrelationId();
|
|
3057
3008
|
if (this.includeInResponse) {
|
|
@@ -3065,8 +3016,8 @@ var CorrelationIdMiddleware = class {
|
|
|
3065
3016
|
}
|
|
3066
3017
|
}
|
|
3067
3018
|
};
|
|
3068
|
-
CorrelationIdMiddleware =
|
|
3069
|
-
|
|
3019
|
+
CorrelationIdMiddleware = _ts_decorate16([
|
|
3020
|
+
Injectable10(),
|
|
3070
3021
|
_ts_metadata10("design:type", Function),
|
|
3071
3022
|
_ts_metadata10("design:paramtypes", [
|
|
3072
3023
|
typeof CorrelationIdMiddlewareOptions === "undefined" ? Object : CorrelationIdMiddlewareOptions
|
|
@@ -3074,13 +3025,13 @@ CorrelationIdMiddleware = _ts_decorate15([
|
|
|
3074
3025
|
], CorrelationIdMiddleware);
|
|
3075
3026
|
|
|
3076
3027
|
// src/logger/logger.module.ts
|
|
3077
|
-
function
|
|
3028
|
+
function _ts_decorate17(decorators, target, key, desc) {
|
|
3078
3029
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3079
3030
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
3080
3031
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
3081
3032
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
3082
3033
|
}
|
|
3083
|
-
__name(
|
|
3034
|
+
__name(_ts_decorate17, "_ts_decorate");
|
|
3084
3035
|
var LOGGER_MODULE_OPTIONS = Symbol("LOGGER_MODULE_OPTIONS");
|
|
3085
3036
|
var DEFAULT_LOGGER_OPTIONS = {
|
|
3086
3037
|
provider: "winston",
|
|
@@ -3090,9 +3041,6 @@ var DEFAULT_LOGGER_OPTIONS = {
|
|
|
3090
3041
|
maxFiles: "14d"
|
|
3091
3042
|
};
|
|
3092
3043
|
var ENVIRONMENT_PRESETS = {
|
|
3093
|
-
/**
|
|
3094
|
-
* Development preset - maximum verbosity for local development
|
|
3095
|
-
*/
|
|
3096
3044
|
development: {
|
|
3097
3045
|
provider: "winston",
|
|
3098
3046
|
level: "debug",
|
|
@@ -3106,9 +3054,6 @@ var ENVIRONMENT_PRESETS = {
|
|
|
3106
3054
|
slowRequestThreshold: 1e3
|
|
3107
3055
|
}
|
|
3108
3056
|
},
|
|
3109
|
-
/**
|
|
3110
|
-
* Staging preset - moderate verbosity with file logging
|
|
3111
|
-
*/
|
|
3112
3057
|
staging: {
|
|
3113
3058
|
provider: "winston",
|
|
3114
3059
|
level: "log",
|
|
@@ -3122,9 +3067,6 @@ var ENVIRONMENT_PRESETS = {
|
|
|
3122
3067
|
slowRequestThreshold: 3e3
|
|
3123
3068
|
}
|
|
3124
3069
|
},
|
|
3125
|
-
/**
|
|
3126
|
-
* Production preset - minimal verbosity with all safety features enabled
|
|
3127
|
-
*/
|
|
3128
3070
|
production: {
|
|
3129
3071
|
provider: "winston",
|
|
3130
3072
|
level: "warn",
|
|
@@ -3138,9 +3080,6 @@ var ENVIRONMENT_PRESETS = {
|
|
|
3138
3080
|
slowRequestThreshold: 5e3
|
|
3139
3081
|
}
|
|
3140
3082
|
},
|
|
3141
|
-
/**
|
|
3142
|
-
* Test preset - errors only, minimal features for faster test execution
|
|
3143
|
-
*/
|
|
3144
3083
|
test: {
|
|
3145
3084
|
provider: "winston",
|
|
3146
3085
|
level: "error",
|
|
@@ -3169,12 +3108,12 @@ function mergeWithDefaults(options = {}) {
|
|
|
3169
3108
|
__name(mergeWithDefaults, "mergeWithDefaults");
|
|
3170
3109
|
function createDefaultLoggerProvider(options) {
|
|
3171
3110
|
return {
|
|
3172
|
-
provide:
|
|
3111
|
+
provide: Logger10,
|
|
3173
3112
|
useFactory: /* @__PURE__ */ __name(() => {
|
|
3174
|
-
const logger = new
|
|
3175
|
-
if (options.level
|
|
3113
|
+
const logger = new Logger10();
|
|
3114
|
+
if (options.level) {
|
|
3176
3115
|
const levels = getLevelsUpTo(options.level);
|
|
3177
|
-
logger.setLogLevels(levels);
|
|
3116
|
+
logger.setLogLevels?.(levels);
|
|
3178
3117
|
}
|
|
3179
3118
|
return logger;
|
|
3180
3119
|
}, "useFactory")
|
|
@@ -3201,7 +3140,7 @@ function createLoggerProviders(options = {}) {
|
|
|
3201
3140
|
inject: [
|
|
3202
3141
|
LOGGER_MODULE_OPTIONS,
|
|
3203
3142
|
{
|
|
3204
|
-
token:
|
|
3143
|
+
token: Logger10,
|
|
3205
3144
|
optional: true
|
|
3206
3145
|
}
|
|
3207
3146
|
]
|
|
@@ -3256,37 +3195,7 @@ var LoggerModule = class _LoggerModule {
|
|
|
3256
3195
|
static {
|
|
3257
3196
|
__name(this, "LoggerModule");
|
|
3258
3197
|
}
|
|
3259
|
-
|
|
3260
|
-
* Configures the logger module with static options.
|
|
3261
|
-
*
|
|
3262
|
-
* Users must explicitly pass `environment` to select a preset.
|
|
3263
|
-
* All preset values can be overridden by passing explicit options.
|
|
3264
|
-
*
|
|
3265
|
-
* @param options - Logger configuration options
|
|
3266
|
-
* @returns Dynamic module configuration
|
|
3267
|
-
*
|
|
3268
|
-
* @example
|
|
3269
|
-
* ```typescript
|
|
3270
|
-
* // Production preset with app name
|
|
3271
|
-
* LoggerModule.forRoot({
|
|
3272
|
-
* environment: 'production',
|
|
3273
|
-
* appName: 'my-service'
|
|
3274
|
-
* })
|
|
3275
|
-
*
|
|
3276
|
-
* // Development preset with custom level
|
|
3277
|
-
* LoggerModule.forRoot({
|
|
3278
|
-
* environment: 'development',
|
|
3279
|
-
* level: 'verbose',
|
|
3280
|
-
* enableFileLogger: true
|
|
3281
|
-
* })
|
|
3282
|
-
*
|
|
3283
|
-
* // Use default NestJS logger
|
|
3284
|
-
* LoggerModule.forRoot({
|
|
3285
|
-
* provider: 'default',
|
|
3286
|
-
* environment: 'development'
|
|
3287
|
-
* })
|
|
3288
|
-
* ```
|
|
3289
|
-
*/
|
|
3198
|
+
// Configures the logger module with static options and environment preset
|
|
3290
3199
|
static forRoot(options = {}) {
|
|
3291
3200
|
const providers = createLoggerProviders(options);
|
|
3292
3201
|
return {
|
|
@@ -3300,49 +3209,7 @@ var LoggerModule = class _LoggerModule {
|
|
|
3300
3209
|
]
|
|
3301
3210
|
};
|
|
3302
3211
|
}
|
|
3303
|
-
|
|
3304
|
-
* Configures the logger module with async options.
|
|
3305
|
-
*
|
|
3306
|
-
* Supports dynamic configuration using:
|
|
3307
|
-
* - `useFactory`: Factory function with dependency injection
|
|
3308
|
-
* - `useClass`: Class implementing `LoggerOptionsFactory`
|
|
3309
|
-
* - `useExisting`: Existing provider implementing `LoggerOptionsFactory`
|
|
3310
|
-
*
|
|
3311
|
-
* Options from the factory/class are merged with environment preset defaults.
|
|
3312
|
-
*
|
|
3313
|
-
* @param options - Async configuration options
|
|
3314
|
-
* @returns Dynamic module configuration
|
|
3315
|
-
*
|
|
3316
|
-
* @example
|
|
3317
|
-
* ```typescript
|
|
3318
|
-
* // Factory with ConfigService
|
|
3319
|
-
* LoggerModule.forRootAsync({
|
|
3320
|
-
* imports: [ConfigModule],
|
|
3321
|
-
* useFactory: (config: ConfigService) => ({
|
|
3322
|
-
* environment: config.get('NODE_ENV', 'development'),
|
|
3323
|
-
* provider: config.get('LOG_PROVIDER', 'winston'),
|
|
3324
|
-
* level: config.get('LOG_LEVEL'),
|
|
3325
|
-
* appName: config.get('APP_NAME'),
|
|
3326
|
-
* }),
|
|
3327
|
-
* inject: [ConfigService]
|
|
3328
|
-
* })
|
|
3329
|
-
*
|
|
3330
|
-
* // Factory class
|
|
3331
|
-
* @Injectable()
|
|
3332
|
-
* class LoggerConfigService implements LoggerOptionsFactory {
|
|
3333
|
-
* createLoggerOptions(): LoggerModuleOptions {
|
|
3334
|
-
* return {
|
|
3335
|
-
* environment: 'production',
|
|
3336
|
-
* appName: 'my-service'
|
|
3337
|
-
* };
|
|
3338
|
-
* }
|
|
3339
|
-
* }
|
|
3340
|
-
*
|
|
3341
|
-
* LoggerModule.forRootAsync({
|
|
3342
|
-
* useClass: LoggerConfigService
|
|
3343
|
-
* })
|
|
3344
|
-
* ```
|
|
3345
|
-
*/
|
|
3212
|
+
// Configures the logger module with async options (useFactory, useClass, useExisting)
|
|
3346
3213
|
static forRootAsync(options) {
|
|
3347
3214
|
const asyncProviders = _LoggerModule.createAsyncProviders(options);
|
|
3348
3215
|
return {
|
|
@@ -3352,13 +3219,13 @@ var LoggerModule = class _LoggerModule {
|
|
|
3352
3219
|
...asyncProviders,
|
|
3353
3220
|
// Default logger provider
|
|
3354
3221
|
{
|
|
3355
|
-
provide:
|
|
3222
|
+
provide: Logger10,
|
|
3356
3223
|
useFactory: /* @__PURE__ */ __name((opts) => {
|
|
3357
3224
|
if (opts.provider === "default") {
|
|
3358
|
-
const logger = new
|
|
3359
|
-
if (opts.level
|
|
3225
|
+
const logger = new Logger10();
|
|
3226
|
+
if (opts.level) {
|
|
3360
3227
|
const levels = getLevelsUpTo(opts.level);
|
|
3361
|
-
logger.setLogLevels(levels);
|
|
3228
|
+
logger.setLogLevels?.(levels);
|
|
3362
3229
|
}
|
|
3363
3230
|
return logger;
|
|
3364
3231
|
}
|
|
@@ -3377,7 +3244,7 @@ var LoggerModule = class _LoggerModule {
|
|
|
3377
3244
|
inject: [
|
|
3378
3245
|
LOGGER_MODULE_OPTIONS,
|
|
3379
3246
|
{
|
|
3380
|
-
token:
|
|
3247
|
+
token: Logger10,
|
|
3381
3248
|
optional: true
|
|
3382
3249
|
}
|
|
3383
3250
|
]
|
|
@@ -3416,15 +3283,10 @@ var LoggerModule = class _LoggerModule {
|
|
|
3416
3283
|
]
|
|
3417
3284
|
};
|
|
3418
3285
|
}
|
|
3419
|
-
|
|
3420
|
-
* Configures middleware for the module.
|
|
3421
|
-
* Middleware is registered globally in main.ts using Fastify hooks.
|
|
3422
|
-
*/
|
|
3286
|
+
// Middleware registration is handled globally in main.ts via Fastify hooks
|
|
3423
3287
|
configure(_consumer) {
|
|
3424
3288
|
}
|
|
3425
|
-
|
|
3426
|
-
* Creates async providers for dynamic module configuration.
|
|
3427
|
-
*/
|
|
3289
|
+
// Creates async providers for dynamic module configuration
|
|
3428
3290
|
static createAsyncProviders(options) {
|
|
3429
3291
|
if (options.useFactory) {
|
|
3430
3292
|
return [
|
|
@@ -3442,9 +3304,7 @@ var LoggerModule = class _LoggerModule {
|
|
|
3442
3304
|
}
|
|
3443
3305
|
return providers;
|
|
3444
3306
|
}
|
|
3445
|
-
|
|
3446
|
-
* Creates the async options provider.
|
|
3447
|
-
*/
|
|
3307
|
+
// Creates the DI provider that resolves and merges async logger options
|
|
3448
3308
|
static createAsyncOptionsProvider(options) {
|
|
3449
3309
|
if (options.useFactory) {
|
|
3450
3310
|
return {
|
|
@@ -3483,11 +3343,412 @@ var LoggerModule = class _LoggerModule {
|
|
|
3483
3343
|
throw new Error("LoggerModule.forRootAsync() requires one of: useFactory, useClass, or useExisting");
|
|
3484
3344
|
}
|
|
3485
3345
|
};
|
|
3486
|
-
LoggerModule =
|
|
3487
|
-
|
|
3488
|
-
|
|
3346
|
+
LoggerModule = _ts_decorate17([
|
|
3347
|
+
Global5(),
|
|
3348
|
+
Module5({})
|
|
3489
3349
|
], LoggerModule);
|
|
3350
|
+
|
|
3351
|
+
// src/root/root.module.ts
|
|
3352
|
+
import { Module as Module6 } from "@nestjs/common";
|
|
3353
|
+
|
|
3354
|
+
// src/root/controllers/app.controller.ts
|
|
3355
|
+
import { Controller, Get } from "@nestjs/common";
|
|
3356
|
+
import { ApiTags } from "@nestjs/swagger";
|
|
3357
|
+
|
|
3358
|
+
// src/root/docs/app.docs.ts
|
|
3359
|
+
import { applyDecorators } from "@nestjs/common";
|
|
3360
|
+
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
|
|
3361
|
+
function ApiHealthCheck() {
|
|
3362
|
+
return applyDecorators(ApiOperation({
|
|
3363
|
+
summary: "Health check endpoint"
|
|
3364
|
+
}), ApiResponse({
|
|
3365
|
+
status: 200,
|
|
3366
|
+
description: "Returns a welcome message indicating the API is running",
|
|
3367
|
+
type: String
|
|
3368
|
+
}));
|
|
3369
|
+
}
|
|
3370
|
+
__name(ApiHealthCheck, "ApiHealthCheck");
|
|
3371
|
+
|
|
3372
|
+
// src/root/services/app.service.ts
|
|
3373
|
+
import { Injectable as Injectable11 } from "@nestjs/common";
|
|
3374
|
+
function _ts_decorate18(decorators, target, key, desc) {
|
|
3375
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3376
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
3377
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
3378
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
3379
|
+
}
|
|
3380
|
+
__name(_ts_decorate18, "_ts_decorate");
|
|
3381
|
+
var AppService = class {
|
|
3382
|
+
static {
|
|
3383
|
+
__name(this, "AppService");
|
|
3384
|
+
}
|
|
3385
|
+
// Returns the API welcome message
|
|
3386
|
+
getHello() {
|
|
3387
|
+
return `Hello World!`;
|
|
3388
|
+
}
|
|
3389
|
+
};
|
|
3390
|
+
AppService = _ts_decorate18([
|
|
3391
|
+
Injectable11()
|
|
3392
|
+
], AppService);
|
|
3393
|
+
|
|
3394
|
+
// src/root/controllers/app.controller.ts
|
|
3395
|
+
function _ts_decorate19(decorators, target, key, desc) {
|
|
3396
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3397
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
3398
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
3399
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
3400
|
+
}
|
|
3401
|
+
__name(_ts_decorate19, "_ts_decorate");
|
|
3402
|
+
function _ts_metadata11(k, v) {
|
|
3403
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
3404
|
+
}
|
|
3405
|
+
__name(_ts_metadata11, "_ts_metadata");
|
|
3406
|
+
var AppController = class {
|
|
3407
|
+
static {
|
|
3408
|
+
__name(this, "AppController");
|
|
3409
|
+
}
|
|
3410
|
+
appService;
|
|
3411
|
+
constructor(appService) {
|
|
3412
|
+
this.appService = appService;
|
|
3413
|
+
}
|
|
3414
|
+
// Returns a welcome message indicating the API is running
|
|
3415
|
+
getHello() {
|
|
3416
|
+
return this.appService.getHello();
|
|
3417
|
+
}
|
|
3418
|
+
};
|
|
3419
|
+
_ts_decorate19([
|
|
3420
|
+
Get(),
|
|
3421
|
+
Public(),
|
|
3422
|
+
ApiHealthCheck(),
|
|
3423
|
+
_ts_metadata11("design:type", Function),
|
|
3424
|
+
_ts_metadata11("design:paramtypes", []),
|
|
3425
|
+
_ts_metadata11("design:returntype", String)
|
|
3426
|
+
], AppController.prototype, "getHello", null);
|
|
3427
|
+
AppController = _ts_decorate19([
|
|
3428
|
+
ApiTags("Health"),
|
|
3429
|
+
Controller(),
|
|
3430
|
+
_ts_metadata11("design:type", Function),
|
|
3431
|
+
_ts_metadata11("design:paramtypes", [
|
|
3432
|
+
typeof AppService === "undefined" ? Object : AppService
|
|
3433
|
+
])
|
|
3434
|
+
], AppController);
|
|
3435
|
+
|
|
3436
|
+
// src/root/controllers/csrf.controller.ts
|
|
3437
|
+
import { Controller as Controller2, Get as Get2, HttpCode, HttpStatus as HttpStatus20, Res } from "@nestjs/common";
|
|
3438
|
+
import { ApiTags as ApiTags2 } from "@nestjs/swagger";
|
|
3439
|
+
|
|
3440
|
+
// src/root/docs/csrf.docs.ts
|
|
3441
|
+
import { applyDecorators as applyDecorators2 } from "@nestjs/common";
|
|
3442
|
+
import { ApiOperation as ApiOperation2, ApiResponse as ApiResponse2 } from "@nestjs/swagger";
|
|
3443
|
+
function ApiGetCsrfToken() {
|
|
3444
|
+
return applyDecorators2(ApiOperation2({
|
|
3445
|
+
summary: "Get CSRF token",
|
|
3446
|
+
description: "Generates and returns a CSRF token that must be included in all state-changing requests (POST, PUT, PATCH, DELETE). The token should be sent in the X-CSRF-Token header."
|
|
3447
|
+
}), ApiResponse2({
|
|
3448
|
+
status: 200,
|
|
3449
|
+
description: "CSRF token generated successfully",
|
|
3450
|
+
schema: {
|
|
3451
|
+
type: "object",
|
|
3452
|
+
properties: {
|
|
3453
|
+
csrfToken: {
|
|
3454
|
+
type: "string",
|
|
3455
|
+
description: "The CSRF token to use in subsequent requests",
|
|
3456
|
+
example: "abc123xyz789"
|
|
3457
|
+
}
|
|
3458
|
+
},
|
|
3459
|
+
required: [
|
|
3460
|
+
"csrfToken"
|
|
3461
|
+
]
|
|
3462
|
+
}
|
|
3463
|
+
}));
|
|
3464
|
+
}
|
|
3465
|
+
__name(ApiGetCsrfToken, "ApiGetCsrfToken");
|
|
3466
|
+
|
|
3467
|
+
// src/root/controllers/csrf.controller.ts
|
|
3468
|
+
function _ts_decorate20(decorators, target, key, desc) {
|
|
3469
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3470
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
3471
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
3472
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
3473
|
+
}
|
|
3474
|
+
__name(_ts_decorate20, "_ts_decorate");
|
|
3475
|
+
function _ts_metadata12(k, v) {
|
|
3476
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
3477
|
+
}
|
|
3478
|
+
__name(_ts_metadata12, "_ts_metadata");
|
|
3479
|
+
function _ts_param6(paramIndex, decorator) {
|
|
3480
|
+
return function(target, key) {
|
|
3481
|
+
decorator(target, key, paramIndex);
|
|
3482
|
+
};
|
|
3483
|
+
}
|
|
3484
|
+
__name(_ts_param6, "_ts_param");
|
|
3485
|
+
var CsrfController = class {
|
|
3486
|
+
static {
|
|
3487
|
+
__name(this, "CsrfController");
|
|
3488
|
+
}
|
|
3489
|
+
// Generates a CSRF token via Fastify's csrf-protection plugin
|
|
3490
|
+
getToken(reply) {
|
|
3491
|
+
const csrfToken = reply.generateCsrf();
|
|
3492
|
+
return {
|
|
3493
|
+
csrfToken
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
};
|
|
3497
|
+
_ts_decorate20([
|
|
3498
|
+
Get2("token"),
|
|
3499
|
+
Public(),
|
|
3500
|
+
HttpCode(HttpStatus20.OK),
|
|
3501
|
+
ApiGetCsrfToken(),
|
|
3502
|
+
_ts_param6(0, Res({
|
|
3503
|
+
passthrough: true
|
|
3504
|
+
})),
|
|
3505
|
+
_ts_metadata12("design:type", Function),
|
|
3506
|
+
_ts_metadata12("design:paramtypes", [
|
|
3507
|
+
typeof FastifyReply === "undefined" ? Object : FastifyReply
|
|
3508
|
+
]),
|
|
3509
|
+
_ts_metadata12("design:returntype", Object)
|
|
3510
|
+
], CsrfController.prototype, "getToken", null);
|
|
3511
|
+
CsrfController = _ts_decorate20([
|
|
3512
|
+
ApiTags2("CSRF"),
|
|
3513
|
+
Controller2("csrf")
|
|
3514
|
+
], CsrfController);
|
|
3515
|
+
|
|
3516
|
+
// src/root/root.module.ts
|
|
3517
|
+
function _ts_decorate21(decorators, target, key, desc) {
|
|
3518
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3519
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
3520
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
3521
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
3522
|
+
}
|
|
3523
|
+
__name(_ts_decorate21, "_ts_decorate");
|
|
3524
|
+
var RootModule = class {
|
|
3525
|
+
static {
|
|
3526
|
+
__name(this, "RootModule");
|
|
3527
|
+
}
|
|
3528
|
+
};
|
|
3529
|
+
RootModule = _ts_decorate21([
|
|
3530
|
+
Module6({
|
|
3531
|
+
controllers: [
|
|
3532
|
+
AppController,
|
|
3533
|
+
CsrfController
|
|
3534
|
+
],
|
|
3535
|
+
providers: [
|
|
3536
|
+
AppService
|
|
3537
|
+
]
|
|
3538
|
+
})
|
|
3539
|
+
], RootModule);
|
|
3540
|
+
|
|
3541
|
+
// src/utils/phone.utils.ts
|
|
3542
|
+
var CALLING_CODE_TO_COUNTRY = {
|
|
3543
|
+
// 3-digit codes
|
|
3544
|
+
"355": "AL",
|
|
3545
|
+
"213": "DZ",
|
|
3546
|
+
"376": "AD",
|
|
3547
|
+
"244": "AO",
|
|
3548
|
+
"672": "AQ",
|
|
3549
|
+
"374": "AM",
|
|
3550
|
+
"297": "AW",
|
|
3551
|
+
"994": "AZ",
|
|
3552
|
+
"973": "BH",
|
|
3553
|
+
"880": "BD",
|
|
3554
|
+
"375": "BY",
|
|
3555
|
+
"501": "BZ",
|
|
3556
|
+
"229": "BJ",
|
|
3557
|
+
"975": "BT",
|
|
3558
|
+
"591": "BO",
|
|
3559
|
+
"387": "BA",
|
|
3560
|
+
"267": "BW",
|
|
3561
|
+
"673": "BN",
|
|
3562
|
+
"359": "BG",
|
|
3563
|
+
"226": "BF",
|
|
3564
|
+
"257": "BI",
|
|
3565
|
+
"855": "KH",
|
|
3566
|
+
"237": "CM",
|
|
3567
|
+
"238": "CV",
|
|
3568
|
+
"236": "CF",
|
|
3569
|
+
"235": "TD",
|
|
3570
|
+
"269": "KM",
|
|
3571
|
+
"242": "CG",
|
|
3572
|
+
"243": "CD",
|
|
3573
|
+
"506": "CR",
|
|
3574
|
+
"385": "HR",
|
|
3575
|
+
"357": "CY",
|
|
3576
|
+
"420": "CZ",
|
|
3577
|
+
"253": "DJ",
|
|
3578
|
+
"593": "EC",
|
|
3579
|
+
"503": "SV",
|
|
3580
|
+
"240": "GQ",
|
|
3581
|
+
"291": "ER",
|
|
3582
|
+
"372": "EE",
|
|
3583
|
+
"251": "ET",
|
|
3584
|
+
"679": "FJ",
|
|
3585
|
+
"358": "FI",
|
|
3586
|
+
"241": "GA",
|
|
3587
|
+
"220": "GM",
|
|
3588
|
+
"995": "GE",
|
|
3589
|
+
"233": "GH",
|
|
3590
|
+
"350": "GI",
|
|
3591
|
+
"299": "GL",
|
|
3592
|
+
"502": "GT",
|
|
3593
|
+
"224": "GN",
|
|
3594
|
+
"245": "GW",
|
|
3595
|
+
"592": "GY",
|
|
3596
|
+
"509": "HT",
|
|
3597
|
+
"504": "HN",
|
|
3598
|
+
"354": "IS",
|
|
3599
|
+
"964": "IQ",
|
|
3600
|
+
"353": "IE",
|
|
3601
|
+
"972": "IL",
|
|
3602
|
+
"225": "CI",
|
|
3603
|
+
"962": "JO",
|
|
3604
|
+
"254": "KE",
|
|
3605
|
+
"686": "KI",
|
|
3606
|
+
"965": "KW",
|
|
3607
|
+
"996": "KG",
|
|
3608
|
+
"856": "LA",
|
|
3609
|
+
"371": "LV",
|
|
3610
|
+
"961": "LB",
|
|
3611
|
+
"266": "LS",
|
|
3612
|
+
"231": "LR",
|
|
3613
|
+
"218": "LY",
|
|
3614
|
+
"423": "LI",
|
|
3615
|
+
"370": "LT",
|
|
3616
|
+
"352": "LU",
|
|
3617
|
+
"389": "MK",
|
|
3618
|
+
"261": "MG",
|
|
3619
|
+
"265": "MW",
|
|
3620
|
+
"960": "MV",
|
|
3621
|
+
"223": "ML",
|
|
3622
|
+
"356": "MT",
|
|
3623
|
+
"692": "MH",
|
|
3624
|
+
"222": "MR",
|
|
3625
|
+
"230": "MU",
|
|
3626
|
+
"262": "YT",
|
|
3627
|
+
"691": "FM",
|
|
3628
|
+
"373": "MD",
|
|
3629
|
+
"377": "MC",
|
|
3630
|
+
"976": "MN",
|
|
3631
|
+
"382": "ME",
|
|
3632
|
+
"258": "MZ",
|
|
3633
|
+
"264": "NA",
|
|
3634
|
+
"674": "NR",
|
|
3635
|
+
"977": "NP",
|
|
3636
|
+
"505": "NI",
|
|
3637
|
+
"227": "NE",
|
|
3638
|
+
"234": "NG",
|
|
3639
|
+
"683": "NU",
|
|
3640
|
+
"968": "OM",
|
|
3641
|
+
"680": "PW",
|
|
3642
|
+
"970": "PS",
|
|
3643
|
+
"507": "PA",
|
|
3644
|
+
"675": "PG",
|
|
3645
|
+
"595": "PY",
|
|
3646
|
+
"351": "PT",
|
|
3647
|
+
"974": "QA",
|
|
3648
|
+
"250": "RW",
|
|
3649
|
+
"685": "WS",
|
|
3650
|
+
"378": "SM",
|
|
3651
|
+
"239": "ST",
|
|
3652
|
+
"966": "SA",
|
|
3653
|
+
"221": "SN",
|
|
3654
|
+
"381": "RS",
|
|
3655
|
+
"248": "SC",
|
|
3656
|
+
"232": "SL",
|
|
3657
|
+
"421": "SK",
|
|
3658
|
+
"386": "SI",
|
|
3659
|
+
"677": "SB",
|
|
3660
|
+
"252": "SO",
|
|
3661
|
+
"211": "SS",
|
|
3662
|
+
"249": "SD",
|
|
3663
|
+
"597": "SR",
|
|
3664
|
+
"268": "SZ",
|
|
3665
|
+
"963": "SY",
|
|
3666
|
+
"992": "TJ",
|
|
3667
|
+
"255": "TZ",
|
|
3668
|
+
"228": "TG",
|
|
3669
|
+
"676": "TO",
|
|
3670
|
+
"216": "TN",
|
|
3671
|
+
"993": "TM",
|
|
3672
|
+
"688": "TV",
|
|
3673
|
+
"256": "UG",
|
|
3674
|
+
"380": "UA",
|
|
3675
|
+
"971": "AE",
|
|
3676
|
+
"598": "UY",
|
|
3677
|
+
"998": "UZ",
|
|
3678
|
+
"678": "VU",
|
|
3679
|
+
"379": "VA",
|
|
3680
|
+
"967": "YE",
|
|
3681
|
+
"260": "ZM",
|
|
3682
|
+
"263": "ZW",
|
|
3683
|
+
// 2-digit codes
|
|
3684
|
+
"93": "AF",
|
|
3685
|
+
"54": "AR",
|
|
3686
|
+
"61": "AU",
|
|
3687
|
+
"43": "AT",
|
|
3688
|
+
"32": "BE",
|
|
3689
|
+
"55": "BR",
|
|
3690
|
+
"56": "CL",
|
|
3691
|
+
"86": "CN",
|
|
3692
|
+
"57": "CO",
|
|
3693
|
+
"53": "CU",
|
|
3694
|
+
"45": "DK",
|
|
3695
|
+
"20": "EG",
|
|
3696
|
+
"33": "FR",
|
|
3697
|
+
"49": "DE",
|
|
3698
|
+
"30": "GR",
|
|
3699
|
+
"36": "HU",
|
|
3700
|
+
"91": "IN",
|
|
3701
|
+
"62": "ID",
|
|
3702
|
+
"98": "IR",
|
|
3703
|
+
"39": "IT",
|
|
3704
|
+
"81": "JP",
|
|
3705
|
+
"82": "KR",
|
|
3706
|
+
"60": "MY",
|
|
3707
|
+
"52": "MX",
|
|
3708
|
+
"31": "NL",
|
|
3709
|
+
"64": "NZ",
|
|
3710
|
+
"47": "NO",
|
|
3711
|
+
"92": "PK",
|
|
3712
|
+
"51": "PE",
|
|
3713
|
+
"63": "PH",
|
|
3714
|
+
"48": "PL",
|
|
3715
|
+
"40": "RO",
|
|
3716
|
+
"65": "SG",
|
|
3717
|
+
"27": "ZA",
|
|
3718
|
+
"34": "ES",
|
|
3719
|
+
"94": "LK",
|
|
3720
|
+
"46": "SE",
|
|
3721
|
+
"41": "CH",
|
|
3722
|
+
"66": "TH",
|
|
3723
|
+
"90": "TR",
|
|
3724
|
+
"44": "GB",
|
|
3725
|
+
"58": "VE",
|
|
3726
|
+
"84": "VN",
|
|
3727
|
+
// 1-digit codes (shared codes default to most common country)
|
|
3728
|
+
"1": "US",
|
|
3729
|
+
"7": "RU"
|
|
3730
|
+
};
|
|
3731
|
+
function extractCountryFromPhone(phone) {
|
|
3732
|
+
const digits = phone.startsWith("+") ? phone.slice(1) : phone;
|
|
3733
|
+
for (const length of [
|
|
3734
|
+
3,
|
|
3735
|
+
2,
|
|
3736
|
+
1
|
|
3737
|
+
]) {
|
|
3738
|
+
const prefix = digits.slice(0, length);
|
|
3739
|
+
if (CALLING_CODE_TO_COUNTRY[prefix]) {
|
|
3740
|
+
return CALLING_CODE_TO_COUNTRY[prefix];
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
return void 0;
|
|
3744
|
+
}
|
|
3745
|
+
__name(extractCountryFromPhone, "extractCountryFromPhone");
|
|
3746
|
+
function normalizePhoneNumber(phone) {
|
|
3747
|
+
return phone.startsWith("+") ? phone : `+${phone}`;
|
|
3748
|
+
}
|
|
3749
|
+
__name(normalizePhoneNumber, "normalizePhoneNumber");
|
|
3490
3750
|
export {
|
|
3751
|
+
AccessToken,
|
|
3491
3752
|
AuthConfigModule,
|
|
3492
3753
|
BadGatewayException,
|
|
3493
3754
|
BadRequestException,
|
|
@@ -3495,12 +3756,15 @@ export {
|
|
|
3495
3756
|
CorrelationIdMiddleware,
|
|
3496
3757
|
DEFAULT_CORRELATION_HEADER,
|
|
3497
3758
|
DatabaseModule,
|
|
3759
|
+
EmailModule,
|
|
3760
|
+
EmailService,
|
|
3498
3761
|
ForbiddenException2 as ForbiddenException,
|
|
3499
3762
|
GoneException,
|
|
3500
3763
|
HttpExceptionFilter,
|
|
3501
3764
|
HttpLoggerInterceptor,
|
|
3502
3765
|
HttpProblemException,
|
|
3503
3766
|
InternalServerErrorException3 as InternalServerErrorException,
|
|
3767
|
+
JwtAuthService,
|
|
3504
3768
|
LOGGER_MODULE_OPTIONS,
|
|
3505
3769
|
LoggerModule,
|
|
3506
3770
|
LoggerService,
|
|
@@ -3513,17 +3777,23 @@ export {
|
|
|
3513
3777
|
PrimaryBaseRepository,
|
|
3514
3778
|
PrimaryDatabaseService,
|
|
3515
3779
|
Public,
|
|
3780
|
+
RESET_KEY,
|
|
3781
|
+
RefreshTokenCookie,
|
|
3516
3782
|
RequestTimeoutException,
|
|
3783
|
+
Reset,
|
|
3784
|
+
RootModule,
|
|
3517
3785
|
SKIP_CSRF_KEY,
|
|
3786
|
+
SelectOptionsQueryDto,
|
|
3518
3787
|
ServiceUnavailableException,
|
|
3788
|
+
SessionData,
|
|
3519
3789
|
SkipCsrf,
|
|
3520
|
-
SseAuthGuard,
|
|
3521
3790
|
Tenant,
|
|
3522
3791
|
TenantBaseRepository,
|
|
3523
3792
|
TenantContextService,
|
|
3524
3793
|
TenantDatabaseService,
|
|
3794
|
+
TokenType,
|
|
3525
3795
|
TooManyRequestsException,
|
|
3526
|
-
|
|
3796
|
+
UnauthorizedException3 as UnauthorizedException,
|
|
3527
3797
|
UnprocessableEntityException,
|
|
3528
3798
|
UnsupportedMediaTypeException,
|
|
3529
3799
|
UserId,
|
|
@@ -3540,8 +3810,11 @@ export {
|
|
|
3540
3810
|
getHttpStatusTitle,
|
|
3541
3811
|
getJwtExpiry,
|
|
3542
3812
|
getRefreshCookieOptions,
|
|
3813
|
+
getTokenExpiry,
|
|
3543
3814
|
hashToken,
|
|
3815
|
+
jwtConfigFactory,
|
|
3544
3816
|
normalizePhoneNumber,
|
|
3817
|
+
parseExpiryToMs,
|
|
3545
3818
|
resetConfig,
|
|
3546
3819
|
runWithCorrelationContext,
|
|
3547
3820
|
updateCorrelationContext,
|