vasuzex 2.1.13 → 2.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FCM Notification Channel
|
|
3
|
+
* Firebase Cloud Messaging channel for push notifications
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Multi-device notification (sends to all user devices)
|
|
7
|
+
* - Automatic invalid token cleanup
|
|
8
|
+
* - Platform-specific configurations
|
|
9
|
+
* - Respects user notification preferences
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Channel } from '../Channel.js';
|
|
13
|
+
import { getMessaging } from '../FirebaseAdmin.js';
|
|
14
|
+
import { Log } from '../../../Support/Facades/index.js';
|
|
15
|
+
|
|
16
|
+
export class FcmChannel extends Channel {
|
|
17
|
+
constructor(tokenRepository) {
|
|
18
|
+
super();
|
|
19
|
+
this.tokenRepository = tokenRepository; // Repository for user_device_tokens
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Send notification via FCM
|
|
24
|
+
* @param {Object} notifiable - User object with id
|
|
25
|
+
* @param {Object} notification - Notification instance
|
|
26
|
+
*/
|
|
27
|
+
async send(notifiable, notification) {
|
|
28
|
+
const messaging = getMessaging();
|
|
29
|
+
|
|
30
|
+
if (!messaging) {
|
|
31
|
+
Log.warning('FCM not available - skipping push notification', {
|
|
32
|
+
userId: notifiable.id,
|
|
33
|
+
notificationType: notification.constructor.name
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Get FCM message from notification
|
|
39
|
+
const fcmData = notification.toFcm
|
|
40
|
+
? notification.toFcm(notifiable)
|
|
41
|
+
: this.buildDefaultMessage(notification, notifiable);
|
|
42
|
+
|
|
43
|
+
if (!fcmData) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get all active tokens for user
|
|
48
|
+
const tokens = await this.getActiveTokens(notifiable.id);
|
|
49
|
+
|
|
50
|
+
if (tokens.length === 0) {
|
|
51
|
+
Log.debug('No FCM tokens found for user', { userId: notifiable.id });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Send to all devices
|
|
56
|
+
await this.sendToMultipleDevices(tokens, fcmData, notifiable.id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get active FCM tokens for user
|
|
61
|
+
*/
|
|
62
|
+
async getActiveTokens(userId) {
|
|
63
|
+
if (!this.tokenRepository) {
|
|
64
|
+
Log.error('Token repository not configured for FcmChannel');
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return await this.tokenRepository.getActiveTokensForUser(userId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Send notification to multiple devices
|
|
73
|
+
*/
|
|
74
|
+
async sendToMultipleDevices(tokens, fcmData, userId) {
|
|
75
|
+
const messaging = getMessaging();
|
|
76
|
+
const tokenStrings = tokens.map(t => t.token);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const message = {
|
|
80
|
+
notification: {
|
|
81
|
+
title: fcmData.title,
|
|
82
|
+
body: fcmData.body,
|
|
83
|
+
...(fcmData.imageUrl && { imageUrl: fcmData.imageUrl })
|
|
84
|
+
},
|
|
85
|
+
data: this.stringifyData(fcmData.data || {}),
|
|
86
|
+
tokens: tokenStrings
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Add platform-specific config
|
|
90
|
+
if (fcmData.android) {
|
|
91
|
+
message.android = fcmData.android;
|
|
92
|
+
}
|
|
93
|
+
if (fcmData.apns) {
|
|
94
|
+
message.apns = fcmData.apns;
|
|
95
|
+
}
|
|
96
|
+
if (fcmData.webpush) {
|
|
97
|
+
message.webpush = fcmData.webpush;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const response = await messaging.sendEachForMulticast(message);
|
|
101
|
+
|
|
102
|
+
Log.info('FCM multicast sent', {
|
|
103
|
+
userId,
|
|
104
|
+
successCount: response.successCount,
|
|
105
|
+
failureCount: response.failureCount,
|
|
106
|
+
totalTokens: tokenStrings.length
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Handle failed tokens
|
|
110
|
+
if (response.failureCount > 0) {
|
|
111
|
+
await this.handleFailedTokens(tokens, response.responses, userId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Update last_used_at for successful sends
|
|
115
|
+
const successfulTokenIds = tokens
|
|
116
|
+
.filter((_, index) => response.responses[index].success)
|
|
117
|
+
.map(t => t.id);
|
|
118
|
+
|
|
119
|
+
if (successfulTokenIds.length > 0 && this.tokenRepository) {
|
|
120
|
+
await this.tokenRepository.updateLastUsed(successfulTokenIds);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return response;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
Log.error('FCM multicast failed', {
|
|
126
|
+
userId,
|
|
127
|
+
error: error.message,
|
|
128
|
+
code: error.code
|
|
129
|
+
});
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Handle failed token deliveries
|
|
136
|
+
* Removes invalid/expired tokens
|
|
137
|
+
*/
|
|
138
|
+
async handleFailedTokens(tokens, responses, userId) {
|
|
139
|
+
const invalidTokenIds = [];
|
|
140
|
+
const invalidReasons = [
|
|
141
|
+
'messaging/invalid-registration-token',
|
|
142
|
+
'messaging/registration-token-not-registered',
|
|
143
|
+
'messaging/invalid-argument'
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
responses.forEach((response, index) => {
|
|
147
|
+
if (!response.success && response.error) {
|
|
148
|
+
const errorCode = response.error.code;
|
|
149
|
+
|
|
150
|
+
if (invalidReasons.includes(errorCode)) {
|
|
151
|
+
invalidTokenIds.push(tokens[index].id);
|
|
152
|
+
Log.info('Removing invalid FCM token', {
|
|
153
|
+
userId,
|
|
154
|
+
tokenId: tokens[index].id,
|
|
155
|
+
platform: tokens[index].platform,
|
|
156
|
+
errorCode
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (invalidTokenIds.length > 0 && this.tokenRepository) {
|
|
163
|
+
await this.tokenRepository.deactivateTokens(invalidTokenIds);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Build default FCM message from notification
|
|
169
|
+
*/
|
|
170
|
+
buildDefaultMessage(notification, notifiable) {
|
|
171
|
+
// Try to get data from toArray or toDatabase
|
|
172
|
+
const data = notification.toArray
|
|
173
|
+
? notification.toArray(notifiable)
|
|
174
|
+
: (notification.toDatabase ? notification.toDatabase(notifiable) : null);
|
|
175
|
+
|
|
176
|
+
if (!data) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
title: data.title || 'Notification',
|
|
182
|
+
body: data.body || data.message || '',
|
|
183
|
+
data: data.data || {},
|
|
184
|
+
imageUrl: data.imageUrl || data.image_url
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Stringify all data values for FCM
|
|
190
|
+
* FCM data payload must have string values only
|
|
191
|
+
*/
|
|
192
|
+
stringifyData(data) {
|
|
193
|
+
const stringified = {};
|
|
194
|
+
for (const [key, value] of Object.entries(data)) {
|
|
195
|
+
stringified[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
|
196
|
+
}
|
|
197
|
+
return stringified;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export default FcmChannel;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Admin SDK Singleton
|
|
3
|
+
* Centralized Firebase Admin initialization for FCM
|
|
4
|
+
*
|
|
5
|
+
* Configuration from environment variables:
|
|
6
|
+
* - FIREBASE_PROJECT_ID
|
|
7
|
+
* - FIREBASE_CLIENT_EMAIL
|
|
8
|
+
* - FIREBASE_PRIVATE_KEY
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import admin from 'firebase-admin';
|
|
12
|
+
import { Config, Log } from '../../Support/Facades/index.js';
|
|
13
|
+
|
|
14
|
+
let firebaseApp = null;
|
|
15
|
+
let isInitialized = false;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize Firebase Admin SDK
|
|
19
|
+
* Safe to call multiple times - will only initialize once
|
|
20
|
+
*/
|
|
21
|
+
export function initializeFirebaseAdmin() {
|
|
22
|
+
if (isInitialized) {
|
|
23
|
+
return firebaseApp;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const projectId = Config.get('notification.fcm.project_id') || process.env.FIREBASE_PROJECT_ID;
|
|
28
|
+
const clientEmail = Config.get('notification.fcm.client_email') || process.env.FIREBASE_CLIENT_EMAIL;
|
|
29
|
+
const privateKey = Config.get('notification.fcm.private_key') || process.env.FIREBASE_PRIVATE_KEY;
|
|
30
|
+
|
|
31
|
+
if (!projectId || !clientEmail || !privateKey) {
|
|
32
|
+
Log.warning('Firebase Admin SDK not configured - FCM notifications disabled', {
|
|
33
|
+
hasProjectId: !!projectId,
|
|
34
|
+
hasClientEmail: !!clientEmail,
|
|
35
|
+
hasPrivateKey: !!privateKey
|
|
36
|
+
});
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Handle escaped newlines in private key
|
|
41
|
+
const formattedPrivateKey = privateKey.replace(/\\n/g, '\n');
|
|
42
|
+
|
|
43
|
+
firebaseApp = admin.initializeApp({
|
|
44
|
+
credential: admin.credential.cert({
|
|
45
|
+
projectId,
|
|
46
|
+
clientEmail,
|
|
47
|
+
privateKey: formattedPrivateKey,
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
isInitialized = true;
|
|
52
|
+
Log.info('Firebase Admin SDK initialized successfully', { projectId });
|
|
53
|
+
|
|
54
|
+
return firebaseApp;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
Log.error('Failed to initialize Firebase Admin SDK', {
|
|
57
|
+
error: error.message,
|
|
58
|
+
stack: error.stack
|
|
59
|
+
});
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get Firebase Messaging instance
|
|
66
|
+
*/
|
|
67
|
+
export function getMessaging() {
|
|
68
|
+
if (!isInitialized) {
|
|
69
|
+
initializeFirebaseAdmin();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!firebaseApp) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return admin.messaging();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if Firebase Admin is initialized
|
|
81
|
+
*/
|
|
82
|
+
export function isFirebaseInitialized() {
|
|
83
|
+
return isInitialized && firebaseApp !== null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default {
|
|
87
|
+
initialize: initializeFirebaseAdmin,
|
|
88
|
+
getMessaging,
|
|
89
|
+
isInitialized: isFirebaseInitialized
|
|
90
|
+
};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { MailChannel } from './Channels/MailChannel.js';
|
|
7
7
|
import { SmsChannel } from './Channels/SmsChannel.js';
|
|
8
8
|
import { DatabaseChannel } from './Channels/DatabaseChannel.js';
|
|
9
|
+
import { FcmChannel } from './Channels/FcmChannel.js';
|
|
9
10
|
|
|
10
11
|
export class NotificationManager {
|
|
11
12
|
constructor(app) {
|
|
@@ -125,6 +126,17 @@ export class NotificationManager {
|
|
|
125
126
|
return new DatabaseChannel(database);
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Create an instance of the FCM driver
|
|
131
|
+
*/
|
|
132
|
+
createFcmDriver() {
|
|
133
|
+
// Get token repository from app container
|
|
134
|
+
const tokenRepository = this.app.has('tokenRepository')
|
|
135
|
+
? this.app.make('tokenRepository')
|
|
136
|
+
: null;
|
|
137
|
+
return new FcmChannel(tokenRepository);
|
|
138
|
+
}
|
|
139
|
+
|
|
128
140
|
/**
|
|
129
141
|
* Get the default channel name
|
|
130
142
|
*/
|
|
@@ -136,9 +136,9 @@ export function TableBody({
|
|
|
136
136
|
// Get icon, class, title, and content
|
|
137
137
|
const Icon = action.icon;
|
|
138
138
|
const className =
|
|
139
|
-
typeof action.
|
|
140
|
-
? action.
|
|
141
|
-
: action.
|
|
139
|
+
typeof action.className === "function"
|
|
140
|
+
? action.className(row)
|
|
141
|
+
: action.className || "";
|
|
142
142
|
const title =
|
|
143
143
|
typeof action.title === "function"
|
|
144
144
|
? action.title(row)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vasuzex",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.15",
|
|
4
4
|
"description": "Laravel-inspired framework for Node.js monorepos - V2 with optimized dependencies",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./framework/index.js",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"./Support/Facades": "./framework/Support/Facades/index.js",
|
|
20
20
|
"./Support/Facades/*": "./framework/Support/Facades/*.js",
|
|
21
21
|
"./Services/*": "./framework/Services/*/index.js",
|
|
22
|
+
"./framework/*": "./framework/*",
|
|
22
23
|
"./client": "./frontend/client/index.js",
|
|
23
24
|
"./client/*": "./frontend/client/*/index.js",
|
|
24
25
|
"./react": "./frontend/react-ui/index.js",
|
|
@@ -105,6 +106,7 @@
|
|
|
105
106
|
"dotenv": "^16.6.1",
|
|
106
107
|
"express": "^5.2.1",
|
|
107
108
|
"express-rate-limit": "^8.2.1",
|
|
109
|
+
"firebase-admin": "^13.6.0",
|
|
108
110
|
"fs-extra": "^11.3.2",
|
|
109
111
|
"guruorm": "^2.0.16",
|
|
110
112
|
"helmet": "^8.1.0",
|