diff --git a/.gitignore b/.gitignore index ebc4fd1..ec998c7 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ jspm_packages/ # Temporary package-lock.json +pnpm-lock.yaml data/ # MacOS diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..304e597 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +.git +.github +node_modules +chk-sig +data diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..20ea860 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "none", + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/app.js b/app.js index ad9562a..491cb17 100644 --- a/app.js +++ b/app.js @@ -10,6 +10,7 @@ const sendRouter = require('./routes/send'); const cancelRouter = require('./routes/cancel_reminders'); const adminRouter = require('./routes/admin'); const healthRouter = require('./routes/health'); +const firebaseRouter = require('./routes/sendFirebase'); const app = express(); @@ -24,15 +25,16 @@ app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); app.use('/register', registerRouter); app.use('/send', sendRouter); +app.use('/send-firebase', firebaseRouter); app.use('/cancel-reminders', cancelRouter); app.use('/super-duper-only', adminRouter); app.use('/health', healthRouter); -app.use(function(err, req, res, next) { +app.use(function (err, _req, res, next) { if (res.headersSent) { - return next(err); + return next(err); } - console.error(err) + console.error(err); return res.json(err); }); module.exports = app; diff --git a/lib/database.js b/lib/database.js index 00e0289..a39899a 100644 --- a/lib/database.js +++ b/lib/database.js @@ -1,7 +1,7 @@ const debug = require('debug')('aurora_push:db_pool'); const { Pool } = require('pg'); -const useSsl = process.env.PG_SSL === "1" || false; +const useSsl = process.env.PG_SSL === '1' || false; const pool = new Pool(useSsl ? { ssl: { rejectUnauthorized: false } } : {}); function query(text, params) { @@ -10,55 +10,79 @@ function query(text, params) { /** * Checks whether this pub_key has a set push notification token - * @param pub_key - * @returns {PromiseLike | Promise} + * @param string pubKey - The parameters to look up the token + * @returns {PromiseLike> | Promise>} */ -function get_user_token(pub_key) { - debug(`Checking for user with pub_key ${pub_key}`); - const q = `SELECT token, sandbox +async function get_user_token(pubKey) { + debug(`Checking for user with pub_key:${pubKey}`); + const q = `SELECT token, sandbox, pub_key from push_tokens WHERE pub_key = $1`; - return pool.query(q, [pub_key.toLowerCase()]).then(res => { - return res.rows; - }); + const res = await pool.query(q, [pubKey?.toLowerCase()]); + return res.rows; +} + +/** + * Checks whether this pub_key has a set push notification token + * @param {Object} params - The parameters to look up the token + * @param {string} params.appId - The application ID + * @param {string} params.userId - The user ID + * @returns {PromiseLike> | Promise>} + */ +async function get_user_token_firebase(params) { + const { appId, userId } = params; + debug(`Checking for user with appId:${appId} userId:${userId}`); + const q = `SELECT token, sandbox, pub_key + from push_tokens + WHERE app_id = $1 OR user_id = $2`; + const res = await pool.query(q, [appId, userId]); + return res.rows; } /** * Inserts/updates device token and platform for a public key - * @param pub_key, token, platform - * @returns {PromiseLike | Promise} + * @param {Object} params - The parameters for registering the token + * @param {string} params.pub_key - The public key associated with the device + * @param {string} params.token - The device's token + * @param {string} params.platform - The platform (e.g., iOS, Android) + * @param {boolean} params.sandbox - Flag to indicate if it's in sandbox mode + * @param {string} params.appId - The application ID + * @param {string} params.userId - The user ID + * @returns {Promise} - Returns a promise resolving to a boolean */ -function register_token(pub_key, token, platform, sandbox) { +async function register_token({ pub_key, token, platform, sandbox, appId, userId }) { const q = ` - INSERT INTO push_tokens (pub_key, token, platform, sandbox, updated_at) - VALUES($1, $2, $3, $4, CURRENT_TIMESTAMP) - ON CONFLICT ON CONSTRAINT index_pub_key_token - DO UPDATE SET platform = $3, sandbox = $4, updated_at = CURRENT_TIMESTAMP - `; + INSERT INTO push_tokens (pub_key, token, platform, sandbox, app_id, user_id, updated_at) + VALUES($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP) + ON CONFLICT ON CONSTRAINT index_pub_key_token + DO UPDATE SET + platform = $3, + sandbox = $4, + app_id = COALESCE(EXCLUDED.app_id, push_tokens.app_id), -- Update app_id if $5 is not null + user_id = COALESCE(EXCLUDED.user_id, push_tokens.user_id), -- Update user_id if $6 is not null + updated_at = CURRENT_TIMESTAMP + `; debug(`Setting token for ${pub_key}`); - return pool.query(q, [pub_key.toLowerCase(), token, platform, sandbox]).then(res => { - const success = res.rowCount > 0; - if (!success) { - throw "pub_key not added/updated."; - } - - return true - }); + const res = await pool.query(q, [pub_key.toLowerCase(), token, platform, sandbox, appId, userId]); + if (res.rowCount === 0) { + throw new Error('Failed to register/update push token'); + } + return true; } /** * Remove a pubkey/token pair from the database * @param {string} pub_key * @param {string} token */ -function delete_token(pub_key, token) { +async function delete_token(pub_key, token) { const q = `DELETE FROM push_tokens WHERE pub_key = $1 AND token = $2`; debug(`Removing token ${token.substring(0, 4)}... for ${pub_key}`); - return pool.query(q, [pub_key.toLowerCase(), token]).then(res => { + return pool.query(q, [pub_key.toLowerCase(), token]).then((res) => { const success = res.rowCount > 0; if (!success) { throw `pub_key for token ${token.substring(0, 4)}... not deleted.`; } - return true + return true; }); } @@ -67,19 +91,19 @@ function delete_token(pub_key, token) { * @param pub_key, reminder_type, send_at * @returns {PromiseLike | Promise} */ -function schedule_reminder(pub_key, reminder_type, send_at) { +async function schedule_reminder(pub_key, reminder_type, send_at) { const q = ` INSERT INTO reminder_notifications (pub_key, reminder_type, send_at) VALUES($1, $2, $3) `; debug(`Scheduling ${reminder_type} notification for ${pub_key}`); - return pool.query(q, [pub_key.toLowerCase(), reminder_type, send_at]).then(res => { + return pool.query(q, [pub_key?.toLowerCase(), reminder_type, send_at]).then((res) => { const success = res.rowCount > 0; if (!success) { - throw "reminder_not_scheduled"; + throw 'reminder_not_scheduled'; } - return true + return true; }); } @@ -88,14 +112,14 @@ function schedule_reminder(pub_key, reminder_type, send_at) { * @param pub_key * @returns {PromiseLike | Promise} */ -function cancel_all_reminders(pub_key) { +async function cancel_all_reminders(pub_key) { const q = ` DELETE FROM reminder_notifications WHERE pub_key = $1 `; debug(`Removing reminders for ${pub_key}`); - return pool.query(q, [pub_key.toLowerCase()]).then(res => { - return true + return pool.query(q, [pub_key.toLowerCase()]).then((_) => { + return true; }); } @@ -104,13 +128,13 @@ function cancel_all_reminders(pub_key) { * @param pub_key * @returns {PromiseLike | Promise} */ -function list_reminders(send_at_before) { +async function list_reminders(send_at_before) { const q = ` SELECT id, pub_key, reminder_type, send_at FROM reminder_notifications WHERE send_at < $1`; - return pool.query(q, [send_at_before]).then(res => { - return res.rows + return pool.query(q, [send_at_before]).then((res) => { + return res.rows; }); } @@ -119,26 +143,26 @@ function list_reminders(send_at_before) { * @param pub_key * @returns {PromiseLike | Promise} */ -function delete_reminder(id) { +async function delete_reminder(id) { const q = ` DELETE FROM reminder_notifications WHERE id = $1 `; - return pool.query(q, [id]).then(res => { - return true + return pool.query(q, [id]).then((_) => { + return true; }); } -function admin_list() { +async function admin_list() { const q = `SELECT platform, updated_at from push_tokens`; - return pool.query(q, []).then(res => { + return pool.query(q, []).then((res) => { return res.rows; }); } -function healthCheck() { +async function healthCheck() { const q = `SELECT 1`; - return pool.query(q, []).then(res => { + return pool.query(q, []).then((res) => { return res.rowCount > 0; }); } @@ -146,6 +170,7 @@ function healthCheck() { module.exports = { query, get_user_token, + get_user_token_firebase, register_token, delete_token, schedule_reminder, diff --git a/lib/push_notifications_firebase.js b/lib/push_notifications_firebase.js new file mode 100644 index 0000000..f038e9d --- /dev/null +++ b/lib/push_notifications_firebase.js @@ -0,0 +1,65 @@ +const admin = require('firebase-admin'); +const debug = require('debug')('aurora_push:routes:send'); + +// Firebase Admin SDK Initialization +const firebaseConfig = { + type: 'service_account', + auth_uri: 'https://accounts.google.com/o/oauth2/auth', + token_uri: 'https://oauth2.googleapis.com/token', + auth_provider_x509_cert_url: 'https://www.googleapis.com/oauth2/v1/certs', + project_id: process.env.FIREBASE_PROJECT_ID, + private_key_id: process.env.FIREBASE_PRIVATE_KEY_ID, + private_key: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'), // Handle multiline env variables + client_email: process.env.FIREBASE_CLIENT_EMAIL, + client_id: process.env.FIREBASE_CLIENT_ID, + client_x509_cert_url: process.env.FIREBASE_CLIENT_X509_CERT_URL +}; + +// Initialize Firebase Admin SDK only once +if (!admin.apps.length) { + admin.initializeApp({ + credential: admin.credential.cert(firebaseConfig) + }); +} + +const sendPushNotification = async (deviceToken, payload) => { + const { title, pushType = 'alert', sound = 'default', body, expiry, topic } = payload; + + let message = { + notification: { title, body }, + data: {}, + android: { notification: { sound } }, + apns: { + headers: { + 'apns-expiration': expiry ? String(expiry) : undefined, + 'apns-push-type': pushType + }, + payload: { + aps: { + alert: { title, body }, + sound, + mutableContent: 1 + } + } + } + }; + + if (topic) { + message.topic = topic; + } else if (deviceToken) { + message.token = deviceToken; + } else { + throw new Error('You must provide either a deviceToken or a topic.'); + } + + try { + const response = await admin.messaging().send(message); + debug(`Notification sent successfully: ${response}`); + return response; + } catch (error) { + debug(`Error sending notification: ${error.message}`); + throw error; + } +}; + +module.exports = { sendPushNotification }; diff --git a/lib/reminders.js b/lib/reminders.js index 13b4b99..32664fe 100644 --- a/lib/reminders.js +++ b/lib/reminders.js @@ -1,73 +1,72 @@ -const debug = require('debug')('aurora_push:reminders'); const db = require('./database'); -const push_notifications = require("../lib/push_notifications").push_notifications_factory(); -const sandbox_push_notifications = require("../lib/push_notifications").sandbox_push_notifications_factory(); +const push_notifications = require('../lib/push_notifications').push_notifications_factory(); +const sandbox_push_notifications = require('../lib/push_notifications').sandbox_push_notifications_factory(); const FIRST_REMINDER_HOURS_FROM_NOW = 0.05; // 24; const SECOND_REMINDER_HOURS_FROM_NOW = 0.1; const EXPIRED_AFTER_HOURS = 0.15; const IGNORE_OLDER_THAN_HOURS = 2; -const EXPIRE_PUSH_AFTER_HOURS = process.env.EXPIRE_PUSH_AFTER_HOURS || 2 +const EXPIRE_PUSH_AFTER_HOURS = process.env.EXPIRE_PUSH_AFTER_HOURS || 2; //reminder_types enum in reminder_notifications table const REMINDER_TYPES = { //When a sender initiates a tx but a recipient has not opened the app to receive recipient_1: { hours_from_now: FIRST_REMINDER_HOURS_FROM_NOW, - title: "recipient_1", - body: "recipient_1", + title: 'recipient_1', + body: 'recipient_1', enabled: true }, recipient_2: { hours_from_now: SECOND_REMINDER_HOURS_FROM_NOW, - title: "recipient_2", - body: "recipient_2", + title: 'recipient_2', + body: 'recipient_2', enabled: true }, //When a sender initiates a tx but it was cancelled because a recipient did not open the app in time recipient_expired: { hours_from_now: EXPIRED_AFTER_HOURS, - title: "recipient_expired", - body: "recipient_expired", + title: 'recipient_expired', + body: 'recipient_expired', enabled: true }, sender_expired: { hours_from_now: EXPIRED_AFTER_HOURS, - title: "sender_expired", - body: "sender_expired", + title: 'sender_expired', + body: 'sender_expired', enabled: true }, //Reminders for when a recipient has received the tx but sender still needs to complete and broadcast sender_1: { hours_from_now: FIRST_REMINDER_HOURS_FROM_NOW, - title: "sender_1", - body: "sender_1", + title: 'sender_1', + body: 'sender_1', enabled: true }, sender_2: { hours_from_now: SECOND_REMINDER_HOURS_FROM_NOW, - title: "sender_2", - body: "sender_2", + title: 'sender_2', + body: 'sender_2', enabled: true }, //Reminders for when a recipient has received the tx but sender still needs to complete and broadcast sender_broadcast_expired: { hours_from_now: EXPIRED_AFTER_HOURS, - title: "sender_broadcast_expired", - body: "sender_broadcast_expired", + title: 'sender_broadcast_expired', + body: 'sender_broadcast_expired', enabled: true }, recipient_broadcast_expired: { hours_from_now: EXPIRED_AFTER_HOURS, - title: "recipient_broadcast_expired", - body: "recipient_broadcast_expired", + title: 'recipient_broadcast_expired', + body: 'recipient_broadcast_expired', enabled: true } }; -Date.prototype.addHours = function(h) { - this.setTime(this.getTime() + (h*60*60*1000)); +Date.prototype.addHours = function (h) { + this.setTime(this.getTime() + h * 60 * 60 * 1000); return this; }; @@ -81,14 +80,30 @@ Date.prototype.addHours = function(h) { */ async function schedule_reminders_for_recipient(recipient_pub_key, sender_pub_key) { try { - await db.schedule_reminder(recipient_pub_key, "recipient_1", (new Date()).addHours(REMINDER_TYPES.recipient_1.hours_from_now)); - await db.schedule_reminder(recipient_pub_key, "recipient_2", (new Date()).addHours(REMINDER_TYPES.recipient_2.hours_from_now)); - await db.schedule_reminder(recipient_pub_key, "recipient_expired", (new Date()).addHours(REMINDER_TYPES.recipient_expired.hours_from_now)); - await db.schedule_reminder(sender_pub_key, "sender_expired", (new Date()).addHours(REMINDER_TYPES.sender_expired.hours_from_now)); + await db.schedule_reminder( + recipient_pub_key, + 'recipient_1', + new Date().addHours(REMINDER_TYPES.recipient_1.hours_from_now) + ); + await db.schedule_reminder( + recipient_pub_key, + 'recipient_2', + new Date().addHours(REMINDER_TYPES.recipient_2.hours_from_now) + ); + await db.schedule_reminder( + recipient_pub_key, + 'recipient_expired', + new Date().addHours(REMINDER_TYPES.recipient_expired.hours_from_now) + ); + await db.schedule_reminder( + sender_pub_key, + 'sender_expired', + new Date().addHours(REMINDER_TYPES.sender_expired.hours_from_now) + ); } catch (error) { - console.error("Error scheduling a reminder for recipient"); + console.error('Error scheduling a reminder for recipient'); console.error(error); - throw error + throw error; } return true; @@ -102,14 +117,30 @@ async function schedule_reminders_for_recipient(recipient_pub_key, sender_pub_ke */ async function schedule_reminders_for_sender(recipient_pub_key, sender_pub_key) { try { - await db.schedule_reminder(sender_pub_key, "sender_1", (new Date()).addHours(REMINDER_TYPES.sender_1.hours_from_now)); - await db.schedule_reminder(sender_pub_key, "sender_2", (new Date()).addHours(REMINDER_TYPES.sender_2.hours_from_now)); - await db.schedule_reminder(sender_pub_key, "sender_broadcast_expired", (new Date()).addHours(REMINDER_TYPES.sender_broadcast_expired.hours_from_now)); - await db.schedule_reminder(recipient_pub_key, "recipient_broadcast_expired", (new Date()).addHours(REMINDER_TYPES.recipient_broadcast_expired.hours_from_now)); + await db.schedule_reminder( + sender_pub_key, + 'sender_1', + new Date().addHours(REMINDER_TYPES.sender_1.hours_from_now) + ); + await db.schedule_reminder( + sender_pub_key, + 'sender_2', + new Date().addHours(REMINDER_TYPES.sender_2.hours_from_now) + ); + await db.schedule_reminder( + sender_pub_key, + 'sender_broadcast_expired', + new Date().addHours(REMINDER_TYPES.sender_broadcast_expired.hours_from_now) + ); + await db.schedule_reminder( + recipient_pub_key, + 'recipient_broadcast_expired', + new Date().addHours(REMINDER_TYPES.recipient_broadcast_expired.hours_from_now) + ); } catch (error) { - console.error("Error scheduling a reminder for recipient"); + console.error('Error scheduling a reminder for recipient'); console.error(error); - throw error + throw error; } return true; @@ -128,13 +159,13 @@ async function process_reminders() { if (hoursPastScheduledTime > IGNORE_OLDER_THAN_HOURS) { console.warn(`Not sending notification older than ${IGNORE_OLDER_THAN_HOURS}.`); await db.delete_reminder(id); - continue + continue; } let device_token; let sandbox = false; try { - const tokenRow = await db.get_user_token(pub_key); + const tokenRow = await db.get_user_token({ pubKey: pub_key }); if (!tokenRow) { console.error(`No token exists for pub_key ${pub_key}`); continue; @@ -146,14 +177,14 @@ async function process_reminders() { continue; } - const expiry = Math.floor(Date.now() / 1000) + (60 * 60 * EXPIRE_PUSH_AFTER_HOURS); + const expiry = Math.floor(Date.now() / 1000) + 60 * 60 * EXPIRE_PUSH_AFTER_HOURS; const payload = { title: REMINDER_TYPES[reminder_type].title, topic: 'com.tari.wallet', body: REMINDER_TYPES[reminder_type].body, badge: 1, - pushType: "alert", + pushType: 'alert', sound: 'ping.aiff', expiry }; @@ -164,10 +195,10 @@ async function process_reminders() { const service = sandbox ? sandbox_push_notifications : push_notifications; const sendResult = await service.send(device_token, payload); if (sendResult[0].success) { - sent_count ++; + sent_count++; await db.delete_reminder(id); } else { - console.error("Failed to send push notification.") + console.error('Failed to send push notification.'); continue; } } catch (error) { @@ -176,7 +207,7 @@ async function process_reminders() { } } - return {found_count: rows.length, sent_count} + return { found_count: rows.length, sent_count }; } //TODO allow recipients to schedule sender reminders once receieved diff --git a/middleware.js b/middleware.js index 626a8de..6f50574 100644 --- a/middleware.js +++ b/middleware.js @@ -1,5 +1,3 @@ -const debug = require('debug')('aurora_push:middleware'); - function create_env(req, _res, next) { req.env = req.env || {}; next(); diff --git a/migrations/0003-app-id-data.sql b/migrations/0003-app-id-data.sql new file mode 100644 index 0000000..81fba75 --- /dev/null +++ b/migrations/0003-app-id-data.sql @@ -0,0 +1,6 @@ +ALTER TABLE push_tokens +ADD COLUMN IF NOT EXISTS user_id VARCHAR(255), +ADD COLUMN IF NOT EXISTS app_id VARCHAR(255); + +CREATE INDEX IF NOT EXISTS index_user_id ON push_tokens (user_id); +CREATE INDEX IF NOT EXISTS index_app_id ON push_tokens (app_id); diff --git a/package.json b/package.json index e16df55..54f54a9 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,14 @@ "debug": "~4.4", "dotenv": "^16.0.0", "express": "~4.21", + "firebase-admin": "^13.0.2", "morgan": "^1.10.0", "node-pushnotifications": "^2.0.1", "pg": "~8.13", "stream-json": "^1.3.3", "tari_chk_sig": "file:./chk-sig/tari_chk_sig_js" + }, + "devDependencies": { + "prettier": "^3.5.0" } } diff --git a/routes/admin.js b/routes/admin.js index ce36f09..7917a8c 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -1,7 +1,6 @@ const express = require('express'); const router = express.Router(); const db = require('../lib/database'); -const reminders = require('../lib/reminders'); router.use(simple_auth); router.get('/list', list); @@ -17,7 +16,7 @@ function simple_auth(req, res, next) { return next(); } -function list(req, res, next) { +function list(_, res, next) { return db.admin_list().then(rows => { return res.json(rows) }).catch(next); diff --git a/routes/cancel_reminders.js b/routes/cancel_reminders.js index 8584d94..94924af 100644 --- a/routes/cancel_reminders.js +++ b/routes/cancel_reminders.js @@ -1,7 +1,5 @@ const express = require('express'); -const debug = require('debug')('aurora_push:routes:cancel_reminders'); const router = express.Router(); -const db = require('../lib/database'); router.post('/', cancelReminders); diff --git a/routes/health.js b/routes/health.js index 3b332e0..0618f82 100644 --- a/routes/health.js +++ b/routes/health.js @@ -29,7 +29,7 @@ const OK = "0"; const DB_UNREACHABLE = "1"; const CERT_UNREADABLE = "2"; -function healthCheck(req, res, next) { +async function healthCheck(_req, res, _next) { return db.healthCheck().then(dbResult => { if (!dbResult) { console.error("Missing DB result"); diff --git a/routes/index.js b/routes/index.js index 2ae3c26..31b9d7c 100644 --- a/routes/index.js +++ b/routes/index.js @@ -3,7 +3,7 @@ const router = express.Router(); const package_json = require('../package.json'); const tari_crypto = require('tari_chk_sig'); -router.get('/', function(req, res, next) { +router.get('/', function(_req, res, _next) { res.json({version: package_json.version, tari_crypto: tari_crypto.version(), production: process.env.NODE_ENV == "production"}); }); diff --git a/routes/register.js b/routes/register.js index 3742c4d..89284bf 100644 --- a/routes/register.js +++ b/routes/register.js @@ -1,10 +1,9 @@ const express = require('express'); const tari_crypto = require('tari_chk_sig'); -const debug = require('debug')('aurora_push:routes:keys'); const router = express.Router(); const db = require('../lib/database'); -const appApiKey = process.env.APP_API_KEY || ""; +const appApiKey = process.env.APP_API_KEY || ''; router.use('/:pub_key', check_pub_key); router.post('/:pub_key', register); @@ -13,7 +12,7 @@ router.delete('/:pub_key', remove_device); // Check that the pub key is accompanied by a valid signature. // signature = pub_key + device token function check_pub_key(req, res, next) { - const pub_key = req.params.pub_key || ""; + const pub_key = req.params.pub_key || ''; const { token, signature, public_nonce } = req.body; const msg = `${appApiKey}${pub_key}${token}`; @@ -26,31 +25,42 @@ function check_pub_key(req, res, next) { //TODO remove this check after apps have had enough time to update to using the api key const check_deprecated = tari_crypto.check_signature(public_nonce, signature, pub_key, `${pub_key}${token}`); if (check_deprecated.result === true) { - console.warn("Using deprecated signature check for register"); + console.warn('Using deprecated signature check for register'); return next(); } - res.status(403).json({ error: `Invalid request signature. ${check.error}`}); + res.status(403).json({ error: `Invalid request signature. ${check.error}` }); } function register(req, res, next) { const pub_key = req.params.pub_key; - const token = req.body.token; - const platform = req.body.platform.toLowerCase(); - const sandbox = !!req.body.sandbox; + const { appId, userId, token, platform, sandbox } = req.body; - return db.register_token(pub_key, token, platform, sandbox).then(() => { - res.json({success: true}) - }).catch(next); + return db + .register_token({ + pub_key, + token, + platform: platform.toLowerCase(), + appId, + userId, + sandbox: Boolean(sandbox) + }) + .then(() => { + res.json({ success: true }); + }) + .catch(next); } function remove_device(req, res, next) { const pub_key = req.params.pub_key; const token = req.body.token; - return db.delete_token(pub_key, token).then(() => { - res.json({success: true}) - }).catch(next); + return db + .delete_token(pub_key, token) + .then(() => { + res.json({ success: true }); + }) + .catch(next); } module.exports = router; diff --git a/routes/send.js b/routes/send.js index 72d2854..b1efb4f 100644 --- a/routes/send.js +++ b/routes/send.js @@ -5,13 +5,13 @@ const router = express.Router(); const db = require('../lib/database'); const reminders = require('../lib/reminders'); -const push_notifications = require("../lib/push_notifications").push_notifications_factory(); -const sandbox_push_notifications = require("../lib/push_notifications").sandbox_push_notifications_factory(); +const push_notifications = require('../lib/push_notifications').push_notifications_factory(); +const sandbox_push_notifications = require('../lib/push_notifications').sandbox_push_notifications_factory(); -const TICKER = process.env.TICKER || "tXTR"; -const APP_API_KEY = process.env.APP_API_KEY || ""; +const TICKER = process.env.TICKER || 'tXTR'; +const APP_API_KEY = process.env.APP_API_KEY || ''; const EXPIRES_AFTER_HOURS = process.env.EXPIRE_PUSH_AFTER_HOURS || 24; -const REMINDER_PUSH_NOTIFICATIONS_ENABLED = !!process.env.REMINDER_PUSH_NOTIFICATIONS_ENABLED +const REMINDER_PUSH_NOTIFICATIONS_ENABLED = !!process.env.REMINDER_PUSH_NOTIFICATIONS_ENABLED; router.use('/:to_pub_key', check_signature); router.post('/:to_pub_key', send); @@ -25,14 +25,19 @@ function check_signature(req, res, next) { const check = tari_crypto.check_signature(public_nonce, signature, from_pub_key, msg); if (check.result === true) { - console.log("Valid with new check"); + console.log('Valid with new check'); return next(); } //TODO remove this check after apps have had enough time to update to using the api key - const check_deprecated = tari_crypto.check_signature(public_nonce, signature, from_pub_key, `${from_pub_key}${to_pub_key}`); + const check_deprecated = tari_crypto.check_signature( + public_nonce, + signature, + from_pub_key, + `${from_pub_key}${to_pub_key}` + ); if (check_deprecated.result === true) { - console.log("Valid with old check"); + console.log('Valid with old check'); return next(); } @@ -41,7 +46,7 @@ function check_signature(req, res, next) { //TODO middleware to throttle senders based on from_pub_key -async function send(req, res, next) { +async function send(req, res, _next) { const to_pub_key = req.params.to_pub_key; const { from_pub_key } = req.body; @@ -66,10 +71,10 @@ async function send(req, res, next) { topic: 'com.tari.wallet', body: `Someone just sent you ${TICKER}.`, badge: 1, - pushType: "alert", + pushType: 'alert', sound: 'ping.aiff', mutableContent: true, - expiry: Math.floor(Date.now() / 1000) + (60 * 60 * EXPIRES_AFTER_HOURS) + expiry: Math.floor(Date.now() / 1000) + 60 * 60 * EXPIRES_AFTER_HOURS }; try { @@ -85,7 +90,7 @@ async function send(req, res, next) { success = true; debug(`Push notification delivered (sandbox=${sandbox})`); } else { - console.error("Push notification failed to deliver.") + console.error('Push notification failed to deliver.'); debug(JSON.stringify(sendResult[0])); success = false; } @@ -100,7 +105,7 @@ async function send(req, res, next) { try { await reminders.schedule_reminders_for_sender(to_pub_key, from_pub_key); } catch (error) { - console.error("Failed to schedule reminder push notifications"); + console.error('Failed to schedule reminder push notifications'); console.error(error); } } diff --git a/routes/sendFirebase.js b/routes/sendFirebase.js new file mode 100644 index 0000000..45d10a7 --- /dev/null +++ b/routes/sendFirebase.js @@ -0,0 +1,100 @@ +const express = require('express'); +const tari_crypto = require('tari_chk_sig'); +const debug = require('debug')('aurora_push:routes:send'); +const firebaseRouter = express.Router(); +const db = require('../lib/database'); +const reminders = require('../lib/reminders'); + +const sandbox_push_notifications = require('../lib/push_notifications').sandbox_push_notifications_factory(); +const firebase_push_notifications = require('../lib/push_notifications_firebase').sendPushNotification; + +const APP_API_KEY = process.env.APP_API_KEY || ''; +const EXPIRES_AFTER_HOURS = process.env.EXPIRE_PUSH_AFTER_HOURS || 24; +const REMINDER_PUSH_NOTIFICATIONS_ENABLED = !!process.env.REMINDER_PUSH_NOTIFICATIONS_ENABLED; + +firebaseRouter.use('/', check_signature); +firebaseRouter.post('/', sendFirebase); + +// Check that the pub key is accompanied by a valid signature. +// signature = from_pub_key + to_pub_key +function check_signature(req, res, next) { + const { from_pub_key, signature, public_nonce } = req.body; + const msg = `${APP_API_KEY}${from_pub_key}`; + const check = tari_crypto.check_signature(public_nonce, signature, from_pub_key, msg); + + if (check.result === true) { + return next(); + } + + res.status(403).json({ error: `Invalid request signature. ${check.error}` }); +} + +async function sendFirebase(req, res, _next) { + const { to_pub_key, from_pub_key, title, body, topic, appId, userId } = req.body; + + let success; + let error; + let tokenRows = []; + + let pubKey = to_pub_key; + + try { + tokenRows = await db.get_user_token({ appId, userId }); + if (!tokenRows || !Array.isArray(tokenRows) || tokenRows.length === 0) { + return res.status(404).json({ success: false }); + } + } catch (error) { + console.error(`Failed to get device tokens for pub_key ${to_pub_key}`); + console.error(error); + return res.status(404).json({ success: false, error: 'Failed to get device tokens' }); + } + + const payload = { + topic, + expiry: Math.floor(Date.now() / 1000) + 60 * 60 * EXPIRES_AFTER_HOURS, + title, + body + }; + + try { + success = false; + for (const { token, sandbox, pub_key } of tokenRows) { + pubKey = pub_key; + const service = sandbox ? sandbox_push_notifications : firebase_push_notifications; + debug(`The send service is (sandbox=${sandbox})`); + + const sendResult = await service(token.trim(), payload); + debug(`The sendResult of the notification service is ${sendResult}`); + if (!sendResult) { + success = false; + error = 'Failed to send push notification'; + break; + } + } + } catch (err) { + console.log(`Error thrown from general try/catch ${err.message} : ${err}`); + success = false; + error = err; + } + + if (REMINDER_PUSH_NOTIFICATIONS_ENABLED) { + try { + await reminders.schedule_reminders_for_sender(to_pub_key, from_pub_key); + } catch (error) { + debug('Failed to schedule reminder push notifications'); + debug(error); + } + } + + if (!success) { + console.error(error); + return res.status(500).json({ + success: false, + error + }); + } + + return res.json({ success: !!success }); +} + +module.exports = firebaseRouter;