feat: add new route for user me and quota
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// export const AUTH_THROTTLE = "AUTH_THROTTLE";
|
// 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_THROTTLE_LIMIT = 5;
|
||||||
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
|
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
|
||||||
export const AUTH__REFRESH_THROTTLE_LIMIT = 10;
|
export const AUTH__REFRESH_THROTTLE_LIMIT = 10;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { UseGuards, applyDecorators } from "@nestjs/common";
|
import { UseGuards, applyDecorators } from "@nestjs/common";
|
||||||
import { Throttle, ThrottlerGuard } from "@nestjs/throttler";
|
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));
|
export const RateLimit = (limit: number, ttl: number) => applyDecorators(Throttle({ default: { limit, ttl } }), UseGuards(ThrottlerGuard));
|
||||||
|
|
||||||
// Predefined rate limits for common scenarios
|
// 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 StandardRateLimit = () => RateLimit(30, 60000); // 30 requests per minute
|
||||||
export const MailSendRateLimit = () => RateLimit(10, 60000); // 10 emails 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
|
export const RefreshTokenRateLimit = () => RateLimit(AUTH__REFRESH_THROTTLE_LIMIT, AUTH__REFRESH_THROTTLE_TTL); // 10 emails per minute
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export function jwtConfig(): JwtModuleAsyncOptions {
|
|||||||
useFactory: (configService: ConfigService) => {
|
useFactory: (configService: ConfigService) => {
|
||||||
return {
|
return {
|
||||||
global: true,
|
global: true,
|
||||||
secret: configService.getOrThrow<string>("JWT_SECRET"),
|
secret: configService.getOrThrow<string>("JWT_SECRET_KEY"),
|
||||||
signOptions: {
|
signOptions: {
|
||||||
issuer: configService.getOrThrow<string>("JWT_ISSUER"),
|
issuer: configService.getOrThrow<string>("JWT_ISSUER"),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,38 +14,34 @@ export const databaseConfig: MikroOrmModuleAsyncOptions = {
|
|||||||
const DB_HOST = configService.getOrThrow<string>("DB_HOST");
|
const DB_HOST = configService.getOrThrow<string>("DB_HOST");
|
||||||
const DB_PORT = configService.getOrThrow<number>("DB_PORT");
|
const DB_PORT = configService.getOrThrow<number>("DB_PORT");
|
||||||
const encodedPassword = encodeURIComponent(DB_PASS);
|
const encodedPassword = encodeURIComponent(DB_PASS);
|
||||||
|
const isProduction = configService.getOrThrow<string>("NODE_ENV") === "production";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
driver: PostgreSqlDriver,
|
driver: PostgreSqlDriver,
|
||||||
autoLoadEntities: true,
|
autoLoadEntities: true,
|
||||||
dbName: configService.getOrThrow<string>("DB_NAME"),
|
dbName: configService.getOrThrow<string>("DB_NAME"),
|
||||||
debug: configService.get<string>("NODE_ENV") !== "production",
|
debug: !isProduction,
|
||||||
clientUrl: `postgres://${DB_USER}:${encodedPassword}@${DB_HOST}:${DB_PORT}`,
|
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,
|
forceUtcTimezone: true,
|
||||||
pool: {
|
pool: {
|
||||||
min: 5,
|
min: isProduction ? 5 : 2,
|
||||||
max: 25,
|
max: isProduction ? 20 : 10,
|
||||||
idleTimeoutMillis: 30000,
|
idleTimeoutMillis: 30000,
|
||||||
acquireTimeoutMillis: 15000,
|
acquireTimeoutMillis: 30000,
|
||||||
reapIntervalMillis: 1000,
|
reapIntervalMillis: 1000,
|
||||||
createTimeoutMillis: 8000,
|
createTimeoutMillis: 15000,
|
||||||
destroyTimeoutMillis: 5000,
|
destroyTimeoutMillis: 5000,
|
||||||
createRetryIntervalMillis: 200,
|
|
||||||
propagateCreateError: false,
|
|
||||||
},
|
},
|
||||||
driverOptions: {
|
logger: isProduction ? (message) => logger.log(message) : (message) => logger.debug(message),
|
||||||
connection: {
|
|
||||||
keepAlive: true,
|
|
||||||
keepAliveInitialDelayMillis: 0,
|
|
||||||
statement_timeout: 60000,
|
|
||||||
query_timeout: 60000,
|
|
||||||
connectionTimeoutMillis: 10000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
logger: (message) => logger.debug(message),
|
|
||||||
schemaGenerator: {
|
schemaGenerator: {
|
||||||
createForeignKey: true,
|
createForeignKey: !isProduction,
|
||||||
disableForeignKeys: false,
|
disableForeignKeys: false,
|
||||||
createIndex: true,
|
createIndex: true,
|
||||||
},
|
},
|
||||||
@@ -59,6 +55,38 @@ export const databaseConfig: MikroOrmModuleAsyncOptions = {
|
|||||||
safe: true,
|
safe: true,
|
||||||
emit: "ts",
|
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;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { LoginDto } from "./DTO/login.dto";
|
|||||||
import { RefreshTokenDto } from "./DTO/refresh-token.dto";
|
import { RefreshTokenDto } from "./DTO/refresh-token.dto";
|
||||||
import { AuthService } from "./services/auth.service";
|
import { AuthService } from "./services/auth.service";
|
||||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
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";
|
import { UserDec } from "../../common/decorators/user.decorator";
|
||||||
|
|
||||||
@ApiTags("Authentication")
|
@ApiTags("Authentication")
|
||||||
@@ -13,6 +13,7 @@ import { UserDec } from "../../common/decorators/user.decorator";
|
|||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private readonly authService: AuthService) {}
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@StrictRateLimit()
|
||||||
@Post("login")
|
@Post("login")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: "Login user" })
|
@ApiOperation({ summary: "Login user" })
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class LocalJwtStrategy extends PassportStrategy(Strategy, LOCAL_JWT_STRAT
|
|||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
secretOrKey: configService.getOrThrow<string>("JWT_SECRET"),
|
secretOrKey: configService.getOrThrow<string>("JWT_SECRET_KEY"),
|
||||||
issuer: configService.getOrThrow<string>("JWT_ISSUER"),
|
issuer: configService.getOrThrow<string>("JWT_ISSUER"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const SUBSCRIPTIONS = Object.freeze({
|
export const SUBSCRIPTIONS = Object.freeze({
|
||||||
PROVISIONING_QUEUE_PREFIX: "provisioning",
|
PROVISIONING_QUEUE_PREFIX: "subs",
|
||||||
PROVISIONING_QUEUE_NAME: "provisioning",
|
PROVISIONING_QUEUE_NAME: "provisioning",
|
||||||
PROVISIONING_JOB_NAME: "business.created",
|
PROVISIONING_JOB_NAME: "business.created",
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export class DomainAutomationService {
|
|||||||
if (!mailServerUser.success) throw new BadRequestException(MailServerMessage.FAILED_TO_CREATE_ACCOUNT);
|
if (!mailServerUser.success) throw new BadRequestException(MailServerMessage.FAILED_TO_CREATE_ACCOUNT);
|
||||||
|
|
||||||
// Check if business has enough quota
|
// 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);
|
if (remainingQuota < QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION) throw new BadRequestException(BusinessMessage.INSUFFICIENT_QUOTA);
|
||||||
|
|
||||||
// Create user in local database
|
// Create user in local database
|
||||||
@@ -152,8 +152,8 @@ export class DomainAutomationService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update business quota after successful user creation
|
// Update business quota after successful user creation
|
||||||
domain.business.usedQuota = (domain.business.usedQuota || 0) + QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION;
|
domain.business.usedQuota = Number(domain.business.usedQuota || 0) + QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION;
|
||||||
domain.business.remainingQuota = (domain.business.quota || 0) - domain.business.usedQuota;
|
domain.business.remainingQuota = Number(domain.business.quota || 0) - Number(domain.business.usedQuota);
|
||||||
|
|
||||||
await this.em.persistAndFlush([user, domain.business]);
|
await this.em.persistAndFlush([user, domain.business]);
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,8 @@ export class DomainsService {
|
|||||||
|
|
||||||
const domain = await this.getDomainById(businessDomain.id, businessId);
|
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 is already verified, skip verification check and return DNS records directly
|
||||||
if (domain.isVerified && domain.status === DomainStatus.VERIFIED) {
|
if (domain.isVerified && domain.status === DomainStatus.VERIFIED) {
|
||||||
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id);
|
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 {
|
return {
|
||||||
dnsRecords,
|
dnsRecords,
|
||||||
@@ -304,7 +306,7 @@ export class DomainsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dnsRecords: verificationStatuses,
|
verificationStatuses,
|
||||||
overallStatus: {
|
overallStatus: {
|
||||||
isVerified,
|
isVerified,
|
||||||
isComplete,
|
isComplete,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export class UserRepository extends EntityRepository<User> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.findAndCount(whereClause, {
|
return this.findAndCount(whereClause, {
|
||||||
|
exclude: ["password"],
|
||||||
populate: ["domain", "business"],
|
populate: ["domain", "business"],
|
||||||
limit,
|
limit,
|
||||||
offset: skip,
|
offset: skip,
|
||||||
|
|||||||
@@ -57,9 +57,24 @@ export class UsersService {
|
|||||||
/*******************************/
|
/*******************************/
|
||||||
|
|
||||||
async getMe(userId: string) {
|
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);
|
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);
|
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
|
||||||
|
|
||||||
// Check if business has enough quota
|
// Check if business has enough quota
|
||||||
const remainingQuota = business.remainingQuota || 0;
|
const remainingQuota = Number(business.remainingQuota || 0);
|
||||||
if (remainingQuota < QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION) {
|
if (remainingQuota < QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION) {
|
||||||
throw new BadRequestException("Insufficient quota to create user. Please upgrade your plan.");
|
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
|
// Update business quota after successful user creation
|
||||||
business.usedQuota = business.usedQuota + QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION;
|
business.usedQuota = Number(business.usedQuota) + QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION;
|
||||||
business.remainingQuota = business.quota - business.usedQuota;
|
business.remainingQuota = Number(business.quota) - Number(business.usedQuota);
|
||||||
|
|
||||||
await this.em.persistAndFlush([user, business]);
|
await this.em.persistAndFlush([user, business]);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { UserListQueryDto } from "./DTO/user-list-query.dto";
|
|||||||
import { UsersService } from "./services/users.service";
|
import { UsersService } from "./services/users.service";
|
||||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
import { BusinessDec } from "../../common/decorators/business.decorator";
|
import { BusinessDec } from "../../common/decorators/business.decorator";
|
||||||
|
import { UserDec } from "../../common/decorators/user.decorator";
|
||||||
import { ParamDto } from "../../common/DTO/param.dto";
|
import { ParamDto } from "../../common/DTO/param.dto";
|
||||||
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
|
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
|
||||||
|
|
||||||
@@ -16,6 +17,13 @@ import { BusinessInterceptor } from "../../core/interceptors/business.intercepto
|
|||||||
export class UsersController {
|
export class UsersController {
|
||||||
constructor(private readonly usersService: UsersService) {}
|
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()
|
@Post()
|
||||||
@ApiOperation({ summary: "Create a new email user" })
|
@ApiOperation({ summary: "Create a new email user" })
|
||||||
@ApiResponse({ status: 201, description: "Email user created successfully" })
|
@ApiResponse({ status: 201, description: "Email user created successfully" })
|
||||||
|
|||||||
Reference in New Issue
Block a user