update: handle user update and quota sync logic
This commit is contained in:
@@ -64,6 +64,7 @@ export const enum AuthMessage {
|
|||||||
USER_ACCOUNT_INACTIVE = "حساب کاربری شما غیر فعال است",
|
USER_ACCOUNT_INACTIVE = "حساب کاربری شما غیر فعال است",
|
||||||
TOKEN_MISSED_OR_EXPIRED = "توکن مفقود یا منقضی شده است",
|
TOKEN_MISSED_OR_EXPIRED = "توکن مفقود یا منقضی شده است",
|
||||||
OLD_PASSWORD_REQUIRED = "رمز عبور قبلی الزامی است",
|
OLD_PASSWORD_REQUIRED = "رمز عبور قبلی الزامی است",
|
||||||
|
PASSWORD_CHANGE_FAILED = "تغییر رمز عبور با مشکل روبرد شد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum UserMessage {
|
export const enum UserMessage {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
import { EntityManager } from "@mikro-orm/postgresql";
|
||||||
|
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { TokensService } from "./tokens.service";
|
import { TokensService } from "./tokens.service";
|
||||||
import { TwoFactorService } from "./two-factor.service";
|
import { TwoFactorService } from "./two-factor.service";
|
||||||
import { AuthMessage, TwoFactorMessage } from "../../../common/enums/message.enum";
|
import { AuthMessage, TwoFactorMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
||||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||||
|
import { User } from "../../users/entities/user.entity";
|
||||||
import { UserRepository } from "../../users/repositories/user.repository";
|
import { UserRepository } from "../../users/repositories/user.repository";
|
||||||
import { PasswordService } from "../../utils/services/password.service";
|
import { PasswordService } from "../../utils/services/password.service";
|
||||||
import { ChangePasswordDto } from "../DTO/change-password.dto";
|
import { ChangePasswordDto } from "../DTO/change-password.dto";
|
||||||
@@ -11,12 +15,15 @@ import { LoginDto } from "../DTO/login.dto";
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
|
private readonly logger = new Logger(AuthService.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly notificationQueue: NotificationQueue,
|
private readonly notificationQueue: NotificationQueue,
|
||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
private readonly tokensService: TokensService,
|
private readonly tokensService: TokensService,
|
||||||
private readonly passwordService: PasswordService,
|
private readonly passwordService: PasswordService,
|
||||||
private readonly twoFactorService: TwoFactorService,
|
private readonly twoFactorService: TwoFactorService,
|
||||||
|
private readonly mailServerService: MailServerService,
|
||||||
|
private readonly em: EntityManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
@@ -69,7 +76,12 @@ export class AuthService {
|
|||||||
//==============================================
|
//==============================================
|
||||||
//==============================================
|
//==============================================
|
||||||
async changePassword(userId: string, changePasswordDto: ChangePasswordDto) {
|
async changePassword(userId: string, changePasswordDto: ChangePasswordDto) {
|
||||||
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
|
const em = this.em.fork();
|
||||||
|
|
||||||
|
try {
|
||||||
|
em.begin();
|
||||||
|
|
||||||
|
const user = await em.findOne(User, { id: userId, deletedAt: null });
|
||||||
if (!user) throw new BadRequestException(AuthMessage.USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(AuthMessage.USER_NOT_FOUND);
|
||||||
|
|
||||||
const passCompare = await this.passwordService.comparePasswords(changePasswordDto.oldPassword, user.password);
|
const passCompare = await this.passwordService.comparePasswords(changePasswordDto.oldPassword, user.password);
|
||||||
@@ -78,11 +90,24 @@ export class AuthService {
|
|||||||
if (changePasswordDto.newPassword !== changePasswordDto.repeatPassword) throw new BadRequestException(AuthMessage.INVALID_REPEAT_PASSWORD);
|
if (changePasswordDto.newPassword !== changePasswordDto.repeatPassword) throw new BadRequestException(AuthMessage.INVALID_REPEAT_PASSWORD);
|
||||||
|
|
||||||
const hashedPassword = await this.passwordService.hashPassword(changePasswordDto.newPassword);
|
const hashedPassword = await this.passwordService.hashPassword(changePasswordDto.newPassword);
|
||||||
await this.userRepository.nativeUpdate({ id: userId }, { password: hashedPassword });
|
|
||||||
|
user.password = hashedPassword;
|
||||||
|
|
||||||
|
const res = await firstValueFrom(this.mailServerService.users.updateUser(user.wildduckUserId, { password: changePasswordDto.newPassword }));
|
||||||
|
if (!res.success) throw new BadRequestException(AuthMessage.PASSWORD_CHANGE_FAILED);
|
||||||
|
|
||||||
|
await em.flush();
|
||||||
|
|
||||||
|
await em.commit();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: AuthMessage.PASSWORD_CHANGED_SUCCESSFULLY,
|
message: AuthMessage.PASSWORD_CHANGED_SUCCESSFULLY,
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await em.rollback();
|
||||||
|
this.logger.error("Error in changePassword of user", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export class EmailPollingService {
|
|||||||
async stopPollingForUser(userId: string): Promise<void> {
|
async stopPollingForUser(userId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const activeJob = this.activeJobs.get(userId);
|
const activeJob = this.activeJobs.get(userId);
|
||||||
console.log(activeJob);
|
|
||||||
if (!activeJob) {
|
if (!activeJob) {
|
||||||
this.logger.debug(`No active polling job found for user ${userId}`);
|
this.logger.debug(`No active polling job found for user ${userId}`);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -42,17 +42,11 @@ export class CreateUserDto {
|
|||||||
@ApiProperty({ description: "Password for the user. You can use false to create an account without a password", example: "secretpass" })
|
@ApiProperty({ description: "Password for the user. You can use false to create an account without a password", example: "secretpass" })
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({ description: "Bcrypt hash for password indicate if the password is hashed", example: false })
|
||||||
description: "Bcrypt hash for password. If provided, plain text password is ignored",
|
|
||||||
example: false,
|
|
||||||
})
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
hashedPassword?: string | boolean;
|
hashedPassword?: boolean;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({ description: "Allow weak passwords", default: true })
|
||||||
description: "Allow weak passwords",
|
|
||||||
default: true,
|
|
||||||
})
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
allowUnsafe?: boolean;
|
allowUnsafe?: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { IsArray, IsBoolean, IsNumber, IsOptional, IsString, IsUrl } from "class-validator";
|
import { IsArray, IsBoolean, IsInt, IsNumber, IsOptional, IsString, IsUrl, Max, Min } from "class-validator";
|
||||||
|
|
||||||
export class UpdateUserDto {
|
export class UpdateUserDto {
|
||||||
@ApiPropertyOptional({ description: "Updated display name for the user", example: "Updated User Name" })
|
@ApiPropertyOptional({ description: "Updated display name for the user", example: "Updated User Name" })
|
||||||
@@ -7,11 +7,6 @@ export class UpdateUserDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Updated password for the user", example: "newsecretpassword123" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
password?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: "Array of email addresses to forward all messages to",
|
description: "Array of email addresses to forward all messages to",
|
||||||
example: ["forward1@example.com", "forward2@example.com"],
|
example: ["forward1@example.com", "forward2@example.com"],
|
||||||
@@ -57,11 +52,6 @@ export class UpdateUserDto {
|
|||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "PGP public key for encryption" })
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
pubKey?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Whether to encrypt stored messages" })
|
@ApiPropertyOptional({ description: "Whether to encrypt stored messages" })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@@ -72,18 +62,40 @@ export class UpdateUserDto {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
encryptForwarded?: boolean;
|
encryptForwarded?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Updated password for the user", example: "newsecretpassword123" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
password?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Current password for verification" })
|
@ApiPropertyOptional({ description: "Current password for verification" })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
existingPassword?: string;
|
existingPassword?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "Session identifier for logging" })
|
@ApiPropertyOptional({ description: "Hashed password for the user indicate if the password is hashed" })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsBoolean()
|
||||||
sess?: string;
|
hashedPassword?: boolean;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: "IP address the request was made from", example: "192.168.1.1" })
|
@ApiPropertyOptional({ description: "Spam level for the user" })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsInt()
|
||||||
ip?: string;
|
@Min(0)
|
||||||
|
@Max(100)
|
||||||
|
spamLevel?: number;
|
||||||
|
|
||||||
|
// @ApiPropertyOptional({ description: "PGP public key for encryption" })
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsString()
|
||||||
|
// pubKey?: string;
|
||||||
|
|
||||||
|
// @ApiPropertyOptional({ description: "Session identifier for logging" })
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsString()
|
||||||
|
// sess?: string;
|
||||||
|
|
||||||
|
// @ApiPropertyOptional({ description: "IP address the request was made from", example: "192.168.1.1" })
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsString()
|
||||||
|
// ip?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ export class WildDuckBaseService {
|
|||||||
return response.data as T;
|
return response.data as T;
|
||||||
}),
|
}),
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
this.logger.error(`WildDuck API error: ${error.message}`, error.stack);
|
this.logger.error(`WildDuck API error message: ${error.message}`, error.stack);
|
||||||
|
this.logger.error(`WildDuck API error data: ${JSON.stringify(error.response?.data)}`);
|
||||||
|
|
||||||
if (error.response?.data?.error) {
|
if (error.response?.data?.error) {
|
||||||
return throwError(() => this.handleMailServerError(error.response.data.error, error.response.status));
|
return throwError(() => this.handleMailServerError(error.response.data.error, error.response.status));
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { DomainStatus } from "../../domains/enums/domain-status.enum";
|
|||||||
import { DomainAutomationService } from "../../domains/services/domain-automation.service";
|
import { DomainAutomationService } from "../../domains/services/domain-automation.service";
|
||||||
import { DomainsService } from "../../domains/services/domains.service";
|
import { DomainsService } from "../../domains/services/domains.service";
|
||||||
import { UpdateUserDto } from "../../mail-server/DTO/update-user.dto";
|
import { UpdateUserDto } from "../../mail-server/DTO/update-user.dto";
|
||||||
|
import { AddressInfo } from "../../mail-server/interfaces/addresses-response.interface";
|
||||||
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
||||||
import { QuotaSyncService } from "../../quota-sync/services/quota-sync.service";
|
import { QuotaSyncService } from "../../quota-sync/services/quota-sync.service";
|
||||||
import { UserSettingsService } from "../../settings/services/user-settings.service";
|
import { UserSettingsService } from "../../settings/services/user-settings.service";
|
||||||
@@ -260,12 +261,7 @@ export class UsersService {
|
|||||||
if (aliases && aliases.length > 0) {
|
if (aliases && aliases.length > 0) {
|
||||||
for (const alias of aliases) {
|
for (const alias of aliases) {
|
||||||
try {
|
try {
|
||||||
await firstValueFrom(
|
await firstValueFrom(this.mailServerService.addresses.createUserAddress(mailServerUser.id, { address: alias, main: false }));
|
||||||
this.mailServerService.addresses.createUserAddress(mailServerUser.id, {
|
|
||||||
address: alias,
|
|
||||||
main: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn(`Failed to create alias ${alias} for user ${emailAddress}:`, error);
|
this.logger.warn(`Failed to create alias ${alias} for user ${emailAddress}:`, error);
|
||||||
}
|
}
|
||||||
@@ -325,9 +321,7 @@ export class UsersService {
|
|||||||
|
|
||||||
await this.em.persistAndFlush(user);
|
await this.em.persistAndFlush(user);
|
||||||
|
|
||||||
//sync quota
|
|
||||||
await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, user.wildduckUserId);
|
await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, user.wildduckUserId);
|
||||||
//sync business quota
|
|
||||||
await this.quotaSyncService.scheduleBusinessQuotaSync(businessId, user.business.name);
|
await this.quotaSyncService.scheduleBusinessQuotaSync(businessId, user.business.name);
|
||||||
|
|
||||||
this.logger.log(`Email user deleted: ${user.emailAddress} for business ${businessId}`);
|
this.logger.log(`Email user deleted: ${user.emailAddress} for business ${businessId}`);
|
||||||
@@ -342,165 +336,112 @@ export class UsersService {
|
|||||||
/*******************************/
|
/*******************************/
|
||||||
|
|
||||||
async updateEmailUser(userId: string, businessId: string, updateEmailUserDto: UpdateEmailUserDto) {
|
async updateEmailUser(userId: string, businessId: string, updateEmailUserDto: UpdateEmailUserDto) {
|
||||||
|
const em = this.em.fork();
|
||||||
|
|
||||||
const { password, displayName, quota, aliases, title, templateId, forwarders } = updateEmailUserDto;
|
const { password, displayName, quota, aliases, title, templateId, forwarders } = updateEmailUserDto;
|
||||||
|
|
||||||
const user = await this.userRepository.findOne(
|
try {
|
||||||
{
|
em.begin();
|
||||||
id: userId,
|
|
||||||
business: { id: businessId },
|
const user = await em.findOne(User, { id: userId, business: { id: businessId }, emailEnabled: true }, { populate: ["business"] });
|
||||||
emailEnabled: true,
|
|
||||||
},
|
|
||||||
{ populate: ["business"] },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND);
|
||||||
|
|
||||||
if (templateId) {
|
if (templateId) {
|
||||||
const template = await this.em.findOne(Template, { id: templateId, business: { id: businessId } });
|
const template = await em.findOne(Template, { id: templateId, business: { id: businessId } });
|
||||||
if (!template) throw new BadRequestException(TemplateMessage.TEMPLATE_NOT_FOUND);
|
if (!template) throw new BadRequestException(TemplateMessage.TEMPLATE_NOT_FOUND);
|
||||||
user.template = template;
|
user.template = template;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate business quota if quota is being updated
|
|
||||||
if (quota) {
|
if (quota) {
|
||||||
const currentQuota = user.emailQuota || 0;
|
const currentQuota = user.emailQuota || 0;
|
||||||
this.validateBusinessQuotaAvailability(user.business, currentQuota, quota);
|
this.validateBusinessQuotaAvailability(user.business, currentQuota, quota);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const updateData: UpdateUserDto = {};
|
const updateData: UpdateUserDto = {};
|
||||||
|
|
||||||
if (password) {
|
if (password) {
|
||||||
updateData.password = password;
|
// updateData.password = password;
|
||||||
const hashedPassword = await this.passwordService.hashPassword(password);
|
const hashedPassword = await this.passwordService.hashPassword(password);
|
||||||
user.password = hashedPassword;
|
user.password = hashedPassword;
|
||||||
|
updateData.password = hashedPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (displayName) updateData.name = displayName;
|
if (displayName) updateData.name = displayName;
|
||||||
|
|
||||||
if (quota) updateData.quota = quota;
|
if (quota) updateData.quota = quota;
|
||||||
|
|
||||||
if (forwarders !== undefined) {
|
if (forwarders !== undefined && forwarders.length > 0) {
|
||||||
updateData.targets = forwarders;
|
updateData.targets = forwarders;
|
||||||
user.forwarders = forwarders;
|
user.forwarders = forwarders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.warn(`Updating email user ${user.emailAddress} with data: ${JSON.stringify(updateData)}`);
|
||||||
|
|
||||||
if (Object.keys(updateData).length > 0) {
|
if (Object.keys(updateData).length > 0) {
|
||||||
await firstValueFrom(this.mailServerService.users.updateUser(user.wildduckUserId!, updateData));
|
await firstValueFrom(this.mailServerService.users.updateUser(user.wildduckUserId, { ...updateData, hashedPassword: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aliases && aliases.length > 0) {
|
if (aliases && aliases.length > 0) {
|
||||||
const currentAliases = await firstValueFrom(this.mailServerService.addresses.listUserAddresses(user.wildduckUserId!));
|
const currentAliases = await firstValueFrom(this.mailServerService.addresses.listUserAddresses(user.wildduckUserId));
|
||||||
|
|
||||||
for (const alias of currentAliases.results) {
|
await this.deleteUserAliases(user, currentAliases.results);
|
||||||
if (!alias.main && alias.address !== user.emailAddress) {
|
|
||||||
try {
|
|
||||||
await firstValueFrom(this.mailServerService.addresses.deleteUserAddress(user.wildduckUserId!, alias.id));
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn(`Failed to delete alias ${alias.address}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const alias of aliases) {
|
await this.createUserAliases(user, aliases);
|
||||||
try {
|
|
||||||
await firstValueFrom(
|
|
||||||
this.mailServerService.addresses.createUserAddress(user.wildduckUserId!, {
|
|
||||||
address: alias,
|
|
||||||
main: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn(`Failed to create alias ${alias}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (displayName) user.displayName = displayName;
|
if (displayName) user.displayName = displayName;
|
||||||
|
if (title) user.title = title;
|
||||||
|
|
||||||
if (quota) {
|
if (quota) {
|
||||||
const currentQuota = user.emailQuota || 0;
|
const currentQuota = user.emailQuota || 0;
|
||||||
user.emailQuota = quota;
|
user.emailQuota = quota;
|
||||||
|
|
||||||
// Update business quota tracking
|
|
||||||
this.updateBusinessQuotaTracking(user.business, currentQuota, quota);
|
this.updateBusinessQuotaTracking(user.business, currentQuota, quota);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (title) user.title = title;
|
await em.flush();
|
||||||
|
|
||||||
await this.em.flush();
|
|
||||||
|
|
||||||
//sync quota
|
|
||||||
if (user.wildduckUserId) {
|
|
||||||
await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, user.wildduckUserId);
|
await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, user.wildduckUserId);
|
||||||
}
|
|
||||||
|
|
||||||
//sync business quota
|
|
||||||
await this.quotaSyncService.scheduleBusinessQuotaSync(businessId, user.business.name);
|
await this.quotaSyncService.scheduleBusinessQuotaSync(businessId, user.business.name);
|
||||||
|
|
||||||
this.logger.log(`Email user updated: ${user.emailAddress} for business ${businessId}`);
|
this.logger.log(`Email user updated: ${user.emailAddress} for business ${businessId}`);
|
||||||
|
|
||||||
return {
|
await em.commit();
|
||||||
message: UserMessage.EMAIL_USER_UPDATED_SUCCESSFULLY,
|
|
||||||
user: {
|
return { message: UserMessage.EMAIL_USER_UPDATED_SUCCESSFULLY, user: { id: user.id, emailAddress: user.emailAddress } };
|
||||||
id: user.id,
|
|
||||||
emailAddress: user.emailAddress!,
|
|
||||||
displayName: user.displayName!,
|
|
||||||
title: user.title,
|
|
||||||
emailQuota: user.emailQuota!,
|
|
||||||
emailEnabled: user.emailEnabled,
|
|
||||||
forwarders: user.forwarders || [],
|
|
||||||
isActive: user.isActive,
|
|
||||||
updatedAt: user.updatedAt,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to update email user ${user.emailAddress}:`, error);
|
await em.rollback();
|
||||||
|
this.logger.error(`Failed to update email user ${userId}:`, error);
|
||||||
throw new BadRequestException("Failed to update email user");
|
throw new BadRequestException("Failed to update email user");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*******************************/
|
||||||
* Update email user quota
|
|
||||||
*/
|
|
||||||
async updateEmailUserQuota(userId: string, businessId: string, newQuota: number) {
|
async updateEmailUserQuota(userId: string, businessId: string, newQuota: number) {
|
||||||
const user = await this.userRepository.findOne(
|
const user = await this.userRepository.findOne({ id: userId, business: { id: businessId }, emailEnabled: true }, { populate: ["business"] });
|
||||||
{
|
|
||||||
id: userId,
|
|
||||||
business: { id: businessId },
|
|
||||||
emailEnabled: true,
|
|
||||||
},
|
|
||||||
{ populate: ["business"] },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND);
|
||||||
|
|
||||||
// Validate business quota before updating
|
|
||||||
const currentQuota = user.emailQuota || 0;
|
const currentQuota = user.emailQuota || 0;
|
||||||
this.validateBusinessQuotaAvailability(user.business, currentQuota, newQuota);
|
this.validateBusinessQuotaAvailability(user.business, currentQuota, newQuota);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (user.wildduckUserId) {
|
if (user.wildduckUserId) {
|
||||||
await firstValueFrom(
|
await firstValueFrom(this.mailServerService.users.updateUser(user.wildduckUserId, { quota: newQuota }));
|
||||||
this.mailServerService.users.updateUser(user.wildduckUserId, {
|
|
||||||
quota: newQuota,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user.emailQuota = newQuota;
|
user.emailQuota = newQuota;
|
||||||
|
|
||||||
// Update business quota tracking
|
|
||||||
this.updateBusinessQuotaTracking(user.business, currentQuota, newQuota);
|
this.updateBusinessQuotaTracking(user.business, currentQuota, newQuota);
|
||||||
|
|
||||||
await this.em.persistAndFlush([user, user.business]);
|
await this.em.persistAndFlush([user, user.business]);
|
||||||
|
|
||||||
//sync quota
|
|
||||||
if (user.wildduckUserId) {
|
if (user.wildduckUserId) {
|
||||||
await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, user.wildduckUserId);
|
await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, user.wildduckUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
//sync business quota
|
|
||||||
await this.quotaSyncService.scheduleBusinessQuotaSync(businessId, user.business.name);
|
await this.quotaSyncService.scheduleBusinessQuotaSync(businessId, user.business.name);
|
||||||
|
|
||||||
this.logger.log(`Email user quota updated: ${user.emailAddress} to ${newQuota} bytes`);
|
this.logger.log(`Email user quota updated: ${user.emailAddress} to ${newQuota} bytes`);
|
||||||
@@ -545,7 +486,7 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//=====================================================
|
//#####################################################
|
||||||
|
|
||||||
private updateBusinessQuotaTracking(business: Business, currentUserQuota: number, newUserQuota: number): void {
|
private updateBusinessQuotaTracking(business: Business, currentUserQuota: number, newUserQuota: number): void {
|
||||||
const quotaDifference = newUserQuota - currentUserQuota;
|
const quotaDifference = newUserQuota - currentUserQuota;
|
||||||
@@ -555,4 +496,30 @@ export class UsersService {
|
|||||||
|
|
||||||
this.logger.log(`Updated business quota for ${business.name}: used=${business.usedQuota}, remaining=${business.remainingQuota}`);
|
this.logger.log(`Updated business quota for ${business.name}: used=${business.usedQuota}, remaining=${business.remainingQuota}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#####################################################
|
||||||
|
|
||||||
|
private async createUserAliases(user: User, aliases: string[]) {
|
||||||
|
for (const alias of aliases) {
|
||||||
|
try {
|
||||||
|
await firstValueFrom(this.mailServerService.addresses.createUserAddress(user.wildduckUserId, { address: alias, main: false }));
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Failed to create alias ${alias} for user ${user.emailAddress}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#####################################################
|
||||||
|
|
||||||
|
private async deleteUserAliases(user: User, aliases: AddressInfo[]) {
|
||||||
|
for (const alias of aliases) {
|
||||||
|
if (!alias.main && alias.address !== user.emailAddress) {
|
||||||
|
try {
|
||||||
|
await firstValueFrom(this.mailServerService.addresses.deleteUserAddress(user.wildduckUserId, alias.id));
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Failed to delete alias ${alias} for user ${user.emailAddress}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user