В этом руководстве мы создадим пример приложения B2B в React, где пользователи могут регистрироваться, входить в систему, управлять своими учетными записями и просматривать информацию об организации и участниках, используя PropelAuth, React и FastAPI.

Мы собираемся использовать следующие технологии для этого сообщения в блоге:

  • React.js — для нашего интерфейса
  • FastAPI (python) — для нашего бэкэнда
  • PropelAuth — для входа и управления пользователями.

Полный код пошагового руководства будет доступен в этих репозиториях Github.

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

Перед созданием нашего приложения мы сначала настроим наш проект в PropelAuth. Проекты в PropelAuth предоставляют все необходимые компоненты аутентификации для ваших приложений, включая размещенные страницы входа, которые мы будем использовать в этом пошаговом руководстве. Вы также можете настроить дополнительные функции, такие как RBAC, SAML, вход через социальные сети/SSO и многое другое. Для получения дополнительной информации о том, как добавить эти параметры, обязательно ознакомьтесь с нашей документацией.

Первый шаг — создать проект в PropelAuth.

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

Первый шаг — настроить внешний вид ваших размещенных страниц аутентификации. Нажав «Просмотреть», вы будете перенаправлены на эту страницу:

Здесь вы можете настроить внешний вид всех размещенных страниц аутентификации, чтобы они наилучшим образом соответствовали вашим предпочтениям по брендингу и стилю. Не стесняйтесь вносить любые изменения, нажмите «Сохранить» и вернитесь к панели управления с помощью левой боковой панели. Теперь, когда мы изменили внешний вид наших страниц, нажмите «Отметить как выполненное» и перейдите к следующему шагу.

Отсюда вы можете настроить другие аспекты аутентификации конечных пользователей, в том числе:

  • Добавление «Войти через Google» или других поставщиков SSO
  • Сбор дополнительных метаданных при регистрации — например, имя пользователя или имя/фамилия.
  • Разрешить вашим пользователям загружать свои собственные изображения профиля
  • Предоставление вашим пользователям возможности создавать организации и приглашать своих коллег (так называемая поддержка B2B).

На данный момент мы нажмем «Отметить как выполненное» на шаге 2, «Добавить социальные логины» и перейдем к шагу 3.

Зарегистрируйтесь как тестовый пользователь

На шаге 3 нажмите «Просмотр», и должна открыться новая вкладка со страницей аутентификации, которую вы настроили выше. Зарегистрируйтесь как пользователь, выйдите из вкладки и отметьте шаг как выполненный.

Создать реагирующий проект

Если у вас еще нет проекта, вы можете создать его с помощью:

$ npx create-react-app frontend

Затем установите пакет @propelauth/react. Он обеспечивает простой интерфейс для доступа к информации о ваших пользователях. Он будет управлять токенами аутентификации для вас и имеет приятные функции, такие как обновление информации аутентификации, когда пользователь повторно подключается к Интернету или переключается обратно на вашу вкладку.

$ npm install @propelauth/react

Наконец, мы установим react-router, чтобы использовать переключение между страницами в нашем приложении.

$ npm install react-router-dom@6

Внешний интерфейс

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

Настроить поставщика аутентификации

Сначала перейдите к файлу index.js в вашем приложении. Здесь мы собираемся добавить наш AuthProvider и BrowserRouter из react-router.

import {AuthProvider} from '@propelauth/react';
import {BrowserRouter} from "react-router-dom";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <AuthProvider authUrl={process.env.REACT_APP_PROPELAUTH_AUTH_URL}>
	    <BrowserRouter>
	      <App/>
	    </BrowserRouter>
    </AuthProvider>
  </React.StrictMode>
);

authUrl доступен в разделе Frontend Integration вашего проекта PropelAuth или на шаге 4 на панели управления.

BrowserRouter будет управлять нашей историей URL-адресов при переключении страниц в нашем приложении. AuthProvider — это поставщик контекста React, который управляет токеном доступа и метаданными текущего пользователя, а все остальные компоненты извлекают информацию из это. Мы размещаем его на верхнем уровне нашего приложения, чтобы он никогда не размонтировался.

Статус входа в систему

Затем в App.js мы добавим функцию withAuthInfo, которая вводит информацию о пользователе в компонент React. В нашем проекте мы будем отображать различные компоненты в зависимости от того, вошел ли пользователь в систему.

import { withAuthInfo } from '@propelauth/react';

const App = withAuthInfo(({isLoggedIn}) => {
  if (isLoggedIn) {
      return <div>The User is logged in</div>
  } else {
      return <div>The User is logged out</div>
  }
})

export default App;

Используя приведенный выше код, если пользователь вошел в систему, он должен увидеть следующее:

Создание кнопок входа/выхода

@propelauth/react также предоставляет хуки React для перенаправления ваших пользователей на размещенные страницы входа/регистрации/учетной записи, которые вы создали в своем проекте PropelAuth, или для выхода ваших пользователей из системы. Давайте добавим их в наш файл App.js.

import { useLogoutFunction, useRedirectFunctions, withAuthInfo } from '@propelauth/react';

const App = withAuthInfo(({isLoggedIn}) => {
  const logoutFn = useLogoutFunction()
  const {redirectToSignupPage, redirectToLoginPage} = useRedirectFunctions();
    
  if (isLoggedIn) {
      return <div>
          The User is logged in
          <button onClick={() => logoutFn()}>
              Click here to log out
          </button>
      </div>
  } else {
      return <div>
          To get started, please log in as test user.
          <br/>
          <button onClick={() => redirectToSignupPage()}>
              Sign up
          </button>
          <button onClick={() => redirectToLoginPage()}>
              Log in
          </button>
      </div>
  }
})

Теперь, если пользователь вышел из системы, он увидит следующее:

И если они вошли в систему, они увидят:

Показать информацию о пользователе

Следующим шагом является построение наших маршрутов и создание маршрута и компонента, которые будут отображать информацию о вошедшем в систему пользователе. Сначала создайте components/Home.jsx и components/UserInfo.jsx, которые будут служить компонентом нашей домашней страницы и маршрутом информации о пользователях соответственно.

В Home.jsx мы импортируем withRequiredAuthInfo, который идентичен withAuthInfo, но компонент не будет отображаться, если пользователь не вошли в систему, и вместо этого по умолчанию будет перенаправление на размещенную страницу регистрации, если не указано иное. Мы также импортируем Link из react-router-dom. Наконец, создадим остальную часть компонента, используя Link:

import {withRequiredAuthInfo} from "@propelauth/react";
import {Link} from "react-router-dom";

function Home(props) {
   return <div>
        <Link to="/user_info">
            Click Here to see user info
        </Link>
   </div>
}

export default withRequiredAuthInfo(Home);

В UserInfo.jsx мы создадим компонент для отображения информации о пользователе, который извлекает объект user, который автоматически внедряется из withAuthInfo.

import {withAuthInfo} from '@propelauth/react';

function UserInfo({user}) {
    return <span>
        <h2>User Info</h2>
        {user && user.pictureUrl && <img src={user.pictureUrl} alt={"profile"} className="pictureUrl" />}
        <pre>user: {JSON.stringify(user, null, 2)}</pre>
    </span>
}

export default withRequiredAuthInfo(UserInfo);

Наконец, в App.js мы настроим маршруты для компонентов Home и User Info в проверке isLoggedIn:

if (isLoggedIn) {
      return <div>
          The User is logged in
          <button onClick={() => logoutFn()}>
              Click here to log out
          </button>
          <Routes>
            <Route exact path="/" element={<Home/>}/>
            <Route path="/user_info" element={<UserInfo/>}/>
          </Routes>
      </div>
  }

Теперь пользователи должны иметь возможность щелкнуть ссылку, если они вошли в систему в компоненте «Главная», и просмотреть свою информацию.

Отправка запросов от внешнего интерфейса к внутреннему

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

Выполнение аутентифицированных запросов

Чтобы сделать аутентифицированный запрос от имени вашего пользователя, вам необходимо предоставить токен доступа. Точно так же, как isLoggedIn и user, токен доступа доступен из withAuthInfo. Вы предоставляете его в запросе в заголовке Authorization, например:

Authorization: Bearer ACCESS_TOKEN

С выборкой это выглядит так:

function fetchWhoAmI(accessToken) {
    return fetch("/whoami", {
        headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${accessToken}`
        }
    }).then(response => {
        if (response.ok) {
            return response.json()
        } else {
            return {status: response.status}
        }
    })
}

Мы можем добавить это в новый компонент components/AuthenticatedRequest.jsx, используя хук React useEffect.

import {withRequiredAuthInfo} from "@propelauth/react";
import {useEffect, useState} from "react";

function fetchWhoAmI(accessToken) {
    return fetch("/whoami", {
        headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${accessToken}`
        }
    }).then(response => {
        if (response.ok) {
            return response.json()
        } else {
            return {status: response.status}
        }
    })
}

function AuthenticatedRequest({accessToken}) {
    const [response, setResponse] = useState(null);
    useEffect(() => {
        fetchWhoAmI(accessToken).then(setResponse)
    }, [accessToken])

    return <span>
        <h2>Server Response</h2>
        <pre>{response ? JSON.stringify(response, null, 2) : "Loading..."}</pre>
    </span>
}

export default withRequiredAuthInfo(AuthenticatedRequest);

Краткая заметка о CORS

Наше приложение React работает на порту 3000, поэтому нам нужно запустить наш сервер на другом порту (в этом руководстве мы используем 3001). Из соображений безопасности браузеры не позволят делать запросы с одного домена на другой, а http://localhost:3000 и http://localhost:3001 считаются разными доменами.

Простой способ решить эту проблему — добавить в package.json следующее:

"proxy": "<http://127.0.0.1:3001/>"

Это автоматически проксирует определенные запросы (например, запросы JSON) на http://127.0.0.1:3001. Для получения дополнительной информации см. официальную документацию React.

Отлично, теперь мы можем делать аутентифицированные запросы к любому серверу, который захотим. Единственная проблема? У нас еще нет бэкэнда — давайте это исправим.

Аутентификация в FastAPI

Создание виртуальной среды

Сначала мы создаем новую виртуальную среду и устанавливаем наши зависимости. Мы будем использовать propelauth-fastapi для проверки токена доступа, отправляемого внешним интерфейсом. Нам также нужен uvicorn для запуска нашего приложения.

$ mkdir backend
$ cd backend
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install fastapi "uvicorn[standard]" propelauth-fastapi python-dotenv

Создание защищенного маршрута

Код достаточно прост, поэтому мы можем сначала посмотреть на него, а потом объяснить:

#main.py
import os

from dotenv import load_dotenv
from fastapi import Depends, FastAPI
from propelauth_fastapi import init_auth
from propelauth_py.user import User

load_dotenv()

app = FastAPI()
auth = init_auth(os.getenv("PROPELAUTH_AUTH_URL"), os.getenv("PROPELAUTH_API_KEY"))

@app.get("/whoami")
def who_am_i(user: User = Depends(auth.require_user)):
    return {"user_id": user.user_id}
  • auth.require_user — это зависимость FastAPI, которая проверяет токен доступа. Если действительный токен доступа не был предоставлен, запрос отклоняется с ошибкой 401 Unauthorized. Если вы хотите, чтобы запрос продолжался даже без действительного токена доступа, используйте вместо этого **auth.Optional_user.**‍
  • PROPELAUTH_AUTH_URL и PROPELAUTH_API_KEY можно найти, нажав Интеграция с серверной частью на боковой панели вашего проекта PropelAuth. Они используются один раз при запуске для получения информации, необходимой для проверки токенов. Маркеры доступа (которые являются JWT) затем быстро проверяются без необходимости делать какие-либо внешние запросы.

Вы можете запустить код с помощью:

$ uvicorn main:app --reload --port=3001

Ваши запросы от браузера должны быть успешными, когда вы вошли в систему, и 401, когда вы вышли из системы.

Теперь, когда у нас есть работающая серверная часть, если мы теперь включим наши components/AuthenticatedRequest.jsx в новый маршрут на App.js:

<Route path="/auth" element={<AuthenticatedRequest/>}/>

И если мы добавим новую ссылку в components/Home.jsx:

<Link to="/auth">
    Click Here to see an authenticated request to the backend
</Link>

Если пользователь вошел в систему, он должен увидеть ответ сервера на наш аутентифицированный запрос:

Информация об организации

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

Показать организации

Во-первых, мы создадим components/ListOfOrgs.jsx, в котором будут перечислены все организации, частью которых является пользователь, или отобразится кнопка для перенаправления в организацию create/ страница приглашения, если они не являются частью каких-либо организаций.

import {useRedirectFunctions, withRequiredAuthInfo} from "@propelauth/react";
import {Link} from "react-router-dom";

function NoOrganizations() {
    const {redirectToCreateOrgPage} = useRedirectFunctions()

    return <div>
        You aren't a member of any organizations.<br/>
        You can either create one below, or ask for an invitation.<br/>
        <button onClick={redirectToCreateOrgPage}>
            Create an organization
        </button>
    </div>
}

function ListOrganizations({orgs}) {
    return <>
        <h3>Your organizations</h3>
        <ul>
            {orgs.map(org => {
                return <li key={org.orgId}>
                    <Link to={`/org/${org.urlSafeOrgName}`}>
                        {org.orgName}
                    </Link>
                </li>
            })}
        </ul>
    </>
}

function ListOfOrgs(props) {
    const orgs = props.orgHelper.getOrgs()
    if (orgs.length === 0) {
        return <NoOrganizations />
    } else {
        return <ListOrganizations orgs={orgs}/>
    }
}

// By default, if the user is not logged in they are redirected to the login page
export default withRequiredAuthInfo(ListOfOrgs);

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

Далее мы создадим components/OrgInfo.jsx и, следуя тому же шаблону, что и с AuthenticatedRequest.jsx, воспользуемся React useEffect. strong> для отправки выборки на новый маршрут, org/orgId.

import {withAuthInfo} from '@propelauth/react';
import {useParams} from "react-router-dom";
import { useEffect, useState } from "react";

function fetchOrgInfo(orgId, accessToken) {
    return fetch(`/org/${orgId}`, {
        headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${accessToken}`
        }
    }).then(response => {
        if (response.ok) {
            return response.json()
        } else {
            return {status: response.status}
        }
    })
}

function OrgInfo({ orgHelper, accessToken }) {
    const {orgName} = useParams();
    const orgId = orgHelper.getOrgByName(orgName).orgId

    const [response, setResponse] = useState(null)
    
    useEffect(() => {
        fetchOrgInfo(orgId, accessToken).then(setResponse)
    }, [orgId, accessToken])

    return <div>
        <p>{response ? JSON.stringify(response) : "Loading..."}</p>
    </div>
}

export default withRequiredAuthInfo(OrgInfo);

Затем нам нужно создать маршрут FastAPI для возврата аутентифицированного ответа, поэтому в файле main.py мы добавим новый маршрут:

@app.get("/org/{org_id}")
async def view_org(org_id: str, current_user: User = Depends(auth.require_user)):
    org = auth.require_org_member(current_user, org_id)
    return {"org": org}

Наконец, нам нужно отобразить новые маршруты в нашем приложении. В App.js нам нужно импортировать и создать новые пути Route для ListOfOrgs.jsx и OrgInfo.jsx.

<Route path="/orgs" element={<ListOfOrgs/>}/>
<Route path="/org/:orgName" element={<OrgInfo/>}/>

А в Home.jsx нам нужно добавить новую ссылку на путь /orgs для отображения ListOfOrgs.jsx.

<Link to="/orgs">
    Click Here to see org info
</Link>

Мы оставляем это на ваше усмотрение, чтобы оно выглядело красиво :)

Подведение итогов

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

Наш интерфейс сделал аутентифицированный запрос к нашему серверу, и наш сервер смог идентифицировать пользователя, который сделал запрос. Вы можете использовать это для таких вещей, как сохранение информации в базе данных для каждого user_id.

Если у вас есть какие-либо вопросы, обращайтесь по адресу [email protected].