Firebase Function - pass in function context - javascript

Below is my firebase function:
const admin = require('firebase-admin');
const firebase_tools = require('firebase-tools');
const functions = require('firebase-functions');
admin.initializeApp();
exports.deleteUser = functions
.runWith({
timeoutSeconds: 540,
memory: '2GB'
})
.https.onCall((data, context) => {
const userId = context.auth.uid;
var promises = [];
// DELETE DATA
var paths = ['users/' + userId, 'messages/' + userId, 'chat/' + userId, 'like/' + userId];
paths.forEach((path) => {
promises.push(
recursiveDelete(path).then( () => {
return 'success';
}
).catch( (error) => {
console.log('Error deleting user data: ', error);
})
);
});
// DELETE FILES
const bucket = admin.storage().bucket();
var image_paths = ["avatar/" + userId, "avatar2/" + userId, "avatar3/" + userId];
image_paths.forEach((path) => {
promises.push(
bucket.file(path).delete().then( () => {
return 'success';
}
).catch( (error) => {
console.log('Error deleting user data: ', error);
})
);
});
// DELETE USER
promises.push(
admin.auth().deleteUser(userId)
.then( () => {
console.log('Successfully deleted user');
return true;
})
.catch((error) => {
console.log('Error deleting user:', error);
})
);
return Promise.all(promises).then(() => {
return true;
}).catch(er => {
console.error('...', er);
});
});
function recursiveDelete(path, context) {
return firebase_tools.firestore
.delete(path, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true,
token: functions.config().fb.token
})
.then(() => {
return {
path: path
}
}).catch( (error) => {
console.log('error: ', error);
return error;
});
}
// [END recursive_delete_function]
When calling this function, how can I pass in the context.auth.id?
Below is what i've tried:
async function deleteAccount(userId) {
const deleteUser = firebase.functions().httpsCallable("deleteUser");
deleteUser({ userId }).then((result) => {
console.log(result.data);
});
}
But im getting the following error:
Unhandled error TypeError: Cannot read property 'uid' of undefined
I know the the context.auth.id is available server side but In this instance I need a way i can pass it in.

You don't have to pass user's UID in callable cloud function. The user must be logged in with Firebase authentication and Firebase SDKs will take care of the rest.
Can you try logging current user in deleteAccount function before calling cloud function just to ensure user is logged in? Also context.auth.uid is UID of user that is calling the function. If you want to access the userId that you are passing in the function, refactor the code as shown below.
The deleteUser() function would take only 1 parameter that's the data you want to pass in Cloud functions.
// not deleteUser({}, { userId })
deleteUser({ userId }).then((result) => {
console.log(result.data);
});
When you are explicitly passing any data in Cloud function, that can be access from data and not context:
const { userId } = data;

Related

Catching Error with Nested Promise confusion

I have been working on my first node.js backend and I am trying to refactor the login function, but i am not understanding something about promise chaining and error handling. I am hoping someone can help point out what I am missing here.
I have read about promise chaining and I understand that one catch should be able to handle any error in the chain, but I cant seem to get it to work without having an individual .catch for every .then
Here is my userLogin function.
const loginUser = handleAsync(async (req, res, next) => {
let userEmail = req.body.email;
let submittedPassword = req.body.password;
userLogin(userEmail, submittedPassword)
.then((userObject) => {
res.json(userObject);
})
.catch((err) => {
res.status(401).send(err);
console.log("any error inside userLogin: " + err);
});
});
async function userLogin(email, submittedPassword) {
console.log("in user login");
return new Promise(function (resolve, reject) {
getUserAccountByEmail(email)
.then((userAccountResults) => {
let email = userAccountResults[0].email;
let storedPassword = userAccountResults[0].password;
//the user exists check the password
//the user exists. Now Check password.
checkUserPassword(submittedPassword, storedPassword)
.then((passwordIsAMatch) => {
//The password is a match. Now Generate JWT Token.
generateJWTToken(email)
.then((tokens) => {
//build json object with user information and tokens.
createUserDataObject(tokens, userAccountResults)
.then((userObject) => {
//send the user object to the front end.
resolve(userObject);
})
.catch((err) => {
reject(err);
});
})
.catch((err) => {
reject(err);
});
})
.catch((err) => {
reject(err);
});
})
.catch((err) => {
reject(err);
});
});
}
And here is one of the functions that is a part of the chain
function getUserAccountByEmail(email) {
return new Promise(function (resolve, reject) {
logger.info("Looking up user email");
const users = User.findAll({
where: {
email: email,
},
limit: 1,
attributes: [
"email",
"password",
"userId",
"stripeConnectAccountId",
"isStripeAccountSet",
"stripeCustomerId",
],
})
.then((userResults) => {
if (doesUserExist(userResults)) {
resolve(userResults);
} else {
console.log("user doesnt exist in getuseraccount");
reject("User Email Does Not Exist In Database");
}
})
.catch((error) => {
console.log("Error Accessing Database: UserAccountByEmail");
logger.error("Error in getUserAccountByEmail: " + error);
});
});
}
Any help would be appreciated. Thanks
I took jfriend00 's advice and refactored using await instead of nesting.
async function userLogin(email, submittedPassword) {
return new Promise(async function (resolve, reject) {
try {
//Get User Account Information
const userAccountResults = await getUserAccountByEmail(email);
//Password Authentication
let storedPassword = userAccountResults[0].password;
const passwordIsAMatch = await checkUserPassword(
submittedPassword,
storedPassword
);
//Generate JWT Tokens
const tokens = await generateJWTToken(email);
//Prepare user data JSON for sending to the frontend.
const userData = await createUserDataObject(tokens, userAccountResults);
resolve(userData);
} catch (error) {
reject(error);
}
});
}

Firebase Cloud function in expo project

So I have a cloud function (this is not in the react native app directory yet):
const admin = require('firebase-admin');
const firebase_tools = require('firebase-tools');
const functions = require('firebase-functions');
admin.initializeApp();
exports.deleteUser = functions
.runWith({
timeoutSeconds: 540,
memory: '2GB'
})
.https.onCall((data, context) => {
const userId = context.auth.uid;
var promises = [];
// DELETE DATA
var paths = ['users/' + userId, 'messages/' + userId, 'chat/' + userId];
paths.forEach((path) => {
promises.push(
recursiveDelete(path).then( () => {
return 'success';
}
).catch( (error) => {
console.log('Error deleting user data: ', error);
})
);
});
// DELETE FILES
const bucket = admin.storage().bucket();
var image_paths = ["avatar/" + userId, "avatar2/" + userId, "avatar3/" + userId];
image_paths.forEach((path) => {
promises.push(
bucket.file(path).delete().then( () => {
return 'success';
}
).catch( (error) => {
console.log('Error deleting user data: ', error);
})
);
});
// DELETE USER
promises.push(
admin.auth().deleteUser(userId)
.then( () => {
console.log('Successfully deleted user');
return true;
})
.catch((error) => {
console.log('Error deleting user:', error);
})
);
return Promise.all(promises).then(() => {
return true;
}).catch(er => {
console.error('...', er);
});
});
function recursiveDelete(path, context) {
return firebase_tools.firestore
.delete(path, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true,
token: functions.config().fb.token
})
.then(() => {
return {
path: path
}
}).catch( (error) => {
console.log('error: ', error);
return error;
});
}
// [END recursive_delete_function]
This is used for my swift app. How Can I use this for my react native app built with Expo?
I have installed the following yarn add #react-native-firebase/functions
I have my firebase.js file set up in the root directory:
import * as firebase from "firebase";
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "test",
authDomain: "test",
databaseURL: "test",
projectId: "test",
storageBucket: "test",
messagingSenderId: "test",
appId: "test"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
export default firebase;
I have a button:
<Text>Delete Account</Text>
<View>
<Button
title="Delete Account"
color="#F9578E"
accessibilityLabel="Delete Account"
/>
</View>
Which when clicked signs the user out and runs the above cloud function.
I'm not versed in react-native and in Expo, but from the #react-native-firebase/functions documentation it seems that you need to do as follows:
import functions from '#react-native-firebase/functions';
function App() {
useEffect(() => {
functions()
.httpsCallable('deleteUser')()
.then(response => {
// ....
});
}, []);
// ...
}
You are not passing any data from your app to your Callable Cloud Function, i.e. you are not using the data object in your Cloud Function, this is why you need to do functions().httpsCallable('deleteUser')().
If you would need to pass some data, the doc shows an example here, passing an object:
functions().httpsCallable('listProducts')({
page: 1,
limit: 15,
})
(This is totally in line with the Firebase JS SDK way of calling a Callable Cloud Function, this is why I answered to the question, even with a lack of knowledge on react-native and on Expo...)

Fetching data from firebase realtime database returns invalid json response

Here I am making a fetch request to an api -
export async function getServerSideProps(ctx) {
let id = ctx.params.id;
let userObject;
let userId;
const cookie = parseCookies(ctx);
if (cookie.auth) {
userObject = JSON.parse(cookie.auth);
userId = userObject.id;
}
if (!userId) {
return {
redirect: {
permanent: false,
destination: '/',
},
};
}
const res = await fetch(`http://localhost:3000/api/tests/${id}`);
console.log(await res.json());
const data = await res.json();
console.log(data);
// return {
// props: { product: data },
// };
return {
props: {},
};
}
Here I am reading data from firebase realtime database -
export default async (req, res) => {
const { id } = req.query;
console.log(id);
let obj;
firebase
.database()
.ref('/test/' + id)
.once('value')
.then(snapshot => {
console.log('here');
const data = snapshot.val();
obj = data;
})
.then(() => res.status(200).json(obj))
.catch(err => console.log(err));
};
Which gives me this error -
Server Error FetchError: invalid json response body at https://localhost:3000/api/tests/-MUT5-DbK6Ff6CstPSGc reason: Unexpected end of JSON input
Everything seems to work except the json response I am getting after making fetch request. I can't even console.log to see what response I am actually getting. What am I missing?
Edit - Here's my firebase database structure, where test node is root node
There is no return in your promise. That's why obj is null. Instead of then just send the response in first capture.
export default async (req, res) => {
const { id } = req.query;
console.log(id);
let obj;
firebase
.database()
.ref('/test/' + id)
.once('value')
.then(snapshot => {
console.log('here');
const data = snapshot.val();
obj = data;
res.status(200).json(obj)
})
.catch(err => console.log(err));
};

Only one of many Firebase Functions being invoked

I have 2 Firebase functions that I want to execute when there is an Http request, one function (createEmailList) to save data in the Firebase database, the other (zohoCrmHook) to to save in a 3rd party CRM called Zoho.
When the functions are deployed to Firebase, the functions log shows that both are properly deployed. However, when the Http request is made from the frontend, the log shows that only one of the functions (createEmailList) is being executed.
As the log shows, the first function createEmailList is being executed and the data shows up in the Firebase database with no problem. However, The second function zohoCrmHook is not even being executed.
index.js
const functions = require('firebase-functions');
const admin = require("firebase-admin")
const serviceAccount = require("./service_account.json");
const createEmailList = require('./createEmailList')
// zoho
const zohoCrmHook = require('./zohoCrmHook')
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://landing-page.firebaseio.com"
})
exports.zohoCrmHook = functions.https.onRequest(zohoCrmHook)
exports.createEmailList = functions.https.onRequest(createEmailList)
createEmailList.js
const admin = require('firebase-admin')
const cors = require('cors')({ origin: true })
module.exports = (req, res) => {
cors(req, res, () => {
if (!req.body.email) {
return res.status(422).send({ error: 'Bad Input'})
}
const email = String(req.body.email)
const firstName = String(req.body.firstName)
const lastName = String(req.body.lastName)
const data = {
email,
firstName,
lastName
}
const db = admin.firestore()
const docRef = db.collection('users')
.doc(email)
.set(data, { merge: false })
.catch(err => res.status(422).send({ error: err }))
res.status(204).end();
})
}
zohoCrmHook.js
const axios = require('axios');
const functions = require('firebase-functions');
// zoho
const clientId = functions.config().zoho.client_id;
const clientSecret = functions.config().zoho.client_secret;
const refreshToken = functions.config().zoho.refresh_token;
const baseURL = 'https://accounts.zoho.com';
module.exports = async (req, res) => {
const newLead = {
'data': [
{
'Email': req.body.email,
'Last_Name': req.body.lastName,
'First_Name': req.body.firstName,
}
],
'trigger': [
'approval',
'workflow',
'blueprint'
]
};
const { data } = await getAccessToken();
const accessToken = data.access_token;
const leads = await getLeads(accessToken);
const result = checkLeads(leads.data.data, newLead.data[0].Email);
if (result.length < 1) {
try {
return res.json(await createLead(accessToken, newLead));
}
catch (e) {
console.log(e);
}
}
else res.json({ message: 'Lead already in CRM' })
}
function getAccessToken () {
const url = `https://accounts.zoho.com/oauth/v2/token?refresh_token=${refreshToken}&client_id=${clientId}&client_secret=${clientSecret}&grant_type=refresh_token`;
return new Promise((resolve, reject) => {
axios.post(url)
.then((response) => {
return resolve(response);
})
.catch(e => console.log(e))
});
}
function getLeads(token) {
const url = 'https://www.zohoapis.com/crm/v2/Leads';
return new Promise((resolve, reject) => {
axios.get(url, {
headers: {
'Authorization': `Zoho-oauthtoken ${token}`
}
})
.then((response) => {
return resolve(response);
})
.catch(e => console.log(e))
})
}
function createLead(token, lead) {
const url = 'https://www.zohoapis.com/crm/v2/Leads';
return new Promise((resolve, reject) => {
const data = JSON.stringify(lead);
axios.post(url, data, {
headers: {
'Authorization': `Zoho-oauthtoken ${token}`
}
})
.then((response) => {
console.log(response.data)
return resolve(response);
})
.catch(e => reject(e))
})
}
function checkLeads(leads, currentLead) {
return leads.filter(lead => lead.Email === currentLead)
}
Since you're exporting two functions.https.onRequest declarations, you'll end up with two Cloud Functions, each with their own URL/endpoint. So if that's what you need, you'll need to configure two web hooks that call these functions.
From reading your question however, it sounds more like you want a single Cloud Function that does two things, in which case you should only have one functions.https.onRequest declaration that then calls two regular JavaScript functions (for example).
So something more like:
exports.myWebHook = functions.https.onRequest(function(req, res) {
zohoCrmHook(...);
createEmailList(...);
})
You'll need to figure out what to pass into the two function calls here, as you can't pass the request and response along.
Alternatively you can call the two Cloud Functions from here, but that typically just drives up your cost with little benefit.

Firebase Cloud Functions with Cloud Firestore trouble

I have used this Firebase Database code in a previous project:
const getDeviceUser = admin.database().ref(`/users/${notification.to}/`).once('value');
I am now trying to convert it for Firestore. I am basically trying to get my users fcm's when a notification is being sent. I have tried many things, but haven't seen the new way to accomplish this.
EDIT: here is my code.
exports.sendFavoriteNotification = functions.firestore.document('users/{userUid}/notifications/{notificationId}').onCreate(event => {
const notification = event.data.data();
const user = event.params.userUid;
const getDeviceUser = admin.database().ref(`/users/${notification.to}/`).once('value');
// Get the follower profile.
const getProfilePromise = admin.auth().getUser(notification.sender);
return Promise.all([getDeviceUser, getProfilePromise]).then(results => {
const tokensSnapshot = results[0];
const liker = results[1];
// Check if there are any device tokens.
if (!tokensSnapshot.hasChildren()) {
return console.log('There are no notification tokens to send to.');
}
//console.log('There are', tokensSnapshot.numChildren(), 'tokens to send notifications to.');
console.log('Fetched follower profile', liker);
// Notification details.
const payload = {
notification : {
title : 'You have a new like!',
body : `${liker.displayName} just liked your photo.`,
badge: '1',
sound: 'default'
}
};
// Listing all tokens.
var tokens = admin.firestore.ref(`/users/${notification.to}/`).get('fcm');
// Send notifications to all tokens.
admin.messaging().sendToDevice(tokens.data(), payload);
return admin.messaging().sendToDevice(tokens, payload).then(response => {
// For each message check if there was an error.
const tokensToRemove = [];
response.results.forEach((result, index) => {
const error = result.error;
if (error) {
console.error('Failure sending notification to', tokens[index], error);
// Cleanup the tokens who are not registered anymore.
if (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered') {
tokensToRemove.push(tokensSnapshot.update({
fcm: FieldValue.delete()
}));
}
}
});
return Promise.all(tokensToRemove);
});
});
});
Hope this will help. This is my code after 2 days of trying to learn how to convert from realtime database to firestore. It is based on a firebase project: https://github.com/MahmoudAlyuDeen/FirebaseIM
let functions = require('firebase-functions');
let admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.sendNotificationToFirestone = functions.firestore.document('/notifications/{pushId}')
.onCreate(event => {
const pushId = event.data.id;
const message = event.data.data();
const senderUid = message.from;
const receiverUid = message.to;
const db = admin.firestore();
if (senderUid === receiverUid) {
console.log('pushId: '+ pushId);
return db.collection('notifications').doc(pushId).delete();;
} else {
const ref = db.collection('users').doc(receiverUid);
const query = new Promise(
function (resolve, reject) {
ref.get()
.then(doc => {
if (!doc.exists) {
console.log('No such document!');
reject(new Error('No such document!'));
} else {
console.log('Document data:', doc.data().instanceId);
resolve(doc.data().instanceId);
}
})
.catch(err => {
console.log('Error getting document', err);
reject(err);
});
});
const getSenderUidPromise = admin.auth().getUser(senderUid);
return Promise.all([query, getSenderUidPromise]).then(results => {
//console.log('instanceId = Result[0]: ' + results[0]);
//console.log('sender = Result[1]: ' + results[1]);
const instanceId = results[0];
const sender = results[1];
//console.log('notifying ' + receiverUid + ' about ' + message.body + ' from ' + senderUid);
//console.log('instanceId este' + instanceId);
const payload = {
notification: {
title: sender.displayName,
body: message.body,
icon: sender.photoURL
}
};
admin.messaging().sendToDevice(instanceId, payload)
.then(function (response) {
console.log("Message sent: ", response);
})
.catch(function (error) {
console.log("Error sending message: ", error);
});
});
}
});

Categories