731 lines
26 KiB
TypeScript
731 lines
26 KiB
TypeScript
import { EntityManager } from "@mikro-orm/postgresql";
|
|
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
|
import { firstValueFrom } from "rxjs";
|
|
|
|
import {
|
|
BusinessMessage,
|
|
CommonMessage,
|
|
DomainMessage,
|
|
MailServerMessage,
|
|
RoleMessage,
|
|
TemplateMessage,
|
|
UserMessage,
|
|
} from "../../../common/enums/message.enum";
|
|
import { QUOTA_CONSTANTS } from "../../businesses/constant";
|
|
import { Business } from "../../businesses/entities/business.entity";
|
|
import { DomainStatus } from "../../domains/enums/domain-status.enum";
|
|
import { DomainAutomationService } from "../../domains/services/domain-automation.service";
|
|
import { DomainsService } from "../../domains/services/domains.service";
|
|
import { UpdateUserDto } from "../../mail-server/DTO/update-user.dto";
|
|
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
|
import { PushNotificationService } from "../../notifications/services/push-notification.service";
|
|
import { QuotaSyncService } from "../../quota-sync/services/quota-sync.service";
|
|
import { UserSettingsService } from "../../settings/services/user-settings.service";
|
|
import { Template } from "../../templates/entities/template.entity";
|
|
import { PasswordService } from "../../utils/services/password.service";
|
|
import { CreateEmailUserDto } from "../DTO/create-email-user.dto";
|
|
import {
|
|
RegisterPushSubscriptionDto,
|
|
TestPushNotificationDto,
|
|
UpdatePushNotificationSettingsDto,
|
|
UpdatePushSubscriptionsDto,
|
|
} from "../DTO/push-notification.dto";
|
|
import { UpdateEmailUserDto } from "../DTO/update-email-user.dto";
|
|
import { UpdateUserProfileDto } from "../DTO/update-user-profile.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";
|
|
import { UserRepository } from "../repositories/user.repository";
|
|
|
|
@Injectable()
|
|
export class UsersService {
|
|
private readonly logger = new Logger(UsersService.name);
|
|
|
|
constructor(
|
|
private readonly userRepository: UserRepository,
|
|
private readonly em: EntityManager,
|
|
private readonly passwordService: PasswordService,
|
|
private readonly mailServerService: MailServerService,
|
|
private readonly domainsService: DomainsService,
|
|
private readonly domainAutomationService: DomainAutomationService,
|
|
private readonly quotaSyncService: QuotaSyncService,
|
|
private readonly userSettingsService: UserSettingsService,
|
|
private readonly pushNotificationService: PushNotificationService,
|
|
) {}
|
|
|
|
async getUserEmailIdFromId(userId: string) {
|
|
const user = await this.userRepository.findOne({ id: userId }, { populate: ["role"] });
|
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
|
return user.wildduckUserId;
|
|
}
|
|
|
|
async findOneWithEmail(email: string, businessId: string) {
|
|
const user = await this.userRepository.findOne({ emailAddress: email, business: { id: businessId } }, { populate: ["role"] });
|
|
return user;
|
|
}
|
|
|
|
async findOneById(id: string) {
|
|
const user = await this.userRepository.findOne({ id }, { populate: ["role"] });
|
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
|
return { user };
|
|
}
|
|
/*******************************/
|
|
|
|
async getMe(userId: string) {
|
|
const user = await this.userRepository.findOne({ id: userId }, { exclude: ["password"], populate: ["role"] });
|
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
|
|
|
const totalQuota = Number(user.emailQuota);
|
|
const usedQuota = Number(user.emailQuotaUsed);
|
|
const remainingQuota = totalQuota - usedQuota;
|
|
|
|
return {
|
|
user,
|
|
quota: {
|
|
total: totalQuota,
|
|
used: usedQuota,
|
|
remaining: remainingQuota,
|
|
totalInMB: Math.round(totalQuota / (1024 * 1024)),
|
|
totalInGB: Math.round(totalQuota / (1024 * 1024 * 1024)),
|
|
usedInMB: Math.round(usedQuota / (1024 * 1024)),
|
|
usedInGB: Math.round(usedQuota / (1024 * 1024 * 1024)),
|
|
remainingInMB: Math.round(remainingQuota / (1024 * 1024)),
|
|
remainingInGB: Math.round(remainingQuota / (1024 * 1024 * 1024)),
|
|
usagePercentage: totalQuota > 0 ? Math.round((usedQuota / totalQuota) * 100) : 0,
|
|
},
|
|
};
|
|
}
|
|
/*******************************/
|
|
async updateProfile(userId: string, updateMeDto: UpdateUserProfileDto) {
|
|
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
|
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
|
|
|
this.userRepository.assign(user, updateMeDto);
|
|
await this.em.flush();
|
|
|
|
return { message: UserMessage.USER_UPDATED_SUCCESSFULLY, user };
|
|
}
|
|
/*******************************/
|
|
|
|
async findOneByIdWithEntityManager(id: string, em: EntityManager) {
|
|
const user = await em.findOne(User, { id }, { populate: ["role"] });
|
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
|
return user;
|
|
}
|
|
|
|
/*******************************/
|
|
async createEmailUser(createEmailUserDto: CreateEmailUserDto, businessId: string) {
|
|
const em = this.em.fork();
|
|
|
|
try {
|
|
em.begin();
|
|
const { username, password, domainId, displayName, quota, aliases, title, templateId, forwarders } = createEmailUserDto;
|
|
|
|
const business = await em.findOne(Business, { id: businessId });
|
|
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
|
|
|
|
// Check if business has enough quota
|
|
const remainingQuota = Number(business.remainingQuota || 0);
|
|
if (remainingQuota < QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION) {
|
|
throw new BadRequestException("Insufficient quota to create user. Please upgrade your plan.");
|
|
}
|
|
|
|
const domain = await this.domainsService.getDomainById(domainId, businessId);
|
|
|
|
if (domain.business.id !== businessId) throw new BadRequestException(DomainMessage.NOT_BELONG_TO_BUSINESS);
|
|
|
|
if (domain.status !== DomainStatus.VERIFIED) throw new BadRequestException(DomainMessage.NOT_VERIFIED);
|
|
|
|
const emailAddress = `${username}@${domain.name}`;
|
|
const finalUserName = `${username}-${domain.name}`;
|
|
const finalDisplayName = `${username}`;
|
|
|
|
const existingUser = await em.findOne(User, { emailAddress });
|
|
if (existingUser) throw new BadRequestException(UserMessage.EMAIL_ADDRESS_ALREADY_EXISTS);
|
|
|
|
const hashedPassword = await this.passwordService.hashPassword(password);
|
|
|
|
const userRole = await em.findOne(Role, { name: RoleEnum.USER });
|
|
if (!userRole) throw new BadRequestException(RoleMessage.NOT_FOUND);
|
|
|
|
const userQuota = quota || QUOTA_CONSTANTS.DEFAULT_EMAIL_QUOTA_BYTES;
|
|
|
|
const mailServerUser = await firstValueFrom(
|
|
this.mailServerService.users.createUser({
|
|
username: finalUserName,
|
|
password,
|
|
address: emailAddress,
|
|
name: displayName || username,
|
|
quota: userQuota,
|
|
targets: forwarders || [],
|
|
}),
|
|
);
|
|
|
|
if (!mailServerUser.success) throw new BadRequestException(MailServerMessage.FAILED_TO_CREATE_ACCOUNT);
|
|
|
|
const user = em.create(User, {
|
|
title: title || displayName || username,
|
|
userName: finalUserName,
|
|
password: hashedPassword,
|
|
emailAddress,
|
|
emailEnabled: true,
|
|
emailQuota: userQuota,
|
|
wildduckUserId: mailServerUser.id,
|
|
displayName: finalDisplayName || displayName || username,
|
|
isActive: true,
|
|
business,
|
|
domain,
|
|
role: userRole,
|
|
forwarders: forwarders || [],
|
|
});
|
|
|
|
await this.userSettingsService.createUserSettings(user.id, em);
|
|
|
|
business.usedQuota = Number(business.usedQuota) + QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION;
|
|
business.remainingQuota = Number(business.quota) - Number(business.usedQuota);
|
|
|
|
await em.persistAndFlush([user, business]);
|
|
|
|
if (templateId) {
|
|
const template = await em.findOne(Template, { id: templateId, business: { id: businessId } });
|
|
if (!template) throw new BadRequestException(TemplateMessage.TEMPLATE_NOT_FOUND);
|
|
user.template = template;
|
|
}
|
|
|
|
if (aliases && aliases.length > 0) {
|
|
for (const alias of aliases) {
|
|
try {
|
|
await firstValueFrom(
|
|
this.mailServerService.addresses.createUserAddress(mailServerUser.id, {
|
|
address: alias,
|
|
main: false,
|
|
}),
|
|
);
|
|
} catch (error) {
|
|
this.logger.warn(`Failed to create alias ${alias} for user ${emailAddress}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
await em.flush();
|
|
|
|
await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, mailServerUser.id);
|
|
await this.quotaSyncService.scheduleBusinessQuotaSync(businessId, business.name);
|
|
|
|
this.logger.log(`Email user created: ${emailAddress} for business ${businessId}`);
|
|
|
|
await this.domainAutomationService.createDefaultMailboxes(mailServerUser.id);
|
|
|
|
await em.commit();
|
|
|
|
return {
|
|
message: UserMessage.EMAIL_USER_CREATED_SUCCESSFULLY,
|
|
user: {
|
|
id: user.id,
|
|
emailAddress: user.emailAddress!,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
await em.rollback();
|
|
this.logger.error(`Failed to create email user ${createEmailUserDto.username}@${createEmailUserDto.domainId}:`, error);
|
|
throw new BadRequestException(MailServerMessage.FAILED_TO_CREATE_ACCOUNT);
|
|
}
|
|
}
|
|
|
|
/*******************************/
|
|
async getEmailUsers(businessId: string, queryDto: UserListQueryDto) {
|
|
const [users, count] = await this.userRepository.getUserListForAdmin(businessId, queryDto);
|
|
|
|
return { users, count, paginate: true };
|
|
}
|
|
|
|
/*******************************/
|
|
async deleteEmailUser(userId: string, businessId: string) {
|
|
const user = await this.userRepository.findOne({
|
|
id: userId,
|
|
business: { id: businessId },
|
|
emailEnabled: true,
|
|
});
|
|
|
|
if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND);
|
|
|
|
try {
|
|
if (user.wildduckUserId) {
|
|
await firstValueFrom(this.mailServerService.users.deleteUser(user.wildduckUserId));
|
|
}
|
|
|
|
user.deletedAt = new Date();
|
|
user.isActive = false;
|
|
user.emailEnabled = false;
|
|
|
|
await this.em.persistAndFlush(user);
|
|
|
|
//sync quota
|
|
await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, user.wildduckUserId);
|
|
//sync business quota
|
|
await this.quotaSyncService.scheduleBusinessQuotaSync(businessId, user.business.name);
|
|
|
|
this.logger.log(`Email user deleted: ${user.emailAddress} for business ${businessId}`);
|
|
|
|
return { message: UserMessage.EMAIL_USER_DELETED_SUCCESSFULLY };
|
|
} catch (error) {
|
|
this.logger.error(`Failed to delete email user ${user.emailAddress}:`, error);
|
|
throw new BadRequestException(UserMessage.FAILED_TO_DELETE_EMAIL_USER);
|
|
}
|
|
}
|
|
|
|
/*******************************/
|
|
|
|
async updateEmailUser(userId: string, businessId: string, updateEmailUserDto: UpdateEmailUserDto) {
|
|
const { password, displayName, quota, aliases, title, templateId, forwarders } = updateEmailUserDto;
|
|
|
|
const user = await this.userRepository.findOne(
|
|
{
|
|
id: userId,
|
|
business: { id: businessId },
|
|
emailEnabled: true,
|
|
},
|
|
{ populate: ["business"] },
|
|
);
|
|
|
|
if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND);
|
|
|
|
if (templateId) {
|
|
const template = await this.em.findOne(Template, { id: templateId, business: { id: businessId } });
|
|
if (!template) throw new BadRequestException(TemplateMessage.TEMPLATE_NOT_FOUND);
|
|
user.template = template;
|
|
}
|
|
|
|
// Validate business quota if quota is being updated
|
|
if (quota) {
|
|
const currentQuota = user.emailQuota || 0;
|
|
this.validateBusinessQuotaAvailability(user.business, currentQuota, quota);
|
|
}
|
|
|
|
try {
|
|
const updateData: UpdateUserDto = {};
|
|
|
|
if (password) {
|
|
updateData.password = password;
|
|
const hashedPassword = await this.passwordService.hashPassword(password);
|
|
user.password = hashedPassword;
|
|
}
|
|
|
|
if (displayName) updateData.name = displayName;
|
|
|
|
if (quota) updateData.quota = quota;
|
|
|
|
if (forwarders !== undefined) {
|
|
updateData.targets = forwarders;
|
|
user.forwarders = forwarders;
|
|
}
|
|
|
|
if (Object.keys(updateData).length > 0) {
|
|
await firstValueFrom(this.mailServerService.users.updateUser(user.wildduckUserId!, updateData));
|
|
}
|
|
|
|
if (aliases && aliases.length > 0) {
|
|
const currentAliases = await firstValueFrom(this.mailServerService.addresses.listUserAddresses(user.wildduckUserId!));
|
|
|
|
for (const alias of 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) {
|
|
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 (quota) {
|
|
const currentQuota = user.emailQuota || 0;
|
|
user.emailQuota = quota;
|
|
|
|
// Update business quota tracking
|
|
this.updateBusinessQuotaTracking(user.business, currentQuota, quota);
|
|
}
|
|
|
|
if (title) user.title = title;
|
|
|
|
await this.em.flush();
|
|
|
|
//sync quota
|
|
if (user.wildduckUserId) {
|
|
await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, user.wildduckUserId);
|
|
}
|
|
|
|
//sync business quota
|
|
await this.quotaSyncService.scheduleBusinessQuotaSync(businessId, user.business.name);
|
|
|
|
this.logger.log(`Email user updated: ${user.emailAddress} for business ${businessId}`);
|
|
|
|
return {
|
|
message: UserMessage.EMAIL_USER_UPDATED_SUCCESSFULLY,
|
|
user: {
|
|
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) {
|
|
this.logger.error(`Failed to update email user ${user.emailAddress}:`, error);
|
|
throw new BadRequestException("Failed to update email user");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update email user quota
|
|
*/
|
|
async updateEmailUserQuota(userId: string, businessId: string, newQuota: number) {
|
|
const user = await this.userRepository.findOne(
|
|
{
|
|
id: userId,
|
|
business: { id: businessId },
|
|
emailEnabled: true,
|
|
},
|
|
{ populate: ["business"] },
|
|
);
|
|
|
|
if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND);
|
|
|
|
// Validate business quota before updating
|
|
const currentQuota = user.emailQuota || 0;
|
|
this.validateBusinessQuotaAvailability(user.business, currentQuota, newQuota);
|
|
|
|
try {
|
|
if (user.wildduckUserId) {
|
|
await firstValueFrom(
|
|
this.mailServerService.users.updateUser(user.wildduckUserId, {
|
|
quota: newQuota,
|
|
}),
|
|
);
|
|
}
|
|
|
|
user.emailQuota = newQuota;
|
|
|
|
// Update business quota tracking
|
|
this.updateBusinessQuotaTracking(user.business, currentQuota, newQuota);
|
|
|
|
await this.em.persistAndFlush([user, user.business]);
|
|
|
|
//sync quota
|
|
if (user.wildduckUserId) {
|
|
await this.quotaSyncService.scheduleUserQuotaSync(user.id, businessId, user.wildduckUserId);
|
|
}
|
|
|
|
//sync business quota
|
|
await this.quotaSyncService.scheduleBusinessQuotaSync(businessId, user.business.name);
|
|
|
|
this.logger.log(`Email user quota updated: ${user.emailAddress} to ${newQuota} bytes`);
|
|
|
|
return { message: UserMessage.EMAIL_USER_QUOTA_UPDATED_SUCCESSFULLY, user };
|
|
} catch (error) {
|
|
this.logger.error(`Failed to update quota for email user ${user.emailAddress}:`, error);
|
|
throw new BadRequestException(UserMessage.FAILED_TO_UPDATE_EMAIL_USER_QUOTA);
|
|
}
|
|
}
|
|
|
|
//=====================================================
|
|
|
|
async updateEmailUserStatus(userId: string, businessId: string) {
|
|
const user = await this.userRepository.findOne({ id: userId, business: { id: businessId }, emailEnabled: true });
|
|
if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND);
|
|
|
|
user.isActive = !user.isActive;
|
|
await this.em.flush();
|
|
|
|
return {
|
|
message: UserMessage.STATUS_UPDATED,
|
|
status: user.isActive,
|
|
};
|
|
}
|
|
|
|
/*******************************/
|
|
private validateBusinessQuotaAvailability(business: Business, currentUserQuota: number, newUserQuota: number): void {
|
|
// Prevent quota downgrade - users can only upgrade their quota
|
|
if (newUserQuota < currentUserQuota) {
|
|
throw new BadRequestException(BusinessMessage.QUOTA_DOWNGRADE_NOT_ALLOWED);
|
|
}
|
|
|
|
const quotaDifference = newUserQuota - currentUserQuota;
|
|
|
|
if (quotaDifference > 0) {
|
|
const remainingQuota = Number(business.remainingQuota) || 0;
|
|
|
|
if (quotaDifference > remainingQuota) {
|
|
throw new BadRequestException(BusinessMessage.INSUFFICIENT_QUOTA);
|
|
}
|
|
}
|
|
}
|
|
|
|
//=====================================================
|
|
|
|
private updateBusinessQuotaTracking(business: Business, currentUserQuota: number, newUserQuota: number): void {
|
|
const quotaDifference = newUserQuota - currentUserQuota;
|
|
|
|
business.usedQuota = Number(business.usedQuota) + quotaDifference;
|
|
business.remainingQuota = Number(business.quota) - Number(business.usedQuota);
|
|
|
|
this.logger.log(`Updated business quota for ${business.name}: used=${business.usedQuota}, remaining=${business.remainingQuota}`);
|
|
}
|
|
|
|
// Push Notification Methods
|
|
async registerPushSubscription(userId: string, registerPushSubscriptionDto: RegisterPushSubscriptionDto) {
|
|
try {
|
|
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
|
|
if (!user) {
|
|
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
|
}
|
|
|
|
// Validate the subscription
|
|
if (!this.pushNotificationService.validateSubscription(registerPushSubscriptionDto.subscription)) {
|
|
throw new BadRequestException("Invalid push subscription format");
|
|
}
|
|
|
|
// Initialize pushTokens array if it doesn't exist (now stores subscriptions)
|
|
if (!user.pushTokens) {
|
|
user.pushTokens = [];
|
|
}
|
|
|
|
// Check if subscription endpoint already exists
|
|
const existingIndex = user.pushTokens.findIndex((sub: any) => sub.endpoint === registerPushSubscriptionDto.subscription.endpoint);
|
|
|
|
if (existingIndex !== -1) {
|
|
// Update existing subscription
|
|
user.pushTokens[existingIndex] = {
|
|
...registerPushSubscriptionDto.subscription,
|
|
deviceType: registerPushSubscriptionDto.deviceType,
|
|
deviceName: registerPushSubscriptionDto.deviceName,
|
|
registeredAt: new Date(),
|
|
};
|
|
} else {
|
|
// Add new subscription (limit to 10 subscriptions per user)
|
|
if (user.pushTokens.length >= 10) {
|
|
// Remove oldest subscription
|
|
user.pushTokens.shift();
|
|
}
|
|
|
|
user.pushTokens.push({
|
|
...registerPushSubscriptionDto.subscription,
|
|
deviceType: registerPushSubscriptionDto.deviceType,
|
|
deviceName: registerPushSubscriptionDto.deviceName,
|
|
registeredAt: new Date(),
|
|
});
|
|
}
|
|
|
|
await this.em.flush();
|
|
|
|
this.logger.log(`Push subscription registered for user ${userId}: ${registerPushSubscriptionDto.deviceType || "unknown device"}`);
|
|
|
|
return {
|
|
message: "Subscription registered successfully",
|
|
subscriptionCount: user.pushTokens.length,
|
|
endpoint: registerPushSubscriptionDto.subscription.endpoint,
|
|
};
|
|
} catch (error) {
|
|
this.logger.error(`Error registering push subscription for user ${userId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async updatePushSubscriptions(userId: string, updatePushSubscriptionsDto: UpdatePushSubscriptionsDto) {
|
|
try {
|
|
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
|
|
if (!user) {
|
|
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
|
}
|
|
|
|
// Validate all subscriptions
|
|
for (const subscription of updatePushSubscriptionsDto.subscriptions) {
|
|
if (!this.pushNotificationService.validateSubscription(subscription)) {
|
|
throw new BadRequestException(`Invalid subscription format for endpoint: ${(subscription as any).endpoint}`);
|
|
}
|
|
}
|
|
|
|
user.pushTokens = updatePushSubscriptionsDto.subscriptions.map((sub) => ({
|
|
...sub,
|
|
updatedAt: new Date(),
|
|
}));
|
|
await this.em.flush();
|
|
|
|
this.logger.log(`Push subscriptions updated for user ${userId}: ${updatePushSubscriptionsDto.subscriptions.length} subscriptions`);
|
|
|
|
return {
|
|
message: CommonMessage.UPDATE_SUCCESS,
|
|
subscriptionCount: user.pushTokens.length,
|
|
};
|
|
} catch (error) {
|
|
this.logger.error(`Error updating push subscriptions for user ${userId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async removePushSubscription(userId: string, endpoint: string) {
|
|
try {
|
|
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
|
|
if (!user) {
|
|
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
|
}
|
|
|
|
if (!user.pushTokens || user.pushTokens.length === 0) {
|
|
throw new BadRequestException("No subscriptions found");
|
|
}
|
|
|
|
const subscriptions = user.pushTokens;
|
|
const initialCount = subscriptions.length;
|
|
user.pushTokens = subscriptions.filter((sub) => sub.endpoint !== endpoint);
|
|
|
|
if (user.pushTokens.length === initialCount) {
|
|
throw new BadRequestException("Subscription not found");
|
|
}
|
|
|
|
await this.em.flush();
|
|
|
|
this.logger.log(`Push subscription removed for user ${userId}`);
|
|
|
|
return {
|
|
message: "Subscription removed successfully",
|
|
subscriptionCount: user.pushTokens.length,
|
|
};
|
|
} catch (error) {
|
|
this.logger.error(`Error removing push subscription for user ${userId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async updatePushNotificationSettings(userId: string, settingsDto: UpdatePushNotificationSettingsDto) {
|
|
try {
|
|
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
|
|
if (!user) {
|
|
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
|
}
|
|
|
|
user.pushNotificationsEnabled = settingsDto.enabled;
|
|
await this.em.flush();
|
|
|
|
this.logger.log(`Push notification settings updated for user ${userId}: ${settingsDto.enabled ? "enabled" : "disabled"}`);
|
|
|
|
return {
|
|
message: CommonMessage.UPDATE_SUCCESS,
|
|
pushNotificationsEnabled: user.pushNotificationsEnabled,
|
|
};
|
|
} catch (error) {
|
|
this.logger.error(`Error updating push notification settings for user ${userId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async testPushNotification(userId: string, _testDto: TestPushNotificationDto) {
|
|
try {
|
|
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
|
|
if (!user) {
|
|
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
|
}
|
|
|
|
if (!user.pushNotificationsEnabled) {
|
|
throw new BadRequestException("Push notifications are disabled for this user");
|
|
}
|
|
|
|
if (!user.pushTokens || user.pushTokens.length === 0) {
|
|
throw new BadRequestException("No push subscriptions registered for this user");
|
|
}
|
|
|
|
if (!this.pushNotificationService.isEnabled()) {
|
|
throw new BadRequestException("Push notification service is disabled");
|
|
}
|
|
|
|
// Convert stored subscriptions to proper format
|
|
const subscriptions = user.pushTokens.map((sub) => ({
|
|
endpoint: sub.endpoint,
|
|
keys: sub.keys,
|
|
}));
|
|
|
|
const success = await this.pushNotificationService.sendTestNotification(subscriptions, user.id);
|
|
|
|
if (!success) {
|
|
throw new BadRequestException("Failed to send test notification");
|
|
}
|
|
|
|
this.logger.log(`Test push notification sent for user ${userId}`);
|
|
|
|
return {
|
|
message: "Test notification sent successfully",
|
|
subscriptionCount: subscriptions.length,
|
|
serviceInfo: this.pushNotificationService.getServiceInfo(),
|
|
};
|
|
} catch (error) {
|
|
this.logger.error(`Error sending test push notification for user ${userId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getPushNotificationInfo(userId: string) {
|
|
try {
|
|
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
|
|
if (!user) {
|
|
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
|
}
|
|
|
|
return {
|
|
pushNotificationsEnabled: user.pushNotificationsEnabled,
|
|
subscriptionCount: user.pushTokens?.length || 0,
|
|
lastPushNotificationAt: user.lastPushNotificationAt,
|
|
serviceEnabled: this.pushNotificationService.isEnabled(),
|
|
vapidPublicKey: this.pushNotificationService.getVAPIDPublicKey(),
|
|
serviceInfo: this.pushNotificationService.getServiceInfo(),
|
|
setup: {
|
|
instructions: "Register your browser for push notifications using the service worker",
|
|
documentation: {
|
|
webPushAPI: "https://developer.mozilla.org/en-US/docs/Web/API/Push_API",
|
|
serviceWorkers: "https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API",
|
|
},
|
|
},
|
|
};
|
|
} catch (error) {
|
|
this.logger.error(`Error getting push notification info for user ${userId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getVAPIDPublicKey() {
|
|
try {
|
|
const vapidPublicKey = this.pushNotificationService.getVAPIDPublicKey();
|
|
|
|
if (!vapidPublicKey) {
|
|
throw new BadRequestException("VAPID public key is not configured");
|
|
}
|
|
|
|
return {
|
|
vapidPublicKey,
|
|
serviceInfo: this.pushNotificationService.getServiceInfo(),
|
|
};
|
|
} catch (error) {
|
|
this.logger.error("Error getting VAPID public key:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|