v0.12.0 codes for FCM and rebranded jama to RosterChirp
This commit is contained in:
@@ -1,50 +1,62 @@
|
||||
const express = require('express');
|
||||
const webpush = require('web-push');
|
||||
const router = express.Router();
|
||||
const { query, queryOne, queryResult, exec } = require('../models/db');
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { query, queryOne, exec } = require('../models/db');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
// VAPID keys are stored in settings; lazily initialised on first request
|
||||
let vapidPublicKey = null;
|
||||
// ── Firebase Admin ─────────────────────────────────────────────────────────────
|
||||
let firebaseAdmin = null;
|
||||
let firebaseApp = null;
|
||||
|
||||
async function getVapidKeys(schema) {
|
||||
const pub = await queryOne(schema, "SELECT value FROM settings WHERE key = 'vapid_public'");
|
||||
const priv = await queryOne(schema, "SELECT value FROM settings WHERE key = 'vapid_private'");
|
||||
if (!pub?.value || !priv?.value) {
|
||||
const keys = webpush.generateVAPIDKeys();
|
||||
await exec(schema,
|
||||
"INSERT INTO settings (key,value) VALUES ('vapid_public',$1) ON CONFLICT(key) DO UPDATE SET value=$1",
|
||||
[keys.publicKey]
|
||||
);
|
||||
await exec(schema,
|
||||
"INSERT INTO settings (key,value) VALUES ('vapid_private',$1) ON CONFLICT(key) DO UPDATE SET value=$1",
|
||||
[keys.privateKey]
|
||||
);
|
||||
console.log('[Push] Generated new VAPID keys');
|
||||
return keys;
|
||||
}
|
||||
return { publicKey: pub.value, privateKey: priv.value };
|
||||
}
|
||||
|
||||
async function initWebPush(schema) {
|
||||
const keys = await getVapidKeys(schema);
|
||||
webpush.setVapidDetails('mailto:admin@jama.local', keys.publicKey, keys.privateKey);
|
||||
return keys.publicKey;
|
||||
}
|
||||
|
||||
// Called from index.js socket push notifications — schema comes from caller
|
||||
async function sendPushToUser(schema, userId, payload) {
|
||||
function getMessaging() {
|
||||
if (firebaseApp) return firebaseAdmin.messaging(firebaseApp);
|
||||
const json = process.env.FIREBASE_SERVICE_ACCOUNT;
|
||||
if (!json) return null;
|
||||
try {
|
||||
if (!vapidPublicKey) vapidPublicKey = await initWebPush(schema);
|
||||
const subs = await query(schema, 'SELECT * FROM push_subscriptions WHERE user_id = $1', [userId]);
|
||||
firebaseAdmin = require('firebase-admin');
|
||||
const svc = JSON.parse(json);
|
||||
firebaseApp = firebaseAdmin.initializeApp({
|
||||
credential: firebaseAdmin.credential.cert(svc),
|
||||
});
|
||||
console.log('[Push] Firebase Admin initialised');
|
||||
return firebaseAdmin.messaging(firebaseApp);
|
||||
} catch (e) {
|
||||
console.error('[Push] Firebase Admin init failed:', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Called from index.js socket push notifications
|
||||
async function sendPushToUser(schema, userId, payload) {
|
||||
const messaging = getMessaging();
|
||||
if (!messaging) return;
|
||||
try {
|
||||
const subs = await query(schema,
|
||||
'SELECT * FROM push_subscriptions WHERE user_id = $1 AND fcm_token IS NOT NULL',
|
||||
[userId]
|
||||
);
|
||||
for (const sub of subs) {
|
||||
try {
|
||||
await webpush.sendNotification(
|
||||
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
|
||||
JSON.stringify(payload)
|
||||
);
|
||||
await messaging.send({
|
||||
token: sub.fcm_token,
|
||||
data: {
|
||||
title: payload.title || 'New Message',
|
||||
body: payload.body || '',
|
||||
url: payload.url || '/',
|
||||
groupId: payload.groupId ? String(payload.groupId) : '',
|
||||
},
|
||||
android: { priority: 'high' },
|
||||
webpush: { headers: { Urgency: 'high' } },
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.statusCode === 410 || err.statusCode === 404) {
|
||||
// Remove stale tokens
|
||||
const stale = [
|
||||
'messaging/registration-token-not-registered',
|
||||
'messaging/invalid-registration-token',
|
||||
'messaging/invalid-argument',
|
||||
];
|
||||
if (stale.includes(err.code)) {
|
||||
await exec(schema, 'DELETE FROM push_subscriptions WHERE id = $1', [sub.id]);
|
||||
}
|
||||
}
|
||||
@@ -54,56 +66,47 @@ async function sendPushToUser(schema, userId, payload) {
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/vapid-public', async (req, res) => {
|
||||
try {
|
||||
if (!vapidPublicKey) vapidPublicKey = await initWebPush(req.schema);
|
||||
res.json({ publicKey: vapidPublicKey });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
// ── Routes ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Public — frontend fetches this to initialise the Firebase JS SDK
|
||||
router.get('/firebase-config', (req, res) => {
|
||||
const apiKey = process.env.FIREBASE_API_KEY;
|
||||
const projectId = process.env.FIREBASE_PROJECT_ID;
|
||||
const messagingSenderId = process.env.FIREBASE_MESSAGING_SENDER_ID;
|
||||
const appId = process.env.FIREBASE_APP_ID;
|
||||
const vapidKey = process.env.FIREBASE_VAPID_KEY;
|
||||
|
||||
if (!apiKey || !projectId || !messagingSenderId || !appId) {
|
||||
return res.status(503).json({ error: 'FCM not configured' });
|
||||
}
|
||||
res.json({ apiKey, projectId, messagingSenderId, appId, vapidKey });
|
||||
});
|
||||
|
||||
// Register / refresh an FCM token for the logged-in user
|
||||
router.post('/subscribe', authMiddleware, async (req, res) => {
|
||||
const { endpoint, keys } = req.body;
|
||||
if (!endpoint || !keys?.p256dh || !keys?.auth)
|
||||
return res.status(400).json({ error: 'Invalid subscription' });
|
||||
const { fcmToken } = req.body;
|
||||
if (!fcmToken) return res.status(400).json({ error: 'fcmToken required' });
|
||||
try {
|
||||
const device = req.device || 'desktop';
|
||||
await exec(req.schema,
|
||||
'DELETE FROM push_subscriptions WHERE endpoint = $1 OR (user_id = $2 AND device = $3)',
|
||||
[endpoint, req.user.id, device]
|
||||
'DELETE FROM push_subscriptions WHERE user_id = $1 AND device = $2',
|
||||
[req.user.id, device]
|
||||
);
|
||||
await exec(req.schema,
|
||||
'INSERT INTO push_subscriptions (user_id, device, endpoint, p256dh, auth) VALUES ($1,$2,$3,$4,$5)',
|
||||
[req.user.id, device, endpoint, keys.p256dh, keys.auth]
|
||||
'INSERT INTO push_subscriptions (user_id, device, fcm_token) VALUES ($1, $2, $3)',
|
||||
[req.user.id, device, fcmToken]
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
router.post('/generate-vapid', authMiddleware, async (req, res) => {
|
||||
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admins only' });
|
||||
try {
|
||||
const keys = webpush.generateVAPIDKeys();
|
||||
await exec(req.schema,
|
||||
"INSERT INTO settings (key,value) VALUES ('vapid_public',$1) ON CONFLICT(key) DO UPDATE SET value=$1",
|
||||
[keys.publicKey]
|
||||
);
|
||||
await exec(req.schema,
|
||||
"INSERT INTO settings (key,value) VALUES ('vapid_private',$1) ON CONFLICT(key) DO UPDATE SET value=$1",
|
||||
[keys.privateKey]
|
||||
);
|
||||
webpush.setVapidDetails('mailto:admin@jama.local', keys.publicKey, keys.privateKey);
|
||||
vapidPublicKey = keys.publicKey;
|
||||
res.json({ publicKey: keys.publicKey });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
// Remove the FCM token for the logged-in user / device
|
||||
router.post('/unsubscribe', authMiddleware, async (req, res) => {
|
||||
const { endpoint } = req.body;
|
||||
if (!endpoint) return res.status(400).json({ error: 'Endpoint required' });
|
||||
try {
|
||||
const device = req.device || 'desktop';
|
||||
await exec(req.schema,
|
||||
'DELETE FROM push_subscriptions WHERE user_id = $1 AND endpoint = $2',
|
||||
[req.user.id, endpoint]
|
||||
'DELETE FROM push_subscriptions WHERE user_id = $1 AND device = $2',
|
||||
[req.user.id, device]
|
||||
);
|
||||
res.json({ success: true });
|
||||
} catch (e) { res.status(500).json({ error: e.message }); }
|
||||
|
||||
Reference in New Issue
Block a user