534 lines
15 KiB
JavaScript
534 lines
15 KiB
JavaScript
const admin = require('firebase-admin');
|
||
|
||
class NotificationService {
|
||
constructor(prisma, io) {
|
||
this.prisma = prisma;
|
||
this.io = io;
|
||
this.initializeFirebase();
|
||
}
|
||
|
||
initializeFirebase() {
|
||
try {
|
||
// Firebase credentials kontrolü
|
||
const hasCredentials = process.env.FIREBASE_PROJECT_ID &&
|
||
process.env.FIREBASE_PRIVATE_KEY &&
|
||
process.env.FIREBASE_CLIENT_EMAIL;
|
||
|
||
if (!hasCredentials) {
|
||
console.log('⚠️ Firebase credentials not configured. Push notifications disabled.');
|
||
return;
|
||
}
|
||
|
||
if (!admin.apps.length) {
|
||
const serviceAccount = {
|
||
type: process.env.FIREBASE_TYPE,
|
||
project_id: process.env.FIREBASE_PROJECT_ID,
|
||
private_key_id: process.env.FIREBASE_PRIVATE_KEY_ID,
|
||
private_key: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
|
||
client_email: process.env.FIREBASE_CLIENT_EMAIL,
|
||
client_id: process.env.FIREBASE_CLIENT_ID,
|
||
auth_uri: process.env.FIREBASE_AUTH_URI,
|
||
token_uri: process.env.FIREBASE_TOKEN_URI,
|
||
auth_provider_x509_cert_url: process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL,
|
||
client_x509_cert_url: process.env.FIREBASE_CLIENT_X509_CERT_URL
|
||
};
|
||
|
||
admin.initializeApp({
|
||
credential: admin.credential.cert(serviceAccount)
|
||
});
|
||
|
||
console.log('✅ Firebase Admin SDK initialized successfully');
|
||
}
|
||
} catch (error) {
|
||
console.error('Firebase initialization error:', error.message);
|
||
console.warn('Push notifications will be disabled.');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Veritabanına bildirim kaydet
|
||
*/
|
||
async createNotification(data) {
|
||
try {
|
||
const notification = await this.prisma.notification.create({
|
||
data: {
|
||
userId: data.userId,
|
||
type: data.type,
|
||
title: data.title,
|
||
message: data.message,
|
||
data: data.data ? JSON.stringify(data.data) : null,
|
||
relatedListId: data.relatedListId || null,
|
||
relatedUserId: data.relatedUserId || null
|
||
},
|
||
include: {
|
||
relatedList: {
|
||
select: {
|
||
id: true,
|
||
name: true
|
||
}
|
||
},
|
||
relatedUser: {
|
||
select: {
|
||
id: true,
|
||
username: true,
|
||
firstName: true,
|
||
lastName: true,
|
||
avatar: true
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
return notification;
|
||
} catch (error) {
|
||
console.error('Error creating notification:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Socket.IO ile gerçek zamanlı bildirim gönder
|
||
*/
|
||
async sendRealtimeNotification(userId, notification) {
|
||
try {
|
||
if (this.io) {
|
||
this.io.to(`user_${userId}`).emit('notification', notification);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error sending realtime notification:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Push notification gönder
|
||
*/
|
||
async sendPushNotification(userId, notification) {
|
||
try {
|
||
// Kullanıcının push notification ayarını kontrol et
|
||
const setting = await this.prisma.setting.findUnique({
|
||
where: {
|
||
userId_key: {
|
||
userId,
|
||
key: 'notification_push_enabled'
|
||
}
|
||
}
|
||
});
|
||
|
||
if (setting && setting.value === 'false') {
|
||
return; // Push notification kapalı
|
||
}
|
||
|
||
// Kullanıcının device token'larını al
|
||
const deviceTokens = await this.prisma.deviceToken.findMany({
|
||
where: {
|
||
userId,
|
||
isActive: true
|
||
}
|
||
});
|
||
|
||
if (deviceTokens.length === 0) {
|
||
return; // Device token yok
|
||
}
|
||
|
||
const tokens = deviceTokens.map(dt => dt.token);
|
||
|
||
const message = {
|
||
notification: {
|
||
title: notification.title,
|
||
body: notification.message
|
||
},
|
||
data: {
|
||
type: notification.type,
|
||
notificationId: notification.id,
|
||
...(notification.data ? JSON.parse(notification.data) : {})
|
||
},
|
||
tokens
|
||
};
|
||
|
||
const response = await admin.messaging().sendMulticast(message);
|
||
|
||
// Başarısız token'ları temizle
|
||
if (response.failureCount > 0) {
|
||
const failedTokens = [];
|
||
response.responses.forEach((resp, idx) => {
|
||
if (!resp.success) {
|
||
failedTokens.push(tokens[idx]);
|
||
}
|
||
});
|
||
|
||
if (failedTokens.length > 0) {
|
||
await this.prisma.deviceToken.updateMany({
|
||
where: {
|
||
token: { in: failedTokens }
|
||
},
|
||
data: {
|
||
isActive: false
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
console.log(`Push notification sent to ${response.successCount} devices`);
|
||
} catch (error) {
|
||
console.error('Error sending push notification:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Tam bildirim gönderme (DB + Realtime + Push)
|
||
*/
|
||
async sendNotification(data) {
|
||
try {
|
||
// Kullanıcının bildirim ayarını kontrol et
|
||
const notificationKey = `notification_${data.type}`;
|
||
const setting = await this.prisma.setting.findUnique({
|
||
where: {
|
||
userId_key: {
|
||
userId: data.userId,
|
||
key: notificationKey
|
||
}
|
||
}
|
||
});
|
||
|
||
if (setting && setting.value === 'false') {
|
||
return; // Bu tür bildirim kapalı
|
||
}
|
||
|
||
// Veritabanına kaydet
|
||
const notification = await this.createNotification(data);
|
||
|
||
// Gerçek zamanlı gönder
|
||
await this.sendRealtimeNotification(data.userId, notification);
|
||
|
||
// Push notification gönder
|
||
await this.sendPushNotification(data.userId, notification);
|
||
|
||
return notification;
|
||
} catch (error) {
|
||
console.error('Error in sendNotification:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Birden fazla kullanıcıya bildirim gönder
|
||
*/
|
||
async sendNotificationToMultipleUsers(userIds, data) {
|
||
try {
|
||
const promises = userIds.map(userId =>
|
||
this.sendNotification({ ...data, userId })
|
||
);
|
||
|
||
const results = await Promise.allSettled(promises);
|
||
|
||
const successful = results.filter(r => r.status === 'fulfilled').length;
|
||
const failed = results.filter(r => r.status === 'rejected').length;
|
||
|
||
console.log(`Notifications sent: ${successful} successful, ${failed} failed`);
|
||
|
||
return { successful, failed };
|
||
} catch (error) {
|
||
console.error('Error sending notifications to multiple users:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Liste üyelerine bildirim gönder
|
||
*/
|
||
async sendNotificationToListMembers(listId, data, excludeUserId = null) {
|
||
try {
|
||
const members = await this.prisma.listMember.findMany({
|
||
where: {
|
||
listId,
|
||
userId: excludeUserId ? { not: excludeUserId } : undefined
|
||
},
|
||
select: {
|
||
userId: true
|
||
}
|
||
});
|
||
|
||
const userIds = members.map(m => m.userId);
|
||
|
||
if (userIds.length === 0) {
|
||
return { successful: 0, failed: 0 };
|
||
}
|
||
|
||
return await this.sendNotificationToMultipleUsers(userIds, {
|
||
...data,
|
||
relatedListId: listId
|
||
});
|
||
} catch (error) {
|
||
console.error('Error sending notifications to list members:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Liste daveti bildirimi
|
||
*/
|
||
async sendListInviteNotification(invitedUserId, inviterUser, list) {
|
||
return await this.sendNotification({
|
||
userId: invitedUserId,
|
||
type: 'list_invite',
|
||
title: 'Liste Daveti',
|
||
message: `${inviterUser.firstName} ${inviterUser.lastName} sizi "${list.name}" listesine davet etti`,
|
||
data: {
|
||
inviterId: inviterUser.id,
|
||
listId: list.id
|
||
},
|
||
relatedListId: list.id,
|
||
relatedUserId: inviterUser.id
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Ürün eklendi bildirimi
|
||
*/
|
||
async sendItemAddedNotification(listId, adderUser, itemName, excludeUserId = null) {
|
||
return await this.sendNotificationToListMembers(listId, {
|
||
type: 'item_added',
|
||
title: 'Ürün Eklendi',
|
||
message: `${adderUser.firstName} "${itemName}" ürününü listeye ekledi`,
|
||
data: {
|
||
adderId: adderUser.id,
|
||
itemName
|
||
},
|
||
relatedUserId: adderUser.id
|
||
}, excludeUserId);
|
||
}
|
||
|
||
/**
|
||
* Ürün silindi bildirimi
|
||
*/
|
||
async sendItemRemovedNotification(listId, removerUser, itemName, excludeUserId = null) {
|
||
return await this.sendNotificationToListMembers(listId, {
|
||
type: 'item_removed',
|
||
title: 'Ürün Silindi',
|
||
message: `${removerUser.firstName} "${itemName}" ürününü listeden sildi`,
|
||
data: {
|
||
removerId: removerUser.id,
|
||
itemName
|
||
},
|
||
relatedUserId: removerUser.id
|
||
}, excludeUserId);
|
||
}
|
||
|
||
/**
|
||
* Ürün satın alındı bildirimi
|
||
*/
|
||
async sendItemPurchasedNotification(listId, purchaserUser, itemName, excludeUserId = null) {
|
||
return await this.sendNotificationToListMembers(listId, {
|
||
type: 'item_purchased',
|
||
title: 'Ürün Satın Alındı',
|
||
message: `${purchaserUser.firstName} "${itemName}" ürününü satın aldı`,
|
||
data: {
|
||
purchaserId: purchaserUser.id,
|
||
itemName
|
||
},
|
||
relatedUserId: purchaserUser.id
|
||
}, excludeUserId);
|
||
}
|
||
|
||
/**
|
||
* Liste paylaşıldı bildirimi
|
||
*/
|
||
async sendListSharedNotification(listId, sharerUser, sharedWithUser, listName) {
|
||
return await this.sendNotification({
|
||
userId: sharedWithUser.id,
|
||
type: 'list_shared',
|
||
title: 'Liste Paylaşıldı',
|
||
message: `${sharerUser.firstName} "${listName}" listesini sizinle paylaştı`,
|
||
data: {
|
||
sharerId: sharerUser.id,
|
||
listName
|
||
},
|
||
relatedListId: listId,
|
||
relatedUserId: sharerUser.id
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Device token kaydet
|
||
*/
|
||
async registerDeviceToken(userId, token, deviceInfo = {}) {
|
||
try {
|
||
// Mevcut token'ı kontrol et
|
||
const existingToken = await this.prisma.deviceToken.findFirst({
|
||
where: {
|
||
token,
|
||
userId
|
||
}
|
||
});
|
||
|
||
if (existingToken) {
|
||
// Token'ı güncelle
|
||
return await this.prisma.deviceToken.update({
|
||
where: { id: existingToken.id },
|
||
data: {
|
||
isActive: true,
|
||
deviceInfo: deviceInfo ? JSON.stringify(deviceInfo) : null,
|
||
updatedAt: new Date()
|
||
}
|
||
});
|
||
} else {
|
||
// Yeni token oluştur
|
||
return await this.prisma.deviceToken.create({
|
||
data: {
|
||
userId,
|
||
token,
|
||
deviceInfo: deviceInfo ? JSON.stringify(deviceInfo) : null
|
||
}
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('Error registering device token:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Device token'ı deaktive et
|
||
*/
|
||
async unregisterDeviceToken(userId, token) {
|
||
try {
|
||
return await this.prisma.deviceToken.updateMany({
|
||
where: {
|
||
userId,
|
||
token
|
||
},
|
||
data: {
|
||
isActive: false
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Error unregistering device token:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Liste üyelerine bildirim gönder
|
||
*/
|
||
async notifyListMembers(listId, excludeUserId, notificationData) {
|
||
try {
|
||
// Liste üyelerini getir
|
||
const listMembers = await this.prisma.listMember.findMany({
|
||
where: {
|
||
listId,
|
||
userId: {
|
||
not: excludeUserId // Aksiyonu yapan kullanıcıyı hariç tut
|
||
}
|
||
},
|
||
include: {
|
||
user: {
|
||
select: {
|
||
id: true,
|
||
firstName: true,
|
||
lastName: true
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Liste sahibini de dahil et (eğer üye değilse)
|
||
const list = await this.prisma.shoppingList.findUnique({
|
||
where: { id: listId },
|
||
include: {
|
||
owner: {
|
||
select: {
|
||
id: true,
|
||
firstName: true,
|
||
lastName: true
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
const memberUserIds = listMembers.map(member => member.userId);
|
||
if (list.owner.id !== excludeUserId && !memberUserIds.includes(list.owner.id)) {
|
||
listMembers.push({
|
||
user: list.owner,
|
||
userId: list.owner.id
|
||
});
|
||
}
|
||
|
||
// Her üyeye bildirim gönder
|
||
const notifications = [];
|
||
for (const member of listMembers) {
|
||
|
||
// Socket.IO ile gerçek zamanlı bildirim
|
||
if (this.io) {
|
||
this.io.to(`user_${member.userId}`).emit('notification', {
|
||
type: notificationData.type,
|
||
title: notificationData.title,
|
||
message: notificationData.message,
|
||
data: notificationData.data,
|
||
timestamp: new Date()
|
||
});
|
||
}
|
||
|
||
// Veritabanına bildirim kaydet
|
||
const notification = await this.createNotification({
|
||
userId: member.userId,
|
||
type: notificationData.type,
|
||
title: notificationData.title,
|
||
message: notificationData.message,
|
||
data: notificationData.data,
|
||
relatedListId: listId,
|
||
relatedUserId: excludeUserId
|
||
});
|
||
|
||
notifications.push(notification);
|
||
|
||
// Push notification gönder
|
||
await this.sendPushNotification(member.userId, {
|
||
title: notificationData.title,
|
||
body: notificationData.message,
|
||
data: notificationData.data
|
||
});
|
||
}
|
||
|
||
return notifications;
|
||
} catch (error) {
|
||
console.error('Error notifying list members:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Singleton instance
|
||
let notificationServiceInstance = null;
|
||
|
||
// Factory function
|
||
const createNotificationService = (prisma, io) => {
|
||
if (!notificationServiceInstance) {
|
||
notificationServiceInstance = new NotificationService(prisma, io);
|
||
}
|
||
return notificationServiceInstance;
|
||
};
|
||
|
||
// Export both class and factory
|
||
module.exports = {
|
||
NotificationService,
|
||
createNotificationService,
|
||
// Default export for backward compatibility
|
||
notifyListMembers: async (...args) => {
|
||
if (!notificationServiceInstance) {
|
||
throw new Error('NotificationService not initialized. Call createNotificationService first.');
|
||
}
|
||
return notificationServiceInstance.notifyListMembers(...args);
|
||
},
|
||
createNotification: async (...args) => {
|
||
if (!notificationServiceInstance) {
|
||
throw new Error('NotificationService not initialized. Call createNotificationService first.');
|
||
}
|
||
return notificationServiceInstance.createNotification(...args);
|
||
},
|
||
sendPushNotification: async (...args) => {
|
||
if (!notificationServiceInstance) {
|
||
throw new Error('NotificationService not initialized. Call createNotificationService first.');
|
||
}
|
||
return notificationServiceInstance.sendPushNotification(...args);
|
||
}
|
||
}; |