chore: add najva push notif service
This commit is contained in:
@@ -1,7 +0,0 @@
|
|||||||
-- Add 2FA fields to users table
|
|
||||||
ALTER TABLE users
|
|
||||||
ADD COLUMN is_2fa_enabled BOOLEAN DEFAULT FALSE NOT NULL,
|
|
||||||
ADD COLUMN two_factor_enabled_at TIMESTAMPTZ DEFAULT NULL;
|
|
||||||
|
|
||||||
-- Create index on is_2fa_enabled for query performance
|
|
||||||
CREATE INDEX idx_users_is_2fa_enabled ON users (is_2fa_enabled);
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
-- Add web push notification fields to users table
|
|
||||||
ALTER TABLE users
|
|
||||||
ADD COLUMN push_tokens JSONB DEFAULT NULL,
|
|
||||||
ADD COLUMN push_notifications_enabled BOOLEAN DEFAULT true,
|
|
||||||
ADD COLUMN last_push_notification_at TIMESTAMPTZ DEFAULT NULL;
|
|
||||||
|
|
||||||
-- Add indexes for better performance
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_push_notifications_enabled ON users (push_notifications_enabled)
|
|
||||||
WHERE
|
|
||||||
push_notifications_enabled = true
|
|
||||||
AND deleted_at IS NULL;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_push_tokens ON users USING GIN (push_tokens)
|
|
||||||
WHERE
|
|
||||||
push_tokens IS NOT NULL
|
|
||||||
AND deleted_at IS NULL;
|
|
||||||
|
|
||||||
-- Add comment for documentation
|
|
||||||
COMMENT ON COLUMN users.push_tokens IS 'JSON array of web push subscription objects containing endpoint and keys';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN users.push_notifications_enabled IS 'Whether web push notifications are enabled for this user';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN users.last_push_notification_at IS 'Timestamp of the last push notification sent to this user';
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
// export const AUTH_THROTTLE = "AUTH_THROTTLE";
|
// export const AUTH_THROTTLE = "AUTH_THROTTLE";
|
||||||
export const AI_CONFIG = "AI_CONFIG";
|
export const AI_CONFIG = "AI_CONFIG";
|
||||||
|
export const MIKRO_ORM_QUERY_LOGGER = "MIKRO_ORM_QUERY_LOGGER";
|
||||||
|
export const NAJVA_CONFIG = "NAJVA_CONFIG";
|
||||||
export const AUTH_THROTTLE_TTL = 1 * 60 * 1000;
|
export const AUTH_THROTTLE_TTL = 1 * 60 * 1000;
|
||||||
export const AUTH_THROTTLE_LIMIT = 5;
|
export const AUTH_THROTTLE_LIMIT = 5;
|
||||||
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
|
export const AUTH__REFRESH_THROTTLE_TTL = 10 * 60 * 1000;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ValueProvider } from "@nestjs/common";
|
||||||
|
import { Logger } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { MIKRO_ORM_QUERY_LOGGER } from "../constants";
|
||||||
|
|
||||||
|
export const mikroOrmQueryLoggerProvider: ValueProvider = {
|
||||||
|
provide: MIKRO_ORM_QUERY_LOGGER, //
|
||||||
|
useValue: new Logger("mikro-orm"),
|
||||||
|
};
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { ValueProvider } from "@nestjs/common";
|
|
||||||
import { Logger } from "@nestjs/common";
|
|
||||||
|
|
||||||
export const MIKRO_ORM_QUERY_LOGGER = "MikroOrmQueryLogger";
|
|
||||||
|
|
||||||
export const MikroOrmQueryLogger: ValueProvider = {
|
|
||||||
provide: MIKRO_ORM_QUERY_LOGGER,
|
|
||||||
useValue: new Logger("mikro-orm"),
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { FactoryProvider } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
|
import { NAJVA_CONFIG } from "../constants";
|
||||||
|
|
||||||
|
export interface NajvaConfigType {
|
||||||
|
apiUrl: string;
|
||||||
|
apiKey: string;
|
||||||
|
websiteId: number;
|
||||||
|
ttl: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const najvaConfigProvider: FactoryProvider<NajvaConfigType> = {
|
||||||
|
provide: NAJVA_CONFIG,
|
||||||
|
useFactory: (configService: ConfigService) => {
|
||||||
|
return {
|
||||||
|
apiUrl: configService.getOrThrow("NAJVA_API_URL"),
|
||||||
|
apiKey: configService.getOrThrow("NAJVA_API_KEY"),
|
||||||
|
websiteId: configService.getOrThrow("NAJVA_WEBSITE_ID"),
|
||||||
|
ttl: configService.getOrThrow("PUSH_TTL"),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
inject: [ConfigService],
|
||||||
|
};
|
||||||
@@ -3,11 +3,12 @@ import { PostgreSqlDriver } from "@mikro-orm/postgresql";
|
|||||||
import { Logger } from "@nestjs/common";
|
import { Logger } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
import { MIKRO_ORM_QUERY_LOGGER, MikroOrmQueryLogger } from "../common/providers/mikro-orm-logger";
|
import { MIKRO_ORM_QUERY_LOGGER } from "../common/constants";
|
||||||
|
import { mikroOrmQueryLoggerProvider } from "../common/providers/mikro-orm-logger.provider";
|
||||||
|
|
||||||
export const databaseConfig: MikroOrmModuleAsyncOptions = {
|
export const databaseConfig: MikroOrmModuleAsyncOptions = {
|
||||||
inject: [ConfigService, MIKRO_ORM_QUERY_LOGGER],
|
inject: [ConfigService, MIKRO_ORM_QUERY_LOGGER],
|
||||||
providers: [MikroOrmQueryLogger],
|
providers: [mikroOrmQueryLoggerProvider],
|
||||||
useFactory: (configService: ConfigService, logger: Logger) => {
|
useFactory: (configService: ConfigService, logger: Logger) => {
|
||||||
const DB_PASS = configService.getOrThrow<string>("DB_PASS");
|
const DB_PASS = configService.getOrThrow<string>("DB_PASS");
|
||||||
const DB_USER = configService.getOrThrow<string>("DB_USER");
|
const DB_USER = configService.getOrThrow<string>("DB_USER");
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { ConfigType, registerAs } from "@nestjs/config";
|
|
||||||
|
|
||||||
export const pushNotificationsConfig = registerAs("pushNotifications", () => ({
|
|
||||||
webPush: {
|
|
||||||
vapidPublicKey: process.env.VAPID_PUBLIC_KEY || "",
|
|
||||||
vapidPrivateKey: process.env.VAPID_PRIVATE_KEY || "",
|
|
||||||
vapidSubject: process.env.VAPID_SUBJECT || "mailto:admin@danakcorp.com",
|
|
||||||
},
|
|
||||||
enabled: process.env.PUSH_NOTIFICATIONS_ENABLED === "true",
|
|
||||||
retryAttempts: parseInt(process.env.PUSH_RETRY_ATTEMPTS || "3", 10),
|
|
||||||
retryDelay: parseInt(process.env.PUSH_RETRY_DELAY || "5000", 10),
|
|
||||||
ttl: parseInt(process.env.PUSH_TTL || "86400", 10), // 24 hours
|
|
||||||
urgency: process.env.PUSH_URGENCY || "normal", // low, normal, high
|
|
||||||
}));
|
|
||||||
|
|
||||||
export type PushNotificationsConfigType = ConfigType<typeof pushNotificationsConfig>;
|
|
||||||
|
|
||||||
export const pushNotificationsConfigFactory = () => ({
|
|
||||||
imports: [],
|
|
||||||
useFactory: (configService: ConfigService) => configService.get("pushNotifications"),
|
|
||||||
inject: [ConfigService],
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export interface INajvaNotificationMessage {
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
icon?: string;
|
||||||
|
image?: string;
|
||||||
|
notification_click: {
|
||||||
|
click_url: string;
|
||||||
|
};
|
||||||
|
button_1?: {
|
||||||
|
title: string;
|
||||||
|
click_url: string;
|
||||||
|
};
|
||||||
|
button_2?: {
|
||||||
|
title: string;
|
||||||
|
click_url: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INajvaPushRequest {
|
||||||
|
website_id: number;
|
||||||
|
ttl: number;
|
||||||
|
tokens: string[];
|
||||||
|
message: INajvaNotificationMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INajvaPushResponse {
|
||||||
|
Message: string;
|
||||||
|
Entries: {
|
||||||
|
request_id: string;
|
||||||
|
tokens: Array<{
|
||||||
|
token: string;
|
||||||
|
status: "Sent" | "InvalidToken";
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,26 +1,27 @@
|
|||||||
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
import { MikroOrmModule } from "@mikro-orm/nestjs";
|
||||||
|
import { HttpModule } from "@nestjs/axios";
|
||||||
import { BullModule } from "@nestjs/bullmq";
|
import { BullModule } from "@nestjs/bullmq";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
|
||||||
|
|
||||||
import { NOTIFICATION } from "./constants";
|
import { NOTIFICATION } from "./constants";
|
||||||
import { Notification } from "./entities/notification.entity";
|
import { Notification } from "./entities/notification.entity";
|
||||||
import { ChangePasswordHandler } from "./handlers/change-password.handler";
|
import { ChangePasswordHandler } from "./handlers/change-password.handler";
|
||||||
import { NewEmailHandler } from "./handlers/new-email.handler";
|
import { NewEmailHandler } from "./handlers/new-email.handler";
|
||||||
import { NotificationHandlerFactory } from "./handlers/notification-handler.factory";
|
import { NotificationHandlerFactory } from "./handlers/notification-handler.factory";
|
||||||
|
import { UserLoginHandler } from "./handlers/user-login.handler";
|
||||||
import { NotificationController } from "./notifications.controller";
|
import { NotificationController } from "./notifications.controller";
|
||||||
import { NotificationProcessor } from "./queue/notification.processor";
|
import { NotificationProcessor } from "./queue/notification.processor";
|
||||||
import { NotificationQueue } from "./queue/notification.queue";
|
import { NotificationQueue } from "./queue/notification.queue";
|
||||||
|
import { NajvaPushService } from "./services/najva-push.service";
|
||||||
import { NotificationsService } from "./services/notifications.service";
|
import { NotificationsService } from "./services/notifications.service";
|
||||||
import { pushNotificationsConfig } from "../../configs/push-notifications.config";
|
import { najvaConfigProvider } from "../../common/providers/najva-config.provider";
|
||||||
import { NotificationSetting } from "../settings/entities/notification-setting.entity";
|
import { NotificationSetting } from "../settings/entities/notification-setting.entity";
|
||||||
import { SettingModule } from "../settings/settings.module";
|
import { SettingModule } from "../settings/settings.module";
|
||||||
import { UtilsModule } from "../utils/utils.module";
|
import { UtilsModule } from "../utils/utils.module";
|
||||||
import { UserLoginHandler } from "./handlers/user-login.handler";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forFeature(pushNotificationsConfig),
|
HttpModule,
|
||||||
MikroOrmModule.forFeature([Notification, NotificationSetting]),
|
MikroOrmModule.forFeature([Notification, NotificationSetting]),
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
name: NOTIFICATION.QUEUE_NAME,
|
name: NOTIFICATION.QUEUE_NAME,
|
||||||
@@ -40,15 +41,16 @@ import { UserLoginHandler } from "./handlers/user-login.handler";
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
NotificationsService,
|
NotificationsService,
|
||||||
|
NajvaPushService,
|
||||||
NotificationQueue,
|
NotificationQueue,
|
||||||
NotificationProcessor,
|
NotificationProcessor,
|
||||||
// notification handlers
|
|
||||||
NotificationHandlerFactory,
|
NotificationHandlerFactory,
|
||||||
UserLoginHandler,
|
UserLoginHandler,
|
||||||
NewEmailHandler,
|
NewEmailHandler,
|
||||||
ChangePasswordHandler,
|
ChangePasswordHandler,
|
||||||
|
najvaConfigProvider,
|
||||||
],
|
],
|
||||||
controllers: [NotificationController],
|
controllers: [NotificationController],
|
||||||
exports: [NotificationsService, NotificationQueue],
|
exports: [NotificationsService, NotificationQueue, NajvaPushService],
|
||||||
})
|
})
|
||||||
export class NotificationModule {}
|
export class NotificationModule {}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { HttpService } from "@nestjs/axios";
|
||||||
|
import { Inject, Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { NAJVA_CONFIG } from "../../../common/constants";
|
||||||
|
import { NajvaConfigType } from "../../../common/providers/najva-config.provider";
|
||||||
|
import { INajvaNotificationMessage, INajvaPushRequest, INajvaPushResponse } from "../interfaces/INajva";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NajvaPushService {
|
||||||
|
private readonly logger = new Logger(NajvaPushService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(NAJVA_CONFIG) private readonly config: NajvaConfigType,
|
||||||
|
private readonly httpService: HttpService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
//===============================================
|
||||||
|
|
||||||
|
async sendPushNotification(tokens: string[], message: INajvaNotificationMessage): Promise<INajvaPushResponse | null> {
|
||||||
|
if (!tokens || tokens.length === 0) {
|
||||||
|
this.logger.warn("No tokens provided for push notification");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validTokens = tokens.filter((token) => token && token.trim() !== "");
|
||||||
|
if (validTokens.length === 0) {
|
||||||
|
this.logger.warn("No valid tokens found for push notification");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: INajvaPushRequest = {
|
||||||
|
website_id: this.config.websiteId,
|
||||||
|
ttl: this.config.ttl,
|
||||||
|
tokens: validTokens,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
apiKey: this.config.apiKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.logger.log(`Sending push notification to ${validTokens.length} tokens`);
|
||||||
|
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
formData.append("website_id", payload.website_id.toString());
|
||||||
|
formData.append("ttl", payload.ttl.toString());
|
||||||
|
|
||||||
|
validTokens.forEach((token) => {
|
||||||
|
formData.append("tokens[]", token);
|
||||||
|
});
|
||||||
|
|
||||||
|
formData.append("message.title", message.title);
|
||||||
|
formData.append("message.body", message.body);
|
||||||
|
|
||||||
|
formData.append("message.notification_click.click_url", message.notification_click.click_url);
|
||||||
|
|
||||||
|
if (message.icon) {
|
||||||
|
formData.append("message.icon", message.icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.image) {
|
||||||
|
formData.append("message.image", message.image);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.button_1) {
|
||||||
|
formData.append("message.button_1.title", message.button_1.title);
|
||||||
|
formData.append("message.button_1.click_url", message.button_1.click_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.button_2) {
|
||||||
|
formData.append("message.button_2.title", message.button_2.title);
|
||||||
|
formData.append("message.button_2.click_url", message.button_2.click_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await firstValueFrom(this.httpService.post<INajvaPushResponse>(this.config.apiUrl, formData.toString(), { headers }));
|
||||||
|
|
||||||
|
this.logger.log(`Push notification sent successfully. Request ID: ${response.data.Entries.request_id}`);
|
||||||
|
|
||||||
|
response.data.Entries.tokens.forEach((tokenInfo) => {
|
||||||
|
if (tokenInfo.status === "InvalidToken") {
|
||||||
|
this.logger.warn(`Invalid token detected: ${tokenInfo.token}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error("Failed to send push notification:", error);
|
||||||
|
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
this.logger.error(`HTTP Status: ${error.response?.status}`);
|
||||||
|
this.logger.error(`Response: ${JSON.stringify(error.response?.data)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//===============================================
|
||||||
|
async sendNotificationToUser(userToken: string, message: INajvaNotificationMessage): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await this.sendPushNotification([userToken], message);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenResult = result.Entries.tokens.find((t) => t.token === userToken);
|
||||||
|
return tokenResult?.status === "Sent";
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to send notification to user token ${userToken}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//===============================================
|
||||||
|
|
||||||
|
async sendNotificationToMultipleUsers(userTokens: string[], message: INajvaNotificationMessage): Promise<{ success: number; failed: number }> {
|
||||||
|
try {
|
||||||
|
const result = await this.sendPushNotification(userTokens, message);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return { success: 0, failed: userTokens.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCount = result.Entries.tokens.filter((t) => t.status === "Sent").length;
|
||||||
|
const failedCount = result.Entries.tokens.filter((t) => t.status === "InvalidToken").length;
|
||||||
|
|
||||||
|
return { success: successCount, failed: failedCount };
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to send notifications to multiple users:`, error);
|
||||||
|
return { success: 0, failed: userTokens.length };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import { EntityManager } from "@mikro-orm/postgresql";
|
import { EntityManager } from "@mikro-orm/postgresql";
|
||||||
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
|
import { NajvaPushService } from "./najva-push.service";
|
||||||
import { CommonMessage, NotificationMessage, UserMessage } from "../../../common/enums/message.enum";
|
import { CommonMessage, NotificationMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||||
import { NotifType } from "../../settings/enums/notif-settings.enum";
|
import { NotifType } from "../../settings/enums/notif-settings.enum";
|
||||||
import { UserSettingsService } from "../../settings/services/user-settings.service";
|
import { UserSettingsService } from "../../settings/services/user-settings.service";
|
||||||
@@ -8,6 +10,7 @@ import { User } from "../../users/entities/user.entity";
|
|||||||
import { CreateNotificationDto } from "../DTO/create-notification.dto";
|
import { CreateNotificationDto } from "../DTO/create-notification.dto";
|
||||||
import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto";
|
import { SearchNotificationQueryDto } from "../DTO/search-notification-query.dto";
|
||||||
import { Notification } from "../entities/notification.entity";
|
import { Notification } from "../entities/notification.entity";
|
||||||
|
import { INajvaNotificationMessage } from "../interfaces/INajva";
|
||||||
import { IBaseNotificationData, IChangePasswordNotificationData, INewEmailNotificationData } from "../interfaces/ISendNotificationData";
|
import { IBaseNotificationData, IChangePasswordNotificationData, INewEmailNotificationData } from "../interfaces/ISendNotificationData";
|
||||||
import { NotificationRepository } from "../repositories/notifications.repository";
|
import { NotificationRepository } from "../repositories/notifications.repository";
|
||||||
|
|
||||||
@@ -18,6 +21,8 @@ export class NotificationsService {
|
|||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
private readonly notificationRepository: NotificationRepository,
|
private readonly notificationRepository: NotificationRepository,
|
||||||
private readonly userSettingsService: UserSettingsService,
|
private readonly userSettingsService: UserSettingsService,
|
||||||
|
private readonly najvaPushService: NajvaPushService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
//************************ */
|
//************************ */
|
||||||
@@ -37,6 +42,16 @@ export class NotificationsService {
|
|||||||
});
|
});
|
||||||
await localEm.persistAndFlush(notification);
|
await localEm.persistAndFlush(notification);
|
||||||
|
|
||||||
|
if (user.pushToken) {
|
||||||
|
await this.sendPushNotificationToUser(user.pushToken, {
|
||||||
|
title: createNotificationDto.title,
|
||||||
|
body: createNotificationDto.message,
|
||||||
|
notification_click: {
|
||||||
|
click_url: `${this.configService.getOrThrow("FRONTEND_URL")}/notifications`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await localEm.commit();
|
await localEm.commit();
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
@@ -59,11 +74,35 @@ export class NotificationsService {
|
|||||||
});
|
});
|
||||||
//
|
//
|
||||||
await em.persistAndFlush(notification);
|
await em.persistAndFlush(notification);
|
||||||
|
|
||||||
|
// Send push notification if user has a push token
|
||||||
|
if (user.pushToken) {
|
||||||
|
await this.sendPushNotificationToUser(user.pushToken, {
|
||||||
|
title: createNotificationDto.title,
|
||||||
|
body: createNotificationDto.message,
|
||||||
|
notification_click: {
|
||||||
|
click_url: `${this.configService.getOrThrow("FRONTEND_URL")}/notifications`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************ */
|
//===============================================
|
||||||
|
|
||||||
|
async sendPushNotificationToUser(userToken: string, message: INajvaNotificationMessage): Promise<boolean> {
|
||||||
|
return this.najvaPushService.sendNotificationToUser(userToken, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
//===============================================
|
||||||
|
|
||||||
|
async sendPushNotificationToMultipleUsers(userTokens: string[], message: INajvaNotificationMessage): Promise<{ success: number; failed: number }> {
|
||||||
|
return this.najvaPushService.sendNotificationToMultipleUsers(userTokens, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
//===============================================
|
||||||
|
|
||||||
async markAsRead(notificationId: string, userId: string) {
|
async markAsRead(notificationId: string, userId: string) {
|
||||||
const notification = await this.notificationRepository.findOne({ id: notificationId, recipient: { id: userId }, deletedAt: null });
|
const notification = await this.notificationRepository.findOne({ id: notificationId, recipient: { id: userId }, deletedAt: null });
|
||||||
@@ -78,7 +117,7 @@ export class NotificationsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************ */
|
//===============================================
|
||||||
|
|
||||||
async getAllNotifications(queryDto: SearchNotificationQueryDto, recipientId: string) {
|
async getAllNotifications(queryDto: SearchNotificationQueryDto, recipientId: string) {
|
||||||
const [notifications, count] = await this.notificationRepository.getAllNotifications(queryDto, recipientId);
|
const [notifications, count] = await this.notificationRepository.getAllNotifications(queryDto, recipientId);
|
||||||
@@ -86,7 +125,7 @@ export class NotificationsService {
|
|||||||
return { notifications, notificationCount, count, paginate: true };
|
return { notifications, notificationCount, count, paginate: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************ */
|
//===============================================
|
||||||
|
|
||||||
async getNotificationById(id: string) {
|
async getNotificationById(id: string) {
|
||||||
const notification = await this.notificationRepository.findOne({ id, deletedAt: null });
|
const notification = await this.notificationRepository.findOne({ id, deletedAt: null });
|
||||||
@@ -94,14 +133,14 @@ export class NotificationsService {
|
|||||||
return { notification };
|
return { notification };
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************ */
|
//===============================================
|
||||||
|
|
||||||
async countUserNotifications(userId: string) {
|
async countUserNotifications(userId: string) {
|
||||||
const notificationCount = await this.notificationRepository.count({ recipient: { id: userId }, isRead: false, deletedAt: null });
|
const notificationCount = await this.notificationRepository.count({ recipient: { id: userId }, isRead: false, deletedAt: null });
|
||||||
return notificationCount;
|
return notificationCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************ */
|
//===============================================
|
||||||
|
|
||||||
async markAllAsRead(userId: string) {
|
async markAllAsRead(userId: string) {
|
||||||
await this.notificationRepository.nativeUpdate({ recipient: { id: userId }, deletedAt: null }, { isRead: true });
|
await this.notificationRepository.nativeUpdate({ recipient: { id: userId }, deletedAt: null }, { isRead: true });
|
||||||
@@ -110,7 +149,7 @@ export class NotificationsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************ */
|
//===============================================
|
||||||
|
|
||||||
async createLoginNotification(recipientId: string, _data: IBaseNotificationData) {
|
async createLoginNotification(recipientId: string, _data: IBaseNotificationData) {
|
||||||
const loginDate = new Date().toLocaleString("fa-IR");
|
const loginDate = new Date().toLocaleString("fa-IR");
|
||||||
@@ -121,7 +160,7 @@ export class NotificationsService {
|
|||||||
return this.createNotification({ title: NotificationMessage.LOGIN, type: NotifType.USER_LOGIN, message, recipientId });
|
return this.createNotification({ title: NotificationMessage.LOGIN, type: NotifType.USER_LOGIN, message, recipientId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//************************ */
|
//===============================================
|
||||||
|
|
||||||
async createNewEmailNotification(recipientId: string, data: INewEmailNotificationData, em: EntityManager) {
|
async createNewEmailNotification(recipientId: string, data: INewEmailNotificationData, em: EntityManager) {
|
||||||
const message = NotificationMessage.NEW_EMAIL_MESSAGE.replace("[subject]", data.subject);
|
const message = NotificationMessage.NEW_EMAIL_MESSAGE.replace("[subject]", data.subject);
|
||||||
@@ -130,7 +169,7 @@ export class NotificationsService {
|
|||||||
return this.createNotification({ title: NotificationMessage.NEW_EMAIL, type: NotifType.NEW_EMAIL, message, recipientId }, em);
|
return this.createNotification({ title: NotificationMessage.NEW_EMAIL, type: NotifType.NEW_EMAIL, message, recipientId }, em);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//************************ */
|
//===============================================
|
||||||
|
|
||||||
async changePasswordNotification(recipientId: string, _data: IChangePasswordNotificationData, em: EntityManager) {
|
async changePasswordNotification(recipientId: string, _data: IChangePasswordNotificationData, em: EntityManager) {
|
||||||
const message = NotificationMessage.CHANGE_PASSWORD_MESSAGE;
|
const message = NotificationMessage.CHANGE_PASSWORD_MESSAGE;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { IsNotEmpty, IsOptional, IsUrl } from "class-validator";
|
import { IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator";
|
||||||
|
|
||||||
import { UserMessage } from "../../../common/enums/message.enum";
|
import { UserMessage } from "../../../common/enums/message.enum";
|
||||||
|
|
||||||
@@ -9,4 +9,9 @@ export class UpdateUserProfileDto {
|
|||||||
@IsUrl({ protocols: ["https"] }, { message: UserMessage.PROFILE_PICTURE_URL })
|
@IsUrl({ protocols: ["https"] }, { message: UserMessage.PROFILE_PICTURE_URL })
|
||||||
@ApiPropertyOptional({ description: "Profile picture URL", example: "https://example.com/profile.jpg" })
|
@ApiPropertyOptional({ description: "Profile picture URL", example: "https://example.com/profile.jpg" })
|
||||||
profilePic?: string;
|
profilePic?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@ApiPropertyOptional({ description: "Push notification token from frontend", example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." })
|
||||||
|
pushToken?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ export class User extends BaseEntity {
|
|||||||
@Property({ type: "timestamptz", nullable: true })
|
@Property({ type: "timestamptz", nullable: true })
|
||||||
twoFactorEnabledAt?: Date;
|
twoFactorEnabledAt?: Date;
|
||||||
|
|
||||||
|
@Property({ type: "varchar", nullable: true })
|
||||||
|
pushToken?: string;
|
||||||
|
|
||||||
//=========================
|
//=========================
|
||||||
|
|
||||||
@OneToMany(() => RefreshToken, (token) => token.user)
|
@OneToMany(() => RefreshToken, (token) => token.user)
|
||||||
|
|||||||
Reference in New Issue
Block a user