Реализована интеграция с СМС провайдером

This commit is contained in:
mi
2026-07-23 11:49:15 +03:00
parent cc0163eb94
commit b1ed714d5b
89 changed files with 5934 additions and 202 deletions
+534
View File
@@ -0,0 +1,534 @@
# usefull commands
- Убрать переносы строк: sed -i 's/\r$//' name-file.sh
- find . -type f \( -name '*.sh' -o -name 'validate-env' \) -exec dos2unix {} +
- docker compose stop # мягко останавливает контейнеры (не удаляет)
- sudo shutdown -h now # выглючить ВМ
- Генерация паролей
- (со спец.символами) openssl rand -base64 32
- (без спец.символов) openssl rand -hex 16
- openssl rand -base64 16 | xclip -selection clipboard # Linux
Туннель до БД: ssh -i C:\Users\MI\.ssh\hansel -L 5433:192.168.0.211:5432 root@135.106.164.58 -N
#Обновление проекта
mkdir -p ~/.ssh
cp /mnt/c/Users/MI/.ssh/hansel ~/.ssh/hansel
chmod 600 ~/.ssh/hansel
'''bash'''
rsync -rltD --no-perms --no-owner --no-group -invc --delete \
--exclude='.env' \
--exclude='*.crt' \
--exclude='*.pem' \
--exclude='*.key' \
--exclude='secrets/' \
-e "ssh -i ~/.ssh/hansel" \
/mnt/c/Users/MI/Documents/Assistent/HAN_chat_specification/codebase/backend/ \
root@135.106.164.58:/opt/han-chat/backend/
-r — рекурсивно.
-l — сохранять символические ссылки.
-t — сохранять время модификации (важно для будущих проверок).
-D — сохранять устройства (на всякий случай, как в -a).
--no-perms --no-owner --no-group — главное исправление: не пытаться копировать права, владельца и группу с Windows на Linux. Это избавит от ложных срабатываний.
-i — покажет только реально измененные файлы (можно заменить на -v, если хотите просто список).
-a (archive) — сохраняет права, время и рекурсивно копирует.
-v (verbose) — выводит список файлов.
-n (dry-run) — главный флаг, показывает, что бы произошло, но не делает этого.
--delete — решение вашей проблемы. Говорит rsync удалять на приемнике (ВМ) файлы, которых нет в источнике (локально).
Важно: не забудьте поставить слэш / в конце пути к локальному проекту, иначе rsync скопирует саму папку внутрь папки на ВМ.
2. Скопировать и автоматически почистить артефакты
Когда вы убедитесь, что вывод предыдущей команды вас устраивает, просто уберите флаг -n:
rsync -rltD --no-perms --no-owner --no-group -ivc --delete \
--exclude='.env' \
--exclude='*.crt' \
--exclude='*.pem' \
--exclude='*.key' \
--exclude='secrets/' \
-e "ssh -i ~/.ssh/hansel" \
/mnt/c/Users/MI/Documents/Assistent/HAN_chat_specification/codebase/backend/ \
root@135.106.164.58:/opt/han-chat/backend/
cd /opt/han-chat/backend
find . -type f \( -name '*.sh' -o -name 'validate-env' \) -exec dos2unix {} +
chmod +x scripts/validate-env deployment/scripts/*.sh redis/scripts/*.sh nginx/scripts/*.sh
docker compose --env-file .env build frontend-static keycloak
docker compose --env-file .env up -d \
--no-deps \
--force-recreate frontend-static keycloak
# Архивный способ копирования:
cd /tmp
rm han-chat-backend.tar.gz
cd /opt/han-chat/backend
#команда складывает архив в ту папку, из которой запускается команда
cd C:\Users\MI\Documents\Assistent\
rm C:\Users\MI\Documents\Assistent\han-chat-backend.tar.gz
tar -C C:\Users\MI\Documents\Assistent\HAN_chat_specification\codebase\backend -czf han-chat-backend.tar.gz .
scp -i C:\Users\MI\.ssh\hansel C:\Users\MI\Documents\Assistent\han-chat-backend.tar.gz root@135.106.164.58:/tmp/han-chat-backend.tar.gz
tar -xzf /tmp/han-chat-backend.tar.gz
find . -type f \( -name '*.sh' -o -name 'validate-env' \) -exec dos2unix {} +
chmod +x scripts/validate-env deployment/scripts/*.sh redis/scripts/*.sh nginx/scripts/*.sh
docker compose --env-file .env build frontend-static keycloak
docker compose --env-file .env up -d \
--no-deps \
--force-recreate frontend-static keycloak
# Разворачиваем инфраструктуру в Селектел ч1
## Создание сети
## Создание групп безопасности для сети и для БД
Для сети открываем порты 80, 22, 443
Для БД открываем порты 5432, 5433 но только из CIDR (192.168.0.211\24)
## Создаем ВМ, подключаем к новой сети (через порт) и перезагружаем (либо руками вводить новые сетевые настройки - надо выбрать)
# Подключение к ВМ и первичный скрипт
++ ssh -i C:\Users\MI\.ssh\hansel root@135.106.164.58
++ scp -i C:\Users\MI\.ssh\hansel -r "C:\Users\MI\Documents\Assistent\HAN_chat_specification\codebase\backend\deployment\scripts\setup-vm.sh" root@135.106.164.58:/tmp/setup-vm.sh
chmod +x /tmp/setup-vm.sh
sudo /tmp/setup-vm.sh
# Скрипт создал деплой пользователя. Нужно Перенести туда публичный ключ и дать права на файл и папку
## Вставляем публичный ключ новой строкой Далее даем права
sudo nano /home/deploy/.ssh/authorized_keys
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys
sudo chown -R deploy:deploy /home/deploy/.ssh
## Пароль деплою для использования sudo
sudo passwd deploy
## После этого можно подключаться под деплой пользователем
ssh -i C:\Users\MI\.ssh\han_chat_deploy deploy@135.106.164.58
## GIT не работал. Проект пришлось копировать.
tar -C HAN_chat_specification/codebase/backend -czf han-chat-backend.tar.gz .
scp -i C:\Users\MI\.ssh\han_chat_deploy C:\Users\MI\Documents\Assistent\han-chat-backend.tar.gz deploy@135.106.164.58:/tmp/
scp -i C:\Users\MI\.ssh\hansel C:\Users\MI\Documents\Assistent\han-chat-backend.tar.gz root@135.106.164.58:/tmp/
## Дальше распаковка
cd /opt/han-chat/backend
tar -xzf /tmp/han-chat-backend.tar.gz
## Правим переносы строк и даем права на исполнение
find . -type f \( -name '*.sh' -o -name 'validate-env' \) -exec dos2unix {} +
chmod +x scripts/validate-env deployment/scripts/*.sh redis/scripts/*.sh nginx/scripts/*.sh
# Разворачиваем инфраструктуру в Селектел ч2
## Создаем БД
В базе подключаем pgcrypto
Чтобы корректно работал PgBouncer выбираем session pooling
### Копируем СА сертификат
Вариант Деплоя:
mkdir -p /opt/han-chat/backend/secrets/pg
scp -i C:\Users\MI\.ssh\han_chat_deploy -r C:\Users\MI\Documents\job\HAN_new_life\HANapp\Production\sertificates\CA.pem deploy@135.106.164.58:/opt/han-chat/backend/secrets/pg/ca.pem
chmod 644 /opt/han-chat/backend/secrets/pg/ca.pem
Вариант Селектела:
mkdir -p ~/.postgresql/
wget https://storage.dbaas.selcloud.ru/CA.pem -O ~/.postgresql/root.crt
chmod 0600 ~/.postgresql/root.crt
### Правки DNS-маршрутизации (в прошлый раз, какие-то трабблы были с базовой маршрутизацией) - скрипт ниже создаст
sudo tee /etc/netplan/99-han-dns.yaml <<'EOF'
network:
version: 2
ethernets:
eth0:
nameservers:
addresses:
- 8.8.8.8
- 1.1.1.1
search:
- selcloud.ru
eth1:
dhcp4-overrides:
use-dns: false
use-domains: false
EOF
sudo chmod 600 /etc/netplan/99-han-dns.yaml
sudo netplan apply
resolvectl flush-caches
### Проверка подключения к Postgers в ВМ
nc -zv 192.168.0.211 5433
sudo apt update
sudo apt install postgresql-client
psql "host=master.ef54e3e4-ad3d-4b80-a6af-d63269e0895a.c.dbaas.selcloud.ru \
port=5432 \
dbname=han_chat \
user=dbAdmin \
sslmode=verify-ca"
(psql "host=master.ef54e3e4-ad3d-4b80-a6af-d63269e0895a.c.dbaas.selcloud.ru port=5432 dbname=han_chat user=dbAdmin sslmode=verify-ca")
### Заводим пользователей и схемы
Пользователей создаем через интерфейс селектела.
Под админом даем права на создание в БД:
GRANT CREATE ON DATABASE han_chat TO han_app;
GRANT CREATE ON DATABASE han_chat TO bitrix_local_app;
GRANT CREATE ON DATABASE han_chat TO bitrix_sync_user;
GRANT CREATE ON DATABASE han_chat TO message_safety_app;
GRANT CREATE ON DATABASE han_chat TO keycloak_user;
GRANT CREATE ON DATABASE han_chat TO sms_user
Если создаем пользователей после того как отозвали права from public, надо давать гранты на коннект:
GRANT CONNECT ON DATABASE han_chat TO sms_user
Схемы создаем от лица пользователей, заходя каждым из них в БД.
+ Запрещаем всем посторонним входить в схему han_app и др.
CREATE SCHEMA IF NOT EXISTS han_app;
REVOKE ALL ON SCHEMA han_app FROM PUBLIC;
ALTER ROLE CURRENT_USER IN DATABASE han_chat SET search_path TO han_app;
SHOW search_path; --чтобы заработало надо переподключиться (должно быть han_app)
CREATE SCHEMA IF NOT EXISTS bitrix_local;
REVOKE ALL ON SCHEMA bitrix_local FROM PUBLIC;
ALTER ROLE CURRENT_USER IN DATABASE han_chat SET search_path TO bitrix_local;
SHOW search_path; --чтобы заработало надо переподключиться
CREATE SCHEMA IF NOT EXISTS bitrix_sync;
REVOKE ALL ON SCHEMA bitrix_sync FROM PUBLIC;
ALTER ROLE CURRENT_USER IN DATABASE han_chat SET search_path TO bitrix_sync;
SHOW search_path; --чтобы заработало надо переподключиться
CREATE SCHEMA IF NOT EXISTS message_safety;
REVOKE ALL ON SCHEMA message_safety FROM PUBLIC;
ALTER ROLE CURRENT_USER IN DATABASE han_chat SET search_path TO message_safety;
SHOW search_path; --чтобы заработало надо переподключиться
CREATE SCHEMA IF NOT EXISTS keycloak;
REVOKE ALL ON SCHEMA keycloak FROM PUBLIC;
ALTER ROLE CURRENT_USER IN DATABASE han_chat SET search_path TO keycloak;
SHOW search_path; --чтобы заработало надо переподключиться
CREATE SCHEMA IF NOT EXISTS sms AUTHORIZATION sms_user;
REVOKE ALL ON SCHEMA sms FROM PUBLIC;
ALTER ROLE sms_user IN DATABASE han_chat SET search_path TO sms, public;
SHOW search_path;
Проверка search_path
SELECT r.rolname, d.datname, s.setconfig
FROM pg_db_role_setting s
JOIN pg_roles r ON r.oid = s.setrole
JOIN pg_database d ON d.oid = s.setdatabase
WHERE r.rolname in ('han_app',
'bitrix_local_app',
'bitrix_sync_user',
'message_safety_app',
'keycloak_user');
После реализации bitrix_sync (проверить, вероятно не на все таблицы права нужны):
-- Даем право на чтение (SELECT) всех СУЩЕСТВУЮЩИХ таблиц в схеме
GRANT SELECT, INSERT ON ALL TABLES IN SCHEMA han_app TO bitrix_sync_user;
-- Настраиваем права по умолчанию для новых таблиц
ALTER DEFAULT PRIVILEGES IN SCHEMA han_app
GRANT SELECT ON TABLES TO bitrix_sync_user;
-- Если нужно дать право на чтение и для новых последовательностей (sequences):
ALTER DEFAULT PRIVILEGES IN SCHEMA han_app
GRANT USAGE, SELECT ON SEQUENCES TO bitrix_sync_user;
## Заводим S3 хранилища:
Создаем 2 сервисных пользователя и заводим им ключи:
- API backend SELECTEL_S3_ACCESS_KEY / SELECTEL_S3_SECRET_KEY
- Message Safety SELECTEL_S3_QUARANTINE_READ_ACCESS_KEY / SELECTEL_S3_QUARANTINE_READ_SECRET_KEY
Создаем 3 приватных бакета
- han-chat-quarantine (политика доступа API backend RW, Message Safety R)
- han-chat-attachments (политика доступа API backend RW)
- han-chat-documents (политика доступа API backend RW)
RW:
ListBucket
GetBucketLocation
PutObject
GetObject
DeleteObject
ListBucketMultipartUploads
ListMultipartUploadParts
AbortMultipartUpload
R:
ListBucket
GetBucketLocation
GetObject
ListBucketMultipartUploads
ListMultipartUploadParts
Политика CORS (бакет han-chat-quarantine, vHosted обязателен):
Allowed origin: https://chat.han0107.ru
Methods: POST, PUT, GET, HEAD
Headers: * (или явно content-type; wildcard x-amz-* в Selectel не работает)
Expose headers: ETag
# Тест инфраструктуры
mkdir -p /opt/han-chat/infratest
cd /opt/han-chat/infratest
scp -i C:\Users\MI\.ssh\hansel -r "C:\Users\MI\Documents\Assistent\HAN_chat_specification\infratest\*" root@135.106.164.58:/opt/han-chat/infratest
--настраиваю env на локальной машине и копирую на диск
scp -i C:\Users\MI\.ssh\hansel -r "C:\Users\MI\Documents\job\HAN_new_life\HANapp\Production\.env" root@135.106.164.58:/opt/han-chat/infratest/
Создаем изолированное окружение:
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install -r requirements.txt
Если не работает, то надо DNS переправить на eth0
resolvectl dns eth0 1.1.1.1 8.8.8.8
resolvectl domain eth0 '~.'
resolvectl flush-caches
resolvectl query pypi.org
# Подготовка к запуску
Сертификат рекомендуется скопировать в папку с секретами
cp /root/.postgresql/root.crt /opt/han-chat/backend/secrets/pg/root.crt
chmod 644 /opt/han-chat/backend/secrets/pg/root.crt
В битрикс регистрируем локальное приложение:
Установка: https://chat.example.ru/bitrix/install
Обработчик: https://chat.example.ru/bitrix/handler
??? (не понадобилось) Placement: https://chat.example.ru/bitrix/placement
--настраиваю env на локальной машине и копирую на диск
scp -i C:\Users\MI\.ssh\hansel -r "C:\Users\MI\Documents\job\HAN_new_life\HANapp\Production\.env" root@135.106.164.58:/opt/han-chat/backend
--Проверка .env
cd /opt/han-chat/backend
chmod 600 .env
./scripts/validate-env .env
docker compose --env-file .env config --quiet
docker compose --env-file .env config --services
python3 -m unittest discover -s tests -v
--Проверка портов (Публиковаться должны только 80 и 443 у nginx)
docker compose --env-file .env config | grep -n 'published:'
--Собираем локальные образы:
docker compose --env-file .env build --pull
docker compose --env-file .env images
## Миграции БД и начальные настройки
cd /opt/han-chat/backend
PITR_MARKER_CONFIRMED=true deployment/scripts/migrate.sh
deployment/scripts/seed.sh
## Запуск внутренних сервисов
Сначала запустите Redis:
```sh
docker compose --env-file .env up -d redis
docker compose --env-file .env ps redis
```
Затем Keycloak и OpenTelemetry:
```sh
docker compose --env-file .env up -d keycloak otel-collector
docker compose --env-file .env ps keycloak otel-collector
```
Первый запуск Keycloak может занять несколько минут: он создаст свои таблицы и
импортирует realm `han-chat`.
После готовности Keycloak:
```sh
docker compose --env-file .env up -d message-safety
docker compose --env-file .env up -d api-backend
docker compose --env-file .env up -d bitrix-local-app bitrix-sync
docker compose --env-file .env up -d \
delivery-worker safety-recovery-worker cleanup-worker
docker compose --env-file .env ps
```
Если сервис не становится healthy:
```sh
docker compose --env-file .env logs --tail=200 <SERVICE_NAME>
docker inspect "$(docker compose --env-file .env ps -q <SERVICE_NAME>)"
```
## Первоначальный выпуск TLS-сертификата
Для ACME требуется работающий nginx по HTTP. В `.env` оставьте
`NGINX_TLS_ENABLED=true`, но первый nginx запустите с временным переопределением:
```sh
NGINX_TLS_ENABLED=false \
docker compose --env-file .env up -d frontend-static nginx
```
Проверьте HTTP:
```sh
curl -I http://chat.han0107.ru/
```
Сначала рекомендуется проверить Certbot через staging:
```sh
docker compose --env-file .env --profile certbot run --rm certbot certonly \
--staging \
--webroot -w /var/www/certbot \
-d chat.han0107.ru \
--cert-name chat.han0107.ru-staging \
--email ap@han.ru \
--agree-tos --no-eff-email --non-interactive
```
После успешного staging-теста выпустите рабочий сертификат с основным cert-name
без `--staging`:
```sh
docker compose --env-file .env --profile certbot run --rm certbot certonly \
--webroot -w /var/www/certbot \
-d chat.han0107.ru \
--cert-name chat.han0107.ru \
--email ap@han.ru \
--agree-tos --no-eff-email --non-interactive
```
Пересоздайте nginx уже с TLS:
```sh
docker compose --env-file .env up -d --force-recreate nginx
docker compose --env-file .env exec -T nginx nginx -t -c /tmp/nginx.conf
curl -I https://chat.han0107.ru/
```
Повторно запустите VM setup, чтобы он обнаружил проект и установил systemd-таймер
продления сертификата:
```sh
sudo /opt/han-chat/backend/deployment/scripts/setup-vm.sh
systemctl status han-chat-ssl-renew.timer
## Запуск всего контура
Теперь можно привести весь проект к состоянию, описанному Compose:
```sh
cd /opt/han-chat/backend
docker compose --env-file .env up -d
docker compose --env-file .env ps
```
Проверьте, что контейнеры не перезапускаются:
```sh
docker compose --env-file .env ps
docker compose --env-file .env logs --since=10m
```
## Публичная проверка
Запустите smoke-тест:
```sh
cd /opt/han-chat/backend
deployment/scripts/smoke.sh
```
Также вручную проверьте:
```sh
curl -fsS https://chat.han0107.ru/api/v1/public/app-config | jq
curl -fsS https://chat.han0107.ru/api/v1/public/content | jq
curl -fsS \
https://chat.han0107.ru/auth/realms/han-chat/.well-known/openid-configuration | jq
```
Внутренний API не должен быть опубликован:
```sh
curl -i https://chat.han0107.ru/internal/safety/v1/messages/check
```
Ожидаемый статус — `404`.
Откройте в браузере:
```text
https://chat.example.ru/
```
Для тестовой авторизации используйте значение `KEYCLOAK_OTP_MOCK_CODE` из `.env`.
Настройки keykcloack:
Для битрикса код установки приложения:
docker compose --env-file .env exec -T api-backend python - <<'PY'
import os
import urllib.request
base = os.environ["BITRIX_LOCAL_APP_BASE_URL"]
token = os.environ["BITRIX_LOCAL_APP_INTERNAL_TOKEN"]
request = urllib.request.Request(
base + "/internal/openlines/v1/setup/retry",
method="POST",
headers={"Authorization": "Bearer " + token},
)
print(urllib.request.urlopen(request).read().decode())
PY
## План включения реальной SMS-авторизации
Этот раздел — чек-лист будущего release из `modules/module-11-idgtl-sms.md`, а не подтверждение готовности текущего Compose. Пока отсутствуют реализованные `sms-service`/worker, migrations, callback route и env validation, оставлять `KEYCLOAK_OTP_MOCK_ENABLED=true`.
Prerequisites без placeholders:
- согласованные i-Digital sender и active approved template `auth_otp` с placeholders `code`, `ttl_min`;
- выданный Direct `TOKEN_1` (`IDGTL_SMS_API_KEY`, без повторного Base64);
- отдельные random callback username/password и публичный HTTPS URL;
- повторно подтверждённый source IP callback Direct;
- фактический статический egress IP, измеренный из `sms-worker`, записанный в inventory и переданный Direct для allowlist; при динамическом IP сначала настроить NAT/static IP.
Rollout:
1. Seed новых `otp.phone.*` в App DB.
2. Создать schema/role `sms`, применить versioned migrations и seed template/settings.
3. Проверить Keycloak→`sms-service`→локальный mock Direct в test environment.
4. Развернуть production `sms-service`/worker и nginx callback route, не выключая mock.
5. Применить Keycloak expand migration/SPI; прежние незавершённые challenges истечь по module-11.
6. Выполнить provider smoke на контролируемом номере; проверить `sms_message_id`, journal, callback и redaction.
7. Переключить `KEYCLOAK_OTP_MOCK_ENABLED=false`.
8. Проверить resend→`superseded`, expiry snapshot, limits и то, что Direct reject/timeout после durable order не меняет verify.
Rollback: вернуть Keycloak в mock mode; не удалять schema/journal. Остановить новые real orders, дать worker завершить либо зафиксировать in-flight/`uncertain`. Schema downgrade только при доказанной backward compatibility, иначе forward-fix.
+405
View File
@@ -0,0 +1,405 @@
# #1 SMS OTP deploy
Безопасный порядок развёртывания `sms-service`/worker на существующей ВМ и включения реальной OTP-доставки: сначала подготовить PostgreSQL и секреты, затем запустить новый контур при `mock=true`, проверить i-Digital и только после этого переключить Keycloak.
Старую сборку Keycloak после expand-миграции возвращать нельзя. Аварийный откат выполняется переключением новой сборки обратно в mock-режим.
## 0. До начала
- Получить у i-Digital:
- `TOKEN_1`;
- согласованное имя отправителя;
- согласованный текст `auth_otp`;
- подтверждённый source IP для callback;
- регистрацию статического egress IP ВМ.
- Создать PITR marker/backup managed PostgreSQL.
- Скопировать `/opt/han-chat/backend/.env` в защищённое место вне каталога релиза.
- Оставить `KEYCLOAK_OTP_MOCK_ENABLED=true` до последнего этапа.
- На production-like при mock-режиме оставить `KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true`.
- Подтвердить у Direct актуальность IP `185.203.96.7`, указанного в `codebase/backend/nginx/templates/site-tls.conf.template`. Если IP другой — обновить allowlist до сборки nginx.
## 1. Создать пользователя и схему PostgreSQL
### 1.1. Создать пользователя
В интерфейсе Selectel создать отдельного пользователя:
```text
sms_user
```
Использовать случайный пароль не короче 32 символов.
### 1.2. Создать схему
Подключиться к `han_chat` под `dbAdmin` и выполнить:
```sql
GRANT CONNECT ON DATABASE han_chat TO sms_user;
GRANT CREATE ON DATABASE han_chat TO sms_user;
CREATE SCHEMA IF NOT EXISTS sms AUTHORIZATION sms_user;
REVOKE ALL ON SCHEMA sms FROM PUBLIC;
ALTER ROLE sms_user IN DATABASE han_chat SET search_path TO sms, public;
```
### 1.3. Проверить
Переподключиться к БД как `sms_user`:
```sql
SELECT current_user;
SHOW search_path;
SELECT
nspname,
pg_get_userbyid(nspowner) AS owner
FROM pg_namespace
WHERE nspname = 'sms';
```
Ожидаемый результат:
- `current_user = sms_user`;
- `search_path = sms, public`;
- владелец схемы `sms``sms_user`.
Не выдавать `sms_user` права на схемы `han_app` и `keycloak`.
## 2. Заполнить `.env` на ВМ
Файл:
```text
/opt/han-chat/backend/.env
```
Добавить или обновить:
```dotenv
SMS_SERVICE_IMAGE=han-chat-sms-service:local
SMS_DATABASE_URL=postgresql+asyncpg://sms_user:<URL_ENCODED_PASSWORD>@<PG_HOST>:<PG_PORT>/han_chat?sslmode=verify-full&sslrootcert=/run/secrets/pg-ca.pem
KEYCLOAK_SMS_SERVICE_URL=http://sms-service:8080
SMS_SERVICE_TOKEN=<openssl rand -hex 32>
KEYCLOAK_SMS_SERVICE_TOKEN=<ТОЧНО ТО ЖЕ ЗНАЧЕНИЕ>
IDGTL_SMS_BASE_URL=https://direct.i-dgtl.ru
IDGTL_SMS_API_KEY=<ГОТОВЫЙ TOKEN_1 БЕЗ ПОВТОРНОГО BASE64>
IDGTL_SMS_CALLBACK_PUBLIC_URL=https://<PUBLIC_HOST>/callbacks/idgtl/sms
IDGTL_SMS_CALLBACK_USERNAME=<openssl rand -hex 16>
IDGTL_SMS_CALLBACK_PASSWORD=<openssl rand -hex 32>
NGINX_RATE_LIMIT_SMS_CALLBACK=120r/m
KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true
```
Важно:
- пароль БД необходимо URL-encode, если он содержит специальные символы;
- `SMS_SERVICE_TOKEN` и `KEYCLOAK_SMS_SERVICE_TOKEN` должны совпадать;
- `IDGTL_SMS_API_KEY` — уже готовое значение Basic API key `TOKEN_1`, повторно кодировать его нельзя;
- `KEYCLOAK_OTP_HMAC_KEY` во время rollout не менять.
### 2.1. Проверить egress IP
Из каталога `/opt/han-chat/backend`:
```bash
docker compose --env-file .env --profile ops run --rm \
--entrypoint curl toolbox -fsS https://api.ipify.org
```
Полученный IP передать Direct для allowlist. При динамическом IP сначала настроить статический IP/NAT.
### 2.2. Проверить конфигурацию
```bash
cd /opt/han-chat/backend
./scripts/validate-env .env
docker compose --env-file .env config --quiet
docker compose --env-file .env config --services
```
## 3. Скопировать и собрать release
Копирование проекта выполняется по инструкции `deploy-steps.md`.
Сначала выполнить `rsync` с флагом `-n` и проверить список изменений. Убедиться, что исключены:
```text
.env
secrets/
*.crt
*.pem
*.key
```
После проверки повторить `rsync` без `-n`.
На ВМ:
```bash
cd /opt/han-chat/backend
find . -type f \( -name '*.sh' -o -name 'validate-env' \) -exec dos2unix {} +
chmod +x scripts/validate-env deployment/scripts/*.sh nginx/scripts/*.sh
./scripts/validate-env .env
docker compose --env-file .env build --pull \
api-backend sms-service keycloak frontend-static nginx
```
На этом этапе `KEYCLOAK_OTP_MOCK_ENABLED` всё ещё должен быть `true`.
## 4. Применить миграции
Перед миграцией создать PITR marker у провайдера БД.
```bash
cd /opt/han-chat/backend
PITR_MARKER_CONFIRMED=true deployment/scripts/migrate.sh
```
Команда применит:
- migration `0005_otp_settings` для `han_app`;
- migration `0001_initial` для схемы `sms`;
- migration `0002_seed` для схемы `sms`;
- остальные штатные migrations проекта.
При необходимости применить production-like settings:
```bash
docker compose --env-file .env --profile ops run --rm seed-settings
```
Проверить версии:
```sql
SELECT version_num FROM han_app.alembic_version;
SELECT version_num FROM sms.alembic_version;
```
Ожидается:
```text
han_app: 0005_otp_settings
sms: 0002_seed
```
## 5. Записать согласованные sender и SMS-шаблон
Миграция намеренно создаёт placeholder. Пока он не заменён, `sms-service` будет возвращать `not_ready`.
Подключиться как `sms_user` и выполнить, подставив согласованные значения:
```sql
UPDATE sms.sms_setting
SET setting_value = to_jsonb('<APPROVED_SENDER>'::text),
updated_at = now()
WHERE setting_key = 'provider.idgtl.default_sender_name';
UPDATE sms.sms_template
SET body_template = 'Код входа в HAN Chat: {code}. Действителен {ttl_min} мин.',
sender_name = NULL,
approved_at = now(),
updated_at = now(),
created_by = 'ops-approved'
WHERE code = 'auth_otp'
AND channel = 'SMS'
AND locale = 'ru'
AND version = 1;
```
Если оператор согласовал другой текст, использовать именно его. В тексте должны остаться ровно два placeholders:
```text
{code}
{ttl_min}
```
Для OTP должно сохраняться:
```text
max_parts = 1
```
Проверить:
```sql
SELECT
code,
channel,
locale,
version,
body_template,
sender_name,
max_parts,
is_active,
approved_at
FROM sms.sms_template
WHERE code = 'auth_otp';
SELECT setting_key, setting_value
FROM sms.sms_setting
ORDER BY setting_key;
```
Должна существовать ровно одна active+approved версия `auth_otp`, а placeholder имени отправителя должен быть заменён.
## 6. Запустить SMS-контур при `mock=true`
```bash
cd /opt/han-chat/backend
docker compose --env-file .env up -d sms-service sms-worker
docker compose --env-file .env ps sms-service sms-worker
docker compose --env-file .env logs --since=10m sms-service sms-worker
```
Ожидается:
- `sms-service` — healthy;
- worker запущен;
- отсутствуют ошибки Direct `401`/`402`;
- отсутствуют contract errors;
- отсутствуют необъяснённые `uncertain`.
Затем запустить обновлённые смежные сервисы, не выключая mock:
```bash
docker compose --env-file .env up -d --force-recreate \
api-backend keycloak frontend-static nginx
docker compose --env-file .env exec -T nginx nginx -t -c /tmp/nginx.conf
deployment/scripts/smoke.sh
```
Первый запуск новой сборки Keycloak применит Liquibase expand migration. Старые незавершённые OTP challenges будут помечены истёкшими, поэтому запускать Keycloak лучше в период низкой активности.
## 7. Проверить i-Digital до включения real mode
Через внутренний endpoint:
```text
POST /internal/sms/v1/send
```
заказать одну SMS на контролируемый номер.
Требования к тесту:
- использовать уникальный `idempotency_key`;
- не записывать service token и OTP в shell history;
- JSON body создать во временном файле с правами `600`;
- после теста удалить временный файл.
Проверить журнал:
```sql
SELECT
id,
created_at,
phone_masked,
send_status,
delivery_status,
provider_message_id,
provider_error_code,
attempt_count,
callback_last_at
FROM sms.sms_outbound_message
ORDER BY created_at DESC
LIMIT 10;
```
Ожидается:
1. После заказа создана одна строка.
2. `send_status` переходит в `accepted`.
3. `provider_message_id` заполнен.
4. Callback меняет `delivery_status` на `sent`/`delivered`.
5. Повтор идентичного запроса возвращает тот же `sms_message_id` и не создаёт вторую SMS.
Проверить edge:
- публичный `/internal/sms/*` возвращает `404`;
- callback не с IP Direct возвращает `403`;
- реальный callback Direct проходит IP allowlist и Basic auth.
## 8. Включить реальные SMS
Только после успешной тестовой отправки изменить:
```dotenv
KEYCLOAK_OTP_MOCK_ENABLED=false
KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=false
KEYCLOAK_OTP_MOCK_CODE=
```
Применить:
```bash
cd /opt/han-chat/backend
./scripts/validate-env .env
docker compose --env-file .env up -d \
--no-deps \
--force-recreate keycloak
docker compose --env-file .env ps keycloak
docker compose --env-file .env logs --since=10m \
keycloak sms-service sms-worker
```
Проверить полный пользовательский сценарий:
1. Ввод номера телефона.
2. Получение реальной SMS.
3. Неверный OTP отклоняется.
4. Верный OTP авторизует пользователя.
5. Resend создаёт новый challenge.
6. Старый challenge получает `superseded`.
7. Старый код больше не принимается.
8. OTP истекает через 60 секунд.
9. Работают лимиты отправок и проверок.
10. Уже active challenge продолжает локально проверяться при временно остановленном worker.
## 9. Аварийный откат
Не выполнять:
- downgrade Alembic;
- downgrade Liquibase;
- возврат старой сборки Keycloak.
После expand migration старая сборка Keycloak несовместима с новыми обязательными полями challenge.
Безопасный rollback — оставить новую сборку и вернуть mock:
```dotenv
KEYCLOAK_OTP_MOCK_ENABLED=true
KEYCLOAK_OTP_MOCK_CODE=<НЕПУБЛИЧНЫЙ 6-ЗНАЧНЫЙ КОД>
KEYCLOAK_OTP_MOCK_RISK_ACCEPTED=true
```
```bash
cd /opt/han-chat/backend
./scripts/validate-env .env
docker compose --env-file .env up -d \
--no-deps \
--force-recreate keycloak
```
`sms-service` и worker можно оставить запущенными для обработки callback и reconciliation. Новые SMS-заказы от Keycloak прекратятся.
Записи со статусом `uncertain` автоматически не переотправлять — их необходимо разбирать вручную.