vasuzex 2.1.13 → 2.1.14

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.extraClass === "function"
140
- ? action.extraClass(row)
141
- : action.extraClass || "";
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.13",
3
+ "version": "2.1.14",
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",