chore: add new logic for the quota
This commit is contained in:
@@ -324,6 +324,7 @@ export const enum BusinessMessage {
|
|||||||
DOMAIN_NOT_CONNECTED = "دامنه به سرور ما متصل نشد",
|
DOMAIN_NOT_CONNECTED = "دامنه به سرور ما متصل نشد",
|
||||||
DOMAIN_NOT_FOUND = "دامنه یافت نشد",
|
DOMAIN_NOT_FOUND = "دامنه یافت نشد",
|
||||||
DOMAIN_VERIFICATION_TOKEN_REQUIRED = "توکن تایید دامنه مورد نیاز است",
|
DOMAIN_VERIFICATION_TOKEN_REQUIRED = "توکن تایید دامنه مورد نیاز است",
|
||||||
|
INSUFFICIENT_QUOTA = "حجم شما برای ساخت ایمیل کافی نمی باشد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum DomainMessage {
|
export const enum DomainMessage {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
export class QuotaInfoDto {
|
||||||
|
@ApiProperty({ description: "Total quota in bytes", example: 134217728 })
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "Used quota in bytes", example: 67108864 })
|
||||||
|
used: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "Remaining quota in bytes", example: 67108864 })
|
||||||
|
remaining: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "Total quota in MB", example: 128 })
|
||||||
|
totalInMB: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "Used quota in MB", example: 64 })
|
||||||
|
usedInMB: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "Remaining quota in MB", example: 64 })
|
||||||
|
remainingInMB: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "Usage percentage", example: 50 })
|
||||||
|
usagePercentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BusinessQuotaResponseDto {
|
||||||
|
@ApiProperty({ description: "Business quota information", type: QuotaInfoDto })
|
||||||
|
quota: QuotaInfoDto;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Body, Controller, Get, Param, Patch, UseInterceptors } from "@nestjs/common";
|
import { Body, Controller, Get, Param, Patch, UseInterceptors } from "@nestjs/common";
|
||||||
import { ApiHeader, ApiOperation } from "@nestjs/swagger";
|
import { ApiHeader, ApiOperation, ApiResponse } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { BusinessQuotaResponseDto } from "./DTO/business-quota-response.dto";
|
||||||
import { BusinessSlugParamDto } from "./DTO/business-slug-param.dto";
|
import { BusinessSlugParamDto } from "./DTO/business-slug-param.dto";
|
||||||
import { UpdateBusinessSettingsDto } from "./DTO/update-business-settings.dto";
|
import { UpdateBusinessSettingsDto } from "./DTO/update-business-settings.dto";
|
||||||
import { BusinessesService } from "./services/businesses.service";
|
import { BusinessesService } from "./services/businesses.service";
|
||||||
@@ -34,4 +35,13 @@ export class BusinessesController {
|
|||||||
getBusinessSettings(@BusinessDec("id") businessId: string) {
|
getBusinessSettings(@BusinessDec("id") businessId: string) {
|
||||||
return this.businessesService.getBusinessSettings(businessId);
|
return this.businessesService.getBusinessSettings(businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("quota")
|
||||||
|
@UseInterceptors(BusinessInterceptor)
|
||||||
|
@ApiOperation({ summary: "Get business quota information" })
|
||||||
|
@ApiResponse({ status: 200, description: "Business quota information retrieved successfully", type: BusinessQuotaResponseDto })
|
||||||
|
@AuthGuards()
|
||||||
|
getBusinessQuota(@BusinessDec("id") businessId: string) {
|
||||||
|
return this.businessesService.getBusinessQuota(businessId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,3 +7,9 @@ export const SUBSCRIPTIONS = Object.freeze({
|
|||||||
SERVICE_SLUG: "Dmail",
|
SERVICE_SLUG: "Dmail",
|
||||||
SERVICE_ID: "e51afdc3-ea0b-49cf-8f49-2a6f131b024e",
|
SERVICE_ID: "e51afdc3-ea0b-49cf-8f49-2a6f131b024e",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const QUOTA_CONSTANTS = Object.freeze({
|
||||||
|
INITIAL_BUSINESS_QUOTA: 1024 * 1024 * 1024, // 1GB initial quota when purchasing dmail service (allows 1 user)
|
||||||
|
USER_QUOTA_DEDUCTION: 128 * 1024 * 1024, // 128MB deducted per user creation
|
||||||
|
DEFAULT_EMAIL_QUOTA_BYTES: 128 * 1024 * 1024, // 128MB per user in bytes
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Cascade, Collection, Entity, EntityRepositoryType, OneToMany, OneToOne, Property, Unique } from "@mikro-orm/core";
|
import { Cascade, Collection, Entity, EntityRepositoryType, OneToMany, OneToOne, Opt, Property, Unique } from "@mikro-orm/core";
|
||||||
|
|
||||||
import { BusinessStaff } from "./business-staff.entity";
|
import { BusinessStaff } from "./business-staff.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
@@ -20,6 +20,15 @@ export class Business extends BaseEntity {
|
|||||||
@Property({ type: "varchar", length: 255, nullable: true })
|
@Property({ type: "varchar", length: 255, nullable: true })
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
|
|
||||||
|
@Property({ type: "bigint", default: 1_073_741_824 }) // 1GB default
|
||||||
|
quota!: number;
|
||||||
|
|
||||||
|
@Property({ type: "bigint", default: 0 })
|
||||||
|
usedQuota!: number & Opt;
|
||||||
|
|
||||||
|
@Property({ type: "bigint", default: 1_073_741_824 })
|
||||||
|
remainingQuota!: number & Opt;
|
||||||
|
|
||||||
//=========================
|
//=========================
|
||||||
|
|
||||||
@OneToOne(() => Domain, (domain) => domain.business, { nullable: true, cascade: [Cascade.ALL] })
|
@OneToOne(() => Domain, (domain) => domain.business, { nullable: true, cascade: [Cascade.ALL] })
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Logger } from "@nestjs/common";
|
|||||||
import { Job } from "bullmq";
|
import { Job } from "bullmq";
|
||||||
|
|
||||||
import { WorkerProcessor } from "../../../common/queues/worker.processor";
|
import { WorkerProcessor } from "../../../common/queues/worker.processor";
|
||||||
import { SUBSCRIPTIONS } from "../constant";
|
import { QUOTA_CONSTANTS, SUBSCRIPTIONS } from "../constant";
|
||||||
import { BusinessStaff } from "../entities/business-staff.entity";
|
import { BusinessStaff } from "../entities/business-staff.entity";
|
||||||
import { Business } from "../entities/business.entity";
|
import { Business } from "../entities/business.entity";
|
||||||
import { IBusinessProvisioningJob } from "../interfaces/IBusiness";
|
import { IBusinessProvisioningJob } from "../interfaces/IBusiness";
|
||||||
@@ -45,9 +45,12 @@ export class BusinessProvisioningProcessor extends WorkerProcessor {
|
|||||||
danakSubscriptionId: subscriptionId,
|
danakSubscriptionId: subscriptionId,
|
||||||
name: businessName,
|
name: businessName,
|
||||||
slug: slug,
|
slug: slug,
|
||||||
|
quota: QUOTA_CONSTANTS.INITIAL_BUSINESS_QUOTA,
|
||||||
|
usedQuota: 0,
|
||||||
|
remainingQuota: QUOTA_CONSTANTS.INITIAL_BUSINESS_QUOTA,
|
||||||
});
|
});
|
||||||
await em.persistAndFlush(business);
|
await em.persistAndFlush(business);
|
||||||
this.logger.log(`Created business for subscription ${subscriptionId}`);
|
this.logger.log(`Created business for subscription ${subscriptionId} with initial quota: ${QUOTA_CONSTANTS.INITIAL_BUSINESS_QUOTA}`);
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug(`Business already exists for subscription ${subscriptionId}`);
|
this.logger.debug(`Business already exists for subscription ${subscriptionId}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,4 +85,25 @@ export class BusinessesService {
|
|||||||
if (!staff) throw new BadRequestException(BusinessMessage.STAFF_NOT_FOUND);
|
if (!staff) throw new BadRequestException(BusinessMessage.STAFF_NOT_FOUND);
|
||||||
return staff;
|
return staff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//************************************************** */
|
||||||
|
async getBusinessQuota(businessId: string) {
|
||||||
|
const business = await this.businessRepository.findOne({ id: businessId, deletedAt: null });
|
||||||
|
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
|
||||||
|
|
||||||
|
return {
|
||||||
|
quota: {
|
||||||
|
total: Number(business.quota),
|
||||||
|
used: Number(business.usedQuota),
|
||||||
|
remaining: Number(business.remainingQuota),
|
||||||
|
totalInMB: Math.round(Number(business.quota) / (1024 * 1024)),
|
||||||
|
totalInGB: Math.round(Number(business.quota) / (1024 * 1024 * 1024)),
|
||||||
|
usedInMB: Math.round(Number(business.usedQuota) / (1024 * 1024)),
|
||||||
|
usedInGB: Math.round(Number(business.usedQuota) / (1024 * 1024 * 1024)),
|
||||||
|
remainingInMB: Math.round(Number(business.remainingQuota) / (1024 * 1024)),
|
||||||
|
remainingInGB: Math.round(Number(business.remainingQuota) / (1024 * 1024 * 1024)),
|
||||||
|
usagePercentage: Math.round((Number(business.usedQuota) / Number(business.quota)) * 100),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ export class DnsService {
|
|||||||
// Check if the expected value matches either the joined record or any individual part
|
// Check if the expected value matches either the joined record or any individual part
|
||||||
return joinedRecord === dnsRecord.value || flatRecords.some((record) => record.includes(dnsRecord.value) || record === dnsRecord.value);
|
return joinedRecord === dnsRecord.value || flatRecords.some((record) => record.includes(dnsRecord.value) || record === dnsRecord.value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.debug(`TXT record verification failed for ${dnsRecord.name}:`, error instanceof Error ? error.message : "Unknown error");
|
this.logger.debug(`TXT record verification failed for ${dnsRecord.fullName}:`, error instanceof Error ? error.message : "Unknown error");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { EntityManager } from "@mikro-orm/core";
|
import { EntityManager } from "@mikro-orm/core";
|
||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { BusinessMessage, MailServerMessage, RoleMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { QUOTA_CONSTANTS } from "../../businesses/constant";
|
||||||
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
||||||
import { MailboxEnum } from "../../mailbox/enums/mailbox.enum";
|
import { MailboxEnum } from "../../mailbox/enums/mailbox.enum";
|
||||||
import { Role } from "../../users/entities/role.entity";
|
import { Role } from "../../users/entities/role.entity";
|
||||||
@@ -114,9 +116,7 @@ export class DomainAutomationService {
|
|||||||
|
|
||||||
// Get user role
|
// Get user role
|
||||||
const userRole = await this.em.findOne(Role, { name: RoleEnum.USER });
|
const userRole = await this.em.findOne(Role, { name: RoleEnum.USER });
|
||||||
if (!userRole) {
|
if (!userRole) throw new BadRequestException(RoleMessage.NOT_FOUND);
|
||||||
throw new Error("User role not found in database");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create user in mail server (WildDuck)
|
// Create user in mail server (WildDuck)
|
||||||
const mailServerUser = await firstValueFrom(
|
const mailServerUser = await firstValueFrom(
|
||||||
@@ -125,14 +125,15 @@ export class DomainAutomationService {
|
|||||||
password,
|
password,
|
||||||
address: infoEmail,
|
address: infoEmail,
|
||||||
name: `Info - ${domain.business.name}`,
|
name: `Info - ${domain.business.name}`,
|
||||||
quota: 1073741824, // 1GB default quota
|
quota: QUOTA_CONSTANTS.DEFAULT_EMAIL_QUOTA_BYTES,
|
||||||
language: "fa",
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!mailServerUser.success) {
|
if (!mailServerUser.success) throw new BadRequestException(MailServerMessage.FAILED_TO_CREATE_ACCOUNT);
|
||||||
throw new Error(`Failed to create mail server user`);
|
|
||||||
}
|
// Check if business has enough quota
|
||||||
|
const remainingQuota = domain.business.remainingQuota || 0;
|
||||||
|
if (remainingQuota < QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION) throw new BadRequestException(BusinessMessage.INSUFFICIENT_QUOTA);
|
||||||
|
|
||||||
// Create user in local database
|
// Create user in local database
|
||||||
const user = this.em.create(User, {
|
const user = this.em.create(User, {
|
||||||
@@ -141,7 +142,7 @@ export class DomainAutomationService {
|
|||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
emailAddress: infoEmail,
|
emailAddress: infoEmail,
|
||||||
emailEnabled: true,
|
emailEnabled: true,
|
||||||
emailQuota: 1073741824, // 1GB default
|
emailQuota: QUOTA_CONSTANTS.DEFAULT_EMAIL_QUOTA_BYTES,
|
||||||
wildduckUserId: mailServerUser.id,
|
wildduckUserId: mailServerUser.id,
|
||||||
displayName: `Info - ${domain.business.name}`,
|
displayName: `Info - ${domain.business.name}`,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
@@ -150,7 +151,11 @@ export class DomainAutomationService {
|
|||||||
role: userRole,
|
role: userRole,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.em.persistAndFlush(user);
|
// 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;
|
||||||
|
|
||||||
|
await this.em.persistAndFlush([user, domain.business]);
|
||||||
|
|
||||||
// Create default mailboxes (Archive and Favorite)
|
// Create default mailboxes (Archive and Favorite)
|
||||||
await this.createDefaultMailboxes(mailServerUser.id);
|
await this.createDefaultMailboxes(mailServerUser.id);
|
||||||
|
|||||||
@@ -216,9 +216,26 @@ export class DomainsService {
|
|||||||
|
|
||||||
const domain = await this.getDomainById(businessDomain.id, businessId);
|
const domain = await this.getDomainById(businessDomain.id, businessId);
|
||||||
|
|
||||||
const { overallStatus, recommendations } = await this.checkDomainVerificationStatus(businessId);
|
// 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);
|
||||||
|
return {
|
||||||
|
dnsRecords,
|
||||||
|
overallStatus: {
|
||||||
|
isVerified: true,
|
||||||
|
isComplete: true,
|
||||||
|
verified: dnsRecords.length,
|
||||||
|
pending: 0,
|
||||||
|
failed: 0,
|
||||||
|
total: dnsRecords.length,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
},
|
||||||
|
recommendations: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dnsRecords, overallStatus, recommendations } = await this.checkDomainVerificationStatus(businessId);
|
||||||
|
|
||||||
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id);
|
|
||||||
return {
|
return {
|
||||||
dnsRecords,
|
dnsRecords,
|
||||||
overallStatus,
|
overallStatus,
|
||||||
@@ -235,17 +252,34 @@ export class DomainsService {
|
|||||||
|
|
||||||
const domain = await this.getDomainById(businessDomain.id, businessId);
|
const domain = await this.getDomainById(businessDomain.id, businessId);
|
||||||
|
|
||||||
// // Check DNS records for verification
|
// If domain is already verified, skip verification check
|
||||||
// await this.dnsService.verifyDomainDNS(domain);
|
if (domain.isVerified && domain.status === DomainStatus.VERIFIED) {
|
||||||
|
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dnsRecords,
|
||||||
|
overallStatus: {
|
||||||
|
isVerified: true,
|
||||||
|
isComplete: true,
|
||||||
|
verified: dnsRecords.length,
|
||||||
|
pending: 0,
|
||||||
|
failed: 0,
|
||||||
|
total: dnsRecords.length,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
},
|
||||||
|
recommendations: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id);
|
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id);
|
||||||
|
|
||||||
// Process DNS records for verification status
|
// Process DNS records for verification status
|
||||||
const { verificationStatuses, statusCounts, recommendations } = await this.processDnsRecordsForVerification(dnsRecords);
|
const { verificationStatuses, statusCounts, recommendations } = await this.processDnsRecordsForVerification(dnsRecords);
|
||||||
|
|
||||||
const isVerified = statusCounts.failed === 0 && statusCounts.pending === 0 && statusCounts.verified > 0;
|
// Only mark as verified if ALL DNS records are verified (no failed, no pending)
|
||||||
|
const isVerified = statusCounts.failed === 0 && statusCounts.pending === 0 && statusCounts.verified === dnsRecords.length;
|
||||||
|
|
||||||
const isComplete = statusCounts.failed === 0 && statusCounts.pending === 0 && statusCounts.verified > 0;
|
const isComplete = statusCounts.failed === 0 && statusCounts.pending === 0 && statusCounts.verified === dnsRecords.length;
|
||||||
|
|
||||||
if (isVerified && isComplete && !domain.isVerified) {
|
if (isVerified && isComplete && !domain.isVerified) {
|
||||||
// Update domain status
|
// Update domain status
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, OneToOne, Property } from "@mikro-orm/core";
|
import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, OneToOne, Opt, Property } from "@mikro-orm/core";
|
||||||
|
|
||||||
import { RefreshToken } from "./refresh-token.entity";
|
import { RefreshToken } from "./refresh-token.entity";
|
||||||
import { Role } from "./role.entity";
|
import { Role } from "./role.entity";
|
||||||
@@ -19,30 +19,30 @@ export class User extends BaseEntity {
|
|||||||
@Property({ type: "varchar", length: 150, nullable: false })
|
@Property({ type: "varchar", length: 150, nullable: false })
|
||||||
password!: string;
|
password!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 255, nullable: true })
|
@Property({ type: "varchar", length: 255, nullable: false })
|
||||||
emailAddress?: string; // Full email address
|
emailAddress!: string;
|
||||||
|
|
||||||
@Property({ type: "boolean", default: false })
|
@Property({ type: "boolean", default: false })
|
||||||
emailEnabled: boolean = false; // Whether email is enabled for this user
|
emailEnabled: boolean & Opt;
|
||||||
|
|
||||||
@Property({ type: "bigint", nullable: true, default: 1073741824 }) // 1GB default
|
@Property({ type: "bigint", nullable: false, default: 134_217_728 }) // 128MB default
|
||||||
emailQuota?: number; // Email quota in bytes
|
emailQuota!: number;
|
||||||
|
|
||||||
@Property({ type: "bigint", nullable: true, default: 0 })
|
@Property({ type: "bigint", nullable: false, default: 0 })
|
||||||
emailQuotaUsed?: number; // Email quota used in bytes
|
emailQuotaUsed!: number & Opt;
|
||||||
|
|
||||||
// Email account integration fields
|
// Email account integration fields
|
||||||
@Property({ type: "varchar", length: 255, nullable: false })
|
@Property({ type: "varchar", length: 255, nullable: false })
|
||||||
wildduckUserId: string; // WildDuck user ID
|
wildduckUserId!: string;
|
||||||
|
|
||||||
@Property({ type: "varchar", length: 255, nullable: true })
|
@Property({ type: "varchar", length: 255, nullable: true })
|
||||||
displayName?: string; // Display name for email
|
displayName?: string;
|
||||||
|
|
||||||
@Property({ type: "timestamptz", nullable: true })
|
@Property({ type: "timestamptz", nullable: true })
|
||||||
lastQuotaSync?: Date; // Last time quota was synced with WildDuck
|
lastQuotaSync?: Date;
|
||||||
|
|
||||||
@Property({ default: true, nullable: false })
|
@Property({ default: true, nullable: false })
|
||||||
isActive: boolean = true;
|
isActive: boolean & Opt;
|
||||||
|
|
||||||
//=========================
|
//=========================
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export class User extends BaseEntity {
|
|||||||
business!: Business;
|
business!: Business;
|
||||||
|
|
||||||
@ManyToOne(() => Domain, { deleteRule: "restrict" })
|
@ManyToOne(() => Domain, { deleteRule: "restrict" })
|
||||||
domain!: Domain; // The domain this email user belongs to
|
domain!: Domain;
|
||||||
|
|
||||||
[EntityRepositoryType]?: UserRepository;
|
[EntityRepositoryType]?: UserRepository;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
DANAK_SMTPS_SERVER,
|
DANAK_SMTPS_SERVER,
|
||||||
} from "../../../common/constants";
|
} from "../../../common/constants";
|
||||||
import { BusinessMessage, DomainMessage, MailServerMessage, RoleMessage, UserMessage } from "../../../common/enums/message.enum";
|
import { BusinessMessage, DomainMessage, MailServerMessage, RoleMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { QUOTA_CONSTANTS } from "../../businesses/constant";
|
||||||
import { Business } from "../../businesses/entities/business.entity";
|
import { Business } from "../../businesses/entities/business.entity";
|
||||||
import { DomainStatus } from "../../domains/enums/domain-status.enum";
|
import { DomainStatus } from "../../domains/enums/domain-status.enum";
|
||||||
import { DomainAutomationService } from "../../domains/services/domain-automation.service";
|
import { DomainAutomationService } from "../../domains/services/domain-automation.service";
|
||||||
@@ -75,6 +76,12 @@ export class UsersService {
|
|||||||
const business = await this.em.findOne(Business, { id: businessId });
|
const business = await this.em.findOne(Business, { id: businessId });
|
||||||
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
|
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
|
||||||
|
|
||||||
|
// Check if business has enough quota
|
||||||
|
const remainingQuota = business.remainingQuota || 0;
|
||||||
|
if (remainingQuota < QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION) {
|
||||||
|
throw new BadRequestException("Insufficient quota to create user. Please upgrade your plan.");
|
||||||
|
}
|
||||||
|
|
||||||
const domain = await this.domainsService.getDomainById(domainId, businessId);
|
const domain = await this.domainsService.getDomainById(domainId, businessId);
|
||||||
|
|
||||||
if (domain.business.id !== businessId) throw new BadRequestException(DomainMessage.NOT_BELONG_TO_BUSINESS);
|
if (domain.business.id !== businessId) throw new BadRequestException(DomainMessage.NOT_BELONG_TO_BUSINESS);
|
||||||
@@ -92,14 +99,15 @@ export class UsersService {
|
|||||||
if (!userRole) throw new BadRequestException(RoleMessage.NOT_FOUND);
|
if (!userRole) throw new BadRequestException(RoleMessage.NOT_FOUND);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const userQuota = quota || QUOTA_CONSTANTS.DEFAULT_EMAIL_QUOTA_BYTES;
|
||||||
|
|
||||||
const mailServerUser = await firstValueFrom(
|
const mailServerUser = await firstValueFrom(
|
||||||
this.mailServerService.users.createUser({
|
this.mailServerService.users.createUser({
|
||||||
username: `${username}-${domain.name}`,
|
username: `${username}-${domain.name}`,
|
||||||
password,
|
password,
|
||||||
address: emailAddress,
|
address: emailAddress,
|
||||||
name: displayName || username,
|
name: displayName || username,
|
||||||
quota: quota || 1073741824,
|
quota: userQuota,
|
||||||
// language: "fa",
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -111,7 +119,7 @@ export class UsersService {
|
|||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
emailAddress,
|
emailAddress,
|
||||||
emailEnabled: true,
|
emailEnabled: true,
|
||||||
emailQuota: quota || 1073741824,
|
emailQuota: userQuota,
|
||||||
wildduckUserId: mailServerUser.id,
|
wildduckUserId: mailServerUser.id,
|
||||||
displayName: displayName || username,
|
displayName: displayName || username,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
@@ -120,7 +128,11 @@ export class UsersService {
|
|||||||
role: userRole,
|
role: userRole,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.em.persistAndFlush(user);
|
// Update business quota after successful user creation
|
||||||
|
business.usedQuota = business.usedQuota + QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION;
|
||||||
|
business.remainingQuota = business.quota - business.usedQuota;
|
||||||
|
|
||||||
|
await this.em.persistAndFlush([user, business]);
|
||||||
|
|
||||||
if (aliases && aliases.length > 0) {
|
if (aliases && aliases.length > 0) {
|
||||||
for (const alias of aliases) {
|
for (const alias of aliases) {
|
||||||
|
|||||||
Reference in New Issue
Block a user