4

What should I add to my project so that the owner who is authenticated can add products or modify them?

What should I do so that the registered user can add products to the catalog from the Administrator Panel?

I have an application that I create with npx create-react-app my-app The Web App is an Administration panel for the products of a restaurant, store, or street vendor.

In it, the business owner adds products to the Firebase database, which can later be viewed from a Mobile App through this Administrator Panel.

To increase my knowledge, I have followed projects and tutorials to add the required registration of the user to the App (Administrator Panel)

Finally, I managed to create a small user registration application with firebase, you can see it in this GitHub repository: https://github.com/miguelitolaparra/react-firebase-email-login

With the same components as in the react-firebase-email-login App I have created a registration system in the Administrator Panel and everything works, you can see in the following link that I uploaded the App to a server:

http://restaurantcliente.webapplicationdeveloper.es/

This should be from where the business owner manages their products, adding or removing from the catalog.

To prevent anyone from registering and modifying the owner's catalog, remove the SignUp part and register the user from the Firebase database.

Change the Firebase Rules to be safe as follows:

service cloud.firestore {
  match / databases / {database} / documents {
    match / {document = **} {
      allow read: if true
      allow write: if request.auth.uid == request.data.author_uid
    }
  }
}

The problem has occurred when I try to add new products from the Administrator Panel, since the products are not added to the database. When I change the rules, so that anyone can read and write, it works and the administrator can add products to the database from the Administrator Panel

service cloud.firestore {
  match / databases / {database} / documents {
    match / {document = **} {
      allow read, write: if true;
    }
  }
}

I've seen several examples and tutorials using Private Routes and other components, but I always run into problems adding those systems to my project, due to the new changes to react-router-dom and react-router, which have been removed elements like <Switch>, createHistory, etc.

I don't know how to make my project secure and when the user is registered, I can add items to the catalog from the Administrator panel, without adding them manually from the database.

I don't know what else to do, after trying with examples that I found on GitHub and not achieving the goal. I cannot add all the examples, as I have tried at least 30 different projects. What should I add to my project so the owner can add products or modify them?

What should I do so that the registered user can add products to the catalog from the Administrator Panel?

EDIT to add more code to the question

I hope this can help me get it to work. You can try the example in the following link:

http://restaurantcliente.webapplicationdeveloper.es

Administrator:

restaurantcliente@webapplicationdeveloper.es

Password: Restaurant1920

The biggest problem is that the Web application, I need to wrap it entirely in the authentication mode, so that all its elements and functionalities are available when I edit the rules "as Safe" But this I don't know how to do it

The database consists of two collections: Orders and Products The Orders collection is created from the Mobile App, when a customer places an order from the mobile application. The Products collection is created by the restaurant owner from the Administrator Panel.

This is the file: NuevoPlato.js

import React, { useContext, useState } from 'react'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { FirebaseContext } from '../../firebase'
import { useNavigate } from 'react-router-dom'
import FileUploader from 'react-firebase-file-uploader'

const NuevoPlato = () => {

  // state para las imagenes
  const [subiendo, guardarSubiendo] = useState(false)
  const [progreso, guardarProgreso] = useState(0)
  const [urlimagen, guardarUrlimagen] = useState('')

  // Context con las operaciones de firebase
  const { firebase } = useContext(FirebaseContext)

  //console.log(firebase)

  // Hook para redireccionar
  const navigate = useNavigate()

  // validación y leer los datos del formulario
  const formik = useFormik({
    initialValues: {
      nombre: '',
      precio: '',
      categoria: '',
      imagen: '',
      descripcion: '',
    },
    validationSchema: Yup.object({

      nombre: Yup.string()
        .min(3, 'Los Platillos deben tener al menos 3 caracteres')
        .required('El Nombre del platillo es obligatorio'),
      precio: Yup.number()
        .min(1, 'Debes agregar un número')
        .required('El Precio es obligatorio'),
      categoria: Yup.string()
        .required('La categoría es obligatoria'),
      descripcion: Yup.string()
        .min(10, 'La descripción debe ser más larga')
        .required('La descripción es obligatoria')

    }),
    onSubmit: plato => {
      try {
        plato.existencia = true
        plato.imagen = urlimagen

        firebase.db.collection('productos').add(plato)

        // Redireccionar
        navigate('/menu')
      } catch (error) {
        console.log(error)
      }
    }
  })
  // Todo sobre las imagenes
  const handleUploadStart = () => {
    guardarProgreso(0)
    guardarSubiendo(true)
  }
  const handleUploadError = error => {
    guardarSubiendo(false)
    console.log(error)
  }
  const handleUploadSuccess = async nombre => {
    guardarProgreso(100)
    guardarSubiendo(false)

    // Almacenar la URL de destino
    const url = await firebase
      .storage
      .ref("productos")
      .child(nombre)
      .getDownloadURL()

    console.log(url)
    guardarUrlimagen(url)
  }
  const handleProgress = progreso => {
    guardarProgreso(progreso)

    console.log(progreso)
  }


  return (
    <>
      <h1 className="text-3xl font-light mb-4">Add new plate</h1>

      <div className="flex justify-center mt-10">
        <div className="w-full max-w-3xl">
          <form
            onSubmit={formik.handleSubmit}
          >
            
// THIS IS THE FORM WITH THE DATA                <input
              type="submit"
              className="bg-gray-800 hover:bg-gray-900 w-full mt-5 p-2 text-white uppercase font-bold"
              value="Agregar Plato"
            />
          </form>
        </div>
      </div>
    </>
  )

}

export default NuevoPlato

From the Administrator panel, the "delivery time" is also managed, the time it will take for the order to be finished and which is defined by the owner of the restaurant from the panel.

But I cannot modify this data either so that it is displayed in the Mobile App, if I edit the "As Safe" rules.

File setting delivery time Orden.js

import React, { useState, useContext} from 'react'
import { FirebaseContext } from '../../firebase'

const Orden = ({ orden }) => {

  const [  tiempoentrega, guardarTiempoEntrega ]= useState(0)

// Context de Firebase
const { firebase } = useContext(FirebaseContext)

// define el tiempo de entrega del pedido en tiempo real
const definirTiempo = id => {
  try {
    firebase.db.collection('ordenes')
        .doc(id)
        .update({
          tiempoentrega
        })
  } catch (error) {
    console.log(error)
  }
}

// Marcar que el pedido esta completado
const completarOrden = id => {
   try {
     firebase.db.collection('ordenes')
           .doc(id)
           .update({
             completado: true
           })
   } catch (error) {
     console.lor(error)
   }
}

  return (
    <div className="sm:w-1/2 lg:w-1/3 px-2 mb-4">
      <div className="p-3 shadow-md bg-white">
        <h1 className="text-yellow-600 text-lg font-bold"> {orden.id} </h1>
        {orden.orden.map(platos => (
          <p className="text-gray-600"> {platos.cantidad} {platos.nombre} </p>
        ))}

        <p className="text-gray-700 font-bold">Total a Pay: {orden.total}$</p>

        {orden.tiempoentrega === 0 && (
          <div className="mb-4">
            <label className="block text-gray-700 text-sm font-bold mb-2">
            Delivery Time
            </label>

            <input
              type="number"
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
              min="1"
              max="30"
              placeholder="Pon El Tiempo"
              value={tiempoentrega}
              onChange={ e => guardarTiempoEntrega( parseInt(e.target.value))}
            />

            <button
            onClick={ () => definirTiempo(orden.id) }
              type="submit"
              className="bg-gray-500 hover:bg-gray-700 w-full mt-5 p-2 text-white uppercase font-bold"
            >
              Define Time
            </button>
          </div>
        )}

        {orden.tiempoentrega > 0 && (
          <p className="text-gray-700">Delivery Time:
          <span className="font-bold"> {orden.tiempoentrega} Minutes </span>
           </p>
        )}

        { !orden.completado && orden.tiempoentrega > 0 &&(
          <button
          type="button"
          className="bg-blue-700 hover:bg-blue-400 w-full mt-5 p-2 text-white uppercase font-bold"
          onClick={ () => completarOrden(orden.id)}
          >
            Declare Done
          </button>
        )}
      </div>
    </div>
  )
}

export default Orden

I show some of the files

File App.js

import React, { useState, useEffect } from "react"
import { Routes, Route } from "react-router"

import firebase, { FirebaseContext } from "./firebase"
//import { auth } from 'firebase'
import firebaseObj from "./firebase"
import Ordenes from "./components/paginas/Ordenes"
import Menu from "./components/paginas/Menu"
import NuevoPlato from "./components/paginas/NuevoPlato"
import Sidebar from "./components/ui/Sidebar"
import Signin from "./components/Signin"

const auth = firebaseObj.auth

function App() {
  const [user, setUser] = useState(null)
  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((userAuth) => {
      const user = {
        uid: userAuth?.uid,
        email: userAuth?.email,
      }
      if (userAuth) {
        console.log(userAuth)
        setUser(user)
      } else {
        setUser(null)
      }
    })
    return unsubscribe;
  }, [])

  if (!user) {
    return (
      <div className="md:flex min-h-screen">
        <div className="md:w-2/5 xl:w-4/5 p-6">
          <Signin />
        </div>
      </div>
    )
  } else {
    return (
      <FirebaseContext.Provider
        value={{
          firebase,
        }}
      >
        <div className="md:flex min-h-screen">
          <Sidebar />
          <div className="md:w-2/5 xl:w-4/5 p-6">
            <Routes>

              <Route path="/" element={<Ordenes />} />
              <Route path="/menu" element={<Menu />} />
              <Route path="/nuevo-plato" element={<NuevoPlato />} />
            </Routes>
          </div>
        </div>

      </FirebaseContext.Provider>
    );
  }
}

export default App

File Signin.js

import React, { useRef } from 'react'
import firebaseObj from '../firebase/firebase'

const auth = firebaseObj.auth

const Signin = () => {
  const emailRef = useRef(null);
  const passwordRef = useRef(null);
  const signUp = e => {
    e.preventDefault();
    auth.createUserWithEmailAndPassword(
      emailRef.current.value,
      passwordRef.current.value
    ).then(user => {
      console.log(user)
    }).catch(err => {
      console.log(err)
    })
  }
  const signIn = e => {
    e.preventDefault();
    auth.signInWithEmailAndPassword(
      emailRef.current.value,
      passwordRef.current.value
    ).then(user => {
      console.log(user)
    }).catch(err => {
      console.log(err)
    })
  }
  return (
    <div className="flex justify-center mt-10">
      <div className="w-full max-w-3xl">
        <h1 className="my-8 text-yellow-600 text-lg font-bold text-center text-7xl"> My humble restaurant </h1>
        <div className="mb-4">
          <form action="">
            <h1 className="text-center m-3">Start the Work Session</h1>
            <div className="mb-4">
              <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="nombre">Email</label>
              <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
                ref={emailRef} type="email" placeholder="email" />
            </div>
            <div className="mb-4">
              <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="nombre">Password</label>
              <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
                ref={passwordRef} type="password" placeholder="password" />
            </div>
            <button
              type="button"
              className="bg-blue-700 hover:bg-blue-400 w-full mt-5 p-2 text-white uppercase font-bold"
              onClick={signIn}
            >
              Log In
            </button>
            <div className="mb-4 mt-8 ">
              <h2 className="block text-gray-700 text-sm font-bold mb-2 text-3xl">Enter the administration panel of your restaurant and start preparing dishes and orders</h2>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}

export default Signin

File package.json

{
      "name": "restaurantcliente",
      "version": "0.1.0",
      "private": true,
      "homepage": "/build", 
      "dependencies": {
        "@testing-library/jest-dom": "^5.11.4",
        "@testing-library/react": "^11.1.0",
        "@testing-library/user-event": "^12.1.10",
        "firebase": "^7.19.0",
        "formik": "^2.2.9",
        "react": "^17.0.2",
        "react-dom": "^17.0.2",
        "react-firebase-file-uploader": "^2.4.4",
        "react-router-dom": "^6.0.0-beta.0",
        "react-scripts": "^4.0.3",
        "web-vitals": "^1.0.1",
        "yup": "^0.32.9"
      },
      "scripts": {
        "start": "npm run watch:css && react-scripts start",
        "build": "npm run build:css && react-scripts build",
        "test": "react-scripts test",
        "eject": "react-scripts eject",
        "build:css": "postcss src/css/tailwind.css -o src/css/main.css",
        "watch:css": "postcss src/css/tailwind.css -o src/css/main.css"
      },
      "eslintConfig": {
        "extends": [
          "react-app",
          "react-app/jest"
        ]
      },
      "browserslist": {
        "production": [
          ">0.2%",
          "not dead",
          "not op_mini all"
        ],
        "development": [
          "last 1 chrome version",
          "last 1 firefox version",
          "last 1 safari version"
        ]
      },
      "devDependencies": {
        "autoprefixer": "^10.3.1",
        "history": "^5.0.0",
        "postcss-cli": "^8.3.1",
        "react-router": "^6.0.0-beta.0",
        "tailwindcss": "^2.2.7"
      }
    }
Miguel Espeso
  • 194
  • 1
  • 7
  • 26
  • 1
    This rule only lets the creator change the data. allow write: if request.auth.uid == request.data.author_uid But I think that you mean that you want to add admins to your database an check if the user is an admin. allow write: if request.auth.uid == request.data.author_uid || exists(/databases/$(database)/documents/admin/$(request.resource.data.author_uid)) – Erik Oct 13 '21 at 09:07
  • From the control panel shown in URL, the owner enters and can add products to the catalog. But when I add these (safe) rules, the owner, who is registered, cannot add items to the catalog, despite being registered. This means that my code is not correctly configured, since it does not allow adding products to the registered person. Do you understand what the problem is. – Miguel Espeso Oct 13 '21 at 10:46
  • 2
    Should you really compare uids from the request? request.auth.uid == request.data.author_uid. Should you not compare request.auth.uid with the author of the document? From: https://firebase.google.com/docs/rules/basics allow read, write: if request.auth != null && request.auth.uid == userId – Erik Oct 13 '21 at 11:10
  • The code of my project is the one that I show in App.js and Signin.js, I don't have any more code. There are the rest of the screens, Orders.js, Menu.js and NewPlate.js. I have no knowledge to do anything else. It took me almost two months to create the app that shows the GitHub link and add it to my Products project. I don't know how to fix it, that's why I came here. – Miguel Espeso Oct 13 '21 at 11:19
  • 1
    could you share the schema of your DB? like what collections you have and how they relate – diedu Oct 16 '21 at 03:55
  • Thank you for your interest and sorry for the delay in responding, I did not have the Internet. Edit the question and add some code. The database consists of two collections, one of Products, which are added from the Admin panel, and the other of Orders, which is added from the Mobile App. I hope you can help me with this data. The problem is that I do not know how to wrap the entire App in authentication, so that all its elements are within Auth and can use the Admin when the rules are With Authentication. Try the Admin panel in the link and you will see that the ducks are not added – Miguel Espeso Oct 17 '21 at 11:39
  • 1
    I see two problems. The first is that you need to use `request.resource.data.author_uid`. Notice `request.resource`. Other I don't see you setting the field `author_uid` anywhere in the code. You need to set this field in the document you are trying to add. – Akshay Jain Oct 17 '21 at 17:01
  • Thanks @AkshayJain . I'm looking for information about what you tell me, my experience is short and I really don't know how to get what you propose. I know the problem is that the application is not involved, (if we can say so) in the authentication. The application does not have these details, it is necessary to add that the routes are protected or what you propose, I will continue looking for information, it has been many days dealing with this problem, I will tell you if I found a solution. Thanks – Miguel Espeso Oct 18 '21 at 07:07
  • 1
    @MiguelEspeso You need to read more about how the security rules work. The rule you added i.e. `request.auth.uid == request.resource.data.author_uid` says that there is a field `author_uid` in the document you are trying to upload and you want to verify that it is equal to the `uid` of the authenticated user. For this to work, you need to have the field `author_uid` in your document and it must be equal to the uid of the authenticated user. – Akshay Jain Oct 18 '21 at 13:40
  • With these rules, everything works, but the products cannot be added from the Admin panel. They are added, but not displayed in the panel: rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.auth != null } }} – Miguel Espeso Oct 18 '21 at 20:34
  • How can I modify my code to achieve what you indicate? I've been testing since your comment, but I can't add the "author_uid" field to my project. Can you tell me a place to find information to correct my code? If you have an idea, you can show it, the reward ends today. Thanks @AkshayJain – Miguel Espeso Oct 19 '21 at 07:39
  • 1
    You have added - `firebase.db.collection('productos').add(plato)`. You need to do `plato.author_uid = userId` before this line where userId is the uid of your current signed in user. – Akshay Jain Oct 19 '21 at 14:34
  • I don't understand what you mean. Write an answer and if it works I will accept it. With your comment, I don't understand what you want me to do, I'm not sure – Miguel Espeso Oct 19 '21 at 15:37

2 Answers2

0

The easiest way would be to go to firebase console, copy the uid of your admin (let's say that the uid of something like XwKD2Mnde8QWqHqN1Q8UfDZe0bF3) . Than your rule should be :

service cloud.firestore {
  match /databases/{database}/documents {
        match /products/{document = **} {
        allow read: if true
        allow write: if request.auth.uid == 'XwKD2Mnde8QWqHqN1Q8UfDZe0bF3'
    }
    match /orders/{document = **} {
      allow read, write: if true
    }
  }
} 

(replace XwKD2Mnde8QWqHqN1Q8UfDZe0bF3 with your admin uid).

You can find the userId here:User UID example

This is the easiest way, but only works with a single predefined value (you also have option to add more than 1 admin account using || so somethink like allow write: if request.auth.uid == 'XwKD2Mnde8QWqHqN1Q8UfDZe0bF3' || request.auth.uid == 'NzGq8...') but this is terrible to maintain if your there are more admins or they might change.

The cleanest way (and easiest to maintain if your users can change) would be to use Custom Claims. But if I understood right what you need (and your experience with firebase) I personally think the first solution might be a better fit for your case.

Or the last option (if you are 100% sure that no user without admin access will ever be present), you can just replace if request.auth.uid == 'XwKD2Mnde8QWqHqN1Q8UfDZe0bF3' with if request.auth.uid != null (But I don't advise this one since if you add any user account at any point and forget to update the rulles, a simple user can edit your product prices for example)

Firebase rules playground: firebase rules playground

Berci
  • 2,876
  • 1
  • 18
  • 28
  • Hello, thank you for your support. I have added your rules to my project, but it doesn't work. When I try to add a new dish from the admin panel they don't show up. All data also disappear from the application, the two collections "Products and Orders" I can't accept your answer – Miguel Espeso Oct 18 '21 at 20:34
  • 1
    Hello. No problem! I am sorry it didn't worked. I also did some tests and updated the answer code sample (with the rules) just now. Could you try the updated version as well ? – Berci Oct 18 '21 at 21:13
  • Thank you for your dedication. I did tests and it does not work either, in fact, with the new rules that you have created, I cannot add Products with a registered user and the products already created also disappear. Thanks – Miguel Espeso Oct 19 '21 at 07:32
  • 1
    I am sorry it's not working either. Just for the context: the data is not displaying because request blocked by some rule (even thought the data still exists you don't have access to it for some reason). If you want to investigate further you can try firestore `Rules Playground` . It will help you track down exactly what rule is blocking a certain request (and in case of success what rule is allowing that request) by highlighting it. I will leave an image with at the end of the original comment. I hope you manage to accomplish it somehow. – Berci Oct 19 '21 at 07:57
  • 1
    Thank you for your dedication of your time – Miguel Espeso Oct 19 '21 at 08:23
0

First and foremost, as I can see, your system needs multiple roles for users are admin (who will use Admin Panel) and customer (who will use the mobile app). You need a way to distinguish these two roles. You need to have a list of users stored somewhere, maybe Firebase's real-time database or Firestore. After that, you need to add one more field to each user like role which is admin or user. You need to understand Firebase Authentication is a different service and it has nothing related to your real database, so you need to store your users, in other words, sync your users from Firebase Authentication to your database (Firestore/Realtime database), like this question.

The next step is in your rules, you need to change the condition into "allow write if the current user is admin or the current user is the author". I think this is not so hard to write, depends on how you set the above field and where you store your users.

Your question about everything in Admin Panel should be "secured", in other words, people need to authenticate to use the Admin panel right? Everything comes from this method auth.onAuthStateChanged. The idea is, whenever user is null, just redirect to your login page. This is how the "protected route" works. You need to implement it by yourself, it's simple by using the Redirect component from react-router-dom.

As I can see, you like to understand the reason why rather than the code itself, it's good. And I don't see I need to provide any code because you did it well.

Dharman
  • 30,962
  • 25
  • 85
  • 135
ShinaBR2
  • 2,519
  • 2
  • 14
  • 26
  • Thanks for your elaborate answer, but it will be difficult for me to assume. I agree with what it says, I know that the problem is unprotected routes, but the problem is that I don't know how to do it. I did tests, and in all of them it does not work. I have created authentication after creating the App and I don't know how to implement it to make it work. I will continue testing, while you share the reward and I will continue without solving my problem. Thank you for dedicating your time, I will continue to try to solve this with your explanations even if it takes days. – Miguel Espeso Oct 20 '21 at 07:20
  • Hi @MiguelEspeso, you can follow a guide here for the protected route with Firebase authentication: https://dev.to/jsbroks/firebase-authentication-with-react-and-guarded-routes-41nm. Please feel free to ask if you got any further issues. – ShinaBR2 Oct 20 '21 at 07:43