From 7177a13e94e66b27c7198e0e695f56fc7c17aa9a Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sat, 12 Jul 2025 12:42:01 +0330 Subject: [PATCH] feat: add new route for user me and quota --- src/common/constants/index.ts | 2 +- src/common/decorators/rate-limit.decorator.ts | 4 +- src/configs/jwt.config.ts | 2 +- src/configs/mikro-orm.config.ts | 66 +++++++++++++------ src/modules/auth/auth.controller.ts | 3 +- .../auth/strategies/local-jwt.strategy.ts | 2 +- src/modules/businesses/constant/index.ts | 2 +- .../services/domain-automation.service.ts | 6 +- .../domains/services/domains.service.ts | 6 +- .../users/repositories/user.repository.ts | 1 + src/modules/users/services/users.service.ts | 25 +++++-- src/modules/users/users.controller.ts | 8 +++ 12 files changed, 91 insertions(+), 36 deletions(-) diff --git a/src/common/constants/index.ts b/src/common/constants/index.ts index 139d86c..dc54905 100755 --- a/src/common/constants/index.ts +++ b/src/common/constants/index.ts @@ -1,5 +1,5 @@ // export const AUTH_THROTTLE = "AUTH_THROTTLE"; -export const AUTH_THROTTLE_TTL = 5 * 60 * 1000; +export const AUTH_THROTTLE_TTL = 1 * 60 * 1000; export const AUTH_THROTTLE_LIMIT = 5; export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000; export const AUTH__REFRESH_THROTTLE_LIMIT = 10; diff --git a/src/common/decorators/rate-limit.decorator.ts b/src/common/decorators/rate-limit.decorator.ts index e3deeca..84bb67d 100644 --- a/src/common/decorators/rate-limit.decorator.ts +++ b/src/common/decorators/rate-limit.decorator.ts @@ -1,12 +1,12 @@ import { UseGuards, applyDecorators } from "@nestjs/common"; import { Throttle, ThrottlerGuard } from "@nestjs/throttler"; -import { AUTH__REFRESH_THROTTLE_LIMIT, AUTH__REFRESH_THROTTLE_TTL } from "../constants"; +import { AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL, AUTH__REFRESH_THROTTLE_LIMIT, AUTH__REFRESH_THROTTLE_TTL } from "../constants"; export const RateLimit = (limit: number, ttl: number) => applyDecorators(Throttle({ default: { limit, ttl } }), UseGuards(ThrottlerGuard)); // Predefined rate limits for common scenarios -export const StrictRateLimit = () => RateLimit(5, 60000); // 5 requests per minute +export const StrictRateLimit = () => RateLimit(AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL); // 5 requests per minute export const StandardRateLimit = () => RateLimit(30, 60000); // 30 requests per minute export const MailSendRateLimit = () => RateLimit(10, 60000); // 10 emails per minute export const RefreshTokenRateLimit = () => RateLimit(AUTH__REFRESH_THROTTLE_LIMIT, AUTH__REFRESH_THROTTLE_TTL); // 10 emails per minute diff --git a/src/configs/jwt.config.ts b/src/configs/jwt.config.ts index 57a3134..75416e7 100755 --- a/src/configs/jwt.config.ts +++ b/src/configs/jwt.config.ts @@ -8,7 +8,7 @@ export function jwtConfig(): JwtModuleAsyncOptions { useFactory: (configService: ConfigService) => { return { global: true, - secret: configService.getOrThrow("JWT_SECRET"), + secret: configService.getOrThrow("JWT_SECRET_KEY"), signOptions: { issuer: configService.getOrThrow("JWT_ISSUER"), }, diff --git a/src/configs/mikro-orm.config.ts b/src/configs/mikro-orm.config.ts index 14d295d..1feb795 100755 --- a/src/configs/mikro-orm.config.ts +++ b/src/configs/mikro-orm.config.ts @@ -14,38 +14,34 @@ export const databaseConfig: MikroOrmModuleAsyncOptions = { const DB_HOST = configService.getOrThrow("DB_HOST"); const DB_PORT = configService.getOrThrow("DB_PORT"); const encodedPassword = encodeURIComponent(DB_PASS); + const isProduction = configService.getOrThrow("NODE_ENV") === "production"; return { driver: PostgreSqlDriver, autoLoadEntities: true, dbName: configService.getOrThrow("DB_NAME"), - debug: configService.get("NODE_ENV") !== "production", + debug: !isProduction, clientUrl: `postgres://${DB_USER}:${encodedPassword}@${DB_HOST}:${DB_PORT}`, - ensureDatabase: { forceCheck: true, create: true, schema: "update" }, + ensureDatabase: isProduction + ? false + : { + forceCheck: true, + create: true, + schema: "update", + }, forceUtcTimezone: true, pool: { - min: 5, - max: 25, + min: isProduction ? 5 : 2, + max: isProduction ? 20 : 10, idleTimeoutMillis: 30000, - acquireTimeoutMillis: 15000, + acquireTimeoutMillis: 30000, reapIntervalMillis: 1000, - createTimeoutMillis: 8000, + createTimeoutMillis: 15000, destroyTimeoutMillis: 5000, - createRetryIntervalMillis: 200, - propagateCreateError: false, }, - driverOptions: { - connection: { - keepAlive: true, - keepAliveInitialDelayMillis: 0, - statement_timeout: 60000, - query_timeout: 60000, - connectionTimeoutMillis: 10000, - }, - }, - logger: (message) => logger.debug(message), + logger: isProduction ? (message) => logger.log(message) : (message) => logger.debug(message), schemaGenerator: { - createForeignKey: true, + createForeignKey: !isProduction, disableForeignKeys: false, createIndex: true, }, @@ -59,6 +55,38 @@ export const databaseConfig: MikroOrmModuleAsyncOptions = { safe: true, emit: "ts", }, + connect: true, + allowGlobalContext: false, + driverOptions: { + connection: { + keepAlive: true, + keepAliveInitialDelayMillis: 10000, + statement_timeout: 60000, + query_timeout: 60000, + idle_in_transaction_session_timeout: 60000, + connectionTimeoutMillis: 10000, + application_name: "fresh-bazzar-api", + }, + connectionRetries: 5, + connectionRetryDelay: 3000, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + validateConnection: (connection: any) => { + try { + return connection && !connection.closed; + } catch (e: unknown) { + logger.error(`Connection validation failed: ${(e as Error).message}`); + return false; + } + }, + poolErrorHandler: (err: Error) => { + logger.error(`PostgreSQL pool error: ${err.message}`); + if (err.message.includes("ECONNRESET") || err.message.includes("Connection terminated unexpectedly")) { + logger.warn("Connection reset detected, will attempt to reconnect"); + return true; + } + return false; + }, + }, }; }, }; diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 38421ed..d00b340 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -5,7 +5,7 @@ import { LoginDto } from "./DTO/login.dto"; import { RefreshTokenDto } from "./DTO/refresh-token.dto"; import { AuthService } from "./services/auth.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; -import { RefreshTokenRateLimit } from "../../common/decorators/rate-limit.decorator"; +import { RefreshTokenRateLimit, StrictRateLimit } from "../../common/decorators/rate-limit.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; @ApiTags("Authentication") @@ -13,6 +13,7 @@ import { UserDec } from "../../common/decorators/user.decorator"; export class AuthController { constructor(private readonly authService: AuthService) {} + @StrictRateLimit() @Post("login") @HttpCode(HttpStatus.OK) @ApiOperation({ summary: "Login user" }) diff --git a/src/modules/auth/strategies/local-jwt.strategy.ts b/src/modules/auth/strategies/local-jwt.strategy.ts index a47d175..78e57fb 100644 --- a/src/modules/auth/strategies/local-jwt.strategy.ts +++ b/src/modules/auth/strategies/local-jwt.strategy.ts @@ -12,7 +12,7 @@ export class LocalJwtStrategy extends PassportStrategy(Strategy, LOCAL_JWT_STRAT super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - secretOrKey: configService.getOrThrow("JWT_SECRET"), + secretOrKey: configService.getOrThrow("JWT_SECRET_KEY"), issuer: configService.getOrThrow("JWT_ISSUER"), }); } diff --git a/src/modules/businesses/constant/index.ts b/src/modules/businesses/constant/index.ts index 005cb7d..fb0e8dc 100644 --- a/src/modules/businesses/constant/index.ts +++ b/src/modules/businesses/constant/index.ts @@ -1,5 +1,5 @@ export const SUBSCRIPTIONS = Object.freeze({ - PROVISIONING_QUEUE_PREFIX: "provisioning", + PROVISIONING_QUEUE_PREFIX: "subs", PROVISIONING_QUEUE_NAME: "provisioning", PROVISIONING_JOB_NAME: "business.created", diff --git a/src/modules/domains/services/domain-automation.service.ts b/src/modules/domains/services/domain-automation.service.ts index 6b4f529..441da0b 100644 --- a/src/modules/domains/services/domain-automation.service.ts +++ b/src/modules/domains/services/domain-automation.service.ts @@ -132,7 +132,7 @@ export class DomainAutomationService { if (!mailServerUser.success) throw new BadRequestException(MailServerMessage.FAILED_TO_CREATE_ACCOUNT); // Check if business has enough quota - const remainingQuota = domain.business.remainingQuota || 0; + const remainingQuota = Number(domain.business.remainingQuota || 0); if (remainingQuota < QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION) throw new BadRequestException(BusinessMessage.INSUFFICIENT_QUOTA); // Create user in local database @@ -152,8 +152,8 @@ export class DomainAutomationService { }); // Update business quota after successful user creation - domain.business.usedQuota = (domain.business.usedQuota || 0) + QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION; - domain.business.remainingQuota = (domain.business.quota || 0) - domain.business.usedQuota; + domain.business.usedQuota = Number(domain.business.usedQuota || 0) + QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION; + domain.business.remainingQuota = Number(domain.business.quota || 0) - Number(domain.business.usedQuota); await this.em.persistAndFlush([user, domain.business]); diff --git a/src/modules/domains/services/domains.service.ts b/src/modules/domains/services/domains.service.ts index 525f89c..8b40eb0 100644 --- a/src/modules/domains/services/domains.service.ts +++ b/src/modules/domains/services/domains.service.ts @@ -216,6 +216,8 @@ export class DomainsService { const domain = await this.getDomainById(businessDomain.id, businessId); + const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id); + // If domain is already verified, skip verification check and return DNS records directly if (domain.isVerified && domain.status === DomainStatus.VERIFIED) { const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id); @@ -234,7 +236,7 @@ export class DomainsService { }; } - const { dnsRecords, overallStatus, recommendations } = await this.checkDomainVerificationStatus(businessId); + const { overallStatus, recommendations } = await this.checkDomainVerificationStatus(businessId); return { dnsRecords, @@ -304,7 +306,7 @@ export class DomainsService { } return { - dnsRecords: verificationStatuses, + verificationStatuses, overallStatus: { isVerified, isComplete, diff --git a/src/modules/users/repositories/user.repository.ts b/src/modules/users/repositories/user.repository.ts index 4cec384..2cedf3f 100644 --- a/src/modules/users/repositories/user.repository.ts +++ b/src/modules/users/repositories/user.repository.ts @@ -29,6 +29,7 @@ export class UserRepository extends EntityRepository { } return this.findAndCount(whereClause, { + exclude: ["password"], populate: ["domain", "business"], limit, offset: skip, diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index f7a694b..722a714 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -57,9 +57,24 @@ export class UsersService { /*******************************/ async getMe(userId: string) { - const user = await this.userRepository.findOne({ id: userId }, { populate: ["role"] }); + const user = await this.userRepository.findOne({ id: userId }, { exclude: ["password"], populate: ["role"] }); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); - return { user }; + + return { + user, + quota: { + total: Number(user.emailQuota), + used: Number(user.emailQuotaUsed), + remaining: Number(user.emailQuota - user.emailQuotaUsed), + totalInMB: Math.round(Number(user.emailQuota) / (1024 * 1024)), + totalInGB: Math.round(Number(user.emailQuota) / (1024 * 1024 * 1024)), + usedInMB: Math.round(Number(user.emailQuotaUsed) / (1024 * 1024)), + usedInGB: Math.round(Number(user.emailQuotaUsed) / (1024 * 1024 * 1024)), + remainingInMB: Math.round(Number(user.emailQuota - user.emailQuotaUsed) / (1024 * 1024)), + remainingInGB: Math.round(Number(user.emailQuota - user.emailQuotaUsed) / (1024 * 1024 * 1024)), + usagePercentage: Math.round((Number(user.emailQuotaUsed) / Number(user.emailQuota)) * 100), + }, + }; } /*******************************/ @@ -77,7 +92,7 @@ export class UsersService { if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND); // Check if business has enough quota - const remainingQuota = business.remainingQuota || 0; + const remainingQuota = Number(business.remainingQuota || 0); if (remainingQuota < QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION) { throw new BadRequestException("Insufficient quota to create user. Please upgrade your plan."); } @@ -129,8 +144,8 @@ export class UsersService { }); // Update business quota after successful user creation - business.usedQuota = business.usedQuota + QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION; - business.remainingQuota = business.quota - business.usedQuota; + business.usedQuota = Number(business.usedQuota) + QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION; + business.remainingQuota = Number(business.quota) - Number(business.usedQuota); await this.em.persistAndFlush([user, business]); diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index ece23bc..900b891 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -6,6 +6,7 @@ import { UserListQueryDto } from "./DTO/user-list-query.dto"; import { UsersService } from "./services/users.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { BusinessDec } from "../../common/decorators/business.decorator"; +import { UserDec } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; import { BusinessInterceptor } from "../../core/interceptors/business.interceptor"; @@ -16,6 +17,13 @@ import { BusinessInterceptor } from "../../core/interceptors/business.intercepto export class UsersController { constructor(private readonly usersService: UsersService) {} + @Get("me") + @ApiOperation({ summary: "Get current user" }) + @ApiResponse({ status: 200, description: "Current user retrieved successfully" }) + getMe(@UserDec("wildduckUserId") userId: string) { + return this.usersService.getMe(userId); + } + @Post() @ApiOperation({ summary: "Create a new email user" }) @ApiResponse({ status: 201, description: "Email user created successfully" })