up
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
.PHONY: check build prod-build publish venue venue-update
|
||||
.PHONY: check build prod-build publish venue venue-update venue-https
|
||||
|
||||
PROD_DIR ?= prod
|
||||
|
||||
@@ -23,8 +23,7 @@ prod-build: build
|
||||
venue:
|
||||
bash venue/deploy.sh
|
||||
|
||||
venue-update:
|
||||
bash venue/deploy.sh --build
|
||||
venue-update venue-https: venue
|
||||
|
||||
publish:
|
||||
@test -n "$(MSG)" || (echo 'Usage: make publish MSG="commit message"' >&2; exit 1)
|
||||
|
||||
@@ -61,20 +61,26 @@ make dev-user
|
||||
По умолчанию создаётся пользователь `dev@example.com` с паролем `dev123456`.
|
||||
Для изменения используйте `DEV_USER_EMAIL` и `DEV_USER_PASSWORD`.
|
||||
|
||||
## Автономная площадка (ED-029, этапы 1–2)
|
||||
## Автономная площадка (ED-029)
|
||||
|
||||
`venue/` — окружение для проведения игры на площадке без интернета.
|
||||
Локальная разработка описана выше и запускается отдельно.
|
||||
|
||||
Из корня workspace запустите:
|
||||
После подготовки сертификата и локального DNS (инструкция ниже) из корня workspace запустите:
|
||||
|
||||
```sh
|
||||
make prod-build
|
||||
make venue
|
||||
```
|
||||
|
||||
`make prod-build` заранее готовит артефакты для `prod/` и `venue/`.
|
||||
Деплой не компилирует Go/Vue и не устанавливает зависимости исходного кода.
|
||||
Если артефактов нет, запуск завершится с предложением выполнить `make prod-build`.
|
||||
Первый запуск с интернетом сам создаёт настройки и случайные секреты в
|
||||
`venue/.env`, готовит образы, запускает миграции и создаёт организатора.
|
||||
Повторные запуски используют готовое окружение без интернета.
|
||||
`venue/.env`, готовит Docker-образы из готовых файлов, запускает миграции и создаёт организатора.
|
||||
Каждый `make venue` пересобирает Docker-образ из готовых файлов и принудительно
|
||||
пересоздаёт контейнеры с HTTPS. При наличии runtime-образов и Alpine запуск
|
||||
работает без интернета; отсутствующие образы скачиваются при первой подготовке.
|
||||
Email и пароль для входа указаны в `VENUE_USER_EMAIL` и `VENUE_USER_PASSWORD`
|
||||
в `venue/.env`; существующий файл настроек не перезаписывается.
|
||||
Аккаунт организатора при запуске синхронизируется с этими значениями.
|
||||
@@ -82,27 +88,25 @@ Email и пароль для входа указаны в `VENUE_USER_EMAIL` и
|
||||
После изменения кода обновите площадку, пока интернет доступен:
|
||||
|
||||
```sh
|
||||
make venue-update
|
||||
make prod-build
|
||||
make venue
|
||||
```
|
||||
|
||||
Сайт доступен по `http://localhost:8088`; API и материалы обслуживаются тем же
|
||||
прокси. Для подключения по LAN задайте `VENUE_BIND_IP=0.0.0.0` и
|
||||
`APP_BASE_URL=http://<постоянный-IP-ноутбука>:8088` в `venue/.env`, затем
|
||||
повторите `make venue`. Прежние домены и доверенный HTTPS
|
||||
требуют отдельной настройки DNS и сертификатов на этапе 3.
|
||||
Сайт доступен по `https://evening-detective.crabs-games.art`; API и материалы
|
||||
обслуживаются тем же прокси. Для подключения по LAN задайте постоянный IP ноутбука
|
||||
в `VENUE_BIND_IP` в `venue/.env` и настройте локальный DNS, как описано ниже.
|
||||
|
||||
Образы приложения, мигратора и прокси готовятся заранее. Runtime запускается
|
||||
без сборки и скачивания; приложение, база и RustFS подключены только к
|
||||
внутренней Docker-сети. Прокси дополнительно подключён к сети публикации порта.
|
||||
Приложение, база и RustFS подключены только к внутренней Docker-сети.
|
||||
Прокси дополнительно подключён к сети публикации порта.
|
||||
База и RustFS сохраняются в именованных томах.
|
||||
Миграции Goose выполняются до старта приложения. Для остановки без потери данных:
|
||||
|
||||
```sh
|
||||
docker compose -f venue/docker-compose.yml --env-file venue/.env down
|
||||
docker compose -f venue/docker-compose.yml -f venue/docker-compose.https.yml --env-file venue/.env down
|
||||
```
|
||||
|
||||
Не используйте `down -v`: это удаляет данные. После изменений кода или миграций
|
||||
выполните `make venue-update`, пока интернет доступен. Секреты, сборка,
|
||||
выполните `make venue`, пока интернет доступен. Секреты, сборка,
|
||||
сертификаты и пакеты игры игнорируются Git в `venue/`.
|
||||
|
||||
`APP_MODE=venue` отключает SMTP, фоновые письма участия/отзывов и внешний webhook
|
||||
@@ -121,8 +125,7 @@ docker compose -f venue/docker-compose.yml --env-file venue/.env down
|
||||
```sh
|
||||
python3 tests/venue/deploy.py
|
||||
python3 tests/venue/smoke.py
|
||||
docker compose -f venue/docker-compose.yml --env-file venue/.env down
|
||||
docker compose -f venue/docker-compose.yml --env-file venue/.env up -d --no-build --pull never
|
||||
make venue
|
||||
python3 tests/venue/smoke.py --verify
|
||||
```
|
||||
|
||||
@@ -130,6 +133,98 @@ python3 tests/venue/smoke.py --verify
|
||||
Тестовые данные остаются в локальной базе; повторное создание запускайте только
|
||||
когда нужен новый набор. Дождитесь ответа `/api/test/ping` перед проверкой.
|
||||
|
||||
### Прежние домены и HTTPS (этап 3)
|
||||
|
||||
Подготовка в репозитории готова; проверка реального Cudy и Android/iPhone без WAN
|
||||
ещё требуется. `make venue` всегда запускает HTTPS с пересборкой Docker-образа
|
||||
и перезапуском контейнеров. Для первого запуска:
|
||||
|
||||
1. Пока интернет доступен, подготовьте артефакты через `make prod-build`.
|
||||
Запуск `make venue` выполните после подготовки сертификата и локального DNS.
|
||||
2. Выпустите отдельный доверенный сертификат с новым ключом для двух имён:
|
||||
`evening-detective.crabs-games.art` и `evening-detective-api.crabs-games.art`.
|
||||
Используйте DNS-01: проверяются TXT-записи `_acme-challenge` в публичном DNS;
|
||||
публичные A/AAAA-записи production менять не нужно. Не копируйте ключ production.
|
||||
Например, на машине с установленным Certbot:
|
||||
|
||||
```sh
|
||||
certbot certonly --manual --preferred-challenges dns \
|
||||
--cert-name evening-detective-venue \
|
||||
-d evening-detective.crabs-games.art \
|
||||
-d evening-detective-api.crabs-games.art
|
||||
```
|
||||
|
||||
Следуйте подсказкам Certbot для TXT-записей. Выпуск сертификата выполняется
|
||||
отдельно от приложения; Certbot не входит в runtime площадки.
|
||||
3. Скопируйте полную цепочку (сертификат сервера и промежуточные сертификаты) в
|
||||
`venue/certificates/fullchain.pem`, незашифрованный закрытый ключ — в
|
||||
`venue/certificates/privkey.pem`. Ограничьте доступ к каталогу и ключу владельцем
|
||||
(режимы 700 и 600); процесс Nginx в контейнере должен иметь доступ на чтение.
|
||||
Каталог игнорируется Git. Перед игрой проверьте дату окончания сертификата:
|
||||
|
||||
```sh
|
||||
openssl x509 -in venue/certificates/fullchain.pem -noout -dates
|
||||
```
|
||||
|
||||
4. На Cudy вручную закрепите IP ноутбука и настройте локальный DNS: оба домена
|
||||
должны указывать на этот IP. DHCP должен выдавать клиентам адрес локального
|
||||
DNS. Возможность такой настройки проверяется на конкретной модели и прошивке;
|
||||
выбор внешнего «Custom DNS» сам по себе не создаёт локальные записи.
|
||||
Проверьте клиентскую изоляцию, DNS rebind protection и IPv6: клиенты должны
|
||||
достигать ноутбука, а AAAA-записи не должны уводить их на production.
|
||||
При отсутствии нужных возможностей прошивки остановите настройку и согласуйте
|
||||
отдельное решение; скрипты проекта не меняют роутер и не прошивают его.
|
||||
5. В `venue/.env` задайте `VENUE_BIND_IP=<постоянный-IP-ноутбука>` и разрешите
|
||||
входящие TCP 80/443 из LAN в межсетевом экране ноутбука. Порты должны быть свободны.
|
||||
Для HTTPS менять `APP_BASE_URL` в `.env` не нужно: Compose-дополнение задаёт
|
||||
прежний HTTPS-домен интерфейса для QR-ссылок и материалов.
|
||||
6. Запустите:
|
||||
|
||||
```sh
|
||||
make venue
|
||||
```
|
||||
|
||||
Команда проверяет доверие цепочке через системное хранилище OpenSSL, оба домена,
|
||||
срок действия и соответствие ключа до обращения к Docker. Ошибка не переключает
|
||||
работающее окружение на HTTP. Docker-образ приложения пересобирается при каждом
|
||||
запуске из готовых артефактов, без компиляции Go/Vue. При наличии всех базовых
|
||||
образов интернет не нужен. Доверие на реальных телефонах проверяется отдельно.
|
||||
|
||||
Интерфейс открывается по `https://evening-detective.crabs-games.art`, API — по
|
||||
`https://evening-detective-api.crabs-games.art`. Интерфейс использует свой `/api/`;
|
||||
прежний API-домен также обслуживается локально. HTTP на портах 80 и прежнем
|
||||
`VENUE_PORT` перенаправляется на HTTPS с сохранением пути и параметров.
|
||||
|
||||
После продления сертификата с доступным интернетом замените оба PEM-файла и
|
||||
повторите `make venue`: Nginx проверяет конфигурацию и перечитывает сертификат.
|
||||
Продление не выполняется автоматически. После обновления кода выполните
|
||||
`make prod-build`, затем `make venue`; контейнеры всегда перезапускаются с HTTPS.
|
||||
|
||||
Для остановки HTTPS-окружения с сохранением данных:
|
||||
|
||||
```sh
|
||||
docker compose -f venue/docker-compose.yml -f venue/docker-compose.https.yml \
|
||||
--env-file venue/.env down
|
||||
```
|
||||
|
||||
Проверка после настройки DNS и запуска (первый smoke создаёт тестовые данные):
|
||||
|
||||
```sh
|
||||
python3 tests/venue/smoke.py
|
||||
python3 tests/venue/smoke.py --verify
|
||||
```
|
||||
|
||||
TLS-проверка в smoke включена; самоподписанный сертификат и обход предупреждений
|
||||
не подходят для приёмки. На Android и iPhone отключите WAN и мобильные данные,
|
||||
подключитесь к Wi-Fi площадки и проверьте DNS обоих доменов, отсутствие TLS-ошибок,
|
||||
вход организатора, материалы и QR-ссылки тестовой игры. Повторите после перезапуска.
|
||||
Проверьте запрос доступа к камере отдельно: распознавание QR в текущем интерфейсе
|
||||
также требует поддержки браузером `BarcodeDetector`.
|
||||
|
||||
Этап 3 сохраняет адреса, но не переносит данные production. Production-QR может
|
||||
вести на игру или билет, которых в локальной базе ещё нет; перенос и возврат
|
||||
данных относятся к этапам 4–11. До их завершения используйте только тестовые игры.
|
||||
|
||||
## Проверки перед изменениями
|
||||
|
||||
Сервер:
|
||||
@@ -178,8 +273,9 @@ API через свой прокси (`VITE_API_URL=/.`). Локальные Goo
|
||||
make build
|
||||
```
|
||||
|
||||
`make prod-build` выполняет ту же общую сборку. `make venue-update` вызывает её
|
||||
и дополнительно подготавливает Docker-образы для автономного запуска. Команды
|
||||
`make prod-build` выполняет ту же общую сборку. `make venue` пересобирает
|
||||
Docker-образ из готовых артефактов и перезапускает площадку с HTTPS.
|
||||
`make venue-update` и `make venue-https` сохранены как алиасы `make venue`. Команды
|
||||
обновляют только генерируемые артефакты в `prod`; конфигурация `prod` не меняется.
|
||||
|
||||
## Окружение и секреты
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
Архив задач, переведённых в статус `DONE`.
|
||||
|
||||
### ED-030 — Индикатор среды сервиса в интерфейсе
|
||||
|
||||
- Статус: `DONE`
|
||||
- Репозитории: `evening_detective_server`, `evening_detective_frontend`.
|
||||
- Связь: дополнение к ED-029; не требует готовности DNS/HTTPS и Cudy.
|
||||
- Цель: пользователь видит, к какой среде подключён интерфейс: `PROD` или `VENUE`.
|
||||
- Решение: показывать компактную подпись среды в общем интерфейсе рядом с навигацией; получать среду от подключённого бэкенда по фактическому режиму запуска (`APP_MODE=venue` — площадка, штатный онлайн-режим — production).
|
||||
- Критерии: подпись видна на компьютере и телефоне; PROD и VENUE различаются текстом, а не только цветом; среда определяется сервером, а не адресом сайта или настройкой сборки фронтенда; при загрузке или ошибке получения среды интерфейс не показывает неподтверждённый PROD/VENUE.
|
||||
- Ограничения: без переключения серверов, изменения игровых правил, показа секретов, новых зависимостей и изменения папки `prod`; использовать существующую навигацию, API и стандарты обоих репозиториев.
|
||||
- Проверка: ручной прогон обоих режимов и недоступного API; Go-тесты; frontend type-check, lint, lint:css и build; при изменении protobuf — штатная генерация обоих клиентов.
|
||||
- Результат: поле `environment` добавлено в существующий публичный GetPermissions API; значение определяется тем же режимом запуска бэкенда, который отключает внешние интеграции площадки. В HeaderMenu добавлены русские подписи «Сервер» (PROD) и «Локальная сеть» (VENUE) через Naive UI; загрузка, неизвестная среда и ошибки не подменяются production. Сведения меню хранятся в общем UI-store; устаревшие ответы не заменяют новые, а права не применяются после смены токена.
|
||||
- Проверки: штатные `make generate` и `npm run proto:gen`, `gofmt`, `go test ./...`, `npm run type-check`, `npm run lint`, `npm run lint:css`, `npm run build` и `git diff --check` прошли. Табличный Go-тест проверяет оба режима для гостя, пользователя и администратора. Firefox headless с подменой API проверил PROD/VENUE до и после входа, загрузку, серверную и сетевую ошибки, неизвестное значение; подпись находится внутри viewport при ширине 1440 и 390 px.
|
||||
- Ревью: diff проверен по стандартам обоих репозиториев; зависимости не добавлены, generated-файлы получены штатной генерацией. Сборочные изменения `dist` исключены из diff; папка `prod` и существующие локальные артефакты бэкенда не менялись.
|
||||
- Ограничение проверки: реальные Android/iPhone и развёртывание обновлённой версии не проверялись; для появления индикатора нужно обновить бэкенд и фронтенд. Миграция БД не требуется. Commit и push не выполнялись.
|
||||
|
||||
### ED-028 — Роль разработчика и общий таскменеджер
|
||||
|
||||
- Статус: `DONE`
|
||||
|
||||
@@ -25,6 +25,29 @@
|
||||
|
||||
## Активные задачи
|
||||
|
||||
### ED-029 — Автономная площадка и панель организатора
|
||||
|
||||
- Статус: `BLOCKED` — подготовка этапа 3 в репозитории завершена; приёмка требует параметров и доступа к Cudy, отдельного доверенного сертификата и проверки Android/iPhone без WAN.
|
||||
- Репозитории: корневой, `evening_detective_server`, `evening_detective_frontend`.
|
||||
- Цель: реализовать принятый пользователем план из 15 последовательных этапов: автономное окружение; локальные организаторы; прежние адреса через Cudy; пакет игры; экспорт с блокировкой; локальный импорт; интерфейс передачи; полный билетный и игровой flow; пакет результатов; возврат онлайн; интерфейс возврата; устройства; панель состояния; сбор данных Cudy; Wi-Fi и испытание площадки.
|
||||
- Критерии: проверяемый результат каждого этапа; цикл «онлайн → ноутбук → проведение → онлайн» через интерфейс с сохранением QR, билетов и связей; повторные импорты без дублей; мониторинг устройств и Wi-Fi; полный прогон без WAN, 100 клиентов и проверка покрытия.
|
||||
- Ограничения: только тестовые игры до завершения возврата; существующие правила игры и формат `offline` сохраняются; без новых зависимостей и отклонений от стандартов; без персональных списков игроков, CPU/памяти Cudy, записи настроек роутера и автоматического переключения серверов; секреты и пакеты вне Git; commit/push выполняет пользователь.
|
||||
- Проверка: Go-стандарт и `go test ./...`; PostgreSQL-интеграционные тесты для атомарности; frontend-стандарт, type-check, lint, lint:css и build; штатная генерация protobuf; ручные контрольные точки после этапов 3, 8, 11, 15.
|
||||
- Прогресс: этапы 1–2 реализованы и проверены в локальном Docker-окружении. Добавлены отдельный Compose, подготовка образов заранее, постоянные тома, миграции перед стартом, healthchecks и прокси. Явный `APP_MODE=venue` отключает SMTP, фоновые письма и внешний webhook; почтовые операции аккаунта возвращают безопасную ошибку до изменения данных. Локальные аккаунты создаются штатным `dev-user.sql`, роли и правила автоматического подтверждения команд сохраняются. Подготовка этапа 3 реализована; его аппаратная приёмка и этапы 4–15 ещё не завершены.
|
||||
- Дополнение по запросу пользователя: структура `venue` приведена к `prod` (`backend/`, `frontend/dist/`, `migrations/`, `Dockerfile.backend`, `docker-compose.yml`, `deploy.sh`); имена основных Compose-сервисов совпадают. Общая генерация `make build` / `make prod-build` готовит артефакты обеих папок; frontend собирается отдельно с настройками своего окружения. Текущие файлы `prod` при этой работе не изменяются; сборка проверяется с production-выводом в `/tmp`.
|
||||
- Упрощение запуска: отдельные `init.sh` и `prepare.sh` удалены, всё собрано в `deploy.sh`. Единственная команда запуска — `make venue`: создаёт `.env` только при его отсутствии, при необходимости готовит зависимости/образы и автоматически создаёт организатора. Повторный готовый запуск не собирает и не скачивает образы. `make venue-update` явно пересобирает окружение с интернетом. Аккаунт организатора синхронизируется со значениями `.env` при каждом запуске. Первый запуск, повторный запуск без интернета и обновление проверяются `python3 tests/venue/deploy.py` с подставными командами; реальный готовый запуск проверен через Docker и API-smoke.
|
||||
- Настройки площадки: по запросу пользователя необязательный `venue/.env.example` удалён; настройки создаются автоматически через `make venue`. Убраны ссылка и проверка примера; `python3 tests/venue/deploy.py` прошёл. Существующая `.env` не изменялась.
|
||||
- Группировка проверок: `deploy.py` и `smoke.py` собраны в `tests/venue/` с поясняющим README, пути к площадке и команды в документации обновлены; для обычного запуска эти файлы не нужны. `python3 tests/venue/deploy.py` и `python3 tests/venue/smoke.py --verify` прошли; diff проверен.
|
||||
- Уточнение названия: автономная площадка называется `venue`, чтобы не смешивать её с локальной разработкой. Папка, Make-команды, образ, Docker-проект, режим `APP_MODE=venue` и настройки `VENUE_*` переименованы; существующая `.env` перенесена с сохранением секретов и учётной записи. Служебные имена прежних томов сохранены для сохранности данных; тома подключаются как внешние и создаются автоматически при первом запуске. Go-тесты, тест запуска и API-smoke после переименования прошли; `prod` не изменён. Ручное ревью diff по стандарту Go выполнено.
|
||||
- Проверка дополнения завершена: `make build PROD_DIR=/tmp/evening-artifacts-check` прошёл, backend и миграции в обоих наборах совпадают; type-check и две frontend-сборки прошли. Проверены Compose config, shell-синтаксис и diff. Local собран и запущен через новый `deploy.sh`; API-smoke и Firefox подтвердили вход, тестовую игру, подтверждённую команду и сохранность материала. Контрольные суммы файлов `prod` до и после работы совпадают. Проверка production-запуска не выполнялась; существующие файлы и конфигурация `prod` не изменены.
|
||||
- Результат проверки этапов 1–2: ручное ревью diff по Go- и frontend-стандартам выполнено, отклонений нет; `gofmt`, `go test ./...`, frontend `type-check`, `lint`, `lint:css`, сборка и полный `sh venue/prepare.sh` прошли. Compose config и shell-синтаксис проверены. Smoke через API создал тестовую игру, подтверждённую команду и материал; после `down` / `up --no-build --pull never` всё сохранилось. Firefox без окна подтвердил реальный вход, экран игры и команду после перезапуска. Приложение не имеет внешнего маршрута сети. Повторная инициализация не перезаписывает секреты. Инструкции запуска и повторяемый API-smoke находятся в корневом README и `tests/venue/smoke.py`. Проверка телефонов при отключённом WAN ещё не выполнена.
|
||||
- Внешние данные: запрошены модель/IP Cudy, постоянный IP ноутбука, доступ к проверенной прошивке и возможность DNS-проверки для сертификатов. Проверки телефонов, 100 клиентов и покрытия требуют оборудования.
|
||||
- Актуальный запуск по последующему запросу пользователя: `make venue` всегда использует HTTPS-дополнение, выполняет `docker compose build --pull=false` из заранее подготовленных артефактов и `up --force-recreate --no-build --pull never`; тома данных сохраняются. `venue-update` и `venue-https` — алиасы единого запуска. Сертификат проверяется до Docker; Go/Vue и npm-зависимости при деплое не собираются. Отсутствующие runtime-образы и базовый Alpine скачиваются, имеющиеся не обновляются автоматически. HTTPS теперь используется smoke-проверкой по умолчанию. README и тесты приведены к единому поведению; `python3 tests/venue/deploy.py`, shell-синтаксис, Python-синтаксис, dry-run всех трёх Make-команд и `git diff --check` прошли; diff вручную проверен. Реальный перезапуск площадки с публичным сертификатом и сборка Docker-образа без WAN не выполнялись; Cudy и проверка телефонов остаются внешней контрольной точкой.
|
||||
- Уточнение сборки по запросу пользователя: деплой больше не вызывает `make build`, Go/Vue-сборку или установку npm-зависимостей. Артефакты заранее готовятся существующим `make prod-build` в обеих папках; при отсутствии бинарника, Goose, bootstrap SQL, frontend или SQL-миграций запуск завершается до Docker с понятной ошибкой. `make venue-update` только упаковывает готовые файлы в Docker-образ и подготавливает runtime-образы. README обновлён; тесты первого, автономного и update-запуска подтверждают отсутствие make/npm, ошибки отсутствующих артефактов и сохранность настроек. `python3 tests/venue/deploy.py`, `python3 tests/venue/https.py`, `bash -n venue/deploy.sh`, `git diff --check` прошли; diff вручную проверен. Повторная реальная сборка приложения и запуск площадки не выполнялись; код дочерних репозиториев не менялся.
|
||||
- Подготовка этапа 3: добавлены `make venue-https`, отдельное Compose-дополнение и Nginx-конфигурация для прежних доменов интерфейса/API. HTTP перенаправляется на HTTPS с сохранением пути и параметров; API и материалы доступны через домен интерфейса и прежний API-домен. HTTPS-дополнение задаёт публичные адреса ссылок без изменения `.env`. Сертификат и ключ подключаются только для чтения из игнорируемого каталога; до Docker проверяются доверие полной цепочке, оба домена, срок действия и соответствие ключа. Повторный HTTPS-запуск перечитывает сертификат без пересборки. Обычный HTTP-запуск сохранён; добавлены HTTPS-smoke и инструкции DNS-01, LAN, продления и ручной приёмки. Код Go/Vue, protobuf, схема БД и конфигурация `prod` не менялись; новых зависимостей нет.
|
||||
- Проверки подготовки этапа 3: `python3 tests/venue/deploy.py`, `python3 tests/venue/https.py --docker`, `bash -n venue/deploy.sh` и `git diff --check` прошли; Python-файлы проверены на синтаксис. Проверены реальный Compose merge, прежние тома и внутренняя сеть backend. Временные Docker-контейнеры с тестовым CA подтвердили проверяемый TLS/SNI, оба домена, redirects, API/материалы, SPA-пути с параметрами, перезапуск и замену сертификата через reload; свои ресурсы тест удалил. Проверки сертификатов покрывают отсутствующие/повреждённые файлы, неверный и зашифрованный ключ, недоверенную/неполную цепочку, отсутствие каждого домена и истечение срока; ошибка возникает до Docker, повторный готовый запуск не скачивает и не собирает образы, `.env` сохраняется. Ручное ревью diff выполнено; стандарты Go/Vue не затронуты. Реальное HTTPS-развёртывание, системное доверие телефонов, Cudy и WAN-off smoke не проверены из-за отсутствия оборудования/сертификата. Commit/push не выполнялись.
|
||||
- Следующий шаг: получить параметры Cudy/IP ноутбука и возможность DNS-проверки; выпустить отдельный доверенный сертификат, вручную настроить локальный DNS, запустить `make venue-https` и проверить этап 3 на Android/iPhone без WAN, затем последовательно выполнить этапы 4–15. Прежние адреса сами по себе не переносят production-игры и билеты. Общая задача не завершена и не переносится в архив.
|
||||
|
||||
### ED-024 — Общая дизайн-система фронтенда
|
||||
|
||||
- Статус: `TODO`
|
||||
|
||||
+1
-1
Submodule prod updated: 7142d2bea7...3dcd84cd68
+12
-2
@@ -2,13 +2,23 @@
|
||||
|
||||
Для работы площадки эти файлы не нужны. Обычный запуск: `make venue`.
|
||||
|
||||
- `deploy.py` — проверяет первый запуск, запуск без интернета и обновление без Docker.
|
||||
- `deploy.py` — запускает проверки единого HTTPS-деплоя из `https.py` без Docker:
|
||||
первый запуск, повторная сборка Docker-образа, принудительный перезапуск,
|
||||
отсутствие артефактов, сертификаты и сохранность настроек.
|
||||
- `smoke.py` — проверяет работающую площадку через API. Без `--verify` создаёт
|
||||
тестовые данные; с `--verify` проверяет ранее созданные данные.
|
||||
тестовые данные; с `--verify` проверяет ранее созданные данные. Всегда
|
||||
использует HTTPS и прежние домены с проверкой доверия TLS; требует локального DNS.
|
||||
- `https.py` — проверяет сертификаты и HTTPS-запуск с подставными командами.
|
||||
`--docker` дополнительно запускает два временных Nginx-контейнера в отдельной
|
||||
сети, проверяет TLS, маршруты, перенаправления и перезапуск, затем удаляет
|
||||
свои контейнеры и сеть. Использует локальный `nginx:alpine` и временный тестовый CA;
|
||||
работающие окружения, production и системное хранилище доверия не меняет.
|
||||
|
||||
Команды из корня проекта:
|
||||
|
||||
```sh
|
||||
python3 tests/venue/deploy.py
|
||||
python3 tests/venue/smoke.py --verify
|
||||
python3 tests/venue/https.py
|
||||
python3 tests/venue/https.py --docker
|
||||
```
|
||||
|
||||
+3
-48
@@ -1,52 +1,7 @@
|
||||
"""Check first, offline and update runs without Docker or production writes."""
|
||||
"""Check the unified HTTPS deployment without Docker or production writes."""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import sys
|
||||
|
||||
|
||||
source = Path(__file__).resolve().parents[2] / "venue" / "deploy.sh"
|
||||
for name, cold, update in [("first", True, False), ("offline", False, False), ("update", False, True)]:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
venue = root / "venue"
|
||||
venue.mkdir()
|
||||
shutil.copy(source, venue / "deploy.sh")
|
||||
log = root / "commands.log"
|
||||
bin_dir = root / "bin"
|
||||
bin_dir.mkdir()
|
||||
for command in ["docker", "make", "npm"]:
|
||||
stub = bin_dir / command
|
||||
stub.write_text(
|
||||
'#!/bin/sh\n'
|
||||
'echo "$(basename "$0") $*" >> "$TEST_LOG"\n'
|
||||
'if [ "$1 $2" = "image inspect" ] && [ "$TEST_COLD" = 1 ]; then exit 1; fi\n'
|
||||
)
|
||||
stub.chmod(0o755)
|
||||
if not cold:
|
||||
(venue / "migrations").mkdir()
|
||||
(venue / ".env").write_text("EXISTING_SECRET=unchanged\n")
|
||||
for artifact in ["backend/evening_detective_server", "backend/goose", "backend/dev-user.sql", "frontend/dist/index.html"]:
|
||||
path = venue / artifact
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.touch()
|
||||
env = {**os.environ, "PATH": str(bin_dir) + os.pathsep + os.environ["PATH"], "TEST_LOG": str(log), "TEST_COLD": str(int(cold))}
|
||||
subprocess.run(["bash", str(venue / "deploy.sh"), *(["--build"] if update else [])], env=env, check=True, capture_output=True)
|
||||
commands = log.read_text()
|
||||
assert "volume create evening-detective-local_postgres_data" in commands, name
|
||||
assert "volume create evening-detective-local_rustfs_data" in commands, name
|
||||
assert "compose up -d --no-build --pull never" in commands, name
|
||||
assert "compose --profile setup run --rm bootstrap" in commands, name
|
||||
assert ("make -C .. build" in commands) == (cold or update), name
|
||||
assert ("compose pull" in commands) == (cold or update), name
|
||||
assert ("compose build --pull=false" in commands) == (cold or update), name
|
||||
if cold:
|
||||
config = dict(line.split("=", 1) for line in (venue / ".env").read_text().splitlines())
|
||||
assert len(config["JWT_SECRET"]) == 64
|
||||
assert len(config["VENUE_USER_PASSWORD"]) == 16
|
||||
assert (venue / ".env").stat().st_mode & 0o777 == 0o600
|
||||
else:
|
||||
assert (venue / ".env").read_text() == "EXISTING_SECRET=unchanged\n", name
|
||||
print("PASS: automatic first setup, offline reuse, explicit update and preserved secrets")
|
||||
subprocess.run([sys.executable, str(Path(__file__).with_name("https.py"))], check=True)
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
"""Check HTTPS preflight; --docker also exercises Nginx in isolated containers."""
|
||||
|
||||
from pathlib import Path
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
workspace = Path(__file__).resolve().parents[2]
|
||||
ui = "evening-detective.crabs-games.art"
|
||||
api = "evening-detective-api.crabs-games.art"
|
||||
|
||||
|
||||
def run(*args, **kwargs):
|
||||
return subprocess.run(args, check=True, capture_output=True, text=True, **kwargs).stdout.strip()
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="venue-https-") as directory:
|
||||
root = Path(directory)
|
||||
venue = root / "venue"
|
||||
venue.mkdir()
|
||||
shutil.copy(workspace / "venue/deploy.sh", venue / "deploy.sh")
|
||||
certificates = venue / "certificates"
|
||||
certificates.mkdir()
|
||||
for artifact in ["backend/evening_detective_server", "backend/goose", "backend/dev-user.sql", "frontend/dist/index.html"]:
|
||||
path = venue / artifact
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.touch()
|
||||
(venue / "migrations").mkdir()
|
||||
(venue / "migrations/001.sql").touch()
|
||||
original_env = "EXISTING_SECRET=unchanged\n"
|
||||
(venue / ".env").write_text(original_env)
|
||||
bin_dir = root / "bin"
|
||||
bin_dir.mkdir()
|
||||
log = root / "commands.log"
|
||||
for command in ["docker", "make", "npm"]:
|
||||
stub = bin_dir / command
|
||||
stub.write_text('#!/bin/sh\necho "$(basename "$0") $*" >> "$TEST_LOG"\n'
|
||||
'if [ "$1 $2" = "image inspect" ] && [ "$TEST_COLD" = 1 ]; then exit 1; fi\n')
|
||||
stub.chmod(0o755)
|
||||
env = {**os.environ, "PATH": str(bin_dir) + os.pathsep + os.environ["PATH"], "TEST_LOG": str(log)}
|
||||
run("openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "2",
|
||||
"-addext", "basicConstraints=critical,CA:TRUE", "-addext", "keyUsage=critical,keyCertSign,cRLSign",
|
||||
"-subj", "/CN=Venue test root", "-keyout", str(root / "ca.key"), "-out", str(root / "ca.pem"))
|
||||
run("openssl", "req", "-new", "-newkey", "rsa:2048", "-nodes", "-subj", "/CN=Venue intermediate",
|
||||
"-keyout", str(root / "intermediate.key"), "-out", str(root / "intermediate.csr"))
|
||||
extension = root / "extensions.cnf"
|
||||
extension.write_text("basicConstraints=critical,CA:TRUE,pathlen:0\nkeyUsage=critical,keyCertSign,cRLSign\n")
|
||||
run("openssl", "x509", "-req", "-in", str(root / "intermediate.csr"), "-CA", str(root / "ca.pem"),
|
||||
"-CAkey", str(root / "ca.key"), "-CAcreateserial", "-days", "2", "-extfile", str(extension),
|
||||
"-out", str(root / "intermediate.pem"))
|
||||
run("openssl", "req", "-new", "-newkey", "rsa:2048", "-nodes", "-subj", "/CN=Venue test server",
|
||||
"-keyout", str(certificates / "privkey.pem"), "-out", str(root / "leaf.csr"))
|
||||
|
||||
def issue(domains, days="1"):
|
||||
extension.write_text("basicConstraints=CA:FALSE\nextendedKeyUsage=serverAuth\nsubjectAltName=" +
|
||||
",".join("DNS:" + domain for domain in domains) + "\n")
|
||||
if days == "-1":
|
||||
(root / "index").touch()
|
||||
(root / "serial").write_text("01\n")
|
||||
config = root / "ca.cnf"
|
||||
config.write_text(f"[ca]\ndefault_ca=issuer\n[issuer]\ndatabase={root}/index\n"
|
||||
f"serial={root}/serial\nnew_certs_dir={root}\ncertificate={root}/intermediate.pem\n"
|
||||
f"private_key={root}/intermediate.key\ndefault_md=sha256\npolicy=policy\n"
|
||||
"x509_extensions=server\n[policy]\ncommonName=supplied\n[server]\n" + extension.read_text())
|
||||
run("openssl", "ca", "-batch", "-notext", "-config", str(config), "-in", str(root / "leaf.csr"),
|
||||
"-startdate", "20200101000000Z", "-enddate", "20200102000000Z", "-out", str(root / "leaf.pem"))
|
||||
else:
|
||||
run("openssl", "x509", "-req", "-in", str(root / "leaf.csr"), "-CA", str(root / "intermediate.pem"),
|
||||
"-CAkey", str(root / "intermediate.key"), "-CAcreateserial", "-days", days, "-extfile", str(extension),
|
||||
"-out", str(root / "leaf.pem"))
|
||||
(certificates / "fullchain.pem").write_text((root / "leaf.pem").read_text() + (root / "intermediate.pem").read_text())
|
||||
|
||||
def check(success, trust=True, cold=False, first=False):
|
||||
log.unlink(missing_ok=True)
|
||||
test_env = {**env, "SSL_CERT_FILE": str(root / "ca.pem" if trust else root / "untrusted.pem"),
|
||||
"TEST_COLD": str(int(cold))}
|
||||
result = subprocess.run(["bash", str(venue / "deploy.sh")], env=test_env,
|
||||
capture_output=True, text=True)
|
||||
assert (result.returncode == 0) == success, result.stderr
|
||||
if first:
|
||||
config = dict(line.split("=", 1) for line in (venue / ".env").read_text().splitlines())
|
||||
assert len(config["JWT_SECRET"]) == 64 and len(config["VENUE_USER_PASSWORD"]) == 16
|
||||
assert config["APP_BASE_URL"] == "https://" + ui
|
||||
assert (venue / ".env").stat().st_mode & 0o777 == 0o600
|
||||
else:
|
||||
assert (venue / ".env").read_text() == original_env
|
||||
if success:
|
||||
commands = log.read_text()
|
||||
prefix = "compose -f docker-compose.yml -f docker-compose.https.yml"
|
||||
assert prefix + " up -d --force-recreate --no-build --pull never" in commands
|
||||
assert prefix + " --profile setup run --rm bootstrap" in commands
|
||||
assert prefix + " exec -T proxy nginx -t" in commands
|
||||
assert prefix + " exec -T proxy nginx -s reload" in commands
|
||||
assert prefix + " build --pull=false" in commands
|
||||
assert commands.index(prefix + " build --pull=false") < commands.index(prefix + " up -d")
|
||||
assert (prefix + " pull " in commands) == cold
|
||||
assert ("docker pull alpine:3.22" in commands) == cold
|
||||
assert "make " not in commands and "npm " not in commands
|
||||
else:
|
||||
assert "HTTPS:" in result.stderr
|
||||
assert not log.exists(), "Invalid certificate reached Docker"
|
||||
|
||||
check(False) # Missing chain.
|
||||
issue([ui, api])
|
||||
check(True)
|
||||
check(True) # Offline reuse, unchanged settings.
|
||||
check(False, trust=False)
|
||||
chain = (certificates / "fullchain.pem").read_text()
|
||||
(certificates / "fullchain.pem").write_text((root / "leaf.pem").read_text())
|
||||
check(False) # Missing intermediate.
|
||||
(certificates / "fullchain.pem").write_text("invalid certificate")
|
||||
check(False)
|
||||
(certificates / "fullchain.pem").write_text(chain)
|
||||
key = (certificates / "privkey.pem").read_text()
|
||||
(certificates / "privkey.pem").write_text((root / "ca.key").read_text())
|
||||
check(False) # Different valid private key.
|
||||
(certificates / "privkey.pem").write_text("invalid key")
|
||||
check(False)
|
||||
(certificates / "privkey.pem").unlink()
|
||||
check(False)
|
||||
(certificates / "privkey.pem").write_text(key)
|
||||
run("openssl", "pkey", "-in", str(certificates / "privkey.pem"), "-aes-256-cbc",
|
||||
"-passout", "pass:test-only", "-out", str(root / "encrypted.pem"))
|
||||
(certificates / "privkey.pem").write_text((root / "encrypted.pem").read_text())
|
||||
check(False)
|
||||
(certificates / "privkey.pem").write_text(key)
|
||||
issue([ui])
|
||||
check(False) # API domain missing.
|
||||
issue([api])
|
||||
check(False) # UI domain missing.
|
||||
issue([ui, api], "-1")
|
||||
check(False) # Expired leaf.
|
||||
issue([ui, api])
|
||||
(venue / ".env").unlink()
|
||||
check(True, cold=True, first=True)
|
||||
(venue / ".env").write_text(original_env)
|
||||
for missing in ["backend/evening_detective_server", "backend/goose", "backend/dev-user.sql", "frontend/dist/index.html", "migrations/001.sql"]:
|
||||
path = venue / missing
|
||||
path.unlink()
|
||||
log.unlink(missing_ok=True)
|
||||
result = subprocess.run(["bash", str(venue / "deploy.sh")],
|
||||
env={**env, "SSL_CERT_FILE": str(root / "ca.pem")}, capture_output=True, text=True)
|
||||
assert result.returncode != 0 and "make prod-build" in result.stderr, missing
|
||||
assert not log.exists(), "Missing artifacts reached Docker"
|
||||
assert (venue / ".env").read_text() == original_env
|
||||
path.touch()
|
||||
print("PASS: HTTPS certificates, first setup, offline Docker rebuild, forced restart, missing artifacts and preserved settings")
|
||||
|
||||
if "--docker" in sys.argv:
|
||||
compose = ["docker", "compose", "--env-file", "/dev/null", "-f", str(workspace / "venue/docker-compose.yml")]
|
||||
compose_env = {**os.environ, **dict.fromkeys([
|
||||
"POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_DB", "JWT_SECRET", "PARTICIPATION_SECRET",
|
||||
"S3_USER", "S3_PASSWORD", "VENUE_USER_PASSWORD"], "test-only"),
|
||||
"APP_BASE_URL": "http://localhost:8088", "VENUE_BIND_IP": "127.0.0.1", "VENUE_PORT": "8088"}
|
||||
http_config = json.loads(run(*compose, "config", "--format", "json", env=compose_env))
|
||||
https = json.loads(run(*compose, "-f", str(workspace / "venue/docker-compose.https.yml"),
|
||||
"config", "--format", "json", env=compose_env))
|
||||
assert http_config["services"]["evening_detective_server"]["environment"]["APP_BASE_URL"] == "http://localhost:8088"
|
||||
server = https["services"]["evening_detective_server"]["environment"]
|
||||
assert server["APP_MODE"] == "venue" and server["APP_BASE_URL"] == "https://" + ui
|
||||
assert server["FILE_PREFIX_DOMAIN"] == server["APP_BASE_URL"] + "/api/files/"
|
||||
assert {str(p["published"]) for p in https["services"]["proxy"]["ports"]} == {"80", "443", "8088"}
|
||||
configs = [v for v in https["services"]["proxy"]["volumes"] if v["target"] == "/etc/nginx/conf.d/default.conf"]
|
||||
assert len(configs) == 1 and configs[0]["source"].endswith("/nginx.https.conf") and configs[0]["read_only"]
|
||||
assert any(v["target"] == "/etc/nginx/certificates" and v["read_only"] for v in https["services"]["proxy"]["volumes"])
|
||||
assert https["networks"]["default"]["internal"] and https["volumes"] == http_config["volumes"]
|
||||
print("PASS: Compose merge, HTTPS addresses, ports, read-only mounts and preserved data volumes")
|
||||
suffix = uuid.uuid4().hex[:10]
|
||||
network = "venue-https-test-" + suffix
|
||||
upstream = network + "-upstream"
|
||||
proxy = network + "-proxy"
|
||||
upstream_config = root / "upstream.conf"
|
||||
upstream_config.write_text('server { listen 8090; return 200 "api:$request_uri"; }\n'
|
||||
'server { listen 8080; return 200 "spa:$request_uri"; }\n')
|
||||
# Test-only CA is explicit; public HTTPS verification is never disabled.
|
||||
context = ssl.create_default_context(cafile=str(root / "ca.pem"))
|
||||
|
||||
def request(domain, path, tls=True):
|
||||
port = int(run("docker", "port", proxy, "443/tcp" if tls else "80/tcp").split(":")[-1])
|
||||
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
|
||||
if tls:
|
||||
connection.sock = context.wrap_socket(socket.create_connection(("127.0.0.1", port), timeout=10),
|
||||
server_hostname=domain)
|
||||
try:
|
||||
connection.request("GET", path, headers={"Host": domain})
|
||||
response = connection.getresponse()
|
||||
return response.status, response.getheader("Location"), response.read().decode()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
def serial():
|
||||
port = int(run("docker", "port", proxy, "443/tcp").split(":")[-1])
|
||||
with context.wrap_socket(socket.create_connection(("127.0.0.1", port), timeout=10), server_hostname=ui) as connection:
|
||||
return connection.getpeercert()["serialNumber"]
|
||||
|
||||
try:
|
||||
run("docker", "network", "create", network)
|
||||
run("docker", "run", "-d", "--name", upstream, "--network", network,
|
||||
"--network-alias", "evening_detective_server", "--network-alias", "evening_detective_frontend",
|
||||
"-v", str(upstream_config) + ":/etc/nginx/conf.d/default.conf:ro", "nginx:alpine")
|
||||
run("docker", "run", "-d", "--name", proxy, "--network", network,
|
||||
"-p", "127.0.0.1::80", "-p", "127.0.0.1::443",
|
||||
"-v", str(workspace / "venue/nginx.https.conf") + ":/etc/nginx/conf.d/default.conf:ro",
|
||||
"-v", str(certificates) + ":/etc/nginx/certificates:ro", "nginx:alpine")
|
||||
run("docker", "exec", proxy, "nginx", "-t")
|
||||
for domain in [ui, api]:
|
||||
assert request(domain, "/api/test/ping?value=1", tls=False)[:2] == (308, "https://" + domain + "/api/test/ping?value=1")
|
||||
assert request(domain, "/api/test/ping?value=1") == (200, None, "api:/api/test/ping?value=1")
|
||||
assert request(domain, "/api/files/material.pdf") == (200, None, "api:/api/files/material.pdf")
|
||||
assert request(ui, "/team-story/1?password=test") == (200, None, "spa:/team-story/1?password=test")
|
||||
assert request(api, "/docs/") == (200, None, "api:/docs/")
|
||||
run("docker", "restart", proxy)
|
||||
run("docker", "exec", proxy, "nginx", "-t")
|
||||
assert request(ui, "/games") == (200, None, "spa:/games")
|
||||
previous_serial = serial()
|
||||
issue([ui, api])
|
||||
run("docker", "exec", proxy, "nginx", "-t")
|
||||
run("docker", "exec", proxy, "nginx", "-s", "reload")
|
||||
for attempt in range(40):
|
||||
if serial() != previous_serial:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
raise AssertionError("Nginx did not reload renewed certificate")
|
||||
assert request(ui, "/games") == (200, None, "spa:/games")
|
||||
print("PASS: real Nginx TLS/SNI, both domains, redirects, API, materials, SPA paths, restart and certificate renewal")
|
||||
finally:
|
||||
for container in [proxy, upstream]:
|
||||
subprocess.run(["docker", "rm", "-f", container], capture_output=True)
|
||||
subprocess.run(["docker", "network", "rm", network], capture_output=True)
|
||||
@@ -20,10 +20,7 @@ config = dict(
|
||||
for line in (root / ".env").read_text().splitlines()
|
||||
if line and not line.startswith("#")
|
||||
)
|
||||
bind = config.get("VENUE_BIND_IP", "127.0.0.1")
|
||||
if bind == "0.0.0.0":
|
||||
bind = "127.0.0.1"
|
||||
base = f'http://{bind}:{config.get("VENUE_PORT", "8088")}'
|
||||
base = "https://evening-detective.crabs-games.art"
|
||||
token = ""
|
||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
|
||||
@@ -57,6 +54,8 @@ def verify(state):
|
||||
|
||||
|
||||
try:
|
||||
with opener.open("https://evening-detective-api.crabs-games.art/api/test/ping", timeout=10) as response:
|
||||
assert response.status == 200, "HTTPS API domain failed"
|
||||
token = request(
|
||||
"/api/auth/login",
|
||||
{
|
||||
|
||||
+55
-28
@@ -2,12 +2,43 @@
|
||||
set -euo pipefail
|
||||
|
||||
cd -- "$(dirname -- "${BASH_SOURCE[0]}")"
|
||||
if [[ $# -ne 0 ]]; then
|
||||
echo 'Используйте make venue' >&2
|
||||
exit 1
|
||||
fi
|
||||
compose=(docker compose -f docker-compose.yml -f docker-compose.https.yml)
|
||||
|
||||
command -v openssl >/dev/null
|
||||
certificate=certificates/fullchain.pem
|
||||
key=certificates/privkey.pem
|
||||
if [[ ! -s "$certificate" || ! -s "$key" ]]; then
|
||||
echo 'HTTPS: нужны venue/certificates/fullchain.pem и privkey.pem.' >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! openssl x509 -in "$certificate" -checkend 0 -noout >/dev/null 2>&1; then
|
||||
echo 'HTTPS: сертификат повреждён или истёк.' >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! certificate_public=$(openssl x509 -in "$certificate" -pubkey -noout 2>/dev/null) ||
|
||||
! key_public=$(openssl pkey -in "$key" -pubout -passin pass: 2>/dev/null) ||
|
||||
[[ "$certificate_public" != "$key_public" ]]; then
|
||||
echo 'HTTPS: ключ повреждён, зашифрован или не соответствует сертификату.' >&2
|
||||
exit 1
|
||||
fi
|
||||
for domain in evening-detective.crabs-games.art evening-detective-api.crabs-games.art; do
|
||||
if ! openssl verify -purpose sslserver -verify_hostname "$domain" \
|
||||
-untrusted "$certificate" "$certificate" >/dev/null 2>&1; then
|
||||
echo "HTTPS: сертификат не доверен, недействителен или не покрывает $domain." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ! -f .env ]]; then
|
||||
command -v openssl >/dev/null
|
||||
(
|
||||
umask 077
|
||||
cat > .env <<EOF
|
||||
APP_BASE_URL=http://localhost:8088
|
||||
APP_BASE_URL=https://evening-detective.crabs-games.art
|
||||
VENUE_BIND_IP=127.0.0.1
|
||||
VENUE_PORT=8088
|
||||
POSTGRES_USER=evening_detective
|
||||
@@ -26,36 +57,32 @@ EOF
|
||||
)
|
||||
fi
|
||||
|
||||
prepare=false
|
||||
case "${1:-}" in
|
||||
--build) prepare=true ;;
|
||||
'') ;;
|
||||
*) echo 'Используйте make venue или make venue-update' >&2; exit 1 ;;
|
||||
esac
|
||||
for image in evening-detective-venue-app:1 postgres:15 rustfs/rustfs:latest byjg/static-httpserver:latest nginx:alpine; do
|
||||
if ! docker image inspect "$image" >/dev/null 2>&1; then prepare=true; fi
|
||||
done
|
||||
for artifact in backend/evening_detective_server backend/goose backend/dev-user.sql frontend/dist/index.html; do
|
||||
if [[ ! -f "$artifact" ]]; then prepare=true; fi
|
||||
if [[ ! -f "$artifact" ]]; then
|
||||
echo "Нет venue/$artifact. Сначала выполните make prod-build." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if [[ ! -d migrations ]]; then prepare=true; fi
|
||||
if [[ "$prepare" == true ]]; then
|
||||
echo 'Подготовка площадки: потребуется интернет.'
|
||||
if ! docker image inspect my-go-builder >/dev/null 2>&1; then
|
||||
make -C ../evening_detective_server build-builder
|
||||
fi
|
||||
if [[ ! -d ../evening_detective_frontend/node_modules ]]; then
|
||||
npm --prefix ../evening_detective_frontend ci
|
||||
fi
|
||||
make -C .. build
|
||||
docker compose pull postgres_evening_detective_server rustfs_evening_detective_server evening_detective_frontend proxy
|
||||
docker pull alpine:3.22
|
||||
docker compose build --pull=false
|
||||
if [[ ! -d migrations ]] || ! compgen -G 'migrations/*.sql' >/dev/null; then
|
||||
echo 'Нет миграций venue. Сначала выполните make prod-build.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for service_image in postgres_evening_detective_server=postgres:15 rustfs_evening_detective_server=rustfs/rustfs:latest evening_detective_frontend=byjg/static-httpserver:latest proxy=nginx:alpine; do
|
||||
if ! docker image inspect "${service_image#*=}" >/dev/null 2>&1; then
|
||||
"${compose[@]}" pull "${service_image%%=*}"
|
||||
fi
|
||||
done
|
||||
if ! docker image inspect alpine:3.22 >/dev/null 2>&1; then
|
||||
docker pull alpine:3.22
|
||||
fi
|
||||
"${compose[@]}" build --pull=false
|
||||
for volume in evening-detective-local_postgres_data evening-detective-local_rustfs_data; do
|
||||
docker volume create "$volume" >/dev/null
|
||||
done
|
||||
docker compose up -d --no-build --pull never
|
||||
docker compose --profile setup run --rm bootstrap
|
||||
docker compose ps
|
||||
echo 'Площадка запущена. Адрес и данные входа находятся в venue/.env.'
|
||||
"${compose[@]}" up -d --force-recreate --no-build --pull never
|
||||
"${compose[@]}" --profile setup run --rm bootstrap
|
||||
"${compose[@]}" exec -T proxy nginx -t
|
||||
"${compose[@]}" exec -T proxy nginx -s reload
|
||||
echo 'Площадка: https://evening-detective.crabs-games.art; данные входа — venue/.env.'
|
||||
"${compose[@]}" ps
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
services:
|
||||
evening_detective_server:
|
||||
environment:
|
||||
APP_BASE_URL: https://evening-detective.crabs-games.art
|
||||
FILE_PREFIX_DOMAIN: https://evening-detective.crabs-games.art/api/files/
|
||||
|
||||
proxy:
|
||||
volumes:
|
||||
- ./nginx.https.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./certificates:/etc/nginx/certificates:ro
|
||||
ports:
|
||||
- '${VENUE_BIND_IP:-127.0.0.1}:80:80'
|
||||
- '${VENUE_BIND_IP:-127.0.0.1}:443:443'
|
||||
@@ -0,0 +1,43 @@
|
||||
ssl_certificate /etc/nginx/certificates/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certificates/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
client_max_body_size 257m;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 120s;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name evening-detective.crabs-games.art;
|
||||
return 308 https://evening-detective.crabs-games.art$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name evening-detective-api.crabs-games.art;
|
||||
return 308 https://evening-detective-api.crabs-games.art$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name evening-detective.crabs-games.art;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://evening_detective_server:8090;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://evening_detective_frontend:8080;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name evening-detective-api.crabs-games.art;
|
||||
|
||||
location / {
|
||||
proxy_pass http://evening_detective_server:8090;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user