Firestore Delete All References of A User - javascript

I have a chat app build in react native. When a user decides to delete their profile, I want to remove all references of them from the database.
The DB has references to their user id in the "matches" table, the "chat" table, and the "messages" table for each of the people the deleted user was chatting with.
I am using firebase functions to handle the deletion of the user doc data and auth but I am not sure what the best way to go about removing all of these references would be. My question is: what is the best way to remove all references of an ID out of a somewhat complex database? I assume this will be taxing to loop through every single user in the DB to search for this one ID.
deleteAccount = () => {
var db = firebase.firestore();
firebase.auth().onAuthStateChanged(async (user) => {
if (user) {
//delete user data
db.collection("Users")
.doc(user.uid)
.delete();
} else {
console.log("user needs to reauth");
return false;
}
});
};
firebase functions
exports.deleteUser = functions.firestore
.document("Users/{userID}")
.onDelete((snap, context) => {
const deletedValue = snap.data();
// Delete the images
try {
admin.auth().deleteUser(deletedValue.id);
const imgRef1 = firebase.storage().ref(user.uid + "/images/0")
? firebase.storage().ref(user.uid + "/images/0")
: null;
const imgRef2 = firebase.storage().ref(user.uid + "/images/1")
? firebase.storage().ref(user.uid + "/images/1")
: null;
const imgRef3 = firebase.storage().ref(user.uid + "/images/2")
? firebase.storage().ref(user.uid + "/images/2")
: null;
imgRef1.delete().then(function() {
imgRef2.delete().then(function() {
imgRef3.delete().then(function() {});
});
});
} catch (e) {
console.log("no images to delete");
}
});

Firebase products such as the databases and storage have no implicit knowledge of what data belongs to what user. That relation only exists because your application code made it.
For that reason you will also have to look up/traverse the relations when deleting the user, to find (and delete) their data. There are no shortcuts in the product here, although there is a open-source library that contains an implementation that works from a configuration file: user-data-protection
Edit: I just realized there's actually an Extension to Delete User Data, which does pretty much the same as the library linked above. It might be worth to have a look if that suits your needs

Related

How to retrieve nested data using Javascript (Firebase Realtime Database)?

I want to retrieve the data from donation and display it in a table. I was able to retrieve the user data from Users and displayed it on a table. But now I don't know how I will be able to retrieve the data from donation.
This is my database structure in Firebase. Note: All of the data that was entered came from a mobile app created in Android Studio.
This is the code that I made when retrieving the User data.
function AddAllITemsToTable(User) {
id=0;
tbody.innerHTML="";
User.forEach(element => {
AddItemToTable(element.uid, element.fullName, element.organization, element.contactPerson, element.contactNo, element.location, element.emailAddress, element.status);
});
}
function GetAllDataRealtime() {
const dbRef = ref(database, 'Users');
onValue(dbRef,(snapshot) => {
var Users = [];
snapshot.forEach(childSnapshot => {
Users.push(childSnapshot.val());
});
AddAllITemsToTable(Users);
})
}
window.onload = GetAllDataRealtime;
Since you're calling onValue on /Users, you already get all data for all users and all of their donations. To process the donations in your code:
const dbRef = ref(database, 'Users');
onValue(dbRef,(snapshot) => {
var Users = [];
snapshot.forEach(userSnapshot => {
Users.push(userSnapshot.val());
userSnapshot.child("donation").forEach((donationSnapshot) => {
console.log(donationSnapshot.key, donationSnapshot.val());
});
});
AddAllITemsToTable(Users);
})
As I said in my comment, I recommend reading the Firebase documentation on structuring data, as the way you nest donations under each user does not follow the guidance on nesting data and keeping your structure flat.

How to make separate login for Users and Freelancer based on Roles that is in my real time database Firebase

Hello I am working a web application with Firebase Realtime Database and Authentication with nodejs or javascript.
This is my real time database and I want to make a login form which if the User = User he will go to User Index and if the User = Freelancer he will go to Freelancer Index.
And this is the line of code that I was tweaking or trying but It doesn't go in my way.
<script>
firebase.auth().onAuthStateChanged(function(user)
{
if(user)
{
var userID = firebase.auth().currentUser.uid;
firebase.database().ref('Users/' + userID).once('value').then(function(snapshot)
{
if (snapshot.val())
{
window.location.href = "index.html";
}
else
{
window.location.href = "okay.html";
}
});
}
});
</script>
Hoping I can get feedbacks or answers here. I am almost trying it for 2days already that's why I seek help here.
Comments and answers are highly appreciated thank you!
With your current data structure you will need to check in two places to determine what role a user has. While this technically possible, it is less efficient and more complex in code.
I recommend instead to store a single top-level node (say UserRoles) where you simply keep the role for each UID:
"UserRoles": {
"uidOfUser1": "Freelancer",
"uidOfUser2": "User"
}
With that in place, you can load it in your onAuthStateChanged callback with:
const ref = firebase.database.ref("UserRoles");
ref.child(user.uid).once("value").then((snapshot) => {
if (snapshot.val() === "Freelancer") {
window.location.href = "okay.html";
}
else if (snapshot.val() === "User") {
window.location.href = "index.html";
}
else {
alert("Can't determine role for user: "+user.uid)
}
});

How do I Collect User IDs + Retrieve Corresponding Tokens + Send a Push Notification Via Firebase Cloud Function (JS)

The Problem:
I have been unable to use Firebase (Google) Cloud Functions to collect and utilize device tokens for the cloud messaging feature.
Context:
I am a self-taught android-Java developer and have no JavaScript experience. Despite that, I believe I have code that should work and am not sure what the problem is. To my understanding, it could be one of three things:
Somehow my Firebase Realtime Database references are being called incorrectly and I am not retrieving data as expected.
I may need to use Promises to wait for all calls to be made before proceeding, however I don't really understand how I would incorporate that into the code I have.
I may be using multiple return statements incorrectly (which I am also fuzzy on).
My error message on the Firebase Realtime Database console is as follows:
#firebase/database: FIREBASE WARNING: Exception was thrown by user callback. Error: Registration token(s) provided to sendToDevice() must be a non-empty string or a non-empty array.
at FirebaseMessagingError.FirebaseError [as constructor] (/srv/node_modules/firebase-admin/lib/utils/error.js:42:28)
at FirebaseMessagingError.PrefixedFirebaseError [as constructor] (/srv/node_modules/firebase-admin/lib/utils/error.js:88:28)
at new FirebaseMessagingError (/srv/node_modules/firebase-admin/lib/utils/error.js:254:16)
at Messaging.validateRegistrationTokensType (/srv/node_modules/firebase-admin/lib/messaging/messaging.js:729:19)
at Messaging.sendToDevice (/srv/node_modules/firebase-admin/lib/messaging/messaging.js:328:14)
at admin.database.ref.once.snapshot (/srv/index.js:84:12)
at onceCallback (/srv/node_modules/#firebase/database/dist/index.node.cjs.js:4933:51)
at /srv/node_modules/#firebase/database/dist/index.node.cjs.js:4549:22
at exceptionGuard (/srv/node_modules/#firebase/database/dist/index.node.cjs.js:698:9)
at EventList.raise (/srv/node_modules/#firebase/database/dist/index.node.cjs.js:9684:17)
The above indicates I am not retrieving data either at all or by the time the return is called. My JavaScript function code is:
'use strict';
const admin = require('firebase-admin');
admin.initializeApp();
exports.pushNotification = functions.database.ref('/Chat Messages/{chatId}/{pushID}').onCreate((snapshot, context) => {
const valueObject = snapshot.after.val();
return admin.database().ref(`/Chat Basics/${valueObject.chatKey}/Chat Users`).once('value', statusSnapshot => {
var index = 0;
var totalkeys = statusSnapshot.numChildren();
var msgIDs = [];
statusSnapshot.forEach(msg=>{
msgIDs.push(msg.key.toString());
if(index === totalkeys - 1){
const payload = {
notification : {
title: valueObject.userName,
body: valueObject.message,
sound: "default"
}
}
sendNotificationPayload(valueObject.uid, payload);
}
index++;
});
});
});
function sendNotificationPayload(uid, payload){
admin.database()
.ref(`/User Token Data/${uid}`)
.once('value', snapshot=> {
var tokens = [];
//if(!snapshot.exists())return;
snapshot.forEach(item =>{
tokens.push(item.val())
});
admin.messaging()
.sendToDevice(tokens, payload)
.then(res => {
return console.log('Notification sent')
})
.catch(err => {
return console.log('Error in sending notification = '+err)
});
});
}
This code is mostly inspired by what was said to be a working example here from another Stack Overflow question here. I have successfully tested sending a notification to a single device by manually copying a device token into my function, so the function does run to completion. My Java code seems to be irrelevant to the problem, so I have not added it (please ask in the comments if you would like it added for further context).
What I Have Tried:
I have tried implementing promises into my code, but I don't think I was doing it properly. My main reference for this was here. I have also looked at the documentation for literally everything related to this topic, however my knowledge of JS is not sufficient to really apply barebones examples to my code.
My Firebase Realtime Database Nodes:
#1: Loop through chat members to collect user IDs:
"Chat Basics" : {
"1607801501690_TQY41wIfArhHDxEisyupZxwyHya2" : {
"Chat Users" : {
"JXrclZuu1aOwEpCe6KW8vSDea9h2" : true,
"TQY41wIfArhHDxEisyupZxwyHya2" : true
},
#2: Collect user tokens from collected IDs (ignore that tokens are matching):
"User Token Data" : {
"JXrclZuu1aOwEpCe6KW8vSDea9h2" : "duDR3KH3i3I:APA91bH_LCeslZlqL8akYw-LrM9Dv__nx4nU1TquCS0j6bGF1tlIARcheREuNdX1FheC92eelatBC8LO4t6gt8liRdFHV-NDuNLa13oHYxKgl3JBPPlrMo5rB5XhH7viTo4vfYOMftRi",
"TQY41wIfArhHDxEisyupZxwyHya2" : "duDR3KH3i3I:APA91bH_LCeslZlqL8akYw-LrM9Dv__nx4nU1TquCS0j6bGF1tlIARcheREuNdX1FheC92eelatBC8LO4t6gt8liRdFHV-NDuNLa13oHYxKgl3JBPPlrMo5rB5XhH7viTo4vfYOMftRi"
}
Conclusion:
Concrete examples would be much appreciated, especially since I am crunching right now. Thanks for your time and help!
Update:
After some more testing, it looks like the problem is definitely due to my lack of understanding of promises in two areas. Firstly, only one user is collected before the final return is called. Secondly, the final return is called before the 2nd forEach() loop can store snapshot data to an array.
For this code then, how may I modify (or rebuild) it so that it collects all keys before proceeding to retrieve token data from all keys - ultimately before returning the notification?
Just as with every question I post, I managed to figure out how to do it (tentatively) a few hours later. Below is a full example of how to send a notification to chat users based on a message sent (although it does not yet exclude the sender) to a given chat. The order of operations are as such:
User message is saved and triggers event. Relevant data the message contains are:
username, chat key, message
These are retrieved, with (username + message) as the (title + body) of the
notification respectively, and the chat key is used for user id reference.
Loop through chat user keys + collect.
Loop through array of chat user keys to collect array of device tokens.
Send notification when complete.
The code:
//Use firebase functions:log to see log
exports.pushNotification = functions.database.ref('/Chat Messages/{chatId}/{pushId}').onWrite((change, context) => {
const valueObject = change.after.val();
return admin.database().ref(`/Chat Basics/${valueObject.chatKey}/Chat Users`).once('value', statusSnapshot => {
var index = 0;
var totalkeys = statusSnapshot.numChildren();
var msgIDs = [];
statusSnapshot.forEach(msg=>{
msgIDs.push(msg.key.toString());
if(index === totalkeys - 1){
const payload = {
notification : {
title: valueObject.userName,
body: valueObject.message,
sound: "default"
}
}
let promises = [];
var tokens = [];
for(let i=0; i < msgIDs.length; i++){
let userId = msgIDs[i];
let promise = admin.database().ref(`/User Token Data/${userId}`).once('value', snapshot=> {
tokens.push(snapshot.val());
})
promises.push(promise);
}
return Promise.all(promises).then(() => {
return admin.messaging().sendToDevice(tokens, payload);
});
}
index++;
return false;
});
});
});

Firebase Auth: Edit UID

Is it possible to to change a user's UID in Firebase programmatically? There can't seem to be a way to do so manually within Firebase's console.
TL;DR: If you need to specify the UID, you'll need to create a new user with that UID.
You can't directly change the UID, but I was able to hack something together using the firebase admin API (docs)
My use case was that I needed to change a user's email address. I tried update email with "Update a User", but this actually ended up changing the UID under the hood. In my app, the UID is tied to so much stuff, that I'd have to do a huge architecture change, so this wasn't an option.
The general way I did this with the API was:
Pull Down a user using admin.auth().getUserByEmail
Delete the user with admin.auth().deleteUser
Create a new user with admin.auth().createUser, using relevant data from the getUserByEmail call above, replacing the email address with the new email.
"reset password" in the firebase admin console (I think there's a way to do this programmatically too)
User gets an email to reset their password and they have a new account with their old UID.
Unlike admin.auth().updateUser, createUser actually lets you specify a UID.
Building on the answer by RoccoB, the below is a complete set of instructions for changing a user's UID:
Create a new folder, and run npm init with default values.
Run npm install firebase-admin.
Create a NodeJS script file (eg. UpdateUserUID.js), with this code:
let admin = require("firebase-admin");
// config
let email = "XXX";
let serviceAccountData = require("XXX.json");
let adminConfig = {
credential: admin.credential.cert(serviceAccountData),
databaseURL: "https://XXX.firebaseio.com",
};
let newUserOverrides = {
uid: "XXX",
};
Start();
async function Start() {
console.log("Initializing firebase. databaseURL:", adminConfig.databaseURL);
admin.initializeApp(adminConfig);
console.log("Starting update for user with email:", email);
let oldUser = await admin.auth().getUserByEmail(email);
console.log("Old user found:", oldUser);
await admin.auth().deleteUser(oldUser.uid);
console.log("Old user deleted.");
let dataToTransfer_keys = ["disabled", "displayName", "email", "emailVerified", "phoneNumber", "photoURL", "uid"];
let newUserData = {};
for (let key of dataToTransfer_keys) {
newUserData[key] = oldUser[key];
}
Object.assign(newUserData, newUserOverrides);
console.log("New user data ready: ", newUserData);
let newUser = await admin.auth().createUser(newUserData);
console.log("New user created: ", newUser);
}
Replace email and adminConfig.databaseURL with the correct values.
Replace newUserOverrides.uid with the desired new uid. (you can change some other fields too)
Generate/download a private key for your project's Firebase Admin service account: https://firebase.google.com/docs/admin/setup (can skip to the "Initialize the SDK" section)
Update the serviceAccountData variable's import to point to the key json-file from the previous step.
Run node ./UpdateUserUID.js.
If applicable (I didn't seem to need it), use the "reset password" option in the Firebase Admin Console, to have a password-reset email sent to the user, apparently completing the account update. (Perhaps I didn't need this step since I don't use the accounts/authentications for anything besides sign-in on my website...)
The UID of a user is controlled by the identity provider that creates that user. This means that you can't change the UID for any of the built-in providers.
But you can control the UID if you create a custom identity provider. Note that this is quite a bit more involved than changing something in the Firebase console. It requires you to write code that runs in a secure/trusted environment, such as a server you control, or Cloud Functions.
You can't, since is the main tree node of possibles more entries inside it, you can get it, modify and then put it inside the same UID (or create a new one) but you can have things inside, for example take this.
You create your main UID which will hold user data (name, phone, email etc) lets say the structure is this:
-9GJ02kdj2GKS55kg
-Name:
-Phone:
-Email:
so, you can get the main user UID 9GJ02kdj2GKS55kg with mAuth.getCurrentUser().getUid(); and then change it and set a new value inside 9GJ02kdj2GKS55kg, this new value should be the same UID you got but changed, and then inside your main UID you can still have the same structure
-9GJ02kdj2GKS55kg
-6GL02kZj2GKS55kN (this is your changed UID)
-Name:
-Phone:
-Email:
or you can get that changed UID and make a new child, and that will be your parent node with custom UID for the data.
Piggybacking on #Vinrynx's post.
I recently created a migration tool where I am migrating collections from 1 Firebase Project to another and it required that after I insert users to "users" collection I also create an authentication record with the same doc.id
Variables in the functions below:
outCollData : Data that I am inserting for the user (contains the email inside it)
sourceDBApp : output of the admin.initializeApp({/*service-account.json file location for source firebase project */});
destDBApp : output of the admin.initializeApp({/*service-account.json file location for destination firebase project */});
async function updateUsersUID(
outCollData: any,
sourceDBApp: admin.app.App | undefined,
destDBApp: admin.app.App | undefined
) {
if (destDBApp === undefined) return;
const admin = destDBApp;
const email = outCollData.personali.email ? outCollData.personali.email : "";
console.log("Email is ", email);
if (email === "" || email === undefined) return;
console.log("Inside updateUsersUID");
let newUserOverrides = {
uid: outCollData._id,
};
let oldUser: any;
try {
console.log("Starting update for user with email:", email);
oldUser = await admin.auth().getUserByEmail(email!);
//console.log("Old user found:", oldUser);
if (oldUser.uid === outCollData._id) {
console.log(
"User " +
email +
" already exists in the destination DB with UID " +
outCollData._id
);
return;
}
await admin.auth().deleteUser(oldUser.uid);
console.log("Old user deleted.");
} catch (e) {
console.log("User not found in destination DB ", email);
console.log("Copying the user data from source DB");
oldUser = await sourceDBApp?.auth().getUserByEmail(email);
}
let dataToTransfer_keys = [
"disabled",
"displayName",
"email",
"emailVerified",
"phoneNumber",
"photoURL",
"uid",
"providerData",
];
let newUserData: any = {};
for (let key of dataToTransfer_keys) {
newUserData[key] = oldUser[key];
}
Object.assign(newUserData, newUserOverrides);
//console.log("New user data ready: ", newUserData);
let newUser = await admin.auth().createUser(newUserData);
console.log("New user created ");
}

How to get database value not related to the event in Cloud Functions for Firebase?

I have a firebase database and I am currently trying to use cloud functions to perform an operation when a value in my database changes. So far, it successfully triggers code to run when the value in my database changes. However, when the database value changes, I now need to check another value to determine it's status, and then perform an action after that. The problem is that I have ~0 experience with JS and I have no way of debugging my code other than deploying, changing the value in my database, and looking at the console log.
Is there any way to look up another value in the database and read it? How about look up a value and then set a value for it? Here is the code:
exports.determineCompletion =
functions.database.ref('/Jobs/{pushId}/client_job_complete')
.onWrite(event => {
const status = event.data.val();
const other = functions.database.ref('/Jobs/' + event.params.pushId + '/other_job_complete');
console.log('Status', status, other);
if(status == true && **other.getValueSomehow** == true) {
return **setAnotherValue**;
}
});
This code partially works, it successfully gets the value associated with client_job_complete and stores it in status. But how do I get the other value?
Additionally, if anyone has any JS or firebase documentation that they think would help me, please share! I have read a bunch on firebase here : https://firebase.google.com/docs/functions/database-events but it only talks about events and is very brief
Thank you for your help!
When writing a database trigger function, the event contains two properties that are references to the location of the data that changed:
event.data.ref
event.data.adminRef
ref is limited to the permissions of the user who triggered the function. adminRef has full access to the database.
Each of those Reference objects has a root property which gives you a reference to the root of your database. You can use that reference to build a path to a reference in another part of your database, and read it with the once() method.
You can also use the Firebase admin SDK.
There are lots of code samples that you should probably look at as well.
I'm maybe a bit late, but I hope my solution can help some people:
exports.processJob = functions.database.ref('/Jobs/{pushId}/client_job_complete').onWrite(event => {
const status = event.data.val();
return admin.database().ref('Jobs/' + event.params.pushId + '/other_job_complete').once('value').then((snap) => {
const other = snap.val();
console.log('Status', status, other);
/** do something with your data here, for example increase its value by 5 */
other = (other + 5);
/** when finished with processing your data, return the value to the {{ admin.database().ref(); }} request */
return snap.ref.set(other).catch((error) => {
return console.error(error);
});
});
});
But take notice of your firebase database rules.
If no user should have access to write Jobs/pushId/other_job_complete, except Your cloud function admin, You need to initialize Your cloud function admin with a recognizable, unique uid.
For example:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const adminCredentials = require('path/to/admin/credentials.json');
admin.initializeApp({
credential: admin.credential.cert(adminCredentials),
databaseURL: "https://your-database-url-com",
databaseAuthVariableOverride: {
uid: 'super-special-unique-firebase-admin-uid'
}
});
Then Your firebase database rule should look something like this:
"client_job_complete": {
".read": "auth !== null",
".write": "auth.uid === 'super-special-unique-firebase-admin-uid'"
}
Hope it helps!
You have to wait on the promise from a once() on the new ref, something like:
exports.processJob = functions.database.ref('/Jobs/{pushId}/client_job_complete')
.onWrite(event => {
const status = event.data.val();
const ref = event.data.adminRef.root.child('Jobs/'+event.params.pushId+'/other_job_complete');
ref.once('value').then(function(snap){
const other = snap.val();
console.log('Status', status, other);
if(status && other) {
return other;
}
});
});
Edit to fix the error that #Doug Stevenson noticed (I did say "something like")

Categories