This commit is contained in:
2026-10-10 14:32:07 +07:00
parent 6a198f2e2c
commit 3efdb52c3c
12 changed files with 524 additions and 105 deletions
+2 -3
View File
@@ -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 PROD_DIR ?= prod
@@ -23,8 +23,7 @@ prod-build: build
venue: venue:
bash venue/deploy.sh bash venue/deploy.sh
venue-update: venue-update venue-https: venue
bash venue/deploy.sh --build
publish: publish:
@test -n "$(MSG)" || (echo 'Usage: make publish MSG="commit message"' >&2; exit 1) @test -n "$(MSG)" || (echo 'Usage: make publish MSG="commit message"' >&2; exit 1)
+115 -19
View File
@@ -61,20 +61,26 @@ make dev-user
По умолчанию создаётся пользователь `dev@example.com` с паролем `dev123456`. По умолчанию создаётся пользователь `dev@example.com` с паролем `dev123456`.
Для изменения используйте `DEV_USER_EMAIL` и `DEV_USER_PASSWORD`. Для изменения используйте `DEV_USER_EMAIL` и `DEV_USER_PASSWORD`.
## Автономная площадка (ED-029, этапы 1–2) ## Автономная площадка (ED-029)
`venue/` — окружение для проведения игры на площадке без интернета. `venue/` — окружение для проведения игры на площадке без интернета.
Локальная разработка описана выше и запускается отдельно. Локальная разработка описана выше и запускается отдельно.
Из корня workspace запустите: После подготовки сертификата и локального DNS (инструкция ниже) из корня workspace запустите:
```sh ```sh
make prod-build
make venue 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` Email и пароль для входа указаны в `VENUE_USER_EMAIL` и `VENUE_USER_PASSWORD`
в `venue/.env`; существующий файл настроек не перезаписывается. в `venue/.env`; существующий файл настроек не перезаписывается.
Аккаунт организатора при запуске синхронизируется с этими значениями. Аккаунт организатора при запуске синхронизируется с этими значениями.
@@ -82,27 +88,25 @@ Email и пароль для входа указаны в `VENUE_USER_EMAIL` и
После изменения кода обновите площадку, пока интернет доступен: После изменения кода обновите площадку, пока интернет доступен:
```sh ```sh
make venue-update make prod-build
make venue
``` ```
Сайт доступен по `http://localhost:8088`; API и материалы обслуживаются тем же Сайт доступен по `https://evening-detective.crabs-games.art`; API и материалы
прокси. Для подключения по LAN задайте `VENUE_BIND_IP=0.0.0.0` и обслуживаются тем же прокси. Для подключения по LAN задайте постоянный IP ноутбука
`APP_BASE_URL=http://<постоянный-IP-ноутбука>:8088` в `venue/.env`, затем в `VENUE_BIND_IP` в `venue/.env` и настройте локальный DNS, как описано ниже.
повторите `make venue`. Прежние домены и доверенный HTTPS
требуют отдельной настройки DNS и сертификатов на этапе 3.
Образы приложения, мигратора и прокси готовятся заранее. Runtime запускается Приложение, база и RustFS подключены только к внутренней Docker-сети.
без сборки и скачивания; приложение, база и RustFS подключены только к Прокси дополнительно подключён к сети публикации порта.
внутренней Docker-сети. Прокси дополнительно подключён к сети публикации порта.
База и RustFS сохраняются в именованных томах. База и RustFS сохраняются в именованных томах.
Миграции Goose выполняются до старта приложения. Для остановки без потери данных: Миграции Goose выполняются до старта приложения. Для остановки без потери данных:
```sh ```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`: это удаляет данные. После изменений кода или миграций Не используйте `down -v`: это удаляет данные. После изменений кода или миграций
выполните `make venue-update`, пока интернет доступен. Секреты, сборка, выполните `make venue`, пока интернет доступен. Секреты, сборка,
сертификаты и пакеты игры игнорируются Git в `venue/`. сертификаты и пакеты игры игнорируются Git в `venue/`.
`APP_MODE=venue` отключает SMTP, фоновые письма участия/отзывов и внешний webhook `APP_MODE=venue` отключает SMTP, фоновые письма участия/отзывов и внешний webhook
@@ -121,8 +125,7 @@ docker compose -f venue/docker-compose.yml --env-file venue/.env down
```sh ```sh
python3 tests/venue/deploy.py python3 tests/venue/deploy.py
python3 tests/venue/smoke.py python3 tests/venue/smoke.py
docker compose -f venue/docker-compose.yml --env-file venue/.env down make venue
docker compose -f venue/docker-compose.yml --env-file venue/.env up -d --no-build --pull never
python3 tests/venue/smoke.py --verify python3 tests/venue/smoke.py --verify
``` ```
@@ -130,6 +133,98 @@ python3 tests/venue/smoke.py --verify
Тестовые данные остаются в локальной базе; повторное создание запускайте только Тестовые данные остаются в локальной базе; повторное создание запускайте только
когда нужен новый набор. Дождитесь ответа `/api/test/ping` перед проверкой. когда нужен новый набор. Дождитесь ответа `/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 build
``` ```
`make prod-build` выполняет ту же общую сборку. `make venue-update` вызывает её `make prod-build` выполняет ту же общую сборку. `make venue` пересобирает
и дополнительно подготавливает Docker-образы для автономного запуска. Команды Docker-образ из готовых артефактов и перезапускает площадку с HTTPS.
`make venue-update` и `make venue-https` сохранены как алиасы `make venue`. Команды
обновляют только генерируемые артефакты в `prod`; конфигурация `prod` не меняется. обновляют только генерируемые артефакты в `prod`; конфигурация `prod` не меняется.
## Окружение и секреты ## Окружение и секреты
+15
View File
@@ -2,6 +2,21 @@
Архив задач, переведённых в статус `DONE`. Архив задач, переведённых в статус `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 — Роль разработчика и общий таскменеджер ### ED-028 — Роль разработчика и общий таскменеджер
- Статус: `DONE` - Статус: `DONE`
+23
View File
@@ -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 — Общая дизайн-система фронтенда ### ED-024 — Общая дизайн-система фронтенда
- Статус: `TODO` - Статус: `TODO`
+1 -1
Submodule prod updated: 7142d2bea7...3dcd84cd68
+12 -2
View File
@@ -2,13 +2,23 @@
Для работы площадки эти файлы не нужны. Обычный запуск: `make venue`. Для работы площадки эти файлы не нужны. Обычный запуск: `make venue`.
- `deploy.py` — проверяет первый запуск, запуск без интернета и обновление без Docker. - `deploy.py` — запускает проверки единого HTTPS-деплоя из `https.py` без Docker:
первый запуск, повторная сборка Docker-образа, принудительный перезапуск,
отсутствие артефактов, сертификаты и сохранность настроек.
- `smoke.py` — проверяет работающую площадку через API. Без `--verify` создаёт - `smoke.py` — проверяет работающую площадку через API. Без `--verify` создаёт
тестовые данные; с `--verify` проверяет ранее созданные данные. тестовые данные; с `--verify` проверяет ранее созданные данные. Всегда
использует HTTPS и прежние домены с проверкой доверия TLS; требует локального DNS.
- `https.py` — проверяет сертификаты и HTTPS-запуск с подставными командами.
`--docker` дополнительно запускает два временных Nginx-контейнера в отдельной
сети, проверяет TLS, маршруты, перенаправления и перезапуск, затем удаляет
свои контейнеры и сеть. Использует локальный `nginx:alpine` и временный тестовый CA;
работающие окружения, production и системное хранилище доверия не меняет.
Команды из корня проекта: Команды из корня проекта:
```sh ```sh
python3 tests/venue/deploy.py python3 tests/venue/deploy.py
python3 tests/venue/smoke.py --verify python3 tests/venue/smoke.py --verify
python3 tests/venue/https.py
python3 tests/venue/https.py --docker
``` ```
+3 -48
View File
@@ -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 from pathlib import Path
import os
import shutil
import subprocess import subprocess
import tempfile import sys
subprocess.run([sys.executable, str(Path(__file__).with_name("https.py"))], check=True)
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")
+239
View File
@@ -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)
+3 -4
View File
@@ -20,10 +20,7 @@ config = dict(
for line in (root / ".env").read_text().splitlines() for line in (root / ".env").read_text().splitlines()
if line and not line.startswith("#") if line and not line.startswith("#")
) )
bind = config.get("VENUE_BIND_IP", "127.0.0.1") base = "https://evening-detective.crabs-games.art"
if bind == "0.0.0.0":
bind = "127.0.0.1"
base = f'http://{bind}:{config.get("VENUE_PORT", "8088")}'
token = "" token = ""
opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
@@ -57,6 +54,8 @@ def verify(state):
try: 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( token = request(
"/api/auth/login", "/api/auth/login",
{ {
+52 -25
View File
@@ -2,12 +2,43 @@
set -euo pipefail set -euo pipefail
cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 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 if [[ ! -f .env ]]; then
command -v openssl >/dev/null command -v openssl >/dev/null
( (
umask 077 umask 077
cat > .env <<EOF 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_BIND_IP=127.0.0.1
VENUE_PORT=8088 VENUE_PORT=8088
POSTGRES_USER=evening_detective POSTGRES_USER=evening_detective
@@ -26,36 +57,32 @@ EOF
) )
fi 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 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 done
if [[ ! -d migrations ]]; then prepare=true; fi if [[ ! -d migrations ]] || ! compgen -G 'migrations/*.sql' >/dev/null; then
if [[ "$prepare" == true ]]; then echo 'Нет миграций venue. Сначала выполните make prod-build.' >&2
echo 'Подготовка площадки: потребуется интернет.' exit 1
if ! docker image inspect my-go-builder >/dev/null 2>&1; then
make -C ../evening_detective_server build-builder
fi fi
if [[ ! -d ../evening_detective_frontend/node_modules ]]; then
npm --prefix ../evening_detective_frontend ci 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 fi
make -C .. build done
docker compose pull postgres_evening_detective_server rustfs_evening_detective_server evening_detective_frontend proxy if ! docker image inspect alpine:3.22 >/dev/null 2>&1; then
docker pull alpine:3.22 docker pull alpine:3.22
docker compose build --pull=false
fi fi
"${compose[@]}" build --pull=false
for volume in evening-detective-local_postgres_data evening-detective-local_rustfs_data; do for volume in evening-detective-local_postgres_data evening-detective-local_rustfs_data; do
docker volume create "$volume" >/dev/null docker volume create "$volume" >/dev/null
done done
docker compose up -d --no-build --pull never "${compose[@]}" up -d --force-recreate --no-build --pull never
docker compose --profile setup run --rm bootstrap "${compose[@]}" --profile setup run --rm bootstrap
docker compose ps "${compose[@]}" exec -T proxy nginx -t
echo 'Площадка запущена. Адрес и данные входа находятся в venue/.env.' "${compose[@]}" exec -T proxy nginx -s reload
echo 'Площадка: https://evening-detective.crabs-games.art; данные входа — venue/.env.'
"${compose[@]}" ps
+13
View File
@@ -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'
+43
View File
@@ -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;
}
}