BenjaminK
BenjaminK

Reputation: 783

Firebase - Best way to safely check if and value exists

I'm having a list of E-Mail subscribers and want to add a new one on button click. This should be possible for non-logged-in users. Therefore there need to be a check if this entry is already in the database. As I don't want to expose the full list to the non-logged-in user, how would I check if an E-Mail address already exists without making a fetch GET request that is having rights to access that list with firebase, as the whole code is exposed in the frontend? I'm working with version Web9.

import { initializeApp } from "firebase/app";
import { getFirestore, collection, addDoc, getDocs, query, where } from "firebase/firestore";
import { getAuth } from "firebase/auth";

import { getAnalytics } from "firebase/analytics";

const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projected: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: "",
  measurementId: "",
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
const db = getFirestore();

const auth = getAuth(app);

Fuction for adding the doc:

const addDocument = (dbName, obj) => {
  return addDoc(collection(db, dbName), obj);
};

Upvotes: 1

Views: 688

Answers (2)

BenjaminK
BenjaminK

Reputation: 783

Here is my code solution that hopefully helps somebody. The goal has been to check if a single document exists and restrict the database access to check existence. I found only one possible option to do this (Excluding cloud functions). Thanks to @RenaudTarnec answer. There is no way to get a single doc with a query by field value. The only option is to set the id of the document to the specified E-Mail address (value you want to query).

Code:

Check if already subscribed, if not add to database of subscribers:

 const [error, setError] = useState("");
 const [message, setMessage] = useState("");

 const handleSubmit = async (values: { email: string }) => {
        const dbName = "newsletterSubscribers";
        const docId = values.email;
        setMessage("");
        setError("");
    
        const docRef = doc(db, dbName, docId);
        const docSnap = await getDoc(docRef);
    
        if (docSnap.exists()) {
          // E-Mail address already subscribed, data: docSnap.data())
          setMessage("E-Mail address already subscribed");
        } else {
          // E-Mail is not yet subscribed
          try {
            const docRef = await setDoc(doc(db, dbName, docId), {
              createdAt: serverTimestamp(),
            });
    
            alert("Thank you for subscribing, you will receive all news and updates to " + values.email);
          } catch (e) {
            // Error adding document
            setError("Failed to subscribe. We're sorry there was an error.");
          }
        }
      };

Security rules:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /newsletterSubscribers/{subscriberId} {

      allow get: if true;
      allow list: if false;
      
      allow create: if true;
    }
}}

Upvotes: 0

Renaud Tarnec
Renaud Tarnec

Reputation: 83093

With the Firestore security rules you can break down a read rule into get and list as explained in the doc.

In your case you would assign read access to everybody with the get rule and deny access to the list rule. This way, the unauthenticated users can only fetch a specific document (identified with a specific email address) and verify if this given "entry" is already in the database. They cannot list all the existing entries.

Of course, a user could try many different email addresses but will not be able to directly get the list of all existing entries in the database with one query.

Upvotes: 2

Related Questions