# ADR-008: хранение интеграционных данных и разграничение доступа **Статус:** Принято **Дата:** 2026-07-23 ## Контекст Основное назначение базы данных — хранение авторизационных данных, токенов привязки, OAuth-токенов и связей между пользователями Битрикс24 и Telegram. Сделки, контакты, компании, стадии и история остаются в Битрикс24 и в локальной базе не дублируются. ## Решение Модель хранения нормализована вокруг портала Битрикс24. ![Схема базы данных](assets/report/database-schema.png) *Рисунок ADR-008/1. Схема базы данных* | Таблица | Ключевые данные | Назначение и ограничения | |------------------------|-------------------------------------------------|-------------------------------------------------------| | binding.portals | member_id, domain | Справочник порталов; member_id уникален | | binding.tokens | token_hash, expires_at, consumed_at, revoked_at | Одноразовые ссылки; токен хранится только как хеш | | binding.user_bindings | portal_id, bitrix_user_id, telegram_user_id | Однозначная привязка пользователей в пределах портала | | oauth.user_credentials | access_token, refresh_token, version, lock | Зашифрованные OAuth-данные и координация обновления | *Таблица ADR-008/1. Назначение таблиц базы данных* Поле `member_id` является устойчивым внешним идентификатором портала, а числовой `bitrix_user_id` имеет смысл только вместе с `portal_id`. Связи с `portals` используют `ON DELETE CASCADE`: удаление портала автоматически удаляет его ссылки, привязки и OAuth-данные. Составной первичный ключ `oauth.user_credentials(portal_id, bitrix_user_id)` исключает две конкурирующие записи учётных данных одного пользователя. Поля `consumed_at` и `revoked_at` разделяют два независимых основания недействительности одноразовой ссылки. Для работы с данными в PostgreSQL созданы две роли: `site_role` и `bot_role`. Они имеют разные права доступа к хранимым функциям и не могут редактировать таблицы напрямую. | Функция | Вызывающая роль | Назначение | |-----------------------------|-----------------|-------------------------------------------------------------------------------------| | binding.issue_v1 | site_role | Сохранить OAuth-данные, отозвать старые ссылки и выпустить новую в одной транзакции | | binding.consume_v1 | bot_role | Однократно погасить ссылку и создать привязку | | binding.find_by_telegram_v1 | bot_role | Получить актуальную привязку Telegram | | oauth.get_credentials_v1 | bot_role | Получить учётные данные связанного пользователя | | oauth.claim_refresh_v1 | bot_role | Получить короткую аренду на обновление токена | | oauth.finish_refresh_v1 | bot_role | Атомарно записать новую пару при совпадении версии | | oauth.release_refresh_v1 | bot_role | Освободить аренду после ошибки | *Таблица ADR-008/2. Контракт хранимых функций PostgreSQL* Сайт использует роль `site_role` и может вызывать только функцию привязки. Telegram-бот использует `bot_role` и может вызывать функции погашения токенов, получения привязки и работы с OAuth. Роль `site_role` не имеет доступа к схеме `oauth` и не может читать сохранённые токены; роль `bot_role` не может выпускать новые ссылки от имени сайта. Все функции объявлены `SECURITY DEFINER` и фиксируют `search_path` в `pg_catalog`, что уменьшает риск подмены объектов. Права `PUBLIC` на таблицы и функции отозваны. ## Последствия Конкурирующие вызовы `binding.consume_v1` для одного хеша не смогут одновременно пройти условие `consumed_at IS NULL`: `UPDATE` блокирует строку, а после завершения первой транзакции второй вызов видит уже установленное время погашения. Проверка и изменение не разделены между приложением и базой, поэтому отсутствует окно гонки между `SELECT` и `UPDATE`.