chore: new feture for quato

This commit is contained in:
mahyargdz
2025-07-07 16:21:30 +03:30
parent 6565d2ae8b
commit b66114c900
21 changed files with 815 additions and 98 deletions
@@ -0,0 +1,49 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsBoolean, IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator";
import { SignatureMessage } from "../../../common/enums/message.enum";
export class CreateEmailSignatureDto {
@IsNotEmpty({ message: SignatureMessage.NAME_NOT_EMPTY })
@IsString({ message: SignatureMessage.NAME_STRING })
@MaxLength(100, { message: SignatureMessage.NAME_MAX_LENGTH })
@ApiProperty({ description: "Name for the email signature", example: "Business Signature", maxLength: 100 })
name: string;
@IsOptional()
@IsNotEmpty({ message: SignatureMessage.TEXT_CONTENT_NOT_EMPTY })
@IsString({ message: SignatureMessage.TEXT_CONTENT_STRING })
@MaxLength(5000, { message: SignatureMessage.TEXT_CONTENT_MAX_LENGTH })
@ApiPropertyOptional({
description: "Plain text content of the signature",
example: "John Doe\nSoftware Engineer\nCompany Inc.\njohn@company.com",
maxLength: 5000,
})
textContent?: string;
@IsOptional()
@IsString()
@MaxLength(10000)
@ApiPropertyOptional({
description: "HTML content of the signature",
example: "<div><strong>John Doe</strong><br>Software Engineer<br>Company Inc.<br><a href='mailto:john@company.com'>john@company.com</a></div>",
maxLength: 10000,
})
htmlContent?: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({
description: "Whether this signature is active",
default: true,
})
isActive?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({
description: "Whether this is the default signature for the user",
default: false,
})
isDefault?: boolean;
}
@@ -0,0 +1,50 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsBoolean, IsOptional, IsString, MaxLength } from "class-validator";
export class UpdateEmailSignatureDto {
@IsOptional()
@IsString()
@MaxLength(100)
@ApiPropertyOptional({
description: "Name for the email signature",
example: "Business Signature",
maxLength: 100,
})
name?: string;
@IsOptional()
@IsString()
@MaxLength(5000)
@ApiPropertyOptional({
description: "Plain text content of the signature",
example: "John Doe\nSoftware Engineer\nCompany Inc.\njohn@company.com",
maxLength: 5000,
})
textContent?: string;
@IsOptional()
@IsString()
@MaxLength(10000)
@ApiPropertyOptional({
description: "HTML content of the signature",
example: "<div><strong>John Doe</strong><br>Software Engineer<br>Company Inc.<br><a href='mailto:john@company.com'>john@company.com</a></div>",
maxLength: 10000,
})
htmlContent?: string;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({
description: "Whether this signature is active",
default: true,
})
isActive?: boolean;
@IsOptional()
@IsBoolean()
@ApiPropertyOptional({
description: "Whether this is the default signature for the user",
default: false,
})
isDefault?: boolean;
}
@@ -0,0 +1,97 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, UseGuards } from "@nestjs/common";
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { CreateEmailSignatureDto } from "./DTO/create-email-signature.dto";
import { UpdateEmailSignatureDto } from "./DTO/update-email-signature.dto";
import { EmailSignature } from "./entities/email-signature.entity";
import { EmailSignaturesService } from "./services/email-signatures.service";
import { UserDec } from "../../common/decorators/user.decorator";
import { JwtAuthGuard } from "../auth/guards/auth.guard";
import { User } from "../users/entities/user.entity";
@ApiTags("Email Signatures")
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller("email-signatures")
export class EmailSignaturesController {
constructor(private readonly emailSignaturesService: EmailSignaturesService) {}
@Post()
@ApiOperation({ summary: "Create a new email signature" })
@ApiResponse({ status: 201, description: "Email signature created successfully", type: EmailSignature })
@ApiResponse({ status: 400, description: "Bad request" })
async create(@UserDec() user: User, @Body() createEmailSignatureDto: CreateEmailSignatureDto): Promise<EmailSignature> {
return this.emailSignaturesService.create(user.id, createEmailSignatureDto);
}
@Get()
@ApiOperation({ summary: "Get all email signatures for the user" })
@ApiResponse({ status: 200, description: "Email signatures retrieved successfully", type: [EmailSignature] })
async findAll(@UserDec() user: User): Promise<EmailSignature[]> {
return this.emailSignaturesService.findAll(user.id);
}
@Get("active")
@ApiOperation({ summary: "Get all active email signatures for the user" })
@ApiResponse({ status: 200, description: "Active email signatures retrieved successfully", type: [EmailSignature] })
async findActive(@UserDec() user: User): Promise<EmailSignature[]> {
return this.emailSignaturesService.findActive(user.id);
}
@Get("default")
@ApiOperation({ summary: "Get the default email signature for the user" })
@ApiResponse({ status: 200, description: "Default email signature retrieved successfully", type: EmailSignature })
@ApiResponse({ status: 404, description: "No default signature found" })
async findDefault(@UserDec() user: User): Promise<EmailSignature | null> {
return this.emailSignaturesService.findDefault(user.id);
}
@Get(":id")
@ApiOperation({ summary: "Get a specific email signature" })
@ApiResponse({ status: 200, description: "Email signature retrieved successfully", type: EmailSignature })
@ApiResponse({ status: 404, description: "Email signature not found" })
async findOne(@Param("id") id: string, @UserDec() user: User): Promise<EmailSignature> {
return this.emailSignaturesService.findOne(id, user.id);
}
@Patch(":id")
@ApiOperation({ summary: "Update an email signature" })
@ApiResponse({ status: 200, description: "Email signature updated successfully", type: EmailSignature })
@ApiResponse({ status: 404, description: "Email signature not found" })
async update(@Param("id") id: string, @UserDec() user: User, @Body() updateEmailSignatureDto: UpdateEmailSignatureDto): Promise<EmailSignature> {
return this.emailSignaturesService.update(id, user.id, updateEmailSignatureDto);
}
@Put(":id/set-default")
@ApiOperation({ summary: "Set an email signature as default" })
@ApiResponse({ status: 200, description: "Email signature set as default successfully", type: EmailSignature })
@ApiResponse({ status: 400, description: "Cannot set inactive signature as default" })
@ApiResponse({ status: 404, description: "Email signature not found" })
async setAsDefault(@Param("id") id: string, @UserDec() user: User): Promise<EmailSignature> {
return this.emailSignaturesService.setAsDefault(id, user.id);
}
@Put(":id/activate")
@ApiOperation({ summary: "Activate an email signature" })
@ApiResponse({ status: 200, description: "Email signature activated successfully", type: EmailSignature })
@ApiResponse({ status: 404, description: "Email signature not found" })
async activate(@Param("id") id: string, @UserDec() user: User): Promise<EmailSignature> {
return this.emailSignaturesService.activate(id, user.id);
}
@Put(":id/deactivate")
@ApiOperation({ summary: "Deactivate an email signature" })
@ApiResponse({ status: 200, description: "Email signature deactivated successfully", type: EmailSignature })
@ApiResponse({ status: 404, description: "Email signature not found" })
async deactivate(@Param("id") id: string, @UserDec() user: User): Promise<EmailSignature> {
return this.emailSignaturesService.deactivate(id, user.id);
}
@Delete(":id")
@ApiOperation({ summary: "Delete an email signature" })
@ApiResponse({ status: 200, description: "Email signature deleted successfully" })
@ApiResponse({ status: 404, description: "Email signature not found" })
async remove(@Param("id") id: string, @UserDec() user: User): Promise<void> {
return this.emailSignaturesService.remove(id, user.id);
}
}
@@ -0,0 +1,15 @@
import { MikroOrmModule } from "@mikro-orm/nestjs";
import { Module } from "@nestjs/common";
import { EmailSignaturesController } from "./email-signatures.controller";
import { EmailSignature } from "./entities/email-signature.entity";
import { EmailSignatureRepository } from "./repositories/email-signature.repository";
import { EmailSignaturesService } from "./services/email-signatures.service";
@Module({
imports: [MikroOrmModule.forFeature([EmailSignature])],
controllers: [EmailSignaturesController],
providers: [EmailSignaturesService, EmailSignatureRepository],
exports: [EmailSignaturesService],
})
export class EmailSignaturesModule {}
@@ -0,0 +1,29 @@
import { Entity, EntityRepositoryType, Index, OneToOne, Opt, Property } from "@mikro-orm/core";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from "../../users/entities/user.entity";
import { EmailSignatureRepository } from "../repositories/email-signature.repository";
@Entity({ repository: () => EmailSignatureRepository })
@Index({ properties: ["user", "isActive"] })
export class EmailSignature extends BaseEntity {
@OneToOne(() => User, (user) => user.emailSignature, { owner: true, deleteRule: "cascade" })
user!: User;
@Property({ type: "varchar", length: 100 })
name!: string;
@Property({ type: "text", nullable: true })
textContent?: string;
// @Property({ type: "text", nullable: true })
// htmlContent?: string;
@Property({ type: "boolean", default: true })
isActive!: boolean & Opt;
@Property({ type: "boolean", default: false })
isDefault!: boolean & Opt;
[EntityRepositoryType]?: EmailSignatureRepository;
}
@@ -0,0 +1,28 @@
import { EntityRepository } from "@mikro-orm/postgresql";
import { EmailSignature } from "../entities/email-signature.entity";
export class EmailSignatureRepository extends EntityRepository<EmailSignature> {
async findByUserId(userId: string): Promise<EmailSignature[]> {
return this.find({ user: userId }, { orderBy: { isDefault: "DESC", createdAt: "DESC" } });
}
async findActiveByUserId(userId: string): Promise<EmailSignature[]> {
return this.find({ user: userId, isActive: true }, { orderBy: { isDefault: "DESC", createdAt: "DESC" } });
}
async findDefaultByUserId(userId: string): Promise<EmailSignature | null> {
return this.findOne({ user: userId, isDefault: true, isActive: true });
}
async findByIdAndUserId(id: string, userId: string): Promise<EmailSignature | null> {
return this.findOne({ id, user: userId });
}
async updateDefaultSignature(userId: string, newDefaultId: string): Promise<void> {
// remove default from all signatures for this user
await this.nativeUpdate({ user: userId }, { isDefault: false });
// set the new default
await this.nativeUpdate({ id: newDefaultId, user: userId }, { isDefault: true });
}
}
@@ -0,0 +1,106 @@
import { EntityManager } from "@mikro-orm/core";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { User } from "../../users/entities/user.entity";
import { CreateEmailSignatureDto } from "../DTO/create-email-signature.dto";
import { UpdateEmailSignatureDto } from "../DTO/update-email-signature.dto";
import { EmailSignature } from "../entities/email-signature.entity";
import { EmailSignatureRepository } from "../repositories/email-signature.repository";
@Injectable()
export class EmailSignaturesService {
constructor(
private readonly emailSignatureRepository: EmailSignatureRepository,
private readonly em: EntityManager,
) {}
async create(userId: string, createDto: CreateEmailSignatureDto): Promise<EmailSignature> {
// Validate that at least one content type is provided
if (!createDto.textContent && !createDto.htmlContent) {
throw new BadRequestException("At least one content type (text or HTML) must be provided");
}
if (createDto.isDefault) await this.emailSignatureRepository.updateDefaultSignature(userId, "");
const signature = this.emailSignatureRepository.create({
...createDto,
user: { id: userId } as User,
});
await this.em.persistAndFlush(signature);
return signature;
}
async findAll(userId: string): Promise<EmailSignature[]> {
return this.emailSignatureRepository.findByUserId(userId);
}
async findActive(userId: string): Promise<EmailSignature[]> {
return this.emailSignatureRepository.findActiveByUserId(userId);
}
async findDefault(userId: string): Promise<EmailSignature | null> {
return this.emailSignatureRepository.findDefaultByUserId(userId);
}
async findOne(id: string, userId: string): Promise<EmailSignature> {
const signature = await this.emailSignatureRepository.findByIdAndUserId(id, userId);
if (!signature) {
throw new NotFoundException("Email signature not found");
}
return signature;
}
async update(id: string, userId: string, updateDto: UpdateEmailSignatureDto): Promise<EmailSignature> {
const signature = await this.findOne(id, userId);
// If updating to default, remove default from other signatures
if (updateDto.isDefault && !signature.isDefault) {
await this.emailSignatureRepository.updateDefaultSignature(userId, id);
}
// Update the signature
Object.assign(signature, updateDto);
await this.em.flush();
return signature;
}
async setAsDefault(id: string, userId: string): Promise<EmailSignature> {
const signature = await this.findOne(id, userId);
if (!signature.isActive) {
throw new BadRequestException("Cannot set inactive signature as default");
}
await this.emailSignatureRepository.updateDefaultSignature(userId, id);
signature.isDefault = true;
return signature;
}
async remove(id: string, userId: string): Promise<void> {
const signature = await this.findOne(id, userId);
await this.em.removeAndFlush(signature);
}
async activate(id: string, userId: string): Promise<EmailSignature> {
const signature = await this.findOne(id, userId);
signature.isActive = true;
await this.em.flush();
return signature;
}
async deactivate(id: string, userId: string): Promise<EmailSignature> {
const signature = await this.findOne(id, userId);
// If this is the default signature, we need to handle it
if (signature.isDefault) {
signature.isDefault = false;
}
signature.isActive = false;
await this.em.flush();
return signature;
}
}
@@ -33,90 +33,76 @@ export interface UserStorageInfo {
quota: number;
}
export interface UserKeyInfo {
name: string;
address: string;
fingerprint: string;
}
export interface UserQuotaLimits {
allowed: number;
used: number;
}
export interface UserTimeLimits {
allowed: number;
used: number;
ttl: number;
}
export interface UserConnectionLimits {
allowed: number;
used: number;
}
export interface UserLimits {
quota: {
allowed: number;
used: number;
};
recipients: {
allowed: number;
used: number;
ttl: number | false;
};
forwards: {
allowed: number;
used: number;
ttl: number | false;
};
received: {
allowed: number;
used: number;
ttl: number | false;
};
imapUpload: {
allowed: number;
used: number;
ttl: number | false;
};
imapDownload: {
allowed: number;
used: number;
ttl: number | false;
};
pop3Download: {
allowed: number;
used: number;
ttl: number | false;
};
imapMaxConnections: {
quota: UserQuotaLimits;
recipients: UserTimeLimits;
forwards: UserTimeLimits;
received: UserTimeLimits;
imapUpload: UserTimeLimits;
imapDownload: UserTimeLimits;
pop3Download: UserTimeLimits;
imapMaxConnections: UserConnectionLimits;
filters: {
allowed: number;
used: number;
};
}
export interface UserTarget {
id: string;
type: "relay" | "webhook" | "http";
value: string;
}
export interface UserProfile {
success: boolean;
id: string;
username: string;
name?: string;
name: string;
address: string;
language?: string;
retention?: number;
enabled2fa?: string[];
autoreply?: boolean;
encryptMessages?: boolean;
encryptForwarded?: boolean;
pubKey?: string;
metaData?: Record<string, any>;
internalData?: Record<string, any>;
enabled2fa: string[];
autoreply: boolean;
encryptMessages: boolean;
encryptForwarded: boolean;
pubKey: string;
keyInfo: UserKeyInfo;
metaData: any;
internalData: object;
targets: string[];
mtaRelay?: string;
spamLevel: number;
limits: UserLimits;
quota: UserQuotaLimits;
tags: string[];
fromWhitelist?: string[];
disabledScopes: ("imap" | "pop3" | "smtp")[];
hasPasswordSet: boolean;
activated: boolean;
disabled: boolean;
suspended: boolean;
tags: string[];
fromWhitelist?: string[];
disabledScopes?: string[];
lastLogin?: {
time: string;
event: string;
};
previousPassword?: {
date: string;
hash: string;
};
tempPassword?: {
created: string;
validUntil: string;
};
quota: {
allowed: number;
used: number;
};
limits: UserLimits;
targets?: Array<{
id: string;
type: "relay" | "webhook" | "http";
value: string;
}>;
}
export interface UserListResponse {
@@ -0,0 +1,39 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
export class QuotaStatisticsDto {
@ApiProperty({ description: "Total number of email users", example: 150 })
totalUsers: number;
@ApiProperty({ description: "Total quota used by all users in bytes", example: 5368709120 })
totalQuotaUsed: number;
@ApiProperty({ description: "Total quota allowed for all users in bytes", example: 21474836480 })
totalQuotaAllowed: number;
@ApiProperty({ description: "Average quota usage percentage", example: 25.0 })
averageUsagePercent: number;
@ApiPropertyOptional({
description: "Last time quota was synced",
example: "2024-01-15T10:30:00Z",
type: String,
format: "date-time",
})
lastSyncTime: Date | null;
}
export class QuotaInfoDto {
@ApiProperty({ description: "Quota used in bytes", example: 1073741824 })
used: number;
@ApiProperty({ description: "Quota allowed in bytes", example: 5368709120 })
allowed: number;
}
export class SyncResultDto {
@ApiProperty({ description: "Number of successfully synced users", example: 145 })
success: number;
@ApiProperty({ description: "Number of users with sync errors", example: 5 })
errors: number;
}
@@ -0,0 +1,11 @@
import { Module } from "@nestjs/common";
import { MailServerModule } from "../mail-server/mail-server.module";
import { QuotaSyncService } from "./services/quota-sync.service";
@Module({
imports: [MailServerModule],
providers: [QuotaSyncService],
exports: [QuotaSyncService],
})
export class QuotaSyncModule {}
@@ -0,0 +1,206 @@
import { EntityManager } from "@mikro-orm/core";
import { Injectable, Logger } from "@nestjs/common";
import { Cron, CronExpression } from "@nestjs/schedule";
import { firstValueFrom } from "rxjs";
import { WildDuckUsersService } from "../../mail-server/services/wildduck-users.service";
import { User } from "../../users/entities/user.entity";
interface QuotaInfo {
used: number;
allowed: number;
}
@Injectable()
export class QuotaSyncService {
private readonly logger = new Logger(QuotaSyncService.name);
constructor(
private readonly wildDuckUsersService: WildDuckUsersService,
private readonly em: EntityManager,
) {}
/**
* Sync quota usage every 15 minutes
*/
@Cron(CronExpression.EVERY_2_HOURS)
// @Cron("0 */15 * * * *")
async syncAllUsersQuota(): Promise<void> {
const em = this.em.fork();
this.logger.log("Starting quota synchronization for all users");
try {
const users = await em.find(User, {
emailEnabled: true,
deletedAt: null,
wildduckUserId: { $ne: null },
});
this.logger.log(`Found ${users.length} users to sync quota for`);
let successCount = 0;
let errorCount = 0;
for (const user of users) {
try {
await this.syncUserQuotaWithEntityManager(user, em);
successCount++;
} catch (error) {
errorCount++;
this.logger.error(`Failed to sync quota for user ${user.id}: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
// Flush all changes at once
await em.flush();
this.logger.log(`Quota sync completed: ${successCount} success, ${errorCount} errors`);
} catch (error) {
this.logger.error("Failed to sync quota for users", `${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Sync quota for a specific user with provided entity manager
*/
private async syncUserQuotaWithEntityManager(user: User, _em: EntityManager): Promise<void> {
if (!user.wildduckUserId) {
this.logger.warn(`User ${user.id} has no WildDuck user ID, skipping quota sync`);
return;
}
try {
const wildduckUser = await firstValueFrom(this.wildDuckUsersService.getUser(user.wildduckUserId));
if (!wildduckUser || !wildduckUser.success) {
this.logger.warn(`Failed to fetch WildDuck user data for user ${user.id}`);
return;
}
const quotaInfo: QuotaInfo = {
used: wildduckUser.limits?.quota?.used || 0,
allowed: wildduckUser.limits?.quota?.allowed || 0,
};
// Update local user quota info
user.emailQuotaUsed = quotaInfo.used;
user.emailQuota = quotaInfo.allowed;
user.lastQuotaSync = new Date();
this.logger.debug(`Synced quota for user ${user.id}: ${quotaInfo.used}/${quotaInfo.allowed} bytes`);
} catch (error) {
this.logger.error(`Error syncing quota for user ${user.id}:`, `${error instanceof Error ? error.message : "Unknown error"}`);
throw error;
}
}
/**
* Sync quota for a specific user
*/
async syncUserQuota(user: User): Promise<void> {
const em = this.em.fork();
// Re-fetch the user with the current entity manager
const managedUser = await em.findOne(User, { id: user.id });
if (!managedUser) {
throw new Error(`User ${user.id} not found`);
}
await this.syncUserQuotaWithEntityManager(managedUser, em);
await em.flush();
}
/**
* Manually trigger quota sync for a specific user
*/
async syncUserQuotaById(userId: string): Promise<QuotaInfo> {
const em = this.em.fork();
const user = await em.findOne(User, {
id: userId,
emailEnabled: true,
deletedAt: null,
});
if (!user) {
throw new Error(`User ${userId} not found or not email enabled`);
}
await this.syncUserQuotaWithEntityManager(user, em);
await em.flush();
return {
used: user.emailQuotaUsed || 0,
allowed: user.emailQuota || 0,
};
}
/**
* Get quota statistics for all users
*/
async getQuotaStatistics(): Promise<{
totalUsers: number;
totalQuotaUsed: number;
totalQuotaAllowed: number;
averageUsagePercent: number;
lastSyncTime: Date | null;
}> {
const em = this.em.fork();
const users = await em.find(User, {
emailEnabled: true,
deletedAt: null,
emailQuotaUsed: { $ne: null },
});
const totalQuotaUsed = users.reduce((sum, user) => sum + (user.emailQuotaUsed || 0), 0);
const totalQuotaAllowed = users.reduce((sum, user) => sum + (user.emailQuota || 0), 0);
const averageUsagePercent = totalQuotaAllowed > 0 ? (totalQuotaUsed / totalQuotaAllowed) * 100 : 0;
// Find the most recent sync time
const lastSyncUser = await em.findOne(
User,
{
emailEnabled: true,
deletedAt: null,
lastQuotaSync: { $ne: null },
},
{
orderBy: { lastQuotaSync: "DESC" },
},
);
return {
totalUsers: users.length,
totalQuotaUsed,
totalQuotaAllowed,
averageUsagePercent: Math.round(averageUsagePercent * 100) / 100,
lastSyncTime: lastSyncUser?.lastQuotaSync || null,
};
}
/**
* Manual sync for all users (useful for admin endpoints)
*/
async forceSyncAllUsers(): Promise<{ success: number; errors: number }> {
this.logger.log("Manual quota sync triggered");
await this.syncAllUsersQuota();
// Return basic statistics
const em = this.em.fork();
const users = await em.find(User, {
emailEnabled: true,
deletedAt: null,
wildduckUserId: { $ne: null },
});
const recentSyncs = await em.count(User, {
emailEnabled: true,
deletedAt: null,
lastQuotaSync: { $gte: new Date(Date.now() - 60000) }, // Last minute
});
return {
success: recentSyncs,
errors: users.length - recentSyncs,
};
}
}
@@ -0,0 +1,19 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsIn, IsOptional, IsString } from "class-validator";
import { PaginationDto } from "../../../common/DTO/pagination.dto";
import { CommonMessage } from "../../../common/enums/message.enum";
export class UserListQueryDto extends PaginationDto {
@IsOptional()
@IsString({ message: CommonMessage.SEARCH_QUERY_STRING })
@ApiPropertyOptional({ description: "Search query", example: "search query" })
q?: string;
@IsOptional()
@Type(() => Number)
@IsIn([1, 0], { message: CommonMessage.IS_ACTIVE_SHOULD_BE_1_0 })
@ApiPropertyOptional({ description: "User status", example: 1 })
isActive?: number;
}
+14 -2
View File
@@ -1,10 +1,11 @@
import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, Property } from "@mikro-orm/core";
import { Collection, Entity, EntityRepositoryType, ManyToOne, OneToMany, OneToOne, Property } from "@mikro-orm/core";
import { RefreshToken } from "./refresh-token.entity";
import { Role } from "./role.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { Business } from "../../businesses/entities/business.entity";
import { Domain } from "../../domains/entities/domain.entity";
import { EmailSignature } from "../../email-signatures/entities/email-signature.entity";
import { UserRepository } from "../repositories/user.repository";
@Entity({ repository: () => UserRepository })
@@ -27,18 +28,29 @@ export class User extends BaseEntity {
@Property({ type: "bigint", nullable: true, default: 1073741824 }) // 1GB default
emailQuota?: number; // Email quota in bytes
@Property({ type: "bigint", nullable: true, default: 0 })
emailQuotaUsed?: number; // Email quota used in bytes
// Email account integration fields
@Property({ type: "varchar", length: 255, nullable: true })
emailAccountId?: string; // WildDuck user ID
wildduckUserId?: string; // WildDuck user ID
@Property({ type: "varchar", length: 255, nullable: true })
displayName?: string; // Display name for email
@Property({ type: "timestamptz", nullable: true })
lastQuotaSync?: Date; // Last time quota was synced with WildDuck
@Property({ default: true, nullable: false })
isActive: boolean = true;
//=========================
@OneToOne(() => EmailSignature, (signature) => signature.user, { nullable: true, deleteRule: "cascade" })
emailSignature?: EmailSignature;
//=========================
@OneToMany(() => RefreshToken, (token) => token.user)
refreshTokens = new Collection<RefreshToken>(this);
@@ -1,5 +1,38 @@
import { EntityRepository } from "@mikro-orm/postgresql";
import { EntityRepository, FilterQuery } from "@mikro-orm/postgresql";
import { PaginationUtils } from "../../utils/services/pagination.utils";
import { UserListQueryDto } from "../DTO/user-list-query.dto";
import { User } from "../entities/user.entity";
export class UserRepository extends EntityRepository<User> {}
export class UserRepository extends EntityRepository<User> {
async getUserListForAdmin(businessId: string, queryDto: UserListQueryDto) {
const { limit, skip } = PaginationUtils(queryDto);
const whereClause: FilterQuery<User> = {
business: { id: businessId },
emailEnabled: true,
deletedAt: null,
};
if (queryDto.q) {
whereClause.$or = [
{ userName: { $like: `%${queryDto.q}%` } },
{ emailAddress: { $like: `%${queryDto.q}%` } },
{ displayName: { $like: `%${queryDto.q}%` } },
];
}
if (queryDto.isActive) {
console.log(queryDto.isActive);
whereClause.isActive = queryDto.isActive === 1;
}
return this.findAndCount(whereClause, {
populate: ["domain", "business"],
limit,
offset: skip,
orderBy: { createdAt: "DESC" },
});
}
}
+11 -21
View File
@@ -1,4 +1,4 @@
import { EntityManager, FilterQuery } from "@mikro-orm/postgresql";
import { EntityManager } from "@mikro-orm/postgresql";
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { firstValueFrom } from "rxjs";
@@ -17,6 +17,7 @@ import { DomainsService } from "../../domains/services/domains.service";
import { MailServerService } from "../../mail-server/services/mail-server.service";
import { PasswordService } from "../../utils/services/password.service";
import { CreateEmailUserDto } from "../DTO/create-email-user.dto";
import { UserListQueryDto } from "../DTO/user-list-query.dto";
import { Role } from "../entities/role.entity";
import { User } from "../entities/user.entity";
import { RoleEnum } from "../enums/role.enum";
@@ -103,7 +104,7 @@ export class UsersService {
emailAddress,
emailEnabled: true,
emailQuota: quota || 1073741824,
emailAccountId: mailServerUser.id,
wildduckUserId: mailServerUser.id,
displayName: displayName || username,
isActive: true,
business,
@@ -135,7 +136,7 @@ export class UsersService {
id: user.id,
emailAddress: user.emailAddress!,
displayName: user.displayName!,
emailAccountId: user.emailAccountId!,
wildduckUserId: user.wildduckUserId!,
emailQuota: user.emailQuota!,
emailEnabled: user.emailEnabled,
domain: domain.name,
@@ -164,21 +165,10 @@ export class UsersService {
}
/*******************************/
async getEmailUsers(businessId: string, domainId?: string) {
const whereClause: FilterQuery<User> = {
business: { id: businessId },
emailEnabled: true,
deletedAt: null,
};
async getEmailUsers(businessId: string, queryDto: UserListQueryDto) {
const [users, count] = await this.userRepository.getUserListForAdmin(businessId, queryDto);
if (domainId) whereClause.domain = { id: domainId };
const users = await this.userRepository.find(whereClause, {
populate: ["domain", "business"],
orderBy: { createdAt: "DESC" },
});
return { users };
return { users, count, paginate: true };
}
/*******************************/
@@ -192,8 +182,8 @@ export class UsersService {
if (!user) throw new NotFoundException(UserMessage.EMAIL_USER_NOT_FOUND);
try {
if (user.emailAccountId) {
await firstValueFrom(this.mailServerService.users.deleteUser(user.emailAccountId));
if (user.wildduckUserId) {
await firstValueFrom(this.mailServerService.users.deleteUser(user.wildduckUserId));
}
user.deletedAt = new Date();
@@ -224,9 +214,9 @@ export class UsersService {
if (!user) throw new NotFoundException(UserMessage.EMAIL_USER_NOT_FOUND);
try {
if (user.emailAccountId) {
if (user.wildduckUserId) {
await firstValueFrom(
this.mailServerService.users.updateUser(user.emailAccountId, {
this.mailServerService.users.updateUser(user.wildduckUserId, {
quota: newQuota,
}),
);
+6 -3
View File
@@ -1,14 +1,17 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common";
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiResponse } from "@nestjs/swagger";
import { CreateEmailUserDto } from "./DTO/create-email-user.dto";
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 { ParamDto } from "../../common/DTO/param.dto";
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
@Controller("users")
@ApiHeader({ name: "x-business-id", description: "Business ID" })
@UseInterceptors(BusinessInterceptor)
@AuthGuards()
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@@ -23,8 +26,8 @@ export class UsersController {
@Get()
@ApiOperation({ summary: "Get email users for business" })
@ApiResponse({ status: 200, description: "Email users retrieved successfully" })
getEmailUsers(@BusinessDec("id") businessId: string, @Query("domainId") domainId?: string) {
return this.usersService.getEmailUsers(businessId, domainId);
getEmailUsers(@BusinessDec("id") businessId: string, @Query() queryDto: UserListQueryDto) {
return this.usersService.getEmailUsers(businessId, queryDto);
}
@Delete(":id")
+2 -1
View File
@@ -6,12 +6,13 @@ import { Role } from "./entities/role.entity";
import { User } from "./entities/user.entity";
import { UsersService } from "./services/users.service";
import { UsersController } from "./users.controller";
import { BusinessesModule } from "../businesses/businesses.module";
import { DomainsModule } from "../domains/domains.module";
import { MailServerModule } from "../mail-server/mail-server.module";
import { UtilsModule } from "../utils/utils.module";
@Module({
imports: [MikroOrmModule.forFeature([User, RefreshToken, Role]), UtilsModule, MailServerModule, DomainsModule],
imports: [MikroOrmModule.forFeature([User, RefreshToken, Role]), UtilsModule, MailServerModule, DomainsModule, BusinessesModule],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],