Давайте создадим приложение React с Firebase Auth, Express Backend и базой данных MongoDB.

Firebase - чрезвычайно хорошая платформа для разработки программного обеспечения, которая дает вам доступ как к аутентификации, ни к базам данных без SQL, ни к хранилищу, так и многому другому. Это настоящий источник энергии, и мне нравится простота использования Firebase и базы данных Firestore.

Все мы знаем, как важно использовать правильный инструмент для работы. Если вы разрабатываете приложение с помощью Firebase, вы можете столкнуться с ситуацией, когда облачных функций недостаточно, и вам понадобится отдельный внутренний сервер для выполнения некоторых задач. Вы даже можете использовать Firebase в качестве основного концентратора и подключать к нему другие микросервисы.

В любом случае, если вы покидаете Firebase, вам потребуется аутентификация. Было бы странно иметь разные системы аутентификации на Firebase и на вашем отдельном внутреннем сервере.

К счастью, Google позволяет нам использовать аутентификацию Firebase вне Firebase. Это работает путем отправки токена аутентификации Firebase с полезной нагрузкой на внутренний сервер и проверки его там. Давайте посмотрим, как мы можем с этим справиться.

Это будет пошаговое руководство, в котором мы создадим простой пример приложения. Мы тщательно пройдемся по каждому этапу, и вам не потребуется никакого опыта работы с Firebase, React или Express.

Оглавление

Что мы строим

Мы создадим простую телефонную книгу. Пользователь сможет добавлять номера телефонов и просматривать список всех номеров телефонов. Мы не будем использовать стили CSS в этом проекте. Давайте посмотрим, какие технологии мы будем использовать.

Firebase
Мы будем использовать Firebase только в качестве платформы аутентификации.
https://firebase.google.com/

React
Мы будем использовать современные хуки и React на основе стрелок для нашего интерфейса.
https://reactjs.org/

Express
Мы напишем наш бэкэнд на NodeJS и будем использовать Express в качестве бэкэнд-фреймворка
https://expressjs.com/

MongoDB (MongoDB Atlas)
Мы будем использовать MongoDB Atlas в качестве нашей базы данных - облачную версию MongoDB, размещенную самой командой MongoDB. Обычная база данных SQL была бы наиболее естественным выбором с точки зрения функциональности этого приложения. Однако с помощью модуля npm Mongoose MongoDB будет действовать как реляционная база данных и предоставлять нам схемы для наших данных. Что хорошо в этом пути, так это то, что мы можем использовать Mongoose для структурированных данных и чистый MongoDB для неструктурированных данных. В этом приложении мы будем использовать MongoDB только через Mongoose.
https://www.mongodb.com/

Github с полным кодом

Полный исходный код этого проекта можно найти по адресу https://github.com/Devalo/Firebase-auth-react-express-mongodb

Это много интересного для такого простого приложения. Мы собираемся оторваться!

Настройка Firebase

Начнем с создания нового приложения Firebase. Перейдите на www.firebase.com, нажмите Начать и войдите в свою учетную запись Google.

Затем мы должны нажать на Добавить проект.

Мы дадим приложению имя

Для этого проекта нам не нужен Google Analytics.

И нажимаем «Создать проект». Через некоторое время наш проект будет готов. Когда все будет готово, нажмите «продолжить».

Если вы посмотрите на левое меню, вы найдете вкладку «Аутентификация». Мы выберем его и нажмем «настроить метод входа».

Появится список различных поставщиков услуг входа. Нам нужен только адрес электронной почты / пароль. Наводим на нее, щелкаем карандашом и включаем.

Теперь мы не будем создавать новых пользователей в этом проекте. Мы разрешим вход только пользователям. По этой причине мы добавим нашего нового пользователя прямо в Firebase Authentication. Нажмите на «пользователи».

и добавить нового пользователя.

Firebase автоматически правильно хеширует пароль. Вы не сможете больше вносить изменения для пользователя с этой панели. Все остальные изменения нужно делать программно. Однако создается наш первый пользователь:

Начало работы с React Frontend

Настройка React

Установив Firebase, мы приступим к настройке нашего интерфейса. Мы будем использовать приложение create-react-app для начальной загрузки нашего интерфейса.

$ npx create-react-app phone-frontend
...
Happy hacking!
$ cd phone-frontend
/phone-frontend $ npm start

Если все пойдет по плану, мы должны увидеть это на http: // localhost: 3000.

Милая. Теперь большая часть нашей работы будет выполняться в папке src. наша структура папок в папке src будет следующей:

/components
  - /sessions
      Login.jsx
  - /phonebook
      AddNumber.jsx
      ListAllNumbers.jsx
/services
  - phonebookServices.js
App.js
index.js

Мы меняем наш index.js на:

// index.js 
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

и наш App.js для:

// App.js
function App() {
  return (
    <div className="App">
      Hello World!
    </div>
  );
}
export default App;

И мы готовы начать работу над нашим интерфейсом React. Примечание. Возможно, вам придется перезапустить сервер реакции на этом этапе, чтобы изменения вступили в силу.

Теперь мы знаем, что у нас будет несколько представлений. Нам нужен способ переключения между страницей входа, перечислением всех телефонных номеров и добавлением номеров. Для этого мы воспользуемся модулем npm, который называется response-router-dom. Установим:

npm install react-router-dom

Нам также необходимо установить пакет firebase:

npm install firebase

Теперь давайте создадим наш маршрут входа и сделаем так, чтобы мы всегда попадали на нашу страницу входа, если мы не вошли в систему.

Добавьте firebase в наш интерфейс

На предыдущем шаге мы создали новое приложение Firebase. Есть некоторые конфигурации, которые нам нужно получить с веб-сайта firebase.

Вернувшись на веб-сайт firebase, щелкните самую верхнюю ссылку на левой панели навигации - Обзор проекта. Нам нужно добавить новое веб-приложение:

Даем ему имя:

И мы копируем объект firebaseConfig, и нажимаем «продолжить консоль».

В производственном приложении вы можете поместить эти конфигурации в переменные среды, но Google заявляет, что в этом нет строгой необходимости. Причина этого в том, что безопасность Firebase основана на нескольких правилах, которые ограничивают и открывают приложение. По моему скромному мнению, такие настройки всегда лучше скрыть. Хотя я могу ошибаться.

внутри нашей папки src мы создадим новый файл - fire.js и добавим в него наши конфигурации. Файл должен выглядеть так:

// fire.js

import firebase from 'firebase';
const firebaseConfig = {
    apiKey: "AIzaSyAHDUwcM0zO4yMCoyazo4w-HOeKfGsQL2g",
    authDomain: "phone-book-fe436.firebaseapp.com",
    databaseURL: "https://phone-book-fe436.firebaseio.com",
    projectId: "phone-book-fe436",
    storageBucket: "phone-book-fe436.appspot.com",
    messagingSenderId: "410083027161",
    appId: "1:410083027161:web:e69ec743153a72d48df6c8"
};
try {
  firebase.initializeApp(firebaseConfig);
} catch (err) {
  if (!/already exists/.test(err.message)) {
    console.error('Firebase initialization error', err.stack);
  }
}
const fire = firebase;
export default fire;

Мы будем использовать firebase в этом файле. Теперь мы готовы приступить к аутентификации пользователей.

Настройка аутентификации

Firebase предоставляет нам очень простой способ проверить, прошли ли мы аутентификацию. Мы создадим новое состояние с помощью хука useState () и сохраним там результат:

// App.js

import React, { useState } from 'react';
import fire from './fire.js';
function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  
    fire.auth().onAuthStateChanged((user) => {
      return user ? setIsLoggedIn(true) : setIsLoggedIn(false);
  });
  
  console.log('logged in?', isLoggedIn);
  return (
    <div className="App">
      Hello World!
    </div>
  );
}
export default App;

Мы импортируем useState из библиотеки React и firebase из нашего файла конфигурации firebase. Мы используем функцию .onAuthStateChanged (), чтобы проверить, вошел ли пользователь в систему или нет. Если мы вошли в систему, мы установим для нашего состояния isLoggedIn значение true.

если мы проверим нашу консоль, она ясно покажет, что мы не вошли в систему.

Создадим наш первый маршрут. Теперь при каждом запросе мы хотим проверять, вошли ли мы в систему, прежде чем продолжить.

// App.js
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
...
<div className="App">
      <Router>
        
        {!isLoggedIn
          ? (
            <>
              <Switch>
                <Route path="/">
                  <Login />
                </Route>
              </Switch>
            </>
          ) 
          : (
            <>
              Hello World!
            </>
          )}
      </Router>
    </div>
...

Все маршруты являются дочерними по отношению к компоненту Switch, который является дочерним по отношению к компоненту Router. Если мы не вошли в систему, мы не увидим ничего, кроме компонента входа. Мы можем проверить это, посмотрев на нашу пустую страницу.

Давайте перейдем к компоненту входа в систему и посмотрим, сможем ли мы разместить что-нибудь на нашей странице.

Мы начнем с создания простой, работоспособной формы, в которой будет записываться как адрес электронной почты, так и пароль:

// components/session/Login.jsx

import React, { useState } from 'react';
import fire from '../../fire.js';
const Login = () => {
    const [email, setEmail] = useState();
    const [password, setPassword] = useState();
    
    const handleSubmit = (e) => {
        e.preventDefault();
        console.log(`submitted email: 
          ${email} password: ${password}`);
    }
    return (
        <div>    
        <h2>Login</h2>
            <form onSubmit={handleSubmit}>
                <input
                    type="text"
                    onChange={({ target }) =>     
                      setEmail(target.value)}
                    placeholder="Email"
                />
                <br />
                <input
                    type="password"
                    onChange={({ target}) => 
                      setPassword(target.value)}
                    placeholder="Password"
                />
                <br />
                <button type="submit">
                    Sign in
                </button>
            </form>
        </div>
    )
};
export default Login

Мы настроили два новых перехватчика состояния, которые будут хранить состояние нашего адреса электронной почты и пароля. Каждый из входов имеет атрибут onChange, который записывает входные данные непосредственно в каждый обработчик состояния. Если мы нажмем кнопку «Войти», введенные нами данные будут отображаться в нашей консоли. Довольно аккуратно. Войти будет очень просто. Нам просто нужно добавить одну строку кода в нашу функцию отправки:

// components/session/Login.jsx
...
const handleSubmit = (e) => {
  e.preventDefault();
  fire.auth().signInWithEmailAndPassword(email, password)
    .catch((error) => {
      console.error('Incorrect username or password');
    });
  }
...

Это простая линия. Если вы ввели неправильный адрес электронной почты и пароль:

Если мы войдем в систему с правильным именем пользователя и паролем, мы вернем наш Hello World! text, и теперь наша консоль выводит истину (которая исходит из обработчика состояния, удерживающего состояние нашего сеанса).

Найдите минутку и дайте ей понять. Это невероятно простая аутентификация. Я не думаю, что вам будет легче, чем это.

Вероятно, нам также следует найти способ выйти из системы. К счастью, Firebase также делает это невероятно простым. Замените текст «Hello World!» В App.js на:

// App.js

<span onClick={signOut}>
  <a href="#">Sign out</a>
</span>

И добавьте новую функцию в компонент App:

const signOut = () => {
    fire.auth().signOut()
};

Вот и все! Как только вы нажмете «Выйти», мы вернемся к форме входа. Хорошее начало!

Создание представления для наших телефонных номеров

Ранее мы создали файл компонента для наших телефонных номеров. Сейчас хорошее время, чтобы начать реализацию способа отображения наших данных. Начнем с создания нового маршрута. Мы разместим наш маршрут под нашей ссылкой "выйти":

// App.js
import ListAllNumbers from './components/phonebook/ListAllNumbers';
...
<span onClick={signOut}>
  <a href="#">Sign out</a>
</span>
<Switch>
  <Route path="/">
    <ListAllNumbers />
  </Route>
</Switch>
...

Приложение выйдет из строя, как только вы сохраните файл. Это нормально. Мы еще не создали компонент ListAllNumbers. Перейдем к файлу компонента.

А пока создадим очень простую таблицу, в которой будем отображать наши данные:

// components/phonebook/ListAllNumbers.jsx
import React from 'react';
import { Link } from 'react-router-dom';
const ListAllNumbers = () => {
  return (
    <div>
      <Link to="/add-number">Add number</Link>
      <h2>Phone numbers</h2>
    
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Number</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>Foo Bar</td>
            <td>999888777</td>
          </tr>
        </tbody>
      </table>
    </div>
  ) 
};
export default ListAllNumbers;

Это шедевр ..

Создание представления для добавления телефонных номеров

Прежде чем приступить к рассмотрению какой-либо логики, мы создадим последнее представление.
Мы создадим новый маршрут поверх предыдущего.

Весь App.js будет выглядеть так:

// App.js

import React, { useState } from 'react';
import { BrowserRouter as Router, Switch, Route} from 'react-router-dom';
import fire from './fire.js';
import Login from './components/session/Login';
import ListAllNumbers from './components/phonebook/ListAllNumbers';
import AddNumber from './components/phonebook/AddNumber';
function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  
    fire.auth().onAuthStateChanged((user) => {
      return user ? setIsLoggedIn(true) : setIsLoggedIn(false);
  });
  
  const signOut = () => {
    fire.auth().signOut()
  };
  
  console.log(isLoggedIn);
  return (
    <div className="App">
      <Router>
        {!isLoggedIn
          ? (
            <>
            <Switch>
              <Route path="/">
                <Login />
              </Route>
            </Switch>
            </>
          ) 
          : (
            <>
            <span onClick={signOut}>
              <a href="#">Sign out</a>
            </span>
            <Switch>
              <Route path="/add-number">
                <AddNumber />
              </Route>
              <Route path="/">
                <ListAllNumbers />
              </Route>
            </Switch>
            </>
          
          )}
      </Router>
    </div>
  );
}
export default App;

Идеально. Наши маршруты настроены. Перейдем к компоненту AddNumber и добавим необходимый код:

// components/phonebook/AddNumber.js
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
const AddNumber = () => {
  const [name, setName] = useState();
  const [phone, setPhone] = useState();
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    console.log(`submitted: ${name} - ${phone}`);
  };
  
  return (
    <div>
      <Link to="/">View phonebook</Link>
    <h2>Add Number</h2>
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        placeholder="Name"
        onChange={({ target }) => setName(target.value)}
      /><br />
      <input
        type="text"
        placeholder="Number"
        onChange={({ target }) => setPhone(target.value)}
      /><br />
      
      <button type="submit">
        Add number
      </button>
    </form>
    </div>
  )
};
export default AddNumber;

Как и в случае с полем входа в систему, мы сохраняем оба поля ввода в ловушке состояния. При отправке мы выводим входные данные на нашу консоль. Идеально. Это так же красиво, как и наш интерфейс. На данный момент мы закончили с интерфейсом. Мы должны создать бэкэнд, а затем подключить базу данных. Мы скоро вернемся к интерфейсу.

Начало работы с серверной частью Express

Мы будем строить наш бэкэнд с помощью фреймворка Express. Наш бэкэнд будет независимым приложением. Давайте выйдем из внешнего интерфейса и создадим новую папку для нашего внутреннего интерфейса:

/phone-frontend $ cd ..
$ mkdir phone-backend
$ cd phone-backend
/phone-backend $ npm init

Мы начинаем новый проект JS с «npm init». Ни один из приведенных здесь вопросов сейчас не имеет для нас никакого значения, поэтому просто нажимайте Enter до конца. После этого для нас будет создан файл package.json в нашей внутренней папке телефона.

Поскольку наш бэкэнд будет JSON-API, мы не будем использовать генераторы Express для начальной загрузки нашего приложения. Начнем с установки Express:

/phone-backend $ npm install express

И создадим наш файл endpoint index.js:

/phone-backend $ touch index.js

В нашем индексном файле мы напишем быстрый сервер:

// index.js

const express = require('express');
const app = express();
app.get('/', (req, res) => {
    res.send('Hello World');
});
const PORT = 3001;
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

Это все, что нужно для создания конечной точки сервера. Мы можем определить маршрут с помощью app.get (). Если вы перейдете на http: // localhost: 3001, вы получите прекрасное сообщение:

Давайте подумаем, каким будет поток приложения.

  • Frontend отправит запрос GET на наш корневой маршрут, запрашивая все записи
  • Бэкэнд получит запрос и загрузит его из БД
  • Frontend отправит запрос POST на наш корневой маршрут, добавив запись
  • Бэкэнд получит запрос и добавит в БД

Лучше всего начать с каркаса маршрутов. Мы будем структурировать наше приложение следующим образом:

- controllers
  phones.js
- models
  phone.js
index.js

Записываем наш контроллер, устанавливаем nodemon

Мы начнем с нашего контроллера, который содержит маршруты. Их всего два, так что кодировать будет довольно просто. Мы будем использовать Express Router, мощный встроенный маршрутизатор.

Наш файл контроллера будет выглядеть так:

// controllers/phones.js

const phonesRouter = require('express').Router();
phonesRouter.get('/', (req, res) => {
  return res.send('Hi, from within the phones router GET'); 
});
phonesRouter.post('/', (req, res) => {
  return res.send('Hi, from within the phones router POST');
});
module.exports = phonesRouter;

Два маршрута. Один для запросов GET и один для запросов POST. При вызове они вернут сообщение. Теперь мы импортируем его в наш index.js и заменим предыдущий маршрут на phoneRouter. Обратите внимание, что мы добавили "/ api" к нашему URL-адресу. URL-адрес, по которому мы запрашиваем наш API, будет http: // localhost: 3001 / api.

const express = require('express');
const phonesRouter = require('./controllers/phones');
const app = express();
app.use('/api', phonesRouter);
const PORT = 8080;
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

Если вы попытаетесь перейти на наш новый URL, ничего не произойдет. Оказывается, наше приложение не перезагружается автоматически, когда мы меняем код. Чтобы бороться с этим, мы установим пакет npm под названием nodemon, который будет делать именно это - перезагружать при изменении кода:

/phone-backend $ npm install nodemon

Чтобы использовать nodemon, мы создадим небольшой скрипт внутри нашего файла package.json:

// package.json
...
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon index.js"
  },
...

сценарий «dev» запустит наше приложение с помощью nodemon. Запускаем наше приложение командой npm run dev:

/phone-backend $ npm run dev
> nodemon index.js
[nodemon] 2.0.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`
Server is running on port 3001

Замечательно, если вы вернетесь на http: // localhost: 3001 / api, вы получите сообщение прямо с маршрута. Мы приближаемся к извлечению данных из базы данных. В связи с этим - давайте настроим новую базу данных MongoDB Atlas.

Настройка Атласа MongoDB

Первое, что нам нужно сделать, это зайти в MongoDB Atlas и создать нашу учетную запись: перейдите на https://www.mongodb.com/cloud/atlas/signup и создайте нового пользователя.

Выберите бесплатный кластер:

Вы сможете выбрать провайдера, на котором будет размещена ваша база данных, и регион. Я собираюсь разместить базу данных в Google. Нажмите «Создать кластер»:

Появится мастер. Давайте выполним его шаги, когда кластер будет запущен и заработает. Это может занять пару минут:

Мы начнем с создания нашего первого пользователя базы данных. Перейдите в Доступ к базе данных:

Создайте нового пользователя:

И добавляем пользователя. После этого нам нужно внести наш сервер в белый список. Здесь мы будем использовать стандартный IP 0.0.0.0:

Внесение IP-адреса в белый список обеспечивает дополнительный уровень безопасности. Однако наш бэкэнд будет входить в базу данных с именем пользователя и паролем, так что в любом случае он не будет полностью открыт.

После этого следующим шагом будет подключение нашей базы данных к нашему бэкэнду.

Мы выберем для подключения наше приложение:

скопируйте URL. Это URL-адрес, который мы будем использовать для подключения к базе данных. Сохраните URL-адрес на будущее:

MongoDB Atlas настроен. Давайте вернемся к нашему бэкэнду и подключим его.

Подключение бэкэнда к MongoDB Atlas

Как было сказано ранее, мы будем использовать Mongoose вместо чистого MongoDB. Это даст нам некоторую структуру. (Это как бы противоречит цели использования базы данных noSQL, но с Mongoose вы можете сделать и то, и другое). Установим:

/phones-backend $ npm install mongoose

Чтобы упростить задачу, мы добавим наш код для подключения к MongoDB в наш index.js. Конечно, вы можете сохранить его в другом месте и импортировать в index.js:

// index.js
const mongoose = require('mongoose');
...
mongoose.connect(
 'mongodb+srv://foobar:[email protected]/phonebook?retryWrites=true&w=majority',
 { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => {
    console.log('Connected to database');
  })
  .catch((err) => {
    console.log('Error connecting to DB', err.message);
  });
...

Вставьте URL-адрес MongoDB, который вы скопировали ранее, в функцию подключения мангуста. Замените ‹password› и ‹dbname› своими учетными данными. Когда вы сохраните, сервер перезагрузится, и вы получите прекрасное сообщение на консоли о том, что вы подключены к базе данных. Если по какой-то причине вы получите сообщение об ошибке, убедитесь, что вы ввели правильные учетные данные.

Теперь соединение между нашим внутренним сервером и базой данных установлено. Это означает, что мы можем начать извлекать данные из базы данных и помещать их в нее. Прежде чем мы это сделаем, вернемся к интерфейсу.

Общение с серверной частью

Если мы вернемся к потоку нашего приложения, которое мы написали немного ранее

  • Frontend отправит запрос GET на наш корневой маршрут, запрашивая все записи
  • Бэкэнд получит запрос и загрузит его из БД
  • Frontend отправит запрос POST на наш корневой маршрут, добавив запись
  • Бэкэнд получит запрос и добавит в БД

Мы видим, что веб-интерфейс должен отправить запрос на получение серверной части. Мы хотим включить в запрос токен аутентификации firebase. Таким образом, серверная часть может проверить пользователя перед тем, как разрешить доступ к базе данных.

Мы хотим отделить код, который взаимодействует с серверной частью, от самого компонента представления. В нашем файле AddNumber.jsx мы отправим данные формы в службу, которая будет заниматься внутренней связью. Если вы не создали файл phonebookServices.js в служебной папке, сейчас было бы отличное время.

Передать данные в сервис

Прежде всего, давайте заставим интерфейс и серверную часть взаимодействовать друг с другом. Мы сосредоточимся на этом, прежде чем приступить к отправке токена firebase. Внутри AddNumber.jsx мы добавим немного кода в нашу функцию handleSubmit ():

// components/phonebook/AddNumber.jsx

import { addToPhonebook } from '../../services/phonebookServices';
...
const handleSubmit = (e) => {
    e.preventDefault();
  if (name && phone) {
      phonebookServices.addToPhonebook(name, phone);
    }
  console.log('You must enter a name and a number');
  };
...

Если оба поля заполнены, мы отправим данные в нашу службу. Если нет, печатаем в консоль. Если вы на самом деле что-то заполните и отправите, приложение взорвется.
А теперь давайте исправим сервис!

Передавать данные из сервиса в базу данных… И обратно.

Служба будет отвечать за обмен данными с серверной службой. Для этого мы могли бы использовать встроенную библиотеку выборки. Вместо этого мы воспользуемся библиотекой axios, так как с ней мне немного легче работать. Установим:

/phone-frontend $ npm install axios

Мы знаем, что наш внутренний сервер работает по адресу http: // localhost: 3001 / api, поэтому пришло время отправить данные формы на этот URL. В нашем служебном файле:

// services/phonebookServices.js

import axios from 'axios';
const url = 'http://localhost:3001/api';
export const addToPhonebook = (name, number) => {
  const payload = {
    name,
    number,
  }
  axios.post(url, payload);
};

Мы создаем новый объект JS и передаем его в axios с URL-адресом серверной части. Взволнованные как никогда, мы заполняем нашу форму и отправляем ее:

Вот это да. Это не сработало. Ну, это из-за совместного использования ресурсов между разными источниками, или для краткости CORS.

от разработчика Mozilla

Совместное использование ресурсов между источниками (CORS) - это механизм, который использует дополнительные заголовки HTTP, чтобы указать браузерам предоставить веб-приложению, работающему в одном источнике, доступ к выбранным ресурсам из другого источника. . Веб-приложение выполняет HTTP-запрос с несколькими источниками, когда оно запрашивает ресурс, который имеет источник, отличный от его собственного (домен, протокол или порт).

Мы разберемся с этим, установив модуль под названием cors. Перейдите на бэкэнд и установите его:

/phone-backend $ npm install cors

И примените его к нашему бэкэнду:

// index.js
const cors = require('cors');
...
const app = express();
app.use(cors());
...

Установив cors, мы снова пробуем наш интерфейс, и все работает как положено:

Это потрясающе. Теперь интерфейс и серверная часть обмениваются данными. Следующее, что нам нужно сделать, это выяснить, как отправить токен Firebase с полезной нагрузкой из внешнего интерфейса в серверный.

Токен Frontend Firebase

Самое замечательное в Firebase - это простота использования. То же самое и с созданием токенов. Мы создадим новую функцию в нашем служебном файле, которая создаст для нас этот токен.

// services/phonebookService.js
import fire from '../fire';
...
const createToken = async () => {
  const user = fire.auth().currentUser;
  const token = user && (await user.getIdToken());
  const payloadHeader = {
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
  };
  return payloadHeader;
}
...

Эта функция выбирает текущего пользователя из библиотеки Firebase Auth. Мы используем текущего пользователя для получения токена id. Нам нужно поместить этот токен в заголовок нашего запроса. Самый простой способ сделать это - создать и вернуть заголовок с включенным токеном.

Внутри функции addToPhonebook мы добавим в код асинхронный блок try… catch и включим наш недавно сгенерированный заголовок auth с полезной нагрузкой:

// services/phonebookServices.js
...
export const addToPhonebook = async (name, number) => {
  const header = await createToken();
  const payload = {
    name,
    number,
  }
  try {
    const res = await axios.post(url, payload, header);
    return res.data;
} catch (e) {
    console.error(e);
  }
};

Пока мы занимаемся этим, давайте создадим функцию, которая будет извлекать все номера телефонов из базы данных. Создаем его в том же файле:

// services/phonebookServices.js
... 
export const getPhonebookEntries = async () => {
  const header = await createToken();
try {
    const res = await axios.get(url, header);
    return res.data;
  } catch (e) {
    console.error(e);
  }
}

Мы снова используем аксиомы. На этот раз мы отправим запрос GET на серверную часть. Это запросит у серверной части все записи телефонной книги. Вернемся к серверной части.

От бэкэнда к интерфейсу

Оглядываясь назад на поток наших приложений

  • Frontend отправит запрос GET на наш корневой маршрут, запрашивая все записи
  • Бэкэнд получит запрос и загрузит его из БД
  • Frontend отправит запрос POST на наш корневой маршрут, добавив запись
  • Бэкэнд получит запрос и добавит в БД

Мы можем пересечь два шага, что сужает их до:

  • Бэкэнд получит запрос и загрузит его из БД
  • Бэкэнд получит запрос и добавит в БД

Прежде чем мы действительно получим и отправим в базу данных, мы должны проверить пользователя. Токен аутентификации включен в запрос. Это означает, что мы можем создать промежуточное ПО, которое будет передавать запрос и немного его изменять. Мы изменим его так, чтобы мы могли найти проверенного пользователя в запросе, как только он достигнет нашего контроллера и выполнит маршрутизацию.

Создание промежуточного программного обеспечения firebase

Поскольку мы используем Firebase в качестве поставщика аутентификации, нам необходимо установить серверный модуль Firebase. Это позволит нам проверить токен аутентификации с помощью Firebase. Давайте установим его:

/phonebook-backend $ npm install firebase-admin

Нам нужно сгенерировать файл закрытого ключа учетной записи службы Google. Следуя инструкциям на странице https://firebase.google.com/docs/admin/setup

нам нужно перейти на https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk, чтобы создать его. также скопируйте URL из примера конфигурации Google

После того, как вы скачали файл JSON, добавьте его в корневой каталог нашей серверной части. Вам также следует изменить его название на более простое. Я позвонил в свой serviceAccount.json. Сейчас самое время поместить конфигурацию в отдельные переменные среды. Не стесняйтесь делать это, если хотите.

Давайте создадим новый файл - authenticateToken.js и поместим его в корневой каталог. Мы напишем наше промежуточное ПО здесь:

// authenticateToken.js
const admin = require('firebase-admin');
const serviceAccount require('./serviceAccount.json');
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: 'https://phone-book-fe436.firebaseio.com',
});
async function decodeIDToken(req, res, next) {
  const header = req.headers?.authorization;
  if (header !== 'Bearer null' && req.headers?.authorization?.startsWith('Bearer ')) {
const idToken = req.headers.authorization.split('Bearer ')[1];
try {
      const decodedToken = await admin.auth().verifyIdToken(idToken);
      req['currentUser'] = decodedToken;
    } catch (err) {
      console.log(err);
    }
  }
next();
}
module.exports = decodeIDToken;

Мы импортируем наш файл JSON с конфигурациями Firebase и используем его для инициализации Firebase на нашем сервере. мы создаем функцию - decodeIDToken, которая проверяет входящий запрос на токен Bearer. если токен существует, мы отправляем его в Firebase для проверки. Если проверено, мы помещаем его в запрос. Таким образом, наши маршруты смогут его использовать.

Нам также нужно добавить его в наш index.js:

// index.js
... 
const decodeIDToken = require('./authenticateToken');
...
app.use(decodeIDToken);
...

Теперь мы можем аутентифицировать каждый входящий запрос:

// controllers/phones.js
... 
phonesRouter.post('/', (req, res) => {
  const auth = req.currentUser;
  if (auth) {
    console.log('authenticated!', auth);
    return res.send('Hi, from within the phones router POST');
  }
  return res.status(403).send('Not authorized')
});

Мы можем получить аутентифицированного пользователя прямо из запроса. Если он существует, мы выводим пользователя на консоль:

Создание телефонной книги

Наконец-то мы можем начать использовать мангуста по-настоящему. Мы создадим новую модель телефонной книги с помощью мангуста. Внутри нашего пустого phone.js:

// models/phone.js
const mongoose = require('mongoose');
const phoneSchema = new mongoose.Schema({
  name: String,
  number: String,
});
phoneSchema.set('toJSON', {
  transform: (doc, returnedObject) => {
    returnedObject.id = returnedObject._id.toString();
    delete returnedObject._id;
    delete returnedObject.__v;
  },
});
module.exports = mongoose.model('Phone', phoneSchema);

Сначала мы создаем схему базы данных. Здесь оба наших поля будут строками. Как поля в нашем интерфейсе. Затем мы убеждаемся, что объект, возвращаемый из базы данных, содержит идентификатор в виде строки. Первоначально это был объект.

Это все для нашей модели. Теперь мы можем отправлять сообщения в базу данных.

Размещение записей в базе данных

Чтобы иметь возможность правильно управлять полезной нагрузкой из внешнего интерфейса, нам нужно добавить еще одно встроенное промежуточное ПО в наше приложение. добавить в index.js:

app.use(express.json());

Мы внесем некоторые изменения в наш маршрут POST:

// controllers/phones.js
...
const Phone = require('../models/phone');
...
phonesRouter.post('/', (req, res) => {
  const auth = req.currentUser;
  if (auth) {
    const phone = new Phone(req.body);
    const savedPhone = phone.save();
return res.status(201).json(savedPhone);
  }
  return res.status(403).send('Not authorized');
});

Проверяем запрос, создаем новый объект Phone из нашей модели и сохраняем его.

Если мы заглянем внутрь нашей коллекции MongoDB:

Спасено!

Пока мы в ударе, давайте запрограммируем маршрут GET.

Получение записей из базы данных

Мы кодируем наш маршрут GET:

// controllers/phones.js
...
phonesRouter.get('/', async (req, res) => {
  const auth = req.currentUser;
  if (auth) {
    const phones = await Phone.find({});
    return res.json(phones.map((phone) => phone.toJSON()));
  }
  return res.status(403).send('Not authorized');
});
...

мангуст, конечно, облегчает нам задачу. Передав пустой объект, мы запрашиваем все документы в коллекции. Затем мы переберем каждый объект, применяя изменения, внесенные в файл модели. Это приведет к удалению поля __v и исправлению идентификатора. После этого мы возвращаем ему фронтенд.

Отображение данных в веб-интерфейсе

Если мы еще раз посмотрим на наши шаги:

  • Бэкэнд получит запрос и загрузит его из БД
  • Бэкэнд получит запрос и добавит в БД

Мы действительно получаем данные из базы данных. Мы также пополняем базу. Это означает, что мы приближаемся к нашей конечной цели. Теперь давайте выясним, как мы можем перечислить все записи в нашем интерфейсе

наш служебный файл возвращает полученные данные. Давайте вызовем getPhonebookEntries () из нашего компонента ListAllNumbers. Полный компонент ListAllNumbers будет выглядеть так:

// components/phonebook/ListAllNumbers.jsx
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { getPhonebookEntries } from '../../services/phonebookServices';
const ListAllNumbers = () => {
  const [entries, setEntries] = useState();
useEffect(() => {
    const fetchEntries = async () => {
      const fetchedEntries = await getPhonebookEntries();
      setEntries(fetchedEntries);
    }
    fetchEntries();
  }, [])
if (entries === undefined) {
    return null;
  }
return (
    <div>
      <Link to="/add-number">Add number</Link>
      <h2>Phone numbers</h2>
      
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Number</th>
          </tr>
        </thead>
        <tbody>
          {entries.map((entry) => (
          <tr>
            <td>{entry.name}</td>
            <td>{entry.number}</td>
          </tr>
          ))}
        </tbody>
      </table>
    </div>
  ) 
};
export default ListAllNumbers;

мы настраиваем состояние, в котором будут храниться наши записи. Мы запрашиваем базу данных, используя наш сервис в хуке useEffect. Внутри шаблона мы перебираем записи с помощью функции карты и отображаем каждую запись.

Из документа React:

Что делает useEffect? С помощью этого хука вы сообщаете React, что ваш компонент должен что-то сделать после рендеринга. React запомнит переданную вами функцию (мы будем называть ее нашим «эффектом») и вызовет ее позже, после выполнения обновлений DOM. В этом случае мы устанавливаем заголовок документа, но мы также можем выполнять выборку данных или вызывать какой-либо другой императивный API.

Мы получаем данные от бэкэнда, и устанавливаем их в состояние. Поскольку запрос к базе данных является асинхронной операцией, мы проверяем, существуют ли записи. Если нет, мы возвращаем null. Как только данные вернутся из бэкэнда, записи будут заполнены автоматически, и наши данные будут присутствовать.

Если мы попытаемся добавить еще одну запись и вернемся в нашу телефонную книгу:

Все сохраняется и извлекается, как ожидалось.

Заключительные мысли

Как я уже говорил ранее, Firebase не всегда подходит для работы. Однако нам всегда нужна какая-то аутентификация. Firebase представляет нам это в действительно отличном пакете вкусностей.

Надеюсь, вы кое-что узнали из этого огромного поста. Вы можете найти полный исходный код по адресу https://github.com/Devalo/Firebase-auth-react-express-mongodb

До следующего раза
Стефан Баккелунд Валуа