Reference.set failed: First argument contains undefined - firebase

I have created a firebase function that listen on onCreate event, however the DocumentSnapshot.data() is returning empty.
The function code is:
exports.createClientAccount = functions.firestore
.document('/userProfile/{userId}/clientList/{clientId}')
.onCreate(async (snap, context) => {
console.log('****snap.data(): ', snap.data()); //Showing Empty from the console.
return admin
.auth()
.createUser({
uid: context.params.clientId,
email: snap.data().email,
password: '123456789',
displayName: snap.data().fullName,
})
.then(userRecord => {
return admin
.database()
.ref(`/userProfile/${userRecord.uid}`)
.set({
fullName: userRecord.displayName, //ERROR here: Reference.set failed: First argument contains undefined
email: userRecord.email,
coachId: context.params.userId,
admin: false,
startingWeight: snap.data().startingWeight,
});
})
.catch(error => {
console.error('****Error creating new user',error);
});
});
The document IS created on the firebase database under
/userProfile/{userId}/clientList/{clientId}
clientId document created on the database
As per the documentation, onCreate listens when a new document is created and returns the snapshot of the data created through the DocumentSnapshot interface. However, I have checked from the console that snap.data() is empty. I don't understand why it is empty if the document is created successfully on the database.
image showing error returned by the functions when creating the userProfile
From the function code, return admin.auth.createUser({}) is creating the user as anonymous because snap.data().email is undefined, but it should create a non anonymous user.

First, please try to change document('/userProfile/{userId}/clientList/{clientId}') to document('userProfile/{userId}/clientList/{clientId}').
path should not start with /.
exports.createClientAccount = functions.firestore
.document('userProfile/{userId}/clientList/{clientId}')

At the end problem was that when I created the document with add({}) I was not including the fields in the instruction. This is the function that creates the client document and now the function gets triggered correctly.
async clientCreate(
fullName: string,
email: string,
startingWeight: number
): Promise<any> {
const newClientRef = await this.firestore
.collection(`userProfile/${this.userId}/clientList/`)
.add({
fullName,
email,
startingWeight: startingWeight * 1,
});
return newClientRef.update({
id: newClientRef.id
});
}
I was calling this.firestore.collection(...).add({}) with no fields, so when it happened, the cloud function got triggered and the DocumentSnapshot.data() was empty thus returning the Reference.set error. The cloud function createClientAccount is correct. Thanks.

Related

Firebase: using DocumentSnapshot id as User uid. Thoughts?

Currently users profiles are created by adding a user to a firestore collection.
I then have an onCreate function that will create a user in Firebase Authentication.
Would there be an issue with using the firestore docId as the created users uid?
Thanks
export const createUser = functions.region('europe-west2')
.firestore.document('users/{user}')
.onCreate(async (snap, context) =>
executeOnce(context.eventId, async (transaction: FirebaseFirestore.Transaction) => {
const data = snap.data();
await auth.createUser({
uid: snap.id,
email: data.email,
password: //,
displayName: `${data.firstName} ${data.lastName}`,
}).then(async function (newUser) {
return db.collection('users').doc(snap.id)
.update({
status: 'Active',
});
}).catch(function (error) {
return console.error(`Error creating user, ${error}`);
});
}));
I don't see why that would be an issue. Usually UID from Firebase Authentication is used as Firestore document ID. It's just an identifier so you can accurately point at the document containing current user's information. (<= just an example)
const uid = firebase.auth().currentUser.uid
const docRef = firebase.firestore().collection("users").doc(uid)
At the end, it's just a unique string. So as long as they both are same, you should be fine. Even if they are not, you could still query Firestore document using .where("userId", "==", uid).

Firebase Cloud Function not firing

I'm trying to run the following Cloud Function:
exports.getUserData = functions.firestore
.document('UserData/{id}')
.onWrite(async (snap, context) => {
const uid = snap.data.id;
let uData;
console.log("onCreate called. uid="+uid);
await admin.auth().getUser(uid)
.then(function(userRecord) {
// See the UserRecord reference doc for the contents of userRecord.
console.log('Successfully fetched user data:', userRecord.toJSON());
uData = userRecord.toJSON();
})
.catch(function(error) {
console.log('Error fetching user data:', error);
});
await admin
.firestore()
.doc('UserData/${uid}')
.set({
userRecord : uData
});
return null;
});
It gets deployed allright, as I can see it in the console. But adding/updating a doc in the collection simply does not trigger the function (nothing shows in log).
A couple of things, as I see a few problems
Seems to me that you want to trigger this function every time there is a new UserData collection. If this is the case, you should use the trigger onCreate. onWrite gets triggered every time a doc is updated, created or deleted.
You function is creating an infinite loop if you use onWrite. You are updating collections which will triggered the same function, over and over.
First argument of the function is not a snapDoc, if you are using onWrite. Check the documentation
This part:
await admin
.firestore()
.doc('UserData/${uid}')
.set({
userRecord : uData
});
'UserData/${uid}' is a string not a template string. Use backtick ` not single quote '
As #renaud-tarnec said, use context.params to get the id parameter
It seems that by doing
exports.getUserData = functions.firestore
.document('UserData/{id}')
.onWrite(async (snap, context) => {
const uid = snap.data.id;
//...
});
you want to assign to the uid variable the value of the {id} wildcard in the 'UserData/{id}'.
For that you should use the context Object, as follows:
const uid = context.params.id;
and as explained here in the doc.

Firebase Admin SDK NodeJS -- "There is no user record corresponding to the provided identifier." Error

Following the Firebase SDK docs on https://firebase.google.com/docs/auth/admin/email-action-links#generate_email_verification_link and getting the following error, which makes little sense as the function is triggered from the server environment using the authenticated admin.auth().
Might anyone know what is causing the issue?
Error from admin.auth().generateEmailVerificationLink : { Error: There is no user record corresponding to the provided identifier.
at FirebaseAuthError.FirebaseError [as constructor] (/srv/node_modules/firebase-admin/lib/utils/error.js:42:28)
at FirebaseAuthError.PrefixedFirebaseError [as constructor] (/srv/node_modules/firebase-admin/lib/utils/error.js:88:28)
at new FirebaseAuthError (/srv/node_modules/firebase-admin/lib/utils/error.js:147:16)
at Function.FirebaseAuthError.fromServerError (/srv/node_modules/firebase-admin/lib/utils/error.js:186:16)
at /srv/node_modules/firebase-admin/lib/auth/auth-api-request.js:1201:49
at <anonymous>
at process._tickDomainCallback (internal/process/next_tick.js:229:7)
errorInfo:
{ code: 'auth/user-not-found',
message: 'There is no user record corresponding to the provided identifier.' },
codePrefix: 'auth' }
The code is just this:
exports = module.exports = functions.firestore
.document("xxx/{docId}")
.onCreate(async (snap, context) => {
let yyy = snap.data();
let { uid, userData } = yyy;
console.log("from sendEmailVerification, uid, userdata: ", userData);
const actionCodeSettings = {
url: `${emailConfirmationUrl}`,
handleCodeInApp: false
};
return admin.auth().generateEmailVerificationLink(userData.email, actionCodeSettings)
.then(async (link) => {
console.log("uid from generateEmailVerificationLink and email: ", uid, userData.email)
await admin.firestore().collection('users').doc(uid).set({
verificationLink: link,
emailVerified: false
}, { merge: true });
return emailfunc.sendCustomVerificationEmail(userData.email, link);
})
.catch((err) => {
console.error("Error from admin.auth().generateEmailVerificationLink :", err);
return Promise.reject(err);
});
});
You read the user's email address from the database (Firestore). That user account, however, doesn't exist in Firebase Auth. It must also exist in Firebase Auth if you wish to use APIs like getUser() and generateEmailVerificationLink(). Having it only in Firestore is not enough.

Is there a way to create Auth object and use that UID to create a doc with GeoFirestore

I am trying to create an Auth object in firebase that returns the User UID. I want to be able to create a document in my collection with that particuar UID but apparently geofirestore doesn't have a feature to add a document with a particular ID.
const storesCollection = geoFirestore.collection("retailers");
export const firstTimeStartCreateRetailer = ( email, password) => async dispatch => {
try {
const { user } = await auth.createUserWithEmailAndPassword(email, password);
await storesCollection.doc(user.uid).add({
coordinates: new firebase.firestore.GeoPoint(33.704381, 72.978839),
name: 'Freshlee',
location: 'F-11',
city: 'Islamabad',
inventory: [],
rating: 5,
categories: []
})
dispatch({ type: LOGIN, payload: { ...user } });
} catch (error) {
console.log(error)
}
};
this code is rejected because geoFirestore doesn't have the .doc(id) referencing feature. How can I achieve this.
You need to do
await storesCollection.doc(user.uid).set({...})
using the set() method. As a matter of fact, there is no add() method for a GeoDocumentReference and storesCollection.doc(user.uid) is a GeoDocumentReference.
The add() method is a method of a GeoCollectionReference.
Because storesCollection is a GeoCollectionReference, the API is not always the same as native Firestore references.
In your particular case, you get the document you want to write to using doc(id), but instead of using add(...) which is used on collections, you need to use set(...) instead to create/overwrite the data for that particular document.
await storesCollection.doc(user.uid).set({
coordinates: new firebase.firestore.GeoPoint(33.704381, 72.978839),
name: 'Freshlee',
location: 'F-11',
city: 'Islamabad',
inventory: [],
rating: 5,
categories: []
});

Flutter calling firebase cloud function admin.auth.updateUser

EDIT**
Ok so I was able to get the parameters working thanks to first answer provided but now I have an issue whereby my function is creating a new user entirely in Firebase and not update an existing one, the uid that i am passing into the auth.admin.updateUser is teh uid of the existing user who's email i want to update. Here is the updated cloud function which is adding a new user rather than updating the existing:
exports.updateEmail = functions.https.onCall((data, context) => {
const email = data.email;
const uid = data.uid;
admin.auth().updateUser(uid, {
email: email
})
.then(function(userRecord) {
// See the UserRecord reference doc for the contents of userRecord.
console.log("Successfully updated user", userRecord.toJSON());
return response.status(200).json(userRecord.toJSON());
})
.catch(function(error) {
console.log("Error updating user:", error);
return response.status(404).json({
error: 'Something went wrong.'
});
});
});
I got the function from the firebase docs but it isn't doing what I intended it to do.
ORIGINAL POST**
I'm having some difficulty getting a cloud function to work when calling the function from within my flutter code. The issue that I am having is that the uid and email fields are undefined even though I am passing them through to the cloud function using busboy fields.
I'm trying to pass the email and uid field though to the function as follows:
final request = http.MultipartRequest('POST', Uri.parse('****************my function url************'));
request.fields['email'] = Uri.encodeComponent(newEmail);
request.fields['uid'] = Uri.encodeComponent(selectedUser.uid);
request.headers['Authorization'] = 'Bearer ${_authenticatedUser.token}';
final http.StreamedResponse streamedResponse = await request.send();
And on the Node.js side I am trying to use these fields using busboy, here is my cloud function in Node.js:
exports.changeEmail = functions.https.onRequest((request, response) => {
if (!request.headers.authorization ||
!request.headers.authorization.startsWith('Bearer ')
) {
return response.status(401).json({
error: 'Unauthorized.'
});
}
let idToken;
idToken = request.headers.authorization.split('Bearer ')[1];
let email;
let uid;
const busboy = new Busboy({
headers: request.headers
});
busboy.on('field', (fieldname, value) => {
if (fieldname == 'email') {
email = decodeURIComponent(value);
}
if (fieldname == 'uid') {
uid = decodeURIComponent(value);
}
});
admin.auth().updateUser(uid, {
email: email
})
.then(function(userRecord) {
// See the UserRecord reference doc for the contents of userRecord.
console.log("Successfully updated user", userRecord.toJSON());
return response.status(200).json(userRecord.toJSON());
})
.catch(function(error) {
console.log("Error updating user:", error);
return response.status(404).json({
error: 'Something went wrong.'
});
});
});
Even though I am passing the fields in with busboy fields they are not getting set in the function, is there something I am doing wrong here?
Why don't you use a callable function? It will automatically receive the authentication data.
The documentation even has examples on how to get the uid and email:
Declare the function:
exports.addMessage = functions.https.onCall((data, context) => {
// ...
});
Get the user properties from the context parameter:
// Message text passed from the client.
const text = data.text;
// Authentication / user information is automatically added to the request.
const uid = context.auth.uid;
const name = context.auth.token.name || null;
const picture = context.auth.token.picture || null;
const email = context.auth.token.email || null;
Call the function from your Flutter code:
Install cloud_functions package and then:
import 'package:cloud_functions/cloud_functions.dart';
await CloudFunctions.instance.call(functionName: 'addMessage');
If the user is authenticated before calling the function that's all you need to do.
You can also pass additional parameters to the function:
await CloudFunctions.instance.call(functionName: 'addMessage', parameters: {"email": "whatever#example.com"});
Any parameters will be passed to the data parameter on the function side.

Resources