Firebase Cloud function in expo project - javascript

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...)

Related

Firebase Function - pass in function context

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;

NodeJS: Server returning "There is no current user." when trying to reference ID token server-side

Whenever I try to reference the user's idToken from server side I get a message saying there is no user logged in. I've been stuck on this for a while so any help would be great
Here is the start.js file:
const cookieParser = require("cookie-parser");
const csrf = require("csurf");
const bodyParser = require("body-parser");
const path = require('path');
const express = require('express');
const admin = require("firebase-admin");
// const fbAuth = require('./routes/fbAuth');
const serviceAccount = require("./serviceAccountKey.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "",
});
app.get("/view", function (req, res) {
const sessionCookie = req.cookies.session || "";
admin
.auth()
.verifySessionCookie(sessionCookie, true /** checkRevoked */)
.then(() => {
res.render("view.html");
})
.catch((error) => {
res.redirect("/login");
});
});
app.post("/sessionLogin", (req, res) => {
const idToken = req.body.idToken.toString();
const expiresIn = 60 * 60 * 24 * 5 * 1000;
admin
.auth()
.createSessionCookie(idToken, { expiresIn })
.then(
(sessionCookie) => {
const options = { maxAge: expiresIn, httpOnly: true };
res.cookie("session", sessionCookie, options);
res.end(JSON.stringify({ status: "success" }));
},
(error) => {
res.status(401).send("UNAUTHORIZED REQUEST!");
}
);
});
app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); });
Here is the client side login:
window.addEventListener("DOMContentLoaded", () =>
{
const firebaseConfig = {
config stuff goes here...
};
firebase.initializeApp(firebaseConfig);
firebase.analytics();
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION);
document
.getElementById("login")
.addEventListener("submit", (event) =>
{
event.preventDefault();
const login = event.target.login.value;
const password = event.target.password.value;
firebase
.auth()
.signInWithEmailAndPassword(login, password)
.then((
{
user
}) =>
{
return user.getIdToken().then((idToken) =>
{
return fetch("/sessionLogin",
{
method: "POST",
headers:
{
Accept: "application/json",
"Content-Type": "application/json",
"CSRF-Token": Cookies.get("XSRF-TOKEN"),
},
body: JSON.stringify(
{
idToken
}),
});
});
})
.then(() =>
{
console.log(firebase.auth().currentUser);
window.location.assign("/view");
});
return false;
});
});
And here is where I try to reference the token in one of the routes files called oss.js (On line 9 is where the problem is identified):
axios(config)
.then(function(response)
{
response = response.data;
console.log(response.progress, response.status);
if (response.progress === "complete")
{
if (response.status === "success")
{
console.log("JOB COMPLETE");
// upload to firebase here
var user = firebase.auth().currentUser;
if (user)
{
admin.auth().verifyIdToken(idToken)
.then(function(decodedToken) {
var uid = decodedToken.uid;
console.log("uid ->", uid);
return uid;
}).catch(function(error)
{
//Handle error
});
}
else
{
console.log("There is no current user.");
}
}
}
})
});
Ok this one requires a bit of wordiness but it's the answer I got with some help of course.
Firstly, in order to reference the uid from the client-side I needed to be able to store it in the browser using 'localStorage.setItem'. Once the user logged in I stored it in the client side script as the following (please note on line 12):
firebase
.auth()
.signInWithEmailAndPassword(login, password)
.then((
{
user
}) =>
{
localStorage.setItem('uid', user.uid);
return user.getIdToken().then((idToken) =>
{
localStorage.setItem('token', idToken);
return fetch("/sessionLogin",
{
method: "POST",
headers:
{
Accept: "application/json",
"Content-Type": "application/json",
"CSRF-Token": Cookies.get("XSRF-TOKEN"),
},
body: JSON.stringify(
{
idToken
}),
});
});
})
Once that was set I needed to reference it on the other pages (i'm not using any front-end framework just now), which was done using the following code that is nested in a script file within the html file. This was attached to a function that initiates a function that allows me to reference it (please note lines 11 & 12):
$('#hiddenUploadField').change(function () {
var node = $('#appBuckets').jstree(true).get_selected(true)[0];
var _this = this;
if (_this.files.length == 0) return;
var file = _this.files[0];
switch (node.type) {
case 'bucket':
var formData = new FormData();
formData.append('fileToUpload', file);
formData.append('bucketKey', node.id);
let uid = localStorage.getItem('uid');
let token = localStorage.getItem('token');
$.ajax({
url: `/api/forge/oss/objects?uid=${uid}&token=${token}`,
data: formData,
processData: false,
contentType: false,
type: 'POST',
success: function (data) {
$('#appBuckets').jstree(true).refresh_node(node);
_this.value = '';
}
});
break;
}
});
});
From there I was able to call the uid and token from the browser to the desired place in the backend using the following (lines 14 & 15):
router.post('/objects', multer(
{
dest: 'uploads/'
}).single('fileToUpload'), async (req, res, next) =>
{
fs.readFile(req.file.path, async (err, data) =>
{
if (err)
{
next(err);
}
try
{
let uid = req.query.uid;
let token =req.query.token;
At this point I could reference the UID and token in the code above using the following:
var user = firebase.auth();
const customToken = admin.auth().createCustomToken(uid)
firebase.auth().onAuthStateChanged(function()
{
if (user)
{
admin.auth().verifyIdToken(token)
.then(function(decodedToken)
{
var uid = decodedToken.uid;
console.log("uid ->", uid);
return uid;
}).catch(function(error)
{
//Handle error
});
}
else
{
console.log("There is no current user.");
}
});

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.

Test Firebase RealtimeDatabase using Jest

My purpose is simply to test one function. I cannot figure out how to mock firebase properly. I try to keep the example with axios mocking from Jest docs. I have the following code:
MusicService.js
import { initializeApp } from "firebase/app";
import "firebase/database";
const firebase = initializeApp({
apiKey: "<API_KEY>",
authDomain: "<PROJECT_ID>.firebaseapp.com",
databaseURL: "https://<DATABASE_NAME>.firebaseio.com",
projectId: "<PROJECT_ID>",
storageBucket: "<BUCKET>.appspot.com",
messagingSenderId: "<SENDER_ID>",
});
export class MusicService {
static getAlbums() {
return firebase.database().ref("albums").once("value")
.then(snapshot => Object.values(snapshot.val()));
}
}
MusicService.test.js
import firebase from 'firebase/app';
import 'firebase/database';
import { MusicService } from './MusicService';
jest.mock('firebase/app');
jest.mock('firebase/database');
test("test", () => {
firebase.initializeApp.mockImplementation(() => {
database: jest.fn(() => {
return {
ref: jest.fn()
}
})
});
MusicService.getAlbums();
});
The problem is that I get the following error:
I tried to mock firebase.database.
test("test", () => {
firebase.mockImplementation(() => {
return {
database: {
}
}
});
MusicService.getAlbums();
});
But in this case I get the error that says:
TypeError: _app.default.mockImplementation is not a function.
I don't expect the working example will be given, but rather could you tell please, what exactly should I mock? The whole firebase library or maybe the part where my function starts - return firebase.database().
I have figured out. I should mock only those modules, a function I am going to test, depends on. For example, I want to test getAlbums function. It uses initializeApp function which is imported from firebase/app module in MusicService.js. So when initializeApp function is being called it should return an object containing database function which in turn returns an object with ref and once functions. Code:
MusicService.test.js.
import { MusicService } from "./FirebaseService";
jest.mock("firebase/app", () => {
const data = { name: "unnamed" };
const snapshot = { val: () => data };
return {
initializeApp: jest.fn().mockReturnValue({
database: jest.fn().mockReturnValue({
ref: jest.fn().mockReturnThis(),
once: jest.fn(() => Promise.resolve(snapshot))
})
})
};
});
test("getAlbums function returns an array", async () => {
const data = await MusicService.getAlbums();
expect(data.constructor).toEqual(Array);
});
this is my current mock implementation for firebase.js.
for me it is working fine.
const firebase = jest.genMockFromModule('firebase');
firebase.initializeApp = jest.fn();
const data = { name: 'data' };
const snapshot = { val: () => data, exportVal: () => data, exists: jest.fn(() => true) };
firebase.database = jest.fn().mockReturnValue({
ref: jest.fn().mockReturnThis(),
on: jest.fn((eventType, callback) => callback(snapshot)),
update: jest.fn(() => Promise.resolve(snapshot)),
remove: jest.fn(() => Promise.resolve()),
once: jest.fn(() => Promise.resolve(snapshot)),
});
firebase.auth = jest.fn().mockReturnValue({
currentUser: true,
signOut() {
return Promise.resolve();
},
signInWithEmailAndPassword(email, password) {
return new Promise((resolve, reject) => {
if (password === 'sign' || password === 'key') {
resolve({ name: 'user' });
}
reject(Error('sign in error '));
});
},
createUserWithEmailAndPassword(email, password) {
return new Promise((resolve, reject) => {
if (password === 'create' || password === 'key') {
resolve({ name: 'createUser' });
}
reject(Error('create user error '));
});
},
});
export default firebase;

How do I set the displayName of Firebase user?

According to the JS Auth documentation on the Firebase website, it only shows how to get the displayName and how to update displayName. So I tried to update it. But it is sort of not logical, because how can you update something without creating it.
So my question here is, how can I set displayName of user during registeration?
function createUser(email, password) {
firebase.auth().createUserWithEmailAndPassword(email, password).catch(function (error) {
error.message.replace(".", "");
alert(error.message + " (" + error.code + ")");
document.getElementById("password").value = "";
});
if (firebase.auth().currentUser != null) {
firebase.auth().currentUser.updateProfile({
displayName: document.getElementById("name").value
}).then(function () {
console.log("Updated");
}, function (error) {
console.log("Error happened");
});
}
}
I have already tried this and it has been proven not to work...
Sincerely,
Farouk
You have to chain the request:
firebase.auth().createUserWithEmailAndPassword(email, password)
.then(function(result) {
return result.user.updateProfile({
displayName: document.getElementById("name").value
})
}).catch(function(error) {
console.log(error);
});`
This is what i used in firebase v9 using async await
// firebase-config.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
const firebaseConfig = {
apiKey: ...,
authDomain: ...,
projectId: ...,
storageBucket: ...,
messagingSenderId: ...,
appId: ...,
measurementId: ...,
}
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
// register.js
import { auth } from "../../services/firebase-config";
import {
createUserWithEmailAndPassword,
sendEmailVerification,
updateProfile,
} from "firebase/auth";
// handleRegister
const register = async (name, email, password) => {
try {
await createUserWithEmailAndPassword(auth, email, password).catch((err) =>
console.log(err)
);
await sendEmailVerification(auth.currentUser).catch((err) =>
console.log(err)
);
await updateProfile(auth.currentUser, { displayName: name }).catch(
(err) => console.log(err)
);
} catch (err) {
console.log(err);
}
};
then you just call this function onClick/onSubmit by passing name, email, and password
here's is my implementation using formik onSubmit
onSubmit={({ name, email, password }) => {
register(name, email, password);
}}
or you can simply call the function in button onClick
<button onClick={() => register(name, email, password)}>submit</button>
I couldĀ“t use ({displayName: name}) directly (sintaxe error in editor). Then, I found another way:
UserUpdateInfo updateInfo = UserUpdateInfo();
updateInfo.displayName = name;
result.user.updateProfile(updateInfo);
This is in Dart (I am using Firebase with Flutter).
firebase
.auth()
.createUserWithEmailAndPassword(newUser.email, newUser.password)
.then((res) => {
const user = firebase.auth().currentUser;
return user.updateProfile({
displayName: newUser.name
})
})
This worked for me.
you need to import updateProfile from firebase/auth
import { getAuth, updateProfile } from "firebase/auth";
const auth = getAuth();
updateProfile(auth.currentUser, {
displayName: "Jane Q. User", photoURL: "https://example.com/jane-q-user/profile.jpg"
}).then(() => {
// Profile updated!
// ...
}).catch((error) => {
// An error occurred
// ...
});
working for me
source: https://firebase.google.com/docs/auth/web/manage-users#update_a_users_profile

Categories