Firebase / Firestore how to store user data - firebase

I have a project where users can create "companies" and invite other users to join this company.
My firestore database looks like this:
In each company the name of the company and the owner (Google uid) is stored.
In addition, in each company there are subcollections with further data about the company. e.g. tasks, which can then be viewed and edited by users who are invited to the company.
Currently there is a "users" collection where all users are stored. If a user is invited to a company it is stored in the "users" subcollection "companies":
match /companies/{compId} {
function hasUserJoinedTheCompany() {
return exists(/databases/$(database)/documents/users/$(request.auth.uid)/companies/$(compId));
}
allow read: if hasUserJoinedTheCompany();
...
}
With this structure and rules, the user can only access the data from the companies to which he is invited.
In my frontend it looks like this:
The user can switch between the companies (to which he is invited) in the upper left corner. And in blue highlighted he sees the "tasks" of the currently selected company.
My problem: I want to create a page where all users in a company are listed.
With my current structure I don't know how to make it work.
I was thinking of putting the users in a subcollection in "companies" (/companies/{companyId}/users/). So that it is easy to list all users in a company. But then I don't know how to list all the companies a user is invited to (as marked in red in the last screen shot).
Is there a solution for this, without firebase functions?

This is a Problem a lot of devs have when starting to use nonSQL databases. You can/should store the same data twice. In SQL databases we are all forced to link the data and not to store anything twice or more. In a nonSQL database you can and sometimes your realy should do that. In your case it is absolutely OK for you to store the data in 3 places:
all users in the users collection
users of a company in the companies/{id}/users collection
companies that a user belogs to to the users/{id}/companies collection
That way you can very easy query all data you need and write very simple database rule to make everything secure. It would even make the automated sync trogh cloud functions more easy because you know when a company changes for what users to update the data and if a users changes for what companies to update the user data.
I would recommend to just save there the basic data like name and if shown in a list some extra ones. But awoid to save the full user data and full company data on all places. All that is just the link for you to know exactly and easy where what belogs to and to get the main data very easy.

Related

Firestore model for fetching a list of friends of an user

I have been using Firestore for a very long time. I am building an app now where scalability and keeping low costs is important. (I am using flutter)
My app has users, which have user profiles, also they can add friends and talk to them (like instagram or facebook).
I have a problem building this friends system.
My model for this friends system currently looks like this:
Users collection. Each document id = user id from auth, those docs contain data like name, username, profile picture, etc.
Friends collection. Each document id = user id from auth. For each user, those docs contain a field called: friends, which is an array with each of his friends user ids.
The model looks like:
Friends collection:
- uid:
- friends_list: [friend_uid1, friend_uid2, ...]
This is how my "backend" looks.
Now I want to show my user a list of his friends. How do I do that?
I want a list that looks like instagram, with a nice UI showing each of my user friend profile pic, name, last message, etc.
I can not find a straight forward way to do this with Firestore and queries.
Let's say I do it like this:
Get all my friends user ids in an array.
Get all their user documents using .get() for each document.
This is not doable in firestore cause it would eliminate all the querying power I have (such being able to query only for users with name "x"), I would have to fetch all users and do the query on my front-end (or in a cloud function, same thing, not scalable).
If I do this like:
Get all document using a query for all users in the Friends collection, where friends_list contains my user id.
Save from those documents only the documentID and fetch all the friends user data manually.
This comes with another problem. In Firestore there is no way of fetching a document without fetching all of its fields, so the first query which I use to get the ids only of my friends would actually give me their id + their friend list instead (cause when I query, it also gets the document id + the data), which is not good.
If I do it like:
When you add a friend, instead of just saving its uid, save its uid + data.
Now I can easily show my user his friends list nicely and do some querying on front-end.
The problem here is that now if one of my friends updates his profile photo, I need to update it in every document of all of his friends, which is very write expensive for just a little profile update.
There is also the problem of watching for more data, maybe I have another collection with Chats, and I want to show the last message of my chat with a friend, now I have to fetch the chat rooms too, which is more hard to query data that comes with all the problems that I mentioned before.
In conclusion: I don't see a good scalable way to do this kind of system within Firestore. It seems a simple system which any basic app should have, but I do not see how I can do it in a way that does not make lots of reads or read more data (or sensitive data) than it should.
What kind of model would you do for a friends system like this?
You're decribing a quintessential drawback of NoSQL Databases.
A similar example is actually given in the Get to Know Cloud Firestore series.
Like others have commented, the answer really depends on your application. And this is the assessment you'll have to do. Like which of the options is cheaper depending on the use case of the app.
For example, if you go with your third option and store the friend's user data that you'll need to populate the list. This means you'll have to implement measures to keep the integrity of the copied data whenever the user updates their information.
You can then look at the usage of your app and determine how often users change their information vs how often you would need to retrieve full users if you don't copy the data to find the cheapest method for your application.

Firestore Sharing data with other users

I'm creating a travel app in flutter for people to create trips and share with other users that can modify its contents. I'm currently using firebase authentication so each user has an unique id. I'm having trouble understanding what concept I should use to share trip1 with user2. I found out documentReferences allow you to share the specific path of the trip with other users but it doesn't necessarily add the trip to user2 so they can see it on their dashboard. Is documentReferences the correct way to share data with other users?
The best way to do this is to create another collection with the name trips or something you want. It is better to separate the user's data and the trip's data since you are going to share it from one user to another. You can create a unique document for trip 1 and add the location's name as trip 1's value. This way you can filter out where each specific trip is by using .where(). Inside trip1 you can store the reviews of each user inside it. Watch these two videos so that you will be able to understand how to implement it:
https://www.youtube.com/watch?v=Ofux_4c94FI and https://www.youtube.com/watch?v=haMOUb3KVSo. Good Luck!

Firestore dynamically update security rules

Imagine we have Chat application and in this application, we have many rooms, some private and some for everyone. Every room has an admin who can manage users (can invite and remove). Only members of the room can read and write messages. An Admin is a person who created a room in this scenario.
I want to create security rules on room creation and update it on membersChange so only members can read and write the content of the message board.
In this case, that's how it could look like:
databse/rooms/
private1
admin: memberX
members: member1, member2
//only admin can write into members fields
messages
message1...
message2...
message3...
//only members can write and read messages
private2
admin: memberXY
members: member1, member4
//only admin can write into members fields
messages
message1...
message2...
message3...
//only members can write and read messages
So is it possible to create and update security rules from cloud function instead of manually updating them in firebase console? Or is there any way to automate this process?
I noticed that I can deploy security rules using CLI. What should be the process here? When do I call it? How can I get members from the database?
EDIT:
for anyone who wants more information check How to Build a Secure App in Firebase
I would rethink this model. Instead of updating the security rules all the time, I see several viable approaches:
Option 1
You can save which users can access a specific room on Firestore, and then on the security rules you can access the document for the room and see which if the authenticated user is in the list of authorized users. The problem with this is cost, because this will fire an extra database read for every operation, which can get expensive.
Option 2
You can create custom claims for the user using a cloud function, like this:
admin.auth().setCustomUserClaims(uid, {"rooms": "room1,room2"})
Then on the security rules you can check if the user has the claims to a specific room:
match /rooms/{roomId} {
allow read: if roomId in request.auth.token.rooms.split(',');
}
I believe you can also save the claim as an array directly, but I haven't tested it.
For this option you need to take into consideration the size of the token, which has a limit and can cause performance problems if it's too big. Depending on your scenario you can create a smaller set of permissions and then set those to the rooms and the users.
Option 3
You could save the uid of the users who can access each document, and then check if the authenticated user's uid exists on that document. But this can get out of hand if you have too many users.
I would go with option 2 if it makes sense for your scenario. Or you could combine more than one of these techniques. My idea was to show a few of the possibilities so that you can choose what works for you.
Having different rules for each room and dynamicly updating your rules is a bad idea. Here are a couples problems that come to mind with this solution:
Who will be updating the rules?
What happens when two rooms get created at the same time?
What will happen when something goes wrong?
How will you maintain your rules when you have a million rooms?
Also It may be a few minutes before changes to your rules take effect.
Instead you can, first of all, split you datastructure into public rooms and private rooms: database/rooms/public/... and database/rooms/private/....
For securing your private rooms you can take a look at rules conditions and do something like: member can read/write IF his UID is in /members (pseudo code, won't work like this).
You can take a look at this question for an example.

Fetch all users initially or on Search?

In order to invite others to one's group they must search for their target by name or email, then all matches are placed in a table. Right now I'm retrieving all the users from my Firebase database when the invites page initially loads but I'm concerned that if many users sign up it could become an unnecessarily large upfront data fetch.
Is it better to just fetch users based off each search or to fetch all of them at the start?

How can Firebase nodes be structured to restrict user access and allow admin to pull report data?

Context: I am putting together a time tracking application using Firebase as my backend. My current node structure has Time Entries and Clients at the root like so:
Time Entry
Entry ID
UserID
clientID, hours, date, description, etc
Clients
ClientID
name, projects, etc
This structure works fine if I'm just adding and pulling time entries based on the user, but I want to start putting together reports on a per client basis. Currently, this means making a separate HTTP request for each user and then filtering by the clientID to get at the data.
The rule structure for Firebase grants access to all child nodes once access is given to the parent node, so one big list doesn't work as it can't restrict users from seeing or editing each other's entries.
Question: Is there a way to structure the nodes that would allow for restricting users to only managing their own time entries, as well as allow for one query to pull all entries tied to a client?
** The only solution I could come up with was duplicating the entries into a single node used just for reporting purposes, but this doesn't seem like a sustainable option
#AL. your answer was what I went up going with after scouring the docs across the web. Duplicating the data is the best route to take.
The new Firestore beta seems to provided some workarounds to this.
The way that I would do this is with Cloud Firestore.
Create a root collection clients and a document for each client. This partitions the data into easily manageable chunks, so that a client admin can see all data for their company.
Within the client document, create a sub-collection called timeEntries. When a user writes to this, they must include a userId field (you can enforce this in the rules) which is equal to request.auth.uid
https://firebase.google.com/docs/firestore/security/rules-conditions#data_validation
You can now create read rules which allow an admin to query any document in the timeEntries sub-collection, but an individual user must query with userId = request.auth.uid in order to only return the entries that they have created.
https://firebase.google.com/docs/firestore/security/rules-conditions#security_rules_and_query_results
Within your users/{uid} collection or clients/{clientId} collection, you can easily create a flag to identify admin users and check this when reading data.
https://firebase.google.com/docs/firestore/security/rules-conditions#access_other_documents

Resources