should now have push notifications, plus fixed some bugs
This commit is contained in:
@@ -263,7 +263,7 @@ const initializeAgenda = async (mongoUri, pool, io) => { // Now accepts pgPool
|
|||||||
agenda.define('deletePasswordResetToken', async (job) => {
|
agenda.define('deletePasswordResetToken', async (job) => {
|
||||||
const { email } = job.attrs.data;
|
const { email } = job.attrs.data;
|
||||||
try {
|
try {
|
||||||
const result = await pool.query('DELETE FROM password_reset_tokens WHERE email = $1', [email]);
|
const result = await sharedPgPool.query('DELETE FROM password_reset_tokens WHERE email = $1', [email]);
|
||||||
if (result.rowCount > 0) {
|
if (result.rowCount > 0) {
|
||||||
console.log(`Deleted expired password reset token for ${email}`);
|
console.log(`Deleted expired password reset token for ${email}`);
|
||||||
}
|
}
|
||||||
@@ -285,8 +285,6 @@ const initializeAgenda = async (mongoUri, pool, io) => { // Now accepts pgPool
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await agenda.start();
|
|
||||||
|
|
||||||
agenda.on('ready', () => console.log('Agenda connected to MongoDB and ready!'));
|
agenda.on('ready', () => console.log('Agenda connected to MongoDB and ready!'));
|
||||||
agenda.on('start', (job) => console.log(`Job "${job.attrs.name}" starting`));
|
agenda.on('start', (job) => console.log(`Job "${job.attrs.name}" starting`));
|
||||||
agenda.on('complete', (job) => console.log(`Job "${job.attrs.name}" complete`));
|
agenda.on('complete', (job) => console.log(`Job "${job.attrs.name}" complete`));
|
||||||
|
|||||||
98
index.js
98
index.js
@@ -1,3 +1,4 @@
|
|||||||
|
const admin = require('firebase-admin');
|
||||||
const { verify, hash } = require('argon2');
|
const { verify, hash } = require('argon2');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { json } = require('express');
|
const { json } = require('express');
|
||||||
@@ -92,6 +93,21 @@ let agenda;
|
|||||||
// Clear all expired pending email changes on startup
|
// Clear all expired pending email changes on startup
|
||||||
await pool.query("DELETE FROM user_pending_emails WHERE expires_at < NOW()");
|
await pool.query("DELETE FROM user_pending_emails WHERE expires_at < NOW()");
|
||||||
console.log("Cleared expired pending email changes");
|
console.log("Cleared expired pending email changes");
|
||||||
|
|
||||||
|
// Add battery_soc column if this is the first deploy with battery support
|
||||||
|
await pool.query("ALTER TABLE devices ADD COLUMN IF NOT EXISTS battery_soc SMALLINT");
|
||||||
|
|
||||||
|
// Add fcm_token column for push notification delivery
|
||||||
|
await pool.query("ALTER TABLE users ADD COLUMN IF NOT EXISTS fcm_token TEXT");
|
||||||
|
|
||||||
|
// Initialise Firebase Admin SDK for push notifications
|
||||||
|
if (process.env.FIREBASE_SERVICE_ACCOUNT_JSON) {
|
||||||
|
admin.initializeApp({
|
||||||
|
credential: admin.credential.cert(JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT_JSON)),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn("FIREBASE_SERVICE_ACCOUNT_JSON not set — push notifications disabled");
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
const JWT_SECRET = process.env.JWT_SECRET;
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
const TOKEN_EXPIRY = '5d';
|
const TOKEN_EXPIRY = '5d';
|
||||||
@@ -1342,12 +1358,10 @@ app.get('/device_name', authenticateToken, async (req, res) => {
|
|||||||
console.log("deviceName");
|
console.log("deviceName");
|
||||||
try {
|
try {
|
||||||
const {deviceId} = req.query;
|
const {deviceId} = req.query;
|
||||||
const {rows} = await pool.query('select device_name, max_ports from devices where id=$1 and user_id=$2',
|
const {rows} = await pool.query('select device_name, max_ports, battery_soc from devices where id=$1 and user_id=$2',
|
||||||
[deviceId, req.user]);
|
[deviceId, req.user]);
|
||||||
if (rows.length != 1) return res.sendStatus(404);
|
if (rows.length != 1) return res.sendStatus(404);
|
||||||
const deviceName = rows[0].device_name;
|
res.status(200).json({device_name: rows[0].device_name, max_ports: rows[0].max_ports, battery_soc: rows[0].battery_soc});
|
||||||
const maxPorts = rows[0].max_ports;
|
|
||||||
res.status(200).json({device_name: deviceName, max_ports: maxPorts});
|
|
||||||
} catch {
|
} catch {
|
||||||
res.sendStatus(500);
|
res.sendStatus(500);
|
||||||
}
|
}
|
||||||
@@ -2373,6 +2387,82 @@ app.post('/update_group_schedule', authenticateToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Store/update the FCM token for the authenticated user
|
||||||
|
app.post('/register_fcm_token', authenticateToken, async (req, res) => {
|
||||||
|
const { token } = req.body;
|
||||||
|
if (!token || typeof token !== 'string') return res.sendStatus(400);
|
||||||
|
try {
|
||||||
|
await pool.query("UPDATE users SET fcm_token=$1 WHERE id=$2", [token, req.user]);
|
||||||
|
res.sendStatus(204);
|
||||||
|
} catch {
|
||||||
|
res.sendStatus(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Battery endpoints (device-authenticated only) ─────────────────────────────
|
||||||
|
|
||||||
|
// Periodic SOC sync — called by wakeTimer every 60 s
|
||||||
|
app.post('/battery_update', authenticateToken, async (req, res) => {
|
||||||
|
if (!req.peripheral) return res.sendStatus(403);
|
||||||
|
try {
|
||||||
|
const { soc } = req.body;
|
||||||
|
if (soc == null || soc < 0 || soc > 100) return res.sendStatus(400);
|
||||||
|
await pool.query("UPDATE devices SET battery_soc=$1 WHERE id=$2", [soc, req.peripheral]);
|
||||||
|
res.sendStatus(204);
|
||||||
|
} catch {
|
||||||
|
res.sendStatus(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Alert events — fires on threshold crossings and critical conditions.
|
||||||
|
// Notifies the user via Socket.IO if connected, and via FCM push if the app is in the background.
|
||||||
|
app.post('/battery_alert', authenticateToken, async (req, res) => {
|
||||||
|
if (!req.peripheral) return res.sendStatus(403);
|
||||||
|
const validTypes = ['overvoltage', 'critical_low', 'low_voltage_warning', 'low_20', 'low_10'];
|
||||||
|
try {
|
||||||
|
const { type, soc } = req.body;
|
||||||
|
if (!validTypes.includes(type) || soc == null) return res.sendStatus(400);
|
||||||
|
|
||||||
|
await pool.query("UPDATE devices SET battery_soc=$1 WHERE id=$2", [soc, req.peripheral]);
|
||||||
|
|
||||||
|
// Socket.IO — real-time delivery when app is open
|
||||||
|
const {rows} = await pool.query(
|
||||||
|
"SELECT ut.socket FROM user_tokens ut JOIN devices d ON d.user_id = ut.user_id WHERE d.id=$1 AND ut.connected=TRUE",
|
||||||
|
[req.peripheral]
|
||||||
|
);
|
||||||
|
if (rows.length === 1) {
|
||||||
|
io.to(rows[0].socket).emit("battery_alert", { deviceId: req.peripheral, type, soc });
|
||||||
|
}
|
||||||
|
|
||||||
|
// FCM — background push for persistent alerts (not transient voltage dips)
|
||||||
|
const fcmPushTypes = ['overvoltage', 'critical_low', 'low_20', 'low_10'];
|
||||||
|
if (fcmPushTypes.includes(type) && admin.apps.length > 0) {
|
||||||
|
const { rows: fcmRows } = await pool.query(
|
||||||
|
"SELECT u.fcm_token FROM users u JOIN devices d ON d.user_id=u.id WHERE d.id=$1 AND u.fcm_token IS NOT NULL",
|
||||||
|
[req.peripheral]
|
||||||
|
);
|
||||||
|
if (fcmRows.length === 1) {
|
||||||
|
const fcmContent = {
|
||||||
|
overvoltage: { title: 'Battery Fault', body: 'Overvoltage detected. Please check your charger.' },
|
||||||
|
critical_low: { title: 'Battery Critical', body: `Battery at ${soc}% — device is shutting down.` },
|
||||||
|
low_20: { title: 'Battery Low', body: `Battery at ${soc}%. Consider charging soon.` },
|
||||||
|
low_10: { title: 'Battery Very Low', body: `Battery at ${soc}% — charge now.` },
|
||||||
|
};
|
||||||
|
const { title, body } = fcmContent[type];
|
||||||
|
await admin.messaging().send({
|
||||||
|
token: fcmRows[0].fcm_token,
|
||||||
|
notification: { title, body },
|
||||||
|
data: { type, soc: String(soc), deviceId: String(req.peripheral) },
|
||||||
|
}).catch(err => console.error('FCM send failed:', err.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendStatus(204);
|
||||||
|
} catch {
|
||||||
|
res.sendStatus(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/group_schedule_list', authenticateToken, async (req, res) => {
|
app.post('/group_schedule_list', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { groupId } = req.body;
|
const { groupId } = req.body;
|
||||||
|
|||||||
4311
package-lock.json
generated
4311
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@
|
|||||||
"pg": "^8.16.0",
|
"pg": "^8.16.0",
|
||||||
"pg-format": "^1.0.4",
|
"pg-format": "^1.0.4",
|
||||||
"rate-limiter-flexible": "^9.0.1",
|
"rate-limiter-flexible": "^9.0.1",
|
||||||
|
"firebase-admin": "^13.0.0",
|
||||||
"socket.io": "^4.8.1"
|
"socket.io": "^4.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user