Firebase insufficient permissions - what am I doing wrong? - javascript

Sorry I am new to Firebase, I am trying to deploy my app to production for the first time but I am struggling with the security rules.
I have a page in my next.js project which pulls data from the firestore (a nested subcollection), like so:
useEffect(() => {
const getKids = async (user: any) => {
if (user) {
const collectionRef = collectionGroup(db, 'kids')
console.log(collectionRef, 'collectionRef')
const q = await query(collectionRef,
where("uid", "==", user.uid)
)
console.log(q, 'q')
const data = await getDocs(q)
data.forEach(doc => {
setKids(data.docs.map((doc) => ({
...doc.data(), id: doc.id
})))
})
}
}
getKids(user)
}, [user?.uid])
It also writes to it on submit, but I'm just trying to read the data onto the page first (with no luck so far)...
Here's how my firestore looks:
My rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{uid}/kids/{document=**} {
allow read, write: if request.auth.uid == uid;
}
}
}
With this I would expect the currently logged in user to be able to view all the documents inside of the kids subcollection inside of users, but it doesn't work.
What am I doing wrong?

Your rule only protects documents nested under users. But your query is using a collection group which could have instances living anywhere. If you review the documentation on security rules for collection groups you'll see that you need to use a different form to allow access for collection group queries.
match /{path=**}/kids/{post} { ... }
However, now you don't have a UID in the path to use to protect the collection group, because collection groups can live anywhere.
The bottom line here is that you'll have to do one of two things:
Don't use a collection group query, and instead refer to the specific subcollection using its full path under users for a specific uid.
Relax your rules somehow for the collection group query to work.

Related

How do I fetch user information to display in a feed of posts using firebase storage solutions?

I'm building a forum-style application where users post content that displays on a global feed. I want to display information about the user in posts (photoURL, displayName) similar to Twitter.
I have firebase v9 using the authentication and firestore for the posts. The reason I want to reference the auth is that I can catch changes to the user's information as it happens, this way the feed is up to date.
I save the user's unique ID with the post so I am able to reference who to display. I can successfully reference the post title and description with doc.title & doc.description however I get stuck when retrieving user information. I'm trying doc.UserID.displayName for the display name but I know this is incorrect. I can't find anything in the docs for this specific use case, is this something that I can do with just firestore and auth?
Do I need to create a reference to the auth storage with doc.UserID?
Here is the code:
// add a new post
addPostForm.addEventListener('submit', (e) => {
e.preventDefault();
onAuthStateChanged(auth, (user) => {
const colRef = collection(db, 'Posts');
console.log(hiddenURL.value);
addDoc(colRef, {
UserID: user.uid,
beatURL: hiddenURL.value,
title: addPostForm.postTitle.value,
description: addPostForm.postDescription.value,
})
.then(() => {
console.log("Document written with ID: ", doc.id);
addPostModal.classList.remove('open');
addPostForm.querySelector('.error').textContent = "";
})
.catch(error => {
addPostForm.querySelector('.error').textContent = error.message;
alert(error);
})
})
});
export const initApp = async () => {
initFirebaseAuth;
const posts = await collection(db, 'Posts');
// render data to the page
return renderPosts(posts);
};
const renderPosts = (posts) => {
const main = document.getElementById("feed");
onSnapshot(posts, (snapshot) => {
let cardsArray = []
snapshot.docs.forEach((doc, user) => {
cardsArray.push({ ...doc.data(), id: doc.id })
name.textContent = `${doc.UserID.displayName}`; // users display name
avatar.src = doc.UserID.photoURL; //user's image
description.textContent = `${post.description}`;
title.textContent = `${post.title}`;
});
console.log(cardsArray);
});
};
There are two cases and approaches at first sight:
1. Your users profiles are only available in the Auth Service
In this case, via the JS SDK, a user X cannot "query" the Auth profile of a user Y.
This means that you need to save the author's displayName together with the author uid when the post is created.
2. Your users profiles are also available in a users collection (a common pattern)
In this case, when you display a post, you could fetch the user's document to get the author's displayName.
However, in the NoSQL world, you should not be afraid to duplicate data and denormalize your data model. When designing your data-model you should think about it from a query perspective, trying to minimize the number of queries for a given screen/use case. So approach #1 is recommended, even if you maintain a user's collection.
In case of changes in the user's profile, in order to synchronyse the post documents and user's data a common approach is to use a set of Cloud Functions (which are executed in the back-end) to update the post documents. The link between the posts and the users profile being the user's uid.

Fetching documents in a subcollection of a cloud firestore database does not return any data

I am using cloud firestore database to store documents for user inside a next.js application, the collection path is as follows
collection "userDocs"
└─ document "userEmail"
└─ collection "docs"
└─ document "documentObject"
I was using Firebase v9 SDK and I downgraded to firebase v8 and I am still facing the same issue.
This code snippet is where I add a new document to the database which is done and reflects successfully in the Firebase console
db.collection("userDocs").doc(session.user.email).collection("docs").add({
fileName: input,
timestamp: firebase.firestore.FieldValue.serverTimestamp(),
});
When trying to fetch documents from the database I tried the following approaches:
1. Using react-firebase-hooks
const [snapshot] = useCollectionOnce(
db
.collection("userDocs")
.doc(session?.user.email)
.collection("docs")
.orderBy("timestamp", "desc")
);
2. Using Firebase query
useEffect(() => {
var query = db.collection("userDocs");
query.get().then((querySnapshot) => {
querySnapshot.forEach((document) => {
document.ref
.collection("docs")
.get()
.then((querySnapshot) => {
console.log(querySnapshot);
});
});
});
});
This is how I tried to achieve it using firebase v9
import {
collection,
query,
orderBy,
serverTimestamp,
setDoc,
doc,
collectionGroup,
onSnapshot,
getDocs,
} from "firebase/firestore";
useEffect(async () => {
const docRef = query(
collection(db, "UserDocs", session?.user.email, "Docs"),
orderBy("timestamp", "desc")
);
const docSnap = await getDocs(docRef);
const allDocs = docSnap.forEach((doc) => {
console.log(`Document ${doc.id} contains ${JSON.stringify(doc.data())}`);
});
}, []);
I managed to get around this using the following approach:
1- I restructured my database to contain "docs" collection which holds inside a document object that has the following attributes {email, fileName, timestamp} instead of creating a subcollection.
I created the following method to fetch data and map it into a state array to be able to render it
const getDocuments = async () => {
await db
.collection("docs")
.where("email", "==", session?.user.email)
.orderBy("timestamp", "desc")
.get()
.then((res) => {
setUserDocs(res.docs);
})
.catch((error) => {
console.log(error);
});
};
I am still not quite sure why exactly did this attempt work rather than all other trials, but I managed to get things working so far so that's the most important thing for the time being.
The problem was that you were not structuring your data correctly. As shown on the Firebase official documentation Choose a data structure,
when you structure your data in Cloud Firestore, you have a few different options:
Documents
Multiple collections
Subcollections within documents
Consider the advantages of each option as they relate to your use case
Therefore, from the code you've shared in your answer, the structure you're using is Nested data in documents.
I've got a little bit lost on your first approach, but since you've finally got it working, the advice is to structure your data according to what is stated in the documentation and what fits your necessity.
See also
https://firebase.google.com/docs/firestore

Firebase auth + firestore (what is the proper way to "link" users)

I'm making smth with backed for the first time in my life, so I'm sorry in advance, I'm making a web chat app. I think I managed to deal with authentication (it seems to be working) and now I want to make connect somehow the authentication user names with chat users... so I tried to
const docRef = await addDoc(collection(database, 'users'), {
name: user.displayName,
});
console.log('Document written with ID: ', docRef.id);
} catch (e) {
console.error('Error adding document: ', e);
}
but it says user not defined because userCredentials is in the scope of authentications functions...
If I paste this code into some function where userCredentials can be found, it says there is some problem with await word...
I want to take userCredential that logged in and use it in the chat app... so I need to link somehow the auth db and the firestore db? or is it done completely differently? Thank you
Could you give a bit of advice? Thank you (edited)
If you want to use the user's name, you first need to make sure that your code only runs once the user is signed in. In Firebase you can do that as shown in the documentation on getting the currently signed in user:
import { getAuth, onAuthStateChanged } from "firebase/auth";
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
if (user) {
// 👇 your application specific code is below
const docRef = await addDoc(collection(database, 'users'), {
name: user.displayName,
});
console.log('Document written with ID: ', docRef.id);
} else {
// User is signed out
// ...
}
})
I'd actually recommend not using addDoc, but basing the document ID on the UID of the user, like this:
if (user) {
// 👇 get UID from user
const uid = user.uid;
// Use as document ID 👇 👇 👇
const docRef = await setDoc(doc(database, 'users', uid), {
name: user.displayName,
});
Since document IDs are unique within a collection, this automatically ensures that each user can only have one document in the users collection. It also makes it easier to find a user's document when needed, and makes it easier to ensure users can only edit their own document.

firestore security referring to data

i have a firestore db and it has some collection. each collection has some documents and the document will have a field called userId. This userId is the thing i want to match when i write the security rule. My data looks like this. look at the users collection one particular object.
Now i want the security rule something like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /Users/{User}{
allow read, write: if resource.data.userId == request.auth.uid;
}
}
}
when i make a call to read the data:
import firestore from '#react-native-firebase/firestore';
console.log(`made db call to update menu for userid ${user.uid}`);
try{
var querySnapshot = await firestore().collection('Users').get();
console.log(querySnapshot.size);
querySnapshot.forEach(documentSnapshot => {
console.log('snapshot ID: ', documentSnapshot.id, documentSnapshot.data());
});
here is the error i get along with the logged userid used:
[Fri Dec 11 2020 15:53:22.649] LOG made db call to update menu for userid CKKMUujnojNiUGO7z8FHxtnquQ53
[Fri Dec 11 2020 15:53:22.808] LOG [Error: [firestore/permission-denied] The caller does not have permission to execute the specified operation.]
i get this error:[Error: [firestore/permission-denied] The caller does not have permission to execute the specified operation.] i am basically not able to put any condition that relies on resource.data. i hardcoded id once and put request.auth.uid =="hard coded id" and that also worked. i think somehow resource.data.userId is not resoving although it looks correct to me.
It's important to realize that Firestore security rules are not filters. Be sure to read that linked documentation. Security rules will not filter the documents to match only what a user is allowed to read.
Your query is demanding all of the documents in Users:
firestore().collection('Users').get();
However, your rules insist that a user must only request documents where their UID is in the userId field of the document:
allow read, write: if resource.data.userId == request.auth.uid;
You can make your query match the requirements of the rules by adding a filter on the userId field:
firestore()
.collection('Users')
.where('userId', '==', user.uid)
.get();

How to access all data in a firestore database with admin approval?

I am currently building a vuejs web app that uses firestore from firebase. The basic structure is a user fills out an application with data and then an admin can look at that data to see if they are eligible for funding. The problem that I am encountering is that when the admin requests the information from firestore, it only ever returns an empty array.
The admin tag is assigned using this firebase cloud function:
const admin = require('firebase-admin');
admin.initializeApp();
exports.addAdminRole = functions.https.onCall((data, context) => {
//get user and add custom claim (admin)
return admin.auth().getUserByEmail(data.email).then(user => {
return admin.auth().setCustomUserClaims(user.uid, {
admin: true
});
}).then(() => {
return {
message: `Success! ${data.email} has been made an admin`
}
}).catch(err => {
return err;
});
});
My firestore structure is as follows:
users
|_user1_uid
|_info
|_user2_uid
|_info
The call I am making to get the information is this:
db.collection('users')
.get()
.then(snapshot => {
console.log(snapshot.docs)
})
As you can see, the call is supposed to get a snapshot of all of the documents I have. (example: the console.log should show that I have three users > (3) [n, n, n]). However, what is returned instead is an empty array.
My firestore rules are this:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth.token.admin == true;
}
match /users/{userId}/{document=**} {
allow create: if request.auth != null;
allow read, write: if request.auth.token.admin == true;
allow read, write: if request.auth.uid == userId;
}
}
}
Any suggestions or pointers on why my admin is unable to get the users' information would be appreciated.
Thanks!
Ok, I figured out what was happening. When I start making my documents I was using this code:
db.collection('users').doc(this.uid)
.collection('info').doc('county').set({
county: this.county
)}
What I didn't realize is that this makes the document that holds this.uid to be virtual. If I had looked a little harder I would have seen this notification on firebase:
I found a relatively simple workaround which was first creating the UID document with dummy data before adding actual info to it like so:
db.collection('users').doc(this.uid).set({
dummy: 'dummy'
})
This fixed my problem. Thanks everyone for your help! I appreciate it!

Categories

Resources