Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds firebase configuration #39

Merged
merged 17 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ jspm_packages/

# Temporary
package-lock.json
pnpm-lock.yaml
data/

# MacOS
Expand Down
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.git
.github
node_modules
chk-sig
data
11 changes: 11 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}
8 changes: 5 additions & 3 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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;
115 changes: 70 additions & 45 deletions lib/database.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -10,55 +10,79 @@ function query(text, params) {

/**
* Checks whether this pub_key has a set push notification token
* @param pub_key
* @returns {PromiseLike<boolean> | Promise<boolean>}
* @param string pubKey - The parameters to look up the token
* @returns {PromiseLike<Array<{token: string, sandbox: boolean}>> | Promise<Array<{token: string, sandbox: boolean}>>}
*/
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<Array<{token: string, sandbox: boolean}>> | Promise<Array<{token: string, sandbox: boolean}>>}
*/
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<boolean> | Promise<boolean>}
* @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<boolean>} - 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;
});
}
Comment on lines +77 to 87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve async pattern consistency and error handling.

The function is marked as async but still uses promise chains. Also, the error message includes potentially sensitive token information.

Apply this diff to improve the implementation:

 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) => {
-        const success = res.rowCount > 0;
-        if (!success) {
-            throw `pub_key for token ${token.substring(0, 4)}... not deleted.`;
-        }
-        return true;
-    });
+    const res = await pool.query(q, [pub_key.toLowerCase(), token]);
+    if (res.rowCount === 0) {
+        throw new Error('Failed to delete push token');
+    }
+    return true;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
});
}
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}`);
const res = await pool.query(q, [pub_key.toLowerCase(), token]);
if (res.rowCount === 0) {
throw new Error('Failed to delete push token');
}
return true;
}


Expand All @@ -67,19 +91,19 @@ function delete_token(pub_key, token) {
* @param pub_key, reminder_type, send_at
* @returns {PromiseLike<boolean> | Promise<boolean>}
*/
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;
});
}
Comment on lines +94 to 108
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve async pattern consistency in schedule_reminder.

The function uses promise chains despite being marked as async.

Apply this diff to improve the implementation:

 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) => {
-        const success = res.rowCount > 0;
-        if (!success) {
-            throw 'reminder_not_scheduled';
-        }
-
-        return true;
-    });
+    const res = await pool.query(q, [pub_key?.toLowerCase(), reminder_type, send_at]);
+    if (res.rowCount === 0) {
+        throw new Error('Failed to schedule reminder');
+    }
+    return true;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
});
}
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}`);
const res = await pool.query(q, [pub_key?.toLowerCase(), reminder_type, send_at]);
if (res.rowCount === 0) {
throw new Error('Failed to schedule reminder');
}
return true;
}


Expand All @@ -88,14 +112,14 @@ function schedule_reminder(pub_key, reminder_type, send_at) {
* @param pub_key
* @returns {PromiseLike<boolean> | Promise<boolean>}
*/
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;
});
}

Expand All @@ -104,13 +128,13 @@ function cancel_all_reminders(pub_key) {
* @param pub_key
* @returns {PromiseLike<boolean> | Promise<boolean>}
*/
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;
});
}

Expand All @@ -119,33 +143,34 @@ function list_reminders(send_at_before) {
* @param pub_key
* @returns {PromiseLike<boolean> | Promise<boolean>}
*/
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;
});
}

module.exports = {
query,
get_user_token,
get_user_token_firebase,
register_token,
delete_token,
schedule_reminder,
Expand Down
65 changes: 65 additions & 0 deletions lib/push_notifications_firebase.js
Original file line number Diff line number Diff line change
@@ -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 };
Loading