Files
rosterchirp-dev/fcm-app/server.js
2026-03-23 19:34:13 -04:00

245 lines
6.8 KiB
JavaScript

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})`);
});