Мультитенантные приложения

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

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

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

Зачем выбирать мультитенантное приложение даже с такой дополнительной работой? Есть несколько очевидных преимуществ, таких как отсутствие необходимости поддерживать или платить за отдельную инфраструктуру для каждого клиента. В этом посте мы покажем, как создать мультитенантное приложение с использованием Next.js, Prisma и PropelAuth.

Что будем строить?

Мы создадим простое B2B-приложение, в котором каждый пользователь может делать «публикации» внутри арендатора. Все пользователи в арендаторе должны иметь возможность читать свои записи, и никто за пределами арендатора не должен иметь возможность просматривать их или получать к ним доступ.

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

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

Как мы поступаем с пользователями в нескольких арендаторах?

Если пользователь находится только в одном тенанте, довольно очевидно, данные какого тенанта мы должны показать. Но что происходит, когда пользователи находятся более чем в одном арендаторе?

На практике эта проблема возникает часто. GitHub позволяет пользователям находиться в нескольких организациях одновременно. Точно так же вы можете использовать столько рабочих пространств Slack, сколько хотите, и у вас может быть несколько учетных записей Google/Twitter, между которыми вы можете переключаться.

То, как вы подходите к этой проблеме, зависит от вашего приложения, но есть несколько точек зрения:

  • У каждого арендатора должен быть свой уникальный URL-адрес (например, https://tenant.example.com или https://example.com/tenant/).
  • Существует только один URL-адрес (например, https://app.example.com), и пользователь выбирает, какой клиент просматривать в пользовательском интерфейсе с помощью раскрывающегося меню или чего-то подобного.

Подробнее о том, какой подход вы выберете, мы написали о плюсах и минусах каждого здесь. В этом примере мы предположим, что пользователи могут просматривать сообщения своего клиента, перейдя по URL-адресу, где в пути указано имя их клиента, например https://example.com/tenant/.

Проверка параметра пути в Next.js

Во-первых, мы создадим хук React, который может сообщить нам, какой арендатор в данный момент просматривается пользователем, проверив путь URL-адреса. Начните с создания нового приложения Next.js.

Next.js поддерживает динамические маршруты, которые делают это довольно просто. Файл, созданный в pages/org/[orgName]/posts.tsx, будет реагировать на любой маршрут, такой как /org/something/posts или /org/somethingelse/posts.

Если мы напишем следующий код в этом файле:

Мы сможем увидеть, какой клиент пытается просмотреть пользователь, на основе его URL-адреса.

Проверка пользователя в данном арендаторе

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

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

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

После настройки наши пользователи теперь могут самостоятельно регистрироваться, входить в систему, создавать арендаторов и т. д. Все, что нам нужно сделать, это проверить, находятся ли они в арендаторе, к которому они пытаются получить доступ. Мы можем сделать это с помощью библиотеки PropelAuth’s React.

Затем, в pages/_app.ts, мы оборачиваем наше приложение с помощью AuthProvider. AuthProvider обращается к нашему экземпляру PropelAuth и извлекает метаданные нашего текущего пользователя, если они вошли в систему. Вам понадобится ваш authUrl, который вы можете найти на панели управления в разделе Frontend Integration.

Вот и все, теперь мы можем получить доступ к информации нашего пользователя в любом месте нашего приложения. Мы можем использовать либо функции более высокого порядка, такие как withAuthInfo, либо хуки, такие как useAuthInfo. Давайте обновим наш файл /org/[orgName]/posts.tsx, чтобы проверить, находится ли пользователь в orgName.

Для любых арендаторов/организаций, членами которых не являются наши пользователи, они получат ошибку «Не найдено».

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

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

Выполнение аутентифицированных запросов к нашему бэкэнду

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

В основном мы вызываем fetchPosts по мере изменения нашего orgMembership и отображаем страницы. Для краткости мы опустим стиль самого поста. Что касается самого метода fetchPosts:

Поскольку наш серверный API не ориентирован на клиента, мы можем передать наш orgId, как захотим. Как и выше, мы можем использовать параметр запроса, продолжать использовать параметры пути, поместить его в тело почтового запроса и т. д.

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

Теперь у нас есть все необходимое на фронтенде — единственная проблема в том, что у нас пока нет бэкенда. Давайте исправим это, используя встроенные маршруты API Next.js.

Создание нашего бэкэнда с маршрутами API Next.js и Prisma

Наш бэкэнд был бы не очень полезен без возможности где-то хранить посты. Мы настроим Prisma, популярную ORM TypeScript, которая будет управлять всеми нашими операциями с базой данных, включая миграции.

После этого у вас должен появиться новый файл в репозитории, prisma/schema.prisma

Здесь мы определим нашу схему. Сама схема довольно проста. Мы хотим, чтобы сообщение содержало некоторый текст для отображения. Пост должен быть связан с арендатором (организацией) и пользователем.

Ничего особенного, каждый пост имеет post_id, который по умолчанию равен новому UUID. Он содержит идентификаторы арендатора (org_id) и пользователя (user_id). И, наконец, он содержит наш текст. Теперь мы можем перенести нашу базу данных, что означает просто создать или обновить ее до самой последней схемы:

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

Использование маршрутов API Next.js

Маршруты API Next.js позволяют создавать собственные API в Next.js. Как говорят сами Next.js:

Любой файл в папке pages/api сопоставляется с /api/* и будет рассматриваться как конечная точка API, а не page. Они являются пакетами только на стороне сервера и не увеличивают размер пакета на стороне клиента.

Создайте новый файл pages/api/post.ts, который будет отвечать на запросы в /api/post.

Сначала нам нужно проверить method и решить, как обрабатывать запрос:

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

Вы могли заметить, что Post, импортируемый из @prisma/client, является нашим типом Post и включает post_id, org_id, user_id и текст.

Но есть несколько проблем. Во-первых, как узнать, для какого арендатора/организации нам следует получать сообщения? И, кроме того, как мы узнаем, находится ли человек, отправляющий запросы к API, в этом арендаторе?

Ранее мы передали две части информации из нашего внешнего интерфейса:

  • org_id как параметр запроса
  • Токен доступа, который проверяется нашим бэкендом.

PropelAuth, помимо внешних библиотек, также предоставляет внутренние библиотеки для проверки этих токенов. Мы будем следовать руководству по началу работы с маршрутами API Next.js, в котором мы используем библиотеку @propelauth/express:

Настройте наш бэкэнд, вызвав initAuth с параметрами, данными нам на нашей панели:

Наконец, мы вызовем requireOrgMember, чтобы убедиться, что был предоставлен действительный токен доступа И что пользователь находится в указанной организации. Собрав все это вместе, наш getHandler должен выглядеть так:

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

Пока вы пишете эту функцию, вы, вероятно, заметите одну из лучших особенностей Prisma — здесь вы получаете и автозаполнение, и типобезопасность:

И это все! Теперь у нас есть все необходимое для тестирования нашего продукта.

Тестирование

Давайте сначала проверим, что мы не можем просматривать данные другого арендатора. Если мы перейдем на страницу сообщений любого произвольного арендатора, мы увидим сообщение «Не найдено».

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

Как и ожидалось, мы получаем 401, потому что мы не указали токен доступа, означающий, что мы являемся действительным пользователем. Если мы передаем действительный токен доступа, но по-прежнему передаем org_id, в котором мы не находимся, мы получим 403, что означает, что у нас нет разрешения на просмотр данных этой организации.

Наконец, мы можем создать организацию в наших размещенных пользовательских интерфейсах, которые предоставляет PropelAuth:

Сделайте публикацию и убедитесь, что мы можем ее просмотреть:

Краткое содержание

Мы создали большинство строительных блоков, необходимых для многопользовательского приложения B2B. Наши пользователи могут регистрироваться, создавать арендаторов и самостоятельно управлять арендаторами. Наш бэкенд работает на высокомасштабируемых маршрутах API Next.js. Благодаря Prisma у нас есть как миграция БД, так и безопасность типов между нашим приложением и базой данных.

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord. Заинтересованы в Взлом роста? Ознакомьтесь с разделом Схема.