This commit is contained in:
pradeepkumar
2026-02-24 22:36:42 +05:30
parent d30358755e
commit 7ab5330a88
9 changed files with 390 additions and 3 deletions

View File

@@ -0,0 +1,231 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { OnEvent } from '@nestjs/event-emitter';
import * as admin from 'firebase-admin';
import { PrismaService } from '../prisma/prisma.service';
interface FcmToken {
token: string;
device: string;
createdAt: string;
}
interface NotificationPayload {
title: string;
body: string;
data?: Record<string, string>;
}
@Injectable()
export class NotificationsService implements OnModuleInit {
private readonly logger = new Logger(NotificationsService.name);
private firebaseInitialized = false;
constructor(
private readonly prisma: PrismaService,
private readonly configService: ConfigService,
) {}
onModuleInit() {
const projectId = this.configService.get<string>('firebase.projectId');
const clientEmail = this.configService.get<string>('firebase.clientEmail');
const privateKey = this.configService.get<string>('firebase.privateKey');
if (!projectId || !clientEmail || !privateKey) {
this.logger.warn(
'Firebase credentials not configured. Push notifications disabled.',
);
return;
}
if (admin.apps.length === 0) {
admin.initializeApp({
credential: admin.credential.cert({
projectId,
clientEmail,
privateKey,
}),
});
}
this.firebaseInitialized = true;
this.logger.log('Firebase Admin initialized for push notifications');
}
async registerToken(
userId: string,
token: string,
device: string = 'web',
): Promise<void> {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { fcmTokens: true },
});
const existingTokens: FcmToken[] = (user?.fcmTokens as FcmToken[]) || [];
// Remove existing entry for this token (if re-registering)
const filtered = existingTokens.filter((t) => t.token !== token);
filtered.push({
token,
device,
createdAt: new Date().toISOString(),
});
await this.prisma.user.update({
where: { id: userId },
data: { fcmTokens: filtered },
});
this.logger.log(`FCM token registered for user ${userId} (${device})`);
}
async removeToken(userId: string, token: string): Promise<void> {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { fcmTokens: true },
});
const existingTokens: FcmToken[] = (user?.fcmTokens as FcmToken[]) || [];
const filtered = existingTokens.filter((t) => t.token !== token);
await this.prisma.user.update({
where: { id: userId },
data: { fcmTokens: filtered.length > 0 ? filtered : null },
});
this.logger.log(`FCM token removed for user ${userId}`);
}
async sendPushNotification(
userId: string,
category: string,
payload: NotificationPayload,
): Promise<void> {
if (!this.firebaseInitialized) {
this.logger.debug('Firebase not initialized, skipping push notification');
return;
}
// Check user notification preferences
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { notificationPreferences: true, fcmTokens: true },
});
if (!user) return;
// Check if inApp notifications are enabled for this category
const prefs = user.notificationPreferences as Record<string, any> | null;
if (prefs?.notifications?.[category]?.inApp === false) {
this.logger.debug(
`Push notification skipped for user ${userId}: ${category} inApp disabled`,
);
return;
}
const tokens: FcmToken[] = (user.fcmTokens as FcmToken[]) || [];
if (tokens.length === 0) {
this.logger.debug(`No FCM tokens for user ${userId}`);
return;
}
const tokenStrings = tokens.map((t) => t.token);
try {
const response = await admin.messaging().sendEachForMulticast({
tokens: tokenStrings,
notification: {
title: payload.title,
body: payload.body,
},
data: payload.data || {},
webpush: {
fcmOptions: {
link: payload.data?.link || '/',
},
},
});
// Clean up invalid tokens
if (response.failureCount > 0) {
const invalidTokens: string[] = [];
response.responses.forEach((resp, idx) => {
if (
!resp.success &&
resp.error?.code === 'messaging/registration-token-not-registered'
) {
invalidTokens.push(tokenStrings[idx]);
}
});
if (invalidTokens.length > 0) {
const validTokens = tokens.filter(
(t) => !invalidTokens.includes(t.token),
);
await this.prisma.user.update({
where: { id: userId },
data: {
fcmTokens: validTokens.length > 0 ? validTokens : null,
},
});
this.logger.log(
`Removed ${invalidTokens.length} invalid FCM tokens for user ${userId}`,
);
}
}
this.logger.log(
`Push notification sent to user ${userId}: ${response.successCount} success, ${response.failureCount} failed`,
);
} catch (error) {
this.logger.error(
`Failed to send push notification to user ${userId}: ${error.message}`,
);
}
}
// ==========================================
// EVENT LISTENERS
// ==========================================
@OnEvent('notification.message')
async handleMessageNotification(event: {
recipientUserId: string;
senderName: string;
messagePreview: string;
conversationId: string;
}) {
await this.sendPushNotification(event.recipientUserId, 'message_updates', {
title: `New message from ${event.senderName}`,
body: event.messagePreview || 'You have a new message',
data: {
type: 'message',
conversationId: event.conversationId,
link: `/messages?conversation=${event.conversationId}`,
},
});
}
@OnEvent('notification.connection_request')
async handleConnectionRequestNotification(event: {
recipientUserId: string;
senderName: string;
connectionRequestId: string;
}) {
await this.sendPushNotification(
event.recipientUserId,
'new_lead_alerts',
{
title: 'New Connection Request',
body: `${event.senderName} wants to connect with you`,
data: {
type: 'connection_request',
connectionRequestId: event.connectionRequestId,
link: '/dashboard/connections',
},
},
);
}
}