Get a auth user by its uid in Firestore cloud function - node.js

I have cloud function triggered when a new object is added to a collection. It looks like this:
exports.emailAdmin = functions.firestore
.document('user-books/{userId}/books/{ASIN}')
.onWrite(event => {
That event.data.data() is an object added to the sub-collection ("books"). The userId comes from the Firebase authentication system. I.e. a user signed in, and added an object to the collection "user-books" with his/her "uid".
I tried:
firestore
.collection('users')
.doc(uid)
.get()
But that, "of course", fails because I don't have a collection called "users". How do I get to the "authentication database"?
The purpose is to convert the "uid" to that person's "email".

Import firebase-admin:
import * as admin from 'firebase-admin';
Then fetch the auth user:
const uid = ...;
const authUser = await admin.auth().getUser(uid); // this returns a promise, so use await or .then()
console.log(authUser.email);

You have no access to the user collection from firebase. What I did was creating a separate User's Table where I store all the users Data. I add the new user to my collection on the onCreate Hook like this:
exports.addNewUserToUserTable = functions.auth.user()
.onCreate(event => {
let user = event.data;
let database = admin.firestore();
return database.collection('user').doc(user.uid).set({
email: user.email
});
Another problem is, that at the moment it is not possible to get the logged in user's ID on Firestore Database Triggers (It works for the real time db). I'm still waiting for this feature to be released...

It seems like your question might be incomplete- you reference an event.data.data() line that didn't appear in your code snippet. Could you review it and add it if it's missing?
Also, just to clarify your purpose- you want to be able to access the email address of a user who has been authenticated, and tag it to the book that has been added by that user?
Thanks.

Since context.auth.uid is not available for firestore. You can assign userId to document and read it in the rules.
match /collectionA/{docId} {
allow update: if request.resource.data.userId == request.auth.uid;
}
exports.createProfile = functions.firestore
.document('collectionA/{docId}')
.onCreate((snap, context) => { // or onUpdate
const newValue = snap.data();
const {userId} = newValue;
});

Related

get user displayName in firebase cloud function using context

In my firebase cloud function, I can get the email of each user that makes a call on the client side using:
context.auth.token.email
Is there a way to obtain the user's display name? I tried context.auth.token.name but it still return an undefined property.
The context.auth.token is an object of type DecodedToken which does not contain user display name. You'll have to use Admin SDK and get user by UID/email.
export const functionName = functions.https.onCall(async (data, ctx) => {
const { uid } = ctx.auth?.token
const user = await getAuth().getUser(uid)
console.log(user.displayName)
// ...
})

Using wildcards in firestore get query

I want to create a cloud function in firebase that gets triggered whenever a user logs in for the first time. The function needs to add the UID from the authentication of the specific user to a specific, already existing document in firestore. The problem is that the UID needs to be added to a document of which I do not know the location. The code I have right now doesn't completely do that, but this is the part where it goes wrong. The database looks like this when simplified
organisations
[randomly generated id]
people
[randomly generated id] (in here, a specific document needs to be found based on known email
adress)
There are multiple different organisations and it is unknown to which organisation the user belongs. I thought of using a wildcard, something like the following:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
console.log('function ready');
//Detect first login from user
//if(firebase.auth.UserCredential.isNewUser()){
if(true){
//User is logged in for the first time
//const userID = firebase.auth().currentUser.UID;
//const userEmail = firebase.auth().currentUser.email;
const userID = '1234567890';
const userEmail = 'example#example.com';
//Get email, either personal or work
console.log('Taking a snapshot...');
const snapshot = db.collection('organisations/{orgID}/people').get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
console.log(doc.data());
});
});
}
I commented out some authentication-based lines for testing purposes. I know the code still runs, because hardcoding the orgID does return the right values. Also, looping trough every organisation is not an option, because I need to have the possibility of having a lot of organisations.
A lot of solutions are based on firestore triggers, like onWrite, where you can use wildcards like this.
However, I don't think that's possible in this case
The solution to the problem above:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
//Add UID to document in DB[FMIS-94]
//Detect first login from user
//if(firebase.auth.UserCredential.isNewUser()){
if(true){
//User is logged in for the first time
//const userID = firebase.auth().currentUser.UID;
//const userEmail = firebase.auth().currentUser.email;
const userID = '1234567890';
const userEmail = 'example#example.com';
var docFound = false;
//Get email, either personal or work
console.log('Taking a snapshot...');
//Test for work email
const snapshot = db.collectionGroup('people').where('email.work', '==', userEmail).get()
.then(function(querySnapshot){
querySnapshot.forEach(function(doc){
//work email found
console.log('work email found');
console.log(doc.data());
docFound = true;
const organisationID = doc.ref.parent.parent.id;
writeUID(doc.id, userID, organisationID);
});
});
if(!docFound){
//Test for personal email
const snapshot = db.collectionGroup('people').where('email.personal', '==', userEmail).get()
.then(function(querySnapshot){
querySnapshot.forEach(function(doc){
//personal email found
console.log('personal email found');
console.log(doc.data());
const organisationID = doc.ref.parent.parent.id;
writeUID(doc.id, userID, organisationID);
});
});
}
}
async function writeUID(doc, uid, organisationID){
const res = db.collection(`organisations/${organisationID}/people`).doc(doc).set({
userId: uid
}, { merge: true });
}
This was exactly what I needed, thanks for all your help everyone!
It is not possible to trigger a Cloud Function when a user logs in to your frontend application. There is no such trigger among the Firebase Authentication triggers.
If you want to update a document based on some characteristics of the user (uid or email), you can do that from the app, after the user has logged in.
You mention, in your question, "in here, a specific document needs to be found based on known email address". You should first build a query to find this document and then update it, all of that from the app.
Another classical approach is to create, for each user, a specific document which uses the user uid as document ID, for example in a users collection. It is then very easy to identify/find this document, since, as soon the user is logged in you know his uid.
I'm not sure I understand you correctly, but if you want to search across all people collections not matter what organizations document they're under, the solution is to use a collection group query for that.
db.collectionGroup('people').get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
console.log("user: "+doc.id+" in organization: "+doc.ref.parent.parent.id);
});
});
This will return a snapshot across all people collections in your entire Firestore database.
First setup Cloud Functions according to the official Documentation.
Then after setting up create functions like this:
exports.YOURFUNCTIONNAME= functions.firestore
.document('organisations/[randomly generated id]/people/[randomly generated id]')
.oncreate(res => {
const data = res.data();
const email = data.email;/----Your field name goes here-----/
/-----------------Then apply your logic here---------/
)}
This will triggers the function whenever you create the People -> Random ID

How to delete a user and their Firestore document in a callable cloud function

I'm creating a cloud function in firebase and need some help,I'm trying to delete a user form firebase and delete his document from firestore in one cloud function.
How can I make a batch job / transaction for both auth and firestore, lets say the user tries to delete his account but for some reason the user.delete() function doesn't work (lets say it's down on firebases side for that moment). The user would get en error message that we couldn't delete his account but when he tries to login again he would also get an error because his document doesn't exist.
I looked at the firebase extension to delete user data but it doesn't delete the user account and it seems to have the same problem.
Do I need to handle such edge case in the app/cloud-functions, is it something firebase should take care of or am I just getting something wrong?
Here is my code, if it would help:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
exports.deleteUser = functions.https.onCall(async (data, context) => {
try {
const uid = context.auth.uid;
const db = admin.firestore();
const collection = db.collection("users");
await collection.doc(uid).delete();
await admin.auth.deleteUser(uid); // what if this line fails?
return "success";
} catch (err) {
console.error(err);
return "error";
}
});
This line isn't doing what you think it's doing:
const user = await admin.auth().currentUser;
user is going to be undefined, because admin.auth() doesn't have a property called currentUser (it's an Auth object). The concept of "current user" doesn't exist on backend SDKs. It's a frontend concept only. What you do have, however, is a string uid which is the UID of the authenticated user that invoked the function.
If you want to use the Firebase Admin SDK to delete the user identified by the uid string, then you just need to call deleteUser(uid):
await admin.auth().deleteUser(uid);
By the way, the Delete User Data extension doesn't have to delete the user, because it works by responding to the user deleting their own account using the client SDK. That should actually be enough to make this work.

Executing compound Firestore query in Cloud Functions

I am writing a few triggers who work on a firestore databse of movies and users.
in this trigger' I am trying to display a new user the top rated movies in a list of genres he chose. for that I have an array of his favourite genres, and I want to query the database inside a foreach loop:
import * as functions from 'firebase-functions';
import { __values } from "tslib";
import * as admin from 'firebase-admin';
admin.initializeApp();
const films = admin.firestore().collection("Films");
export const new_user_recommend = functions.firestore
.document('/Users/{email}')
.onCreate((snap, context) => {
const new_user = snap.data();
if(new_user){
const new_email = new_user.email;
const favourite_genres = new_user.favourite_genres;
let film_id_arr: string[] = new Array();
favourite_genres.forEach((genre: string) => {
films.where('genres', 'array-contains', genre).orderBy('avg_rating', 'desc').limit(5).get()
.then((list_book: any)=>{
list_book.forEach((element: any) => {
const film_element = JSON.parse(element.data());
film_id_arr.push(film_element.data().id);
});
return film_id_arr;
})
.catch((error: any)=>{
console.log(error);
console.log("Error loading films");
});
});
}
return 0;
});
the problem is I always get the error message in the catch clause (so for each genre in the array I get the error message). I double checked to make sure the strings in the array are the same as the strings who might be written in 'genres' field for each film document.
This lead me to believe my query is somehow incorrect. Is that the case?
I edit to add some important details. the exact error I get is "Could not load the default credentials", So would that indicate a problem with user authentication rather than the query itself?
After quick search it seems that your problem is that you're using firebase-admin without providing valid credentials website.
Atleast that's how it seems judging from the error code.
The credentials for deployed functions are necessary only for firebase-admin but not for firebase-functions.

How to update a quantity in another document when creating a new document in the firebase firestore collection?

When I create a new document in the note collection, I want to update the quantity in the info document. What am I doing wrong?
exports.addNote = functions.region('europe-west1').firestore
.collection('users/{userId}/notes').onCreate((snap,context) => {
const uid = admin.user.uid.toString();
var t;
db.collection('users').doc('{userId}').collection('info').doc('info').get((querySnapshot) => {
querySnapshot.forEach((doc) => {
t = doc.get("countMutable").toString();
});
});
let data = {
countMutable: t+1;
};
db.collection("users").doc(uid).collection("info").doc("info").update({countMutable: data.get("countMutable")});
});
You have... a lot going on here. A few problems:
You can't trigger firestore functions on collections, you have to supply a document.
It isn't clear you're being consistent about how to treat the user id.
You aren't using promises properly (you need to chain them, and return them out of the function if you want them to execute properly).
I'm not clear about the relationship between the userId context parameter and the uid you are getting from the auth object. As far as I can tell, admin.user isn't actually part of the Admin SDK.
You risk multiple function calls doing an increment at the same time giving inconsistent results, since you aren't using a transaction or the increment operation. (Learn More Here)
The document won't be created if it doesn't already exist. Maybe this is ok?
In short, this all means you can do this a lot more simply.
This should do you though. I'm assuming that the uid you actually want is actually the one on the document that is triggering the update. If not, adjust as necessary.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
exports.addNote = functions.firestore.document('users/{userId}/notes/{noteId}').onCreate((snap,context) => {
const uid = context.params.userId;
return db.collection("users").doc(uid).collection("info").doc("info").set({
countMutable: admin.firestore.FieldValue.increment(1)
}, { merge: true });
});
If you don't want to create the info document if it doesn't exist, and instead you want to get an error, you can use update instead of set:
return db.collection("users").doc(uid).collection("info").doc("info").update({
countMutable: admin.firestore.FieldValue.increment(1)
});

Resources