require('dotenv').config(); const express = require('express'); const path = require('path'); const cors = require('cors'); const admin = require('firebase-admin'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(cors()); app.use(express.json()); app.use(express.static('public')); // In-memory storage for FCM tokens (in production, use a database) const userTokens = new Map(); // Load tokens from file on startup (for persistence) const fs = require('fs'); const TOKENS_FILE = './data/tokens.json'; function loadTokens() { try { if (fs.existsSync(TOKENS_FILE)) { const data = fs.readFileSync(TOKENS_FILE, 'utf8'); const tokens = JSON.parse(data); for (const [user, tokenArray] of Object.entries(tokens)) { userTokens.set(user, new Set(tokenArray)); } console.log(`Loaded tokens for ${userTokens.size} users from file`); } } catch (error) { console.log('No existing tokens file found, starting fresh'); } } function saveTokens() { const tokens = {}; for (const [user, tokenSet] of userTokens.entries()) { tokens[user] = Array.from(tokenSet); } fs.writeFileSync(TOKENS_FILE, JSON.stringify(tokens, null, 2)); } // Load existing tokens on startup loadTokens(); // Auto-save tokens every 30 seconds setInterval(() => { try { saveTokens(); } catch (error) { console.error('Auto-save tokens failed:', error); } }, 30000); // Initialize Firebase Admin if (process.env.FIREBASE_PRIVATE_KEY) { const serviceAccount = { projectId: process.env.FIREBASE_PROJECT_ID, privateKeyId: process.env.FIREBASE_PRIVATE_KEY_ID, privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'), clientEmail: process.env.FIREBASE_CLIENT_EMAIL, clientId: process.env.FIREBASE_CLIENT_ID, authUri: process.env.FIREBASE_AUTH_URI, tokenUri: process.env.FIREBASE_TOKEN_URI, authProviderX509CertUrl: process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL, clientC509CertUrl: process.env.FIREBASE_CLIENT_X509_CERT_URL }; admin.initializeApp({ credential: admin.credential.cert(serviceAccount) }); console.log('Firebase Admin initialized successfully'); } else { console.log('Firebase Admin not configured. Please set up .env file'); } // Routes app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Register FCM token app.post('/register-token', (req, res) => { const { username, token } = req.body; console.log(`Token registration request:`, { username, token: token?.substring(0, 20) + '...' }); if (!username || !token) { console.log('Token registration failed: missing username or token'); return res.status(400).json({ error: 'Username and token are required' }); } // Store token for user if (!userTokens.has(username)) { userTokens.set(username, new Set()); } const userTokenSet = userTokens.get(username); if (userTokenSet.has(token)) { console.log(`Token already registered for user: ${username}`); } else { userTokenSet.add(token); console.log(`New token registered for user: ${username}`); // Save immediately after new registration try { saveTokens(); } catch (saveError) { console.error('Failed to persist tokens to disk:', saveError); } } console.log(`Total tokens for ${username}: ${userTokenSet.size}`); console.log(`Total registered users: ${userTokens.size}`); res.json({ success: true, message: 'Token registered successfully' }); }); // Send notification to all other users app.post('/send-notification', async (req, res) => { const { fromUser, title, body } = req.body; if (!fromUser || !title || !body) { return res.status(400).json({ error: 'fromUser, title, and body are required' }); } if (!admin.apps.length) { return res.status(500).json({ error: 'Firebase Admin not initialized' }); } try { let totalRecipients = 0; const promises = []; // Send to all users except the sender for (const [username, tokens] of userTokens.entries()) { if (username === fromUser) continue; // Skip sender for (const token of tokens) { const message = { token: token, notification: { title: title, body: body }, webpush: { headers: { 'Urgency': 'high' }, notification: { icon: '/icon-192.png', badge: '/icon-192.png', tag: 'fcm-test' }, fcm_options: { link: '/' } }, android: { priority: 'high', notification: { sound: 'default', click_action: '/' } }, apns: { payload: { aps: { sound: 'default', badge: 1 } } } }; promises.push( admin.messaging().send(message) .then(() => { console.log(`Notification sent to ${username} successfully`); totalRecipients++; }) .catch((error) => { console.error(`Error sending notification to ${username}:`, error); // Remove invalid token if (error.code === 'messaging/registration-token-not-registered') { tokens.delete(token); } }) ); } } await Promise.all(promises); res.json({ success: true, recipients: totalRecipients, message: `Notification sent to ${totalRecipients} recipient(s)` }); } catch (error) { console.error('Error sending notifications:', error); res.status(500).json({ error: 'Failed to send notifications' }); } }); // Get all registered users (for debugging) app.get('/users', (req, res) => { const users = {}; console.log('Current userTokens map:', userTokens); console.log('Number of registered users:', userTokens.size); for (const [username, tokens] of userTokens.entries()) { users[username] = { tokenCount: tokens.size, tokens: Array.from(tokens) }; } res.json(users); }); // Debug endpoint to check server status app.get('/debug', (req, res) => { res.json({ firebaseAdminInitialized: admin.apps.length > 0, registeredUsers: userTokens.size, userTokens: Object.fromEntries( Array.from(userTokens.entries()).map(([user, tokens]) => [user, { count: tokens.size, tokens: Array.from(tokens) }]) ), timestamp: new Date().toISOString() }); }); // Start server app.listen(PORT, '0.0.0.0', () => { console.log(`FCM Test PWA server running on port ${PORT}`); console.log(`Open http://localhost:${PORT} in your browser`); console.log(`Server listening on all interfaces (0.0.0.0:${PORT})`); });