Extract specific node value via Firebase Cloud functions - javascript

Ok so I'm going to start with some background (skip to MY ISSUE for tl;dr):
I have an application in development that passes data from a Google Sheet into a Firebase Realtime Database in the form of a 2d Array. The data layout of the Google sheet is as shown below:
This data is passed into a Firebase Realtime Database under the node masterSheet via an Apps Script function result shown below:
Which is used as the live database for my mobile web application I am developing using the Ionic Framework (preview below):
I have functions which deal with the setting of "Y" and "N" flags at the correct positions for the sub tasks of each job and a function which sets the overall job completion status flag to "Y" when all sub tasks are done working as intended.
I am trying to add in an automatic email service via Firebase Cloud Functions that sends off a "job completion notification" whenever a job's overall "Completed" status is set to "Y" (i.e the value at ref: 'masterSheet/0/1' is equal to "Y").
So far I have managed to get it to successfully send off the emails via a Firebase Cloud Function using nodemailer and the Firebase Admin SDK to all registered users of the Firebase app whenever a job's overall completed status is changed from an "N" to a "Y" via the onUpdate() method and the .ref() of the location to listen at.
Below is my Index.js file containing the cloud function I am using:
// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');
// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp();
// The mail service used
const nodemailer = require('nodemailer');
// Cloud Fucntion to export:
exports.onMessageUpdate = functions.database.ref('/masterSheet/{subArray}/1')
.onUpdate((change) => {
var changeRef = change.after.ref.path;
console.log('changeRef: ' + changeRef);
var newVal = change.after.val();
if (newVal == "Y"){
getUsers();
}
})
// Fucntion to get all registers users of the Firebase Project
function getUsers(){
var userEmails = [];
admin.auth().listUsers()
.then(function(listUsersResult) {
listUsersResult.users.forEach(function(userRecord) {
console.log(userRecord);
userEmails.push(userRecord.email);
sendCompletionEmail(userRecord.email)
});
})
.catch(function(error) {
console.log("Error listing users:", error);
});
}
// Function to send automatic emails
function sendCompletionEmail(email){
var transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
type: 'OAuth2',
user: 'xxxxxxxx#gmail.com',
clientId: 'xxxxxxxx.apps.googleusercontent.com',
clientSecret: 'xxxxxxxxxxxxxxx',
refreshToken: 'xxxxxxxxxxxxxx'
}
})
// Email details:
var mailOptions = {
from: 'xxxxxxx',
to: email,
subject: 'Job completion notification',
text: 'This is an automated message to inform you that a job has been fully completed ' +
'with all of its tasks marked as done. \n\nYou can view this (along with any others) from the Completed ' +
'Jobs page within the app.'
}
transporter.sendMail(mailOptions, function (err, res) {
if(err){
console.log('Error');
} else {
console.log('Email Sent');
}
})
}
MY ISSUE:
I want to be able to include the job title in this automatic email that is sent.
logging the result of change.after.ref.path used in the snippet below:
// Cloud Fucntion to export:
exports.onMessageUpdate = functions.database.ref('/masterSheet/{subArray}/1')
.onUpdate((change) => {
var changeRef = change.after.ref.path;
console.log('changeRef: ' + changeRef);
var newVal = change.after.val();
if (newVal == "Y"){
getUsers();
}
})
Produces this log output:
which contains exactly what I want within it... But I don't know how to get it out...
How can I retrieve the second value from the changeRef variable so that I can pass this onto the sendCompletionEmail() function and use it to refer to the item at position [0] for that node?
something like:
var subArray = changeRef[1]
to get the value: 0 out of masterSheet/0/1
which i can store as a variable and use to refer to the job title of the job that has just been completed in the sent off email.
Thanks for any help!

If you're looking for the 0 from the request, that is available from the second parameter that is passed into your Cloud Function (but that you're not declaring).
exports.onMessageUpdate = functions.database.ref('/masterSheet/{subArray}/1')
.onUpdate((change, context) => {
console.log(context.params.subArray);
})
See the Firebase documentation on handling event data and the reference docs for onUpdate.

Related

How to query realtime database properly for cloud fucntion?

UPDATED
I am trying to query my pricing data based on the user it is saved under and send it back in my stripe checkout cloud function. It keeps giving me an error stating that no value has been assigned to my variables when I have. I read the docs on how to do this, but I kinda got confused towards the end. I then saw something similar to what I was trying to do on a couple of other places, but then I got the codes mixed up. How can I call the variable names from the other function to put them in the pricing info?
Sources I used:
How to query specific data from Firebase using Cloud functions
How to run query from inside of Cloud function?
https://firebase.google.com/docs/database/extend-with-functions
https://firebase.google.com/docs/functions/database-events
This is how my data is set up in my real time database:
studiopick
studio
users
Gcsh31DCGAS2u2XXLuh8AbwBeap1
email : "Test#gmail.com"
firstName : "Test"
lastName : "one"
phoneNumber : "2223334567"
prices
| roomA
| serviceOne
| numberInput : "300"
| serviceType : "mix n master"
studioName : "Studio One"
uid : "Gcsh31DCGAS2u2XXLuh8AbwBeap1"
This is how my cloud function is set up:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
let price;
let info;
admin.initializeApp(functions.config().firebase);
exports.createStripeCheckout = functions.https.onCall(async (data, context) => {
const querySnapshot = await ref
.orderByChild("numberInput, serviceInput")
.equalTo(price, info)
.once("value");
// Stripe init
const stripe = require("stripe")(functions.config().stripe.secret_key);
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
mode: "payment",
success_url: "http://localhost:5500/success",
cancel_url: "http://localhost:5500/cancel",
shipping_address_collection: {
allowed_countries: ["US"],
},
line_items: [
{
quantity: 1,
price_data: {
currency: "usd",
unit_amount: price * 100, // 10000 = 100 USD
product_data: {
name: info,
},
},
},
],
});
return {
id: session.id,
};
});
exports.stripeWebhook = functions.https.onRequest(async (req, res) => {
const stripe = require("stripe")(functions.config().stripe.token);
let event;
try {
const whSec = functions.config().stripe.payments_webhook_secret;
event = stripe.webhooks.constructEvent(
req.rawBody,
req.headers["stripe-signature"],
whSec
);
} catch (err) {
console.error("⚠️ Webhook signature verification failed.");
return res.sendStatus(400);
}
const dataObject = event.data.object;
await admin.firestore().collection("orders").doc().set({
checkoutSessionId: dataObject.id,
paymentStatus: dataObject.payment_status,
shippingInfo: dataObject.shipping,
amountTotal: dataObject.amount_total,
});
return res.sendStatus(200);
});
Cloud Functions run in their own isolated containers when deployed.
When you call your retreivefromdatabase function, an instance of your Cloud Functions code is spun up, then the request is handled and the instance hibernates when it finishes (and it will be shut down later if not called upon again). When you call your createStripeCheckout function, a new instance of your Cloud Functions code is spun up, then the request is handled and the instance hibernates (and shuts down later).
Because these functions are hosted and handled by separate instances, you can't pass information between functions using global state.
Unfortunately the local testing emulator doesn't completely isolate functions in the same way (nor does it emulate throttling), which is what misled you to believe that it should function just fine in production.

Make http cloud function executable only by the project owner

I am using http cloud function ( https://firebase.google.com/docs/functions/http-events ) to write documents to a firestore collection:
exports.hello = functions.https.onRequest(
(req: { query: { name: string } }, res: { send: (arg0: string) => void }) => {
console.log(req.query.name);
var name = req.query.name || 'unknown';
res.send('hello' + name);
admin
.firestore()
.collection('ulala')
.doc()
.set({ token: 'asd' }, { merge: true });
}
);
this is a test. The problem is that, once you deploy and get the link to the function, it is executable by everyone. I would like instead that only I (project owner) can use it . Is it possible to do this?
One possible solution is to restrict your HTTPS Cloud Function to only a specific "Admin" user of your app.
There is an official Cloud Function sample which shows how to restrict an HTTPS Function to only the Firebase users of the app/Firebase project: Authorized HTTPS Endpoint sample.
You need to adapt it to check if the user is the Admin user. For example by checking the userID in the try/catch block at line 60 of the index.js file (untested).
try {
const decodedIdToken = await admin.auth().verifyIdToken(idToken);
if (decodedToken.uid !== "<the admin user uid>") {
throw new Error("Wrong user");
} else {
req.user = decodedIdToken;
next();
}
return;
} catch (error) {
functions.logger.error('Error while verifying Firebase ID token:', error);
res.status(403).send('Unauthorized');
return;
}
The two drawbacks of this approach are:
Your Admin user needs to be declared as a Firebase project user in the Authentication service
You hard code the Admin userID in your Cloud Function (you could use the Google Cloud Secret Manager service to securely store it as a configuration value, see the doc).
IMPORTANT SIDE NOTE:
In your Cloud Function you call the send() method before the asynchronous work is complete:
res.send('hello' + name);
admin
.firestore()
.collection('ulala')
.doc()
.set({ token: 'asd' }, { merge: true });
By calling the send() method, you actually terminate the Cloud Function, indicating to the Cloud Functions instance running your function that it can shut down. Therefore in the majority of the cases the asynchronous set() operation will not be executed.
You need to do as follows:
admin
.firestore()
.collection('ulala')
.doc()
.set({ token: 'asd' }, { merge: true })
.then(() => {
res.send('hello' + name);
})
I would suggest you watch the 3 videos about "JavaScript Promises" from the Firebase video series as well as read this page of the documentation which explain this key point.

How to create user auth without closing the current firebase session [duplicate]

This question already has answers here:
Firebase kicks out current user
(19 answers)
Closed 1 year ago.
I want to make a system where the administrator can create user auth from an email. I have developed as the documentation says but the current session is closed. I only want to create the auth to get the uid and then create a user in the database with the data I want to store.
This is what I have:
var email = emailInput.value;
var password = "Abcd1234";
firebase.auth().createUserWithEmailAndPassword(email, password).then((userCredential) => {
var user = userCredential.user;
//user.uid contains the id I want to create the instance on ref usuarios
database.ref("usuarios/"+ user.uid).set({...});
});
Edit:
You cannot create new users using client SDK. By that I mean a user creating new users as required. You need to use Firebase Admin SDK (which must be in a secure server environment - like Firebase Cloud Functions).
You can write a cloud function like this:
exports.createNewUser = functions.https.onCall((data, context) => {
if (isAdmin(context.auth.uid)) {
return admin.auth().createUser({
email: data.email,
password: data.password,
displayName: data.name
}).then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
console.log('Successfully created new user:', userRecord.uid);
return { uid: userRecord.uid }
}).catch((error) => {
console.log('Error creating new user:', error);
return { error: "Something went wrong" }
});
}
return {error: "unauthorized" }
})
Now there are multiple ways you could verify that the user who is calling this function is an admin. First one would be using Firebase Custom Claims which are somewhat like roles you assign to users. Another option would be storing UID of using in database and checking the UID exists in admin node of db. Just make sure only you can edit that part of the database.
To call the function from client:
const createNewUser = firebase.functions().httpsCallable('createNewUser');
createNewUser({ name: "test", email: "test#test.test", password: "122345678" })
.then((result) => {
// Read result of the Cloud Function.
var response = result.data;
});

Notification not being sent via firebase functions. "undefined" logged in console

I am trying to send a sample notification to all devices according to their token, however the token is being logged as "undefined" and the notification subsequently fails to deliver
The following lines from my code successfully show me the data from the database:
const notificationSnapshot = change.after.val(); //get new value
console.info(notificationSnapshot);
However, the following gives "undefined", despite the above retrieving the data successfully.
const userToken = notificationSnapshot.token;
console.info(userToken);
Is this not the correct way to retrieve the token to send the notification to all the registered devices in my firebase database?
my whole function (index.js)
// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');
// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp();
exports.sendSampleNotification = functions.database.ref('/User')
.onWrite((change, context) => {
const notificationSnapshot = change.after.val(); //get new value
const userToken = notificationSnapshot.token;
const name = notificationSnapshot.name;
const surname = notificationSnapshot.surname;
console.info(notificationSnapshot);
console.info(userToken);
var message = {
notification: {
title: 'test title',
body: 'test message'
},
token: userToken
};
admin.messaging().send(message).then((response) => {
console.log("Message sent successfully:", response);
return response;
})
.catch((error) => {
console.log("Error sending message: ", error);
});
});
I would say that your issue is very similar to this one since you are having a missing token (showed as undefined) due to the executions times, more or less what Doug was pointing out.
Note that the solution relies on considering the execution times and I’ve seen also that the implementation differs in some method executions but I would say the generals point in the same direction.

How to pass data between firebase cloud functions

I'm trying to fetch the current user id from the app by making https requests to call functions from the client app. It's working and I'm fetching the uid. The problem is whenever the onWrite() function is triggered I need to send notifications to a specific user under a specific user id. How can I pass the uid that I got from my https functions to the trigger functions?
I just retrieved the user id of the current user from the client app using an https.onCall().
Then in the trigger function, I'm trying to send notifications to a specific user classified by uid using expo push notifications API.
functions/index.js
const functions = require('firebase-functions');
var fetch = require('node-fetch')
const admin = require('firebase-admin')
admin.initializeApp(functions.config().firebase)
exports.getUid = functions.https.onCall((data, context) => {
uid = data.text
console.log(data.text)
})
exports.makerOrders = functions.database.ref('orders')
.onWrite((snapShot, context) => {
console.log('functions is triggered :)')
return admin.database().ref('Notifications').child(uid)
.once('value')
.then((shot) => {
var message = []
var tokens = shot.val().expoTokens;
if (tokens) {
message.push({
"to": tokens,
"body": "Notifications are working fine :)"
})
}
return Promise.all(message)
}).then(message => {
fetch('http://exp.host/--/api/v2/push/send', {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(message)
})
return Promise.all(message)
})
})
App.js
var uid = firebase.auth().currentUser.uid
var getUid = firebase.functions().httpsCallable('getUid')
getUid({text: uid}).then(result => {
var msg = result.data
console.log(msg)
console.log('Called successfully :)')
}).catch(error => {
console.log('Error :( in sending the requests')
});
I'm expecting to fetch the uid value of the current user from the app so that I can send push notifications to that specific user.
I can't able to retrieve the uid for the currently authed user.
In order to pass data between functions there are two ways of doing this. You can use a function that it is triggered by HTTP request or a function that is triggered by Pub/Sub topic.
HTTP:
Create a Cloud Function with HTTP trigger
Open the details of the created function and under the Trigger tab you will find the URL that triggers the function. Use that URL to parse data from another function.
In the other function run a request using that URL and add at the end '?data=DATA_T0_SEND'
Catch the data from the second function using return request.args.get('data')
Pub/Sub:
Create a Cloud Function that triggers by a Pub/Sub topic.
On the other function use the Pub/Sub library to send the data to the topic
When the functions will be triggered with the Pub/Sub event get the data
Now process the data from that event

Categories