Этап 0: документация и макеты
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
3400f58a5a
75 files changed
+14283
No files matched your search
@@ -0,0 +1,77 @@
|
||||
# 00. Карта документации
|
||||
|
||||
## 1. Документы
|
||||
|
||||
| Файл | О чём |
|
||||
|---|---|
|
||||
| [01-product.md](01-product.md) | Бренды, домены, продукты, тарифы, бизнес-правила |
|
||||
| [02-architecture.md](02-architecture.md) | Стек, одно приложение на все домены, процессы, окружения, выкладка |
|
||||
| [03-data-model.md](03-data-model.md) | Коллекции Payload, поля, связи, статусы |
|
||||
| [04-billing.md](04-billing.md) | Счёт, заказы, платежи, продления, возвраты, ручные операции, рефералы |
|
||||
| [05-auth.md](05-auth.md) | Вход по коду, сессии, капча, Telegram, объединение аккаунтов, админка |
|
||||
| [06-i18n.md](06-i18n.md) | Языки, URL, определение языка, валютный переключатель, переводы |
|
||||
| [07-frontend.md](07-frontend.md) | Макеты, маршруты, токены, темы, производительность |
|
||||
| [08-integrations.md](08-integrations.md) | Почта, ALTCHA, MaxMind, Chatwoot, Twenty, Telegram, платёжные системы, Umami |
|
||||
| [09-infrastructure.md](09-infrastructure.md) | Сервер, nginx, systemd, PostgreSQL, DNS, бэкапы, задачи для root |
|
||||
| [10-security.md](10-security.md) | Безопасность и приватность |
|
||||
| [decisions/](decisions/) | Принятые решения: по файлу на решение |
|
||||
| [stages/](stages/) | Этапы: цель, границы, предусловия, шаги, критерии готовности |
|
||||
|
||||
Исходная постановка — [history/HUMAN_IDEA.md](history/HUMAN_IDEA.md) (копия файла пользователя). Это история, а не источник истины: всё актуальное — в этих документах.
|
||||
|
||||
## 2. Этапы
|
||||
|
||||
| Этап | Содержание | Статус |
|
||||
|---|---|---|
|
||||
| [0](stages/stage-0-docs.md) | Документация | готово |
|
||||
| [1a](stages/stage-1a-foundation.md) | Фундамент: Next + Payload, сайты, домены, языки, валюты, темы | не начат |
|
||||
| [1b](stages/stage-1b-marketing.md) | Маркетинговые страницы обоих брендов, контент, формы с ALTCHA, SEO, юр. тексты | не начат |
|
||||
| [1c](stages/stage-1c-auth.md) | Вход по коду, почта, сессии | не начат |
|
||||
| [1d](stages/stage-1d-billing.md) | Биллинг без эквайринга, оформление заказа, продления | не начат |
|
||||
| [1e](stages/stage-1e-keys.md) | Ключи и подключения, ручная выдача | не начат |
|
||||
| [1f](stages/stage-1f-cabinet.md) | Веб-кабинеты обоих брендов | не начат |
|
||||
| [1g](stages/stage-1g-referral.md) | Реферальная программа | не начат |
|
||||
| [1h](stages/stage-1h-launch.md) | Переводы, Umami и баннер cookies, PageSpeed, выкладка в prod | не начат |
|
||||
| [2](stages/stage-2-support-crm.md) | Chatwoot и Twenty CRM | не начат |
|
||||
| [3](stages/stage-3-telegram.md) | Telegram: боты, MiniApp, вход и привязка | не начат |
|
||||
| [4](stages/stage-4-payments.md) | Платёжные системы: Stripe, RUB-процессинг, крипта | не начат |
|
||||
| [5](stages/stage-5-keys-automation.md) | Автоматизация выдачи ключей | обсуждение при старте |
|
||||
|
||||
## 3. Как запускать этап в новой сессии
|
||||
|
||||
1. Открыть сессию Claude под пользователем vx_dev в `/home/vx_dev/veilix_dev`.
|
||||
2. Написать: «Этап 1a. Прочитай CLAUDE.md и docs/stages/stage-1a-foundation.md, сверим план».
|
||||
3. Claude сверяет план и открытые вопросы этапа, вы подтверждаете, дальше — работа.
|
||||
4. В конце Claude обновляет статус этапа в этой таблице и документацию.
|
||||
|
||||
## 4. Открытые вопросы
|
||||
|
||||
### 4.1 Решённые
|
||||
|
||||
| № | Вопрос | Решение | Где отражено |
|
||||
|---|---|---|---|
|
||||
| Q-01 | Вторая валюта | USD | [D-010](decisions/D-010-currencies.md) |
|
||||
| Q-03 | Валюта по умолчанию для нового посетителя | По языку: ru → RUB, остальные → USD; настраивается у домена | [06-i18n.md](06-i18n.md), раздел 6 |
|
||||
| Q-04 | Валюта аккаунта | Задаётся при регистрации из выбранной; меняется, пока баланс нулевой и нет ключей; дальше — через поддержку | [04-billing.md](04-billing.md), раздел 2 |
|
||||
| Q-05 | WG-конфиги mass на этапе 1 | Конфиги, добавленные администратором, по странам; другая страна — через поддержку; автоматически — этап 5 | [D-020](decisions/D-020-key-models.md) |
|
||||
| Q-06 | Где Umami | На этом сервере, без Docker, под пользователем `umami`; панель — `umi.veilix.cc` | [D-018](decisions/D-018-analytics.md) |
|
||||
| Q-07 | Как запускать сервисы | Каждый под своим пользователем ОС: vx_dev (dev), vx_prod (prod), umami; systemd user-юниты | [D-004](decisions/D-004-environments.md) |
|
||||
| Q-15 | Защита prod от сессий Claude | Отдельно не нужна: сессии Claude работают под vx_dev без доступа к prod, а под vx_prod нет ни редактора, ни ИИ-агентов | [D-004](decisions/D-004-environments.md) |
|
||||
|
||||
### 4.2 Открытые
|
||||
|
||||
Предложение по умолчанию действует, пока не решено иначе.
|
||||
|
||||
| № | Вопрос | Предложение | Нужно к этапу |
|
||||
|---|---|---|---|
|
||||
| Q-02 | Цены mass в USD, суммы пополнения и реферального бонуса в USD для обоих брендов | — нужны цифры | 1b |
|
||||
| Q-08 | Email поддержки брендов | `help@veilix.app` и `help@veilixvpn.com` (в макете были домены .cc) | 1b |
|
||||
| Q-09 | Часовой пояс часов поддержки (10–20 в рабочие дни) | МСК | 1b |
|
||||
| Q-10 | Имена Telegram-ботов: 2 основных, 2 поддержки, тестовые | — | 3 |
|
||||
| Q-11 | Оплата в Telegram: Stars, внешняя страница или только с баланса | — | 3 |
|
||||
| Q-12 | RUB-процессинг (KZ, TR) и криптопроцессинг: какие провайдеры | — | 4 |
|
||||
| Q-13 | Юрлицо для юридических текстов | Пока заглушки | 1h |
|
||||
| Q-14 | Куда складывать бэкапы prod-базы | — | 1h |
|
||||
| Q-16 | Где принимать почту поддержки (`help@…`): свой почтовый хостинг или пересылка в Chatwoot | — | 1b |
|
||||
| Q-17 | Нужны ли разные аккаунты одного провайдера для разных брендов | Пока нет. Если понадобятся — платёжной системе добавим поле «только для сайтов» | 4 |
|
||||
| Q-18 | Кто запускает выкладку в prod | Пользователь одной командой; по желанию — sudo-правило, разрешающее vx_dev запускать только команду выкладки от имени vx_prod | 1h |
|
||||
@@ -0,0 +1,155 @@
|
||||
# 01. Продукт
|
||||
|
||||
## 1. Бренды
|
||||
|
||||
| | Вейликс | ВейликсВПН |
|
||||
|---|---|---|
|
||||
| Код сайта | `dedicated` | `mass` |
|
||||
| Название | RU «Вейликс», EN «Veilix» | RU «ВейликсВПН», EN «VeilixVPN» |
|
||||
| Цвет | фиолетовый | зелёный (бирюзовый) |
|
||||
| Суть | личный VPN: у каждого ключа свой индивидуальный IP в выбранной стране | классический VPN: общий сервер, ключ даёт все страны тарифа |
|
||||
| Протоколы | WireGuard / Amnezia WG и Xray (VLESS, Hysteria2, XHTTP) | Xray и WireGuard |
|
||||
|
||||
1. Это два разных продукта на одном бэкенде и одной базе. Сайт определяется по домену.
|
||||
2. veilixvpn.com продаёт только mass. Перекрёстные ссылки между брендами допустимы: на mass есть блок «нужен свой IP — тарифы Вейликс».
|
||||
3. Логотип — вариант «Слои» из макета (два перекрывающихся скруглённых квадрата). Подборщик вариантов знака из макета в продукт не переносим.
|
||||
|
||||
## 2. Домены
|
||||
|
||||
| Назначение | Вейликс prod | Вейликс dev | ВейликсВПН prod | ВейликсВПН dev |
|
||||
|---|---|---|---|---|
|
||||
| Сайт | veilix.app | dev.veilix.app | veilixvpn.com | dev.veilixvpn.com |
|
||||
| Кабинет и оформление заказа | my.veilix.app | dev.my.veilix.app | my.veilixvpn.com | dev.my.veilixvpn.com |
|
||||
| MiniApp | tg.veilix.app | dev.tg.veilix.app | tg.veilixvpn.com | dev.tg.veilixvpn.com |
|
||||
|
||||
1. Админка одна на оба бренда: `plc.veilix.cc` (prod) и `dev.plc.veilix.cc` (dev). Аналитика Umami — `umi.veilix.cc`.
|
||||
2. Все домены указывают на 169.58.76.183 (этот сервер).
|
||||
3. Домены хранятся в базе (коллекция «Домены»), у каждого свои языки и валюты. Новое зеркало добавляется записью в базе плюс блоком nginx и сертификатом.
|
||||
|
||||
## 3. Состав каждого бренда
|
||||
|
||||
1. Сайт: главная, цены, локации, поддержка, юридические страницы.
|
||||
2. Веб-кабинет, в том числе оформление заказа (оформление живёт на my.*, а не на сайте).
|
||||
3. MiniApp в Telegram — этап 3.
|
||||
4. Два Telegram-бота: основной (команды, MiniApp, уведомления) и бот поддержки, подключённый к Chatwoot. Этапы 2–3.
|
||||
|
||||
## 4. Термины
|
||||
|
||||
1. **Сайт** — бренд (тенант). Почти все сущности привязаны к сайту.
|
||||
2. **Домен** — хост, на котором открывается сайт, кабинет или MiniApp. У домена свои языки и валюты.
|
||||
3. **Тариф** — то, что продаётся: цены в валютах, период, лимит устройств, страны.
|
||||
4. **Страна** — общий справочник. **Локация** — страна на конкретном сайте: город, статус наличия.
|
||||
5. **Ключ** — купленная единица доступа пользователя со сроком действия и автопродлением.
|
||||
6. **Подключение** — конкретная конфигурация внутри ключа: WG или AWG-конфиг устройства, ссылка Happ, ссылка Xray.
|
||||
7. **Заказ, Платёж, Движение по счёту** — см. [04-billing.md](04-billing.md).
|
||||
8. **Валюта** и **Платёжная система** — справочники; платёжная система доступна в определённых валютах.
|
||||
|
||||
## 5. Dedicated (Вейликс)
|
||||
|
||||
1. Ключ — это индивидуальный IP в выбранной стране. Страна выбирается при покупке и не меняется никогда. Нужна другая страна — пользователь покупает новый ключ в другой локации и отключает автопродление старого. Отдельного функционала смены страны нет.
|
||||
2. В каждый ключ входит выделенный RU-адрес: работает и как прямое соединение, и как мост в обе стороны.
|
||||
3. До 5 устройств на ключ (поле тарифа), все с одним IP. WireGuard — по конфигу на устройство. Xray — ссылка для Happ (обычная или зашифрованная crypt-ссылка Happ, которая скрывает адрес подписки).
|
||||
4. Тариф — ценовой уровень со своим набором стран:
|
||||
|
||||
| Тариф | Страны | Первый месяц | Далее в месяц | USD (из макета) |
|
||||
|---|---|---|---|---|
|
||||
| Базовый | 23 страны (см. раздел 7) | 4 900 ₽ | 2 499 ₽ | $49 / $24 |
|
||||
| Редкие локации | CH, AE, SG, JP, KR | 6 900 ₽ | 3 499 ₽ | $69 / $34 |
|
||||
| Дальние локации | AU, BR | 9 900 ₽ | 4 999 ₽ | $99 / $49 |
|
||||
|
||||
5. Первый месяц дороже, потому что включает подготовку сервера: выделение адреса, настройку протоколов и маршрутизации. Период — месяц.
|
||||
6. Статусы локаций: «Серверы в наличии» и «Установим за 24 часа». Оплаченный период начинается с момента выдачи ключа. На этапе 1 ключи выдаёт администратор вручную, поэтому так работает для обоих статусов.
|
||||
7. Каждый ключ продлевается независимо, в свою дату.
|
||||
|
||||
## 6. Mass (ВейликсВПН)
|
||||
|
||||
1. Ключ включает все страны тарифа. Страна при покупке не выбирается: список стран на сайте только показывает, что входит в ключ.
|
||||
2. Happ — одна ссылка со всеми странами. WireGuard — пользователь может получить конфиги любой страны тарифа. На этапе 1 это конфиги, которые добавил администратор, по странам; нужна другая страна — через поддержку. Автоматически — на этапе 5.
|
||||
3. Тарифы: «Месяц» 699 ₽ и «Год» 6 990 ₽ («582 ₽ в месяц, экономия 1 398 ₽»). Платы за подключение и доплат за страну нет: первый период стоит столько же, сколько продление.
|
||||
4. До 5 устройств на ключ (поле тарифа).
|
||||
5. Статусы локаций: «Включено» и «Скоро». Локации «Скоро» показываются, но не работают.
|
||||
6. В кабинете — «Список ключей», как у dedicated; внутри ключа ссылка Xray и конфиги WG. Формулировки «текущая/запасная локация» из макета не используем.
|
||||
7. Цены в USD для mass в макете нет — вопрос Q-02.
|
||||
|
||||
## 7. Локации из макета
|
||||
|
||||
Город, статус и ценовой уровень dedicated. На mass те же 30 стран: «готова» = «Включено», «24 часа» = «Скоро».
|
||||
|
||||
| Код | Страна | Город | Статус | Уровень |
|
||||
|---|---|---|---|---|
|
||||
| NL | Нидерланды | Амстердам | готова | базовый |
|
||||
| DE | Германия | Франкфурт | готова | базовый |
|
||||
| US | США | Нью-Йорк | готова | базовый |
|
||||
| GB | Великобритания | Лондон | готова | базовый |
|
||||
| FR | Франция | Париж | готова | базовый |
|
||||
| FI | Финляндия | Хельсинки | готова | базовый |
|
||||
| SE | Швеция | Стокгольм | готова | базовый |
|
||||
| NO | Норвегия | Осло | 24 часа | базовый |
|
||||
| DK | Дания | Копенгаген | 24 часа | базовый |
|
||||
| CH | Швейцария | Цюрих | готова | редкие |
|
||||
| AT | Австрия | Вена | готова | базовый |
|
||||
| ES | Испания | Мадрид | готова | базовый |
|
||||
| IT | Италия | Милан | готова | базовый |
|
||||
| PL | Польша | Варшава | готова | базовый |
|
||||
| CZ | Чехия | Прага | готова | базовый |
|
||||
| RO | Румыния | Бухарест | 24 часа | базовый |
|
||||
| LT | Литва | Вильнюс | готова | базовый |
|
||||
| LV | Латвия | Рига | готова | базовый |
|
||||
| EE | Эстония | Таллин | 24 часа | базовый |
|
||||
| TR | Турция | Стамбул | готова | базовый |
|
||||
| KZ | Казахстан | Алматы | 24 часа | базовый |
|
||||
| AM | Армения | Ереван | 24 часа | базовый |
|
||||
| GE | Грузия | Тбилиси | 24 часа | базовый |
|
||||
| AE | ОАЭ | Дубай | готова | редкие |
|
||||
| SG | Сингапур | Сингапур | готова | редкие |
|
||||
| JP | Япония | Токио | готова | редкие |
|
||||
| KR | Южная Корея | Сеул | 24 часа | редкие |
|
||||
| CA | Канада | Торонто | готова | базовый |
|
||||
| AU | Австралия | Сидней | 24 часа | дальние |
|
||||
| BR | Бразилия | Сан-Паулу | 24 часа | дальние |
|
||||
|
||||
Координаты для карты — в массиве `L(...)` в `design/handoff/project/Price.dc.html`. Количество свободных серверов из MiniApp (NL 12, DE 10, …) — демо-данные. Показывать ли счётчик на сайте — решается в этапе 1b.
|
||||
|
||||
## 8. Пользователи
|
||||
|
||||
1. Аккаунты раздельные по сайтам: один email на veilix.app и на veilixvpn.com — два разных пользователя.
|
||||
2. Регистрация и вход — одно поле email и одноразовый код. Отдельной регистрации нет.
|
||||
3. Telegram (этап 3): аккаунт можно создать по Telegram без почты. Почта привязывается кодом, после этого работает вход на сайте. На сайте есть кнопка «Войти через Telegram». Правила объединения аккаунтов в пределах сайта — [05-auth.md](05-auth.md).
|
||||
|
||||
## 9. Деньги
|
||||
|
||||
1. У пользователя есть счёт (баланс) в валюте аккаунта.
|
||||
2. Валюты — отдельный справочник: RUB и USD. Переключатель валюты не зависит от языка: из России часто заходят через VPN, поэтому страна по IP для выбора валюты не годится. По умолчанию валюта зависит от языка: ru → RUB, остальные → USD.
|
||||
3. Платёжные системы привязаны только к валютам. Где система доступна, следует из цепочки: валюты → домены, на которых они включены → сайты:
|
||||
a. RUB — процессинги, принимающие карты РФ (вероятно, из Казахстана и Турции), без автосписаний. ЮKassa — опционально, только для пополнений;
|
||||
b. USD — Stripe (карты, сохранение карты, автосписания) и криптовалюта.
|
||||
4. Карта привязывается к аккаунту, а не к ключу (Stripe). Отвязать карту можно в аккаунте.
|
||||
5. Продление: у каждого ключа галочка автопродления и кнопка «Продлить сейчас». Сначала списывается баланс, нехватка — с карты.
|
||||
6. Не хватило денег: льготный период 3 дня (ключ работает), затем ключ истекает. Напоминание за 3 дня до окончания.
|
||||
7. Ручные операции в админке: платёж через платёжную систему manual (проводится как настоящий), корректировка баланса, возврат на баланс.
|
||||
8. Возвраты (политика из макета): если сервер не заработал по нашей вине — полностью в течение первых 7 дней; если пользователь передумал — остаток неиспользованного периода.
|
||||
9. Подробно — [04-billing.md](04-billing.md).
|
||||
|
||||
## 10. Реферальная программа
|
||||
|
||||
1. «Приведите друга — вы оба получите по месяцу в подарок».
|
||||
2. «Месяц» начисляется деньгами на баланс обоим после первой оплаты заказа другом. Сумма задаётся в админке для каждого сайта и валюты; в макете — 2 499 ₽ (dedicated) и 699 ₽ (mass).
|
||||
3. Бонус попадает на баланс и поэтому уходит в ближайшее списание.
|
||||
4. Ссылки: `https://my.veilix.app/r/<код>` (для mass — `my.veilixvpn.com`) и `t.me/<основной бот>?start=<код>`.
|
||||
5. Приглашения между брендами не засчитываются, самоприглашения отсекаются.
|
||||
6. В кабинете показываем: приглашено, начислено, ожидает оплаты; список приглашённых с замаскированной почтой.
|
||||
|
||||
## 11. Поддержка и заявки
|
||||
|
||||
1. Страница поддержки: Telegram (бот поддержки бренда) и email, FAQ по разделам, форма связи.
|
||||
2. Заявка «Нужны индивидуальные условия» (dedicated) и «Нужны особые условия» (mass): имя, email, Telegram, WhatsApp, коротко о задаче.
|
||||
3. Заявки и обращения сохраняются в Payload и приходят на почту поддержки (этап 1), затем уходят в Twenty CRM и Chatwoot (этап 2).
|
||||
4. Часы поддержки из макета: с 10 до 20 в рабочие дни (часовой пояс — Q-09).
|
||||
|
||||
## 12. Языки
|
||||
|
||||
en (по умолчанию, без префикса в URL), ru, de, fr, es. Позже — pt-br и ja. Подробно — [06-i18n.md](06-i18n.md).
|
||||
|
||||
## 13. Юридические страницы
|
||||
|
||||
Условия использования, политика конфиденциальности, политика возвратов, политика cookies. Черновики на RU и EN готовятся в этапе 1b, с заглушками вместо данных юрлица (Q-13). Это черновики, не юридическая консультация: перед запуском — показать юристу. Stripe при подключении проверяет эти страницы.
|
||||
@@ -0,0 +1,116 @@
|
||||
# 02. Архитектура
|
||||
|
||||
## 1. Стек
|
||||
|
||||
| Что | Версия на 20.10.2026 | Комментарий |
|
||||
|---|---|---|
|
||||
| Node.js | 24 LTS (на сервере 24.22) | |
|
||||
| pnpm | 12.x | ставится root через `corepack enable pnpm`; версия фиксируется полем `packageManager` |
|
||||
| Next.js | 16.4.x | App Router, React Server Components |
|
||||
| React | 19.x | |
|
||||
| Payload CMS | 3.91.x | Payload 4 в бете; переход после стабильного релиза ([D-002](decisions/D-002-payload-version.md)) |
|
||||
| PostgreSQL | 18 (на хосте) | адаптер `@payloadcms/db-postgres`, `idType: 'uuid'` |
|
||||
| Tailwind CSS | 4.x | токены из макета как CSS-переменные |
|
||||
| next-intl | 4.x | переводы интерфейса, префиксы языков в URL |
|
||||
| Nodemailer | через `@payloadcms/email-nodemailer` | транспорты SMTP, sendmail, «только журнал» |
|
||||
| ALTCHA | 3.x | капча без третьих сторон |
|
||||
| grammY | 1.x | Telegram-боты, этап 3 |
|
||||
| Stripe SDK | актуальный | этап 4 |
|
||||
|
||||
Точные версии фиксируются в `package.json` на этапе 1a. Если к старту этапа 1a выйдет стабильный Payload 4, начинаем сразу на нём.
|
||||
|
||||
## 2. Одно приложение на все домены
|
||||
|
||||
1. Одно Next.js-приложение, Payload установлен внутрь как библиотека: админка, API и фронтенды — один процесс ([D-001](decisions/D-001-single-app.md)).
|
||||
2. Proxy Next.js (`src/proxy.ts`, бывший middleware) по заголовку Host находит запись в коллекции «Домены» и узнаёт сайт (`dedicated` или `mass`) и тип фронтенда: сайт, кабинет или MiniApp. Список доменов кэшируется в памяти и сбрасывается при изменении записи.
|
||||
3. Proxy переписывает запрос во внутренний маршрут вида `/{тип}/{сайт}/{язык}/…`, для маркетинговых страниц ещё и `/{валюта}/`. Снаружи эти пути недоступны: прямой запрос к ним даёт 404.
|
||||
4. Хост админки (`plc.veilix.cc`, `dev.plc.veilix.cc`) задаётся в `.env`. Только на нём работают `/admin` и REST API Payload. На доменах брендов эти пути закрыты.
|
||||
5. Сайтам, кабинетам и MiniApp хост админки не нужен:
|
||||
a. данные Payload они читают в том же процессе через Local API — это вызов функции, а не HTTP-запрос;
|
||||
b. запросы браузера (формы, вход, действия в кабинете) идут на тот домен, где открыта страница: server actions и эндпоинты `/svc/…`;
|
||||
c. вебхуки платёжных систем и Telegram приходят на публичные домены, не на хост админки;
|
||||
d. файлы медиатеки отдаются через домены сайтов.
|
||||
6. Ядро общее (биллинг, ключи, вход, рефералы, почта), UI-кит общий, а страницы у каждого бренда свои: данные в дизайнах представлены по-разному.
|
||||
|
||||
## 3. Структура кода (план)
|
||||
|
||||
```
|
||||
src/
|
||||
proxy.ts — домен → сайт, тип фронтенда, язык, валюта
|
||||
payload.config.ts
|
||||
app/
|
||||
(payload)/ — админка и REST API Payload (стандартные файлы)
|
||||
(front)/m/[site]/[locale]/[currency]/… — маркетинговые сайты
|
||||
(front)/c/[site]/[locale]/… — кабинеты и оформление заказа
|
||||
(front)/t/[site]/… — MiniApp (этап 3)
|
||||
svc/… — свои эндпоинты: ALTCHA, вебхуки, health
|
||||
payload/
|
||||
collections/ globals/ hooks/ access/ fields/
|
||||
core/ — бизнес-логика без привязки к UI
|
||||
billing/ keys/ auth/ referral/ email/ payments/ currency/ geo/
|
||||
brands/
|
||||
dedicated/ mass/ — страницы и компоненты каждого бренда
|
||||
ui/ — общие компоненты: кнопки, поля, шиты, переключатели, флаги, карта
|
||||
i18n/ — конфиг next-intl и messages/{en,ru,de,fr,es}.json
|
||||
styles/ — Tailwind, токены брендов, темы
|
||||
```
|
||||
|
||||
Бизнес-логика живёт в `core/` и вызывается одинаково из хуков Payload, server actions, вебхуков, фоновых задач и (на этапе 3) бота.
|
||||
|
||||
## 4. Окружения
|
||||
|
||||
| | dev | prod |
|
||||
|---|---|---|
|
||||
| Пользователь ОС | vx_dev (под ним работают редактор и сессии Claude) | vx_prod (без редактора и ИИ-агентов) |
|
||||
| Папка | `/home/vx_dev/veilix_dev` — рабочая копия git | `/home/vx_prod/veilix` — `releases/`, симлинк `current`, `shared/` |
|
||||
| Запуск | `pnpm dev` (горячая перезагрузка) | `pnpm start` из `current` после `pnpm build` |
|
||||
| Порт | 3100 | 3000 |
|
||||
| База | `veilix_dev` | `veilix_prod` |
|
||||
| Схема базы | синхронизируется автоматически (push) | миграции Payload, применяются при выкладке |
|
||||
| Домены | `dev.*`, `dev.plc.veilix.cc` | основные, `plc.veilix.cc` |
|
||||
| Секреты | `/home/vx_dev/veilix_dev/.env` | `/home/vx_prod/veilix/shared/.env` |
|
||||
| Файлы (media) | `/home/vx_dev/veilix_dev/media` | `/home/vx_prod/veilix/shared/media` |
|
||||
|
||||
1. Без Docker: оба окружения — обычные процессы Node, каждый под своим пользователем ОС, запускаются одинаково — systemd user-юнитами: `veilix-dev.service` у vx_dev, `veilix.service` у vx_prod. У vx_dev нет доступа к файлам, секретам, логам и базе prod. Подробно — [D-003](decisions/D-003-hosting.md), [D-004](decisions/D-004-environments.md).
|
||||
2. Dev закрыт от посторонних в nginx и запрещён к индексации, см. [09-infrastructure.md](09-infrastructure.md).
|
||||
3. Платёжные системы в dev — только в тестовом режиме.
|
||||
|
||||
## 5. Выкладка в prod
|
||||
|
||||
1. В dev: изменения схемы оформляются миграцией (`payload migrate:create`), коммит, push в Gitea, тег релиза.
|
||||
2. Выкладка — команда `deploy <тег>` пользователя vx_prod, появится на этапе 1h. Запускает её пользователь; по желанию — sudo-правило, разрешающее vx_dev запускать только эту команду (Q-18). Шаги:
|
||||
a. взять тег из Gitea (у vx_prod ключ только на чтение) в новую папку `releases/<дата-тег>`;
|
||||
b. `pnpm install --frozen-lockfile` и `pnpm build`; ссылки на `shared/.env` и `shared/media`;
|
||||
c. применить миграции к `veilix_prod`;
|
||||
d. переключить симлинк `current` на новый релиз и перезапустить `veilix.service`;
|
||||
e. проверить `/svc/health`; хранить 5 последних релизов.
|
||||
3. Откат: переключить `current` на прошлый релиз и перезапустить. Миграции пишем так, чтобы прошлый релиз продолжал работать с новой схемой (сначала добавляем, удаляем позже).
|
||||
4. Простой при выкладке — только время перезапуска процесса, несколько секунд.
|
||||
|
||||
## 6. Рендеринг и кэширование
|
||||
|
||||
1. Маркетинговые страницы собираются статически во всех вариантах «язык × валюта» и кэшируются. Proxy выбирает вариант по префиксу языка и cookie валюты.
|
||||
2. Изменение в админке (цена, статус локации, FAQ) вызывает хук Payload, и Next.js пересобирает затронутые страницы за секунды (on-demand revalidation по тегам). Запасной вариант — плановое обновление раз в 5 минут.
|
||||
3. При оформлении заказа наличие и цена проверяются заново на сервере. Устаревшая страница не может продать то, чего нет.
|
||||
4. Кабинет рендерится на сервере (SSR) для каждого запроса, без кэша.
|
||||
5. Маркетинговые домены не ставят cookies, кроме выбора языка, валюты и темы.
|
||||
|
||||
## 7. Фоновые задачи
|
||||
|
||||
Встроенная очередь Payload (Jobs Queue) с расписанием cron. Исполнитель работает внутри процесса приложения (`autoRun`), в dev его можно отключить переменной окружения.
|
||||
|
||||
| Задача | Расписание | Что делает |
|
||||
|---|---|---|
|
||||
| `renewals` | каждые 15 минут | продление ключей, у которых срок подходит (см. [04-billing.md](04-billing.md)) |
|
||||
| `reminders` | каждый час | напоминания за 3 дня до окончания |
|
||||
| `expiry` | каждые 15 минут | перевод в льготный период и в «истёк» |
|
||||
| `send-email` | по событию | отправка письма с повторами при ошибке |
|
||||
| `cleanup` | раз в сутки | старые коды входа, сессии, записи журнала писем |
|
||||
| `reconcile` | раз в сутки | сверка кэша баланса с суммой движений по счёту |
|
||||
|
||||
## 8. Чего не используем
|
||||
|
||||
1. Docker — не нужен ни в dev, ни в prod ([D-003](decisions/D-003-hosting.md)).
|
||||
2. n8n — боты пишем в коде на grammY ([D-015](decisions/D-015-telegram-bots.md)).
|
||||
3. listmonk — шаблоны писем в Payload, отправка своим транспортом ([D-014](decisions/D-014-email.md)).
|
||||
4. Redis — стоит на сервере, но пока не нужен: лимиты запросов и очередь задач работают на PostgreSQL.
|
||||
@@ -0,0 +1,374 @@
|
||||
# 03. Модель данных
|
||||
|
||||
## 1. Соглашения
|
||||
|
||||
1. Все `id` — UUID (`idType: 'uuid'` в адаптере Postgres). Номера для людей (заказы, ключи, платежи, реферальные коды) — случайные коды, не последовательные.
|
||||
2. Сайт — тенант плагина `@payloadcms/plugin-multi-tenant`. Поле тенанта называется `site`. Коллекции с пометкой «сайт» содержат его автоматически; в админке у них есть переключатель сайта.
|
||||
3. «(L)» — локализованное поле Payload: значение хранится на каждом языке, если перевода нет — показывается EN.
|
||||
4. Деньги — целое число в минимальных единицах (`amount`: 249900 = 2 499,00 ₽) плюс `currency` — код ISO 4217 (`RUB`, `USD`). Код проверяется по справочнику «Валюты».
|
||||
5. Даты — `timestamptz`, в UTC.
|
||||
6. Секреты в полях (конфиги WG, ссылки подписок, SMTP-пароль, id сохранённых карт) шифруются приложением (AES-256-GCM, ключ в `.env`).
|
||||
|
||||
## 2. Схема связей
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
SITES ||--o{ DOMAINS : "домены"
|
||||
SITES ||--o{ LOCATIONS : "локации"
|
||||
SITES ||--o{ TARIFFS : "тарифы"
|
||||
SITES ||--o{ CUSTOMERS : "пользователи"
|
||||
COUNTRIES ||--o{ LOCATIONS : "страна"
|
||||
TARIFFS }o--o{ COUNTRIES : "страны тарифа"
|
||||
TARIFFS }o--o{ TAGS : "теги"
|
||||
DOMAINS }o--o{ CURRENCIES : "валюты домена"
|
||||
PAYMENT_SYSTEMS }o--o{ CURRENCIES : "валюты системы"
|
||||
CUSTOMERS ||--o{ KEYS : "ключи"
|
||||
TARIFFS ||--o{ KEYS : "тариф"
|
||||
LOCATIONS ||--o{ KEYS : "локация (dedicated)"
|
||||
KEYS ||--o{ CONNECTIONS : "подключения"
|
||||
CUSTOMERS ||--o{ ORDERS : "заказы"
|
||||
KEYS ||--o{ ORDERS : "продления"
|
||||
ORDERS ||--o{ PAYMENTS : "оплаты"
|
||||
CUSTOMERS ||--o{ PAYMENTS : "платежи"
|
||||
PAYMENT_SYSTEMS ||--o{ PAYMENTS : "через"
|
||||
CUSTOMERS ||--o{ LEDGER_ENTRIES : "движения"
|
||||
PAYMENTS ||--o{ LEDGER_ENTRIES : "зачисление"
|
||||
ORDERS ||--o{ LEDGER_ENTRIES : "списание, возврат"
|
||||
CUSTOMERS ||--o{ PAYMENT_METHODS : "карты"
|
||||
CUSTOMERS ||--o{ REFERRALS : "пригласил"
|
||||
CUSTOMERS ||--o{ CUSTOMER_SESSIONS : "сессии"
|
||||
```
|
||||
|
||||
## 3. Настройки и справочники
|
||||
|
||||
### 3.1 `sites` — Сайты (тенанты)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| code | select `dedicated` \| `mass`, уникальное | бренд: по нему выбираются тема и страницы |
|
||||
| name | text (L) | «Вейликс» / «Veilix» |
|
||||
| orderPrefix | text | префикс номеров: `VX`, `VV` |
|
||||
| supportEmail | email | почта поддержки (Q-08) |
|
||||
| emailFrom | group: name, address | отправитель писем, напр. «Вейликс» <no-reply@veilix.app> |
|
||||
| supportBot, mainBot | text | имена ботов (этап 3) |
|
||||
| referralEnabled | checkbox | |
|
||||
| referralBonus | array: currency, amount | бонус «месяц в подарок» в каждой валюте |
|
||||
| topupPresets | array: currency, amounts[] | суммы пополнения в кабинете |
|
||||
| graceDays | number, по умолчанию 3 | льготный период |
|
||||
| reminderDays | number, по умолчанию 3 | за сколько дней напоминать |
|
||||
| renewalLeadHours | number, по умолчанию 24 | за сколько часов до окончания пробовать продлить |
|
||||
| expiringBadgeDays | number, по умолчанию 7 | когда показывать «Истекает» |
|
||||
| seo | group: titleTemplate (L), description (L), ogImage | значения по умолчанию |
|
||||
|
||||
### 3.2 `domains` — Домены (сайт)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| hostname | text, уникальное | `veilix.app`, `my.veilix.app`, … |
|
||||
| app | select `marketing` \| `cabinet` \| `miniapp` | тип фронтенда |
|
||||
| locales | multi-select из языков конфига | включённые языки |
|
||||
| defaultLocale | select | обычно `en` |
|
||||
| currencies | multi-select валют | включённые валюты |
|
||||
| defaultCurrency | select | валюта по умолчанию |
|
||||
| localeCurrency | array: locale, currency | валюта по умолчанию для языка: ru → RUB |
|
||||
| isPrimary | checkbox | основной домен этого типа; используется в ссылках и sitemap |
|
||||
| indexable | checkbox | разрешена ли индексация (для кабинета и MiniApp — нет) |
|
||||
| umamiWebsiteId | text | этап 1h |
|
||||
| active | checkbox | |
|
||||
|
||||
### 3.3 `currencies` — Валюты
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| code | text, уникальное | ISO 4217: `RUB`, `USD` |
|
||||
| symbol | text | `₽`, `$` |
|
||||
| name | text (L) | |
|
||||
| minorUnits | number | 2 |
|
||||
| active, sort | | |
|
||||
|
||||
### 3.4 `payment-systems` — Платёжные системы
|
||||
|
||||
Платёжная система привязана только к валютам. Где она доступна, следует из цепочки: валюты → домены, на которых они включены → сайты. В оформлении заказа и пополнении показываются системы в валюте аккаунта.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| name | text | название в админке |
|
||||
| adapter | select | реализация в коде: `manual`; на этапе 4 — `stripe`, `yookassa`, `crypto_*`, `rucard_*` |
|
||||
| title | text (L) | что видит пользователь: «Банковская карта», «Карта МИР», «СБП», «Криптовалюта» |
|
||||
| description | text (L) | подсказка: «USDT, BTC, ETH, курс фиксируется на 15 минут» |
|
||||
| icon | upload | |
|
||||
| currencies | multi-select валют | в каких валютах доступна |
|
||||
| purposes | multi-select `order` \| `topup` | оплата заказа, пополнение |
|
||||
| visibleToCustomers | checkbox | у `manual` выключено: ей пользуются только администраторы |
|
||||
| mode | select `test` \| `live` | |
|
||||
| limits | array: currency, min, max | |
|
||||
| capabilities | только чтение, из адаптера | `recurring`, `refunds`, `saveMethod` |
|
||||
| active, sort | | |
|
||||
|
||||
Ключи доступа к провайдерам — только в `.env` (`PAYMENT_<КОД>_…`), не в базе.
|
||||
|
||||
### 3.5 `countries` — Страны (общие)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| iso2 | text, уникальное | `NL`, `DE`, … |
|
||||
| name | text (L) | |
|
||||
| lat, lng | number | точка на карте по умолчанию |
|
||||
| sort | number | |
|
||||
|
||||
### 3.6 `locations` — Локации (сайт)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| country | relationship → countries | уникальна в пределах сайта |
|
||||
| city | text (L) | |
|
||||
| lat, lng | number | точка города на карте |
|
||||
| status | select `available` \| `on_demand` \| `soon` | dedicated: «Серверы в наличии» / «Установим за 24 часа»; mass: «Включено» / «Скоро» |
|
||||
| freeServers | number, необязательное | счётчик (решение о показе — этап 1b) |
|
||||
| visible, sort | | |
|
||||
|
||||
### 3.7 `tags` — Теги (общие)
|
||||
|
||||
`name`, `code` (уникальное). Для фильтрации тарифов и будущей автоматизации выдачи ключей.
|
||||
|
||||
### 3.8 `tariffs` — Тарифы (сайт)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| name | text (L) | |
|
||||
| code | text, уникальное в сайте | |
|
||||
| type | select `dedicated` \| `mass` | поведение ключа: dedicated — страна выбирается при покупке, mass — все страны тарифа |
|
||||
| description | text (L) | |
|
||||
| features | array of text (L) | пункты «что входит» |
|
||||
| period | group: unit `month` \| `year`, count | |
|
||||
| devicesLimit | number, по умолчанию 5 | |
|
||||
| prices | array: currency, firstPeriodAmount, renewalAmount | строка на валюту; нет строки — тариф в этой валюте не продаётся |
|
||||
| countries | relationship → countries, many | страны тарифа |
|
||||
| tags | relationship → tags, many | |
|
||||
| badge | text (L) | «выгоднее» |
|
||||
| isDefault | checkbox | выбран по умолчанию на странице цен |
|
||||
| active, sort | | |
|
||||
|
||||
## 4. Люди
|
||||
|
||||
### 4.1 `admins` — Администраторы
|
||||
|
||||
Стандартная авторизация Payload (email и пароль), `maxLoginAttempts: 5`, `lockTime: 15 мин`.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| name | text | |
|
||||
| roles | multi-select `owner` \| `admin` \| `support` \| `content` | см. раздел 8 |
|
||||
| sites | доступ к тенантам | owner и admin видят все сайты |
|
||||
|
||||
### 4.2 `customers` — Пользователи (сайт)
|
||||
|
||||
Коллекция авторизации Payload со своей стратегией (вход по коду, [05-auth.md](05-auth.md)).
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| email | email, null | уникален в пределах сайта, хранится в нижнем регистре |
|
||||
| emailVerifiedAt | date | |
|
||||
| telegramId | text, null | уникален в пределах сайта (этап 3) |
|
||||
| telegramUsername, telegramName | text | этап 3 |
|
||||
| locale | select | язык писем и бота |
|
||||
| currency | select | валюта аккаунта ([04-billing.md](04-billing.md), раздел 2) |
|
||||
| balance | number, только чтение | кэш суммы движений по счёту |
|
||||
| referralCode | text, уникальное в сайте | случайный |
|
||||
| referredBy | relationship → customers | |
|
||||
| status | select `active` \| `blocked` | |
|
||||
| registeredVia | select `email` \| `telegram` \| `admin` | |
|
||||
| registrationIpHash | text | HMAC от IP: только для сравнения при проверке самоприглашений |
|
||||
| marketingConsent | checkbox | |
|
||||
| lastLoginAt | date | |
|
||||
| defaultPaymentMethod | relationship → payment-methods | |
|
||||
| adminNotes | textarea | видят только администраторы |
|
||||
|
||||
### 4.3 `customer-sessions` — Сессии (служебная)
|
||||
|
||||
`customer`, `kind` (`web` \| `miniapp`), `tokenHash` (SHA-256, уникальное), `createdAt`, `expiresAt`, `lastSeenAt`, `ipHash`, `userAgent`, `revokedAt`.
|
||||
|
||||
### 4.4 `auth-codes` — Коды входа (служебная)
|
||||
|
||||
`site`, `email`, `purpose` (`login` \| `link_email`), `customer` (для привязки почты), `codeHash`, `expiresAt`, `attempts`, `consumedAt`, `requestIpHash`.
|
||||
|
||||
## 5. Ключи
|
||||
|
||||
### 5.1 `keys` — Ключи (сайт)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| code | text, уникальное | публичный код, напр. `K-7F2C9Q` |
|
||||
| customer | relationship | |
|
||||
| tariff | relationship | |
|
||||
| type | select `dedicated` \| `mass` | копия из тарифа |
|
||||
| location | relationship → locations | только dedicated |
|
||||
| status | select | см. 5.3 |
|
||||
| autoRenew | checkbox, по умолчанию да | |
|
||||
| activatedAt, expiresAt, graceUntil, cancelledAt | date | |
|
||||
| renewalAmount, currency | | цена продления, зафиксированная при покупке; администратор может изменить |
|
||||
| period, devicesLimit | | копии из тарифа на момент покупки |
|
||||
| ipAddress, ruIpAddress | text | для показа (dedicated) |
|
||||
| adminNotes | textarea | |
|
||||
|
||||
### 5.2 `connections` — Подключения
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| key | relationship | |
|
||||
| kind | select `wireguard` \| `amneziawg` \| `happ` \| `xray` \| `other` | `happ` — ссылка подписки или crypt-ссылка Happ, `xray` — ссылка `vless://` и подобные |
|
||||
| title | text | «Устройство 1», «Ноутбук» |
|
||||
| deviceSlot | number | номер устройства для WG/AWG |
|
||||
| country | relationship → countries | для WG у mass — страна конфига; для RU-адреса — RU |
|
||||
| isRu | checkbox | подключение к RU-адресу (dedicated) |
|
||||
| payload | text, зашифрованное | содержимое конфига или URL |
|
||||
| fileName | text | имя файла при скачивании |
|
||||
| status | select `active` \| `revoked` | |
|
||||
| source | select `manual` \| `auto` | |
|
||||
| createdBy | relationship → admins | |
|
||||
|
||||
QR-коды строятся на лету из `payload`, не хранятся.
|
||||
|
||||
### 5.3 Статусы ключа
|
||||
|
||||
| Статус | Значение |
|
||||
|---|---|
|
||||
| `provisioning` | оплачен, готовится. Срок ещё не идёт. На этапе 1 так создаются все ключи: подключения добавляет администратор |
|
||||
| `active` | работает, срок идёт с `activatedAt` |
|
||||
| `grace` | срок истёк, оплата не прошла, ключ ещё работает до `graceUntil` |
|
||||
| `expired` | истёк, подключения отключаются |
|
||||
| `cancelled` | отменён (возврат) |
|
||||
| `suspended` | заблокирован администратором |
|
||||
|
||||
Переходы — в [04-billing.md](04-billing.md).
|
||||
|
||||
## 6. Деньги
|
||||
|
||||
### 6.1 `orders` — Заказы (сайт)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| number | text, уникальное | `VX-7K3M9Q` |
|
||||
| customer | relationship | |
|
||||
| type | select `new_key` \| `renewal` | |
|
||||
| tariff | relationship | |
|
||||
| location | relationship | для `new_key` у dedicated |
|
||||
| key | relationship | для `renewal`; для `new_key` заполняется после оплаты |
|
||||
| renewalFor | date | `expiresAt` ключа, который продлеваем; уникально вместе с `key` |
|
||||
| currency, amount | | полная стоимость |
|
||||
| status | select `pending` \| `paid` \| `cancelled` \| `expired` \| `refunded` \| `partially_refunded` | |
|
||||
| source | select `web` \| `miniapp` \| `bot` \| `admin` \| `auto_renewal` | |
|
||||
| paidAt, cancelledAt, validUntil | date | `validUntil` — неоплаченный заказ истекает через 24 часа |
|
||||
| refundedAmount | number | |
|
||||
| adminNotes | textarea | |
|
||||
|
||||
### 6.2 `payments` — Платежи (сайт)
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| number | text, уникальное | |
|
||||
| customer | relationship | |
|
||||
| paymentSystem | relationship | |
|
||||
| adapter | text | копия из платёжной системы |
|
||||
| purpose | select `order` \| `topup` | |
|
||||
| order | relationship | для `order` |
|
||||
| currency, amount | | |
|
||||
| status | select `pending` \| `succeeded` \| `failed` \| `cancelled` \| `refunded` \| `partially_refunded` | |
|
||||
| providerPaymentId | text | уникально вместе с `adapter` |
|
||||
| providerStatus, providerData | text, json | только для администраторов |
|
||||
| methodSummary | group: type, brand, last4 | «Карта ···· 4242», «USDT (TRC-20)» |
|
||||
| savedPaymentMethod | relationship | если карта сохранена |
|
||||
| idempotencyKey | text, уникальное | |
|
||||
| succeededAt, processedAt, failedAt | date | `processedAt` — цепочка действий выполнена |
|
||||
| failureReason | text | |
|
||||
| createdBy | relationship → admins | для ручных |
|
||||
| comment | textarea | |
|
||||
|
||||
### 6.3 `ledger-entries` — Движения по счёту (сайт)
|
||||
|
||||
Только создание. Изменять и удалять нельзя никому, ошибка исправляется встречной записью.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| customer | relationship | |
|
||||
| currency | | валюта аккаунта |
|
||||
| amount | number со знаком | `+` зачисление, `−` списание |
|
||||
| type | select `topup` \| `order_charge` \| `refund` \| `referral_bonus` \| `adjustment` \| `payout` | `payout` — возврат денег с баланса на карту (этап 4) |
|
||||
| payment, order, key, referral | relationship | источник записи |
|
||||
| balanceAfter | number | баланс после записи |
|
||||
| comment | text | обязателен для `adjustment` |
|
||||
| createdBy | relationship → admins, null | null — система |
|
||||
|
||||
Уникальность: (`payment`, `topup`), (`order`, `order_charge`), (`referral`, `customer`, `referral_bonus`). Это защищает от двойного проведения.
|
||||
|
||||
### 6.4 `payment-methods` — Сохранённые способы оплаты (сайт)
|
||||
|
||||
`customer`, `paymentSystem`, `providerMethodId` (зашифрованное), `type`, `brand`, `last4`, `expMonth`, `expYear`, `isDefault`, `status` (`active` \| `detached`), `detachedAt`.
|
||||
|
||||
### 6.5 `referrals` — Приглашения (сайт)
|
||||
|
||||
`referrer`, `referee` (уникальное), `status` (`pending` \| `review` \| `rewarded` \| `rejected`), `qualifyingOrder`, `rewardedAt`, `rejectReason`.
|
||||
|
||||
## 7. Контент и коммуникации
|
||||
|
||||
### 7.1 `pages` — Страницы (сайт)
|
||||
|
||||
`slug` (уникальное в сайте: `terms`, `privacy`, `refund`, `cookies`), `title` (L), `content` (L, Lexical), `seo` (title (L), description (L)). Черновики и версии Payload включены.
|
||||
|
||||
### 7.2 `faq` — Вопросы и ответы (сайт)
|
||||
|
||||
`question` (L), `answer` (L, rich text), `placements` (multi: `home`, `pricing`, `support_connection`, `support_billing`, `support_locations`, `cabinet`, `miniapp`), `sort`, `active`. Один вопрос можно показать в нескольких местах.
|
||||
|
||||
### 7.3 `email-templates` — Шаблоны писем (сайт)
|
||||
|
||||
`code` (уникальное в сайте), `subject` (L), `body` (L, rich text с переменными `{{code}}`, `{{keyTitle}}`, `{{expiresAt}}`, `{{amount}}` …), `active`. Шапка и подвал письма — в коде, для каждого бренда свои.
|
||||
|
||||
Коды шаблонов: `login_code`, `link_email_code`, `order_paid`, `key_provisioning`, `key_ready`, `renewal_reminder`, `expiry_reminder`, `renewal_succeeded`, `renewal_failed`, `grace_started`, `key_expired`, `topup_succeeded`, `referral_reward`, `refund_issued`, `admin_new_order`, `admin_provisioning_needed`, `admin_new_lead`.
|
||||
|
||||
### 7.4 `leads` — Заявки (сайт)
|
||||
|
||||
`type` (`individual_terms` \| `contact`), `name`, `email`, `telegram`, `whatsapp`, `topic`, `message`, `locale`, `pageUrl`, `customer`, `status` (`new` \| `in_progress` \| `done` \| `spam`), `crmId` и `chatwootId` (этап 2), `ipHash`.
|
||||
|
||||
### 7.5 `media` — Файлы
|
||||
|
||||
Загрузки Payload: изображения для контента, иконки платёжных систем, картинки для соцсетей.
|
||||
|
||||
## 8. Журналы
|
||||
|
||||
### 8.1 `email-log` — Журнал писем
|
||||
|
||||
`site`, `to`, `templateCode`, `subject`, `transport` (`smtp` \| `sendmail` \| `log_only`), `status` (`queued` \| `sent` \| `failed`), `error`, `messageId`, `sentAt`, `customer`, `debugCode`.
|
||||
|
||||
`debugCode` — код входа открытым текстом. Заполняется, только если в настройках почты включено «Записывать коды входа в журнал». Видят роли owner и admin. Записи с кодом удаляются через сутки, остальные — через `logRetentionDays`.
|
||||
|
||||
### 8.2 `audit-log` — Журнал действий
|
||||
|
||||
`actor` (администратор или система), `action` (`payment.manual`, `ledger.adjustment`, `key.activate`, `order.refund`, `customer.merge`, …), `entityType`, `entityId`, `before`, `after`, `ipHash`, `createdAt`. Только создание.
|
||||
|
||||
## 9. Глобальные настройки
|
||||
|
||||
### 9.1 `email-settings` — Почта
|
||||
|
||||
| Поле | Описание |
|
||||
|---|---|
|
||||
| transport | `smtp` \| `sendmail` \| `log_only` (только журнал, без отправки) |
|
||||
| smtp | host, port, secure, user, password (зашифрованный) |
|
||||
| sendmailPath | по умолчанию `/usr/sbin/sendmail` |
|
||||
| logLoginCodes | «Записывать коды входа в журнал». В prod по умолчанию выключено; когда включено, в админке висит предупреждение |
|
||||
| logRetentionDays | по умолчанию 30 |
|
||||
| Кнопка | «Отправить тестовое письмо» на указанный адрес |
|
||||
|
||||
## 10. Доступ
|
||||
|
||||
| Коллекция | owner, admin | support | content | пользователь (через приложение) |
|
||||
|---|---|---|---|---|
|
||||
| Настройки и справочники | всё | чтение | чтение | — |
|
||||
| pages, faq, email-templates, локализация | всё | чтение | всё | — |
|
||||
| customers | всё | чтение, правка заметок, блокировка | — | только свой профиль |
|
||||
| keys, connections | всё | всё | — | только свои, чтение |
|
||||
| orders, payments | всё | чтение, ручной платёж | — | только свои, чтение |
|
||||
| ledger-entries | создание корректировок, чтение | чтение | — | только свои, чтение |
|
||||
| leads | всё | всё | — | — |
|
||||
| email-log, audit-log | чтение | чтение без `debugCode` | — | — |
|
||||
|
||||
Пользователь никогда не ходит в REST Payload напрямую: кабинет читает данные на сервере через Local API с `overrideAccess: false` и `user`.
|
||||
@@ -0,0 +1,188 @@
|
||||
# 04. Биллинг
|
||||
|
||||
## 1. Принципы
|
||||
|
||||
1. Всё через счёт. Любой успешный платёж зачисляется на баланс (движение `topup`). Любой заказ оплачивается списанием с баланса (`order_charge`). Поэтому оплата картой, оплата с баланса, доплата разницы и пополнение заранее работают одним механизмом.
|
||||
2. Баланс — это сумма движений по счёту. Поле `customers.balance` — кэш, он обновляется в той же транзакции базы, что и запись движения. Раз в сутки задача `reconcile` сверяет кэш с суммой.
|
||||
3. Движения неизменяемы. Ошибка исправляется встречной записью.
|
||||
4. Одна точка проведения платежа — `core/billing/processPayment(paymentId)`. Её вызывают вебхук провайдера, ручной платёж из админки и тесты. Повторный вызов ничего не делает.
|
||||
5. Суммы — в минимальных единицах валюты аккаунта.
|
||||
|
||||
## 2. Валюта аккаунта
|
||||
|
||||
1. При регистрации валюта аккаунта = валюта, выбранная в переключателе (или валюта по умолчанию домена).
|
||||
2. Сменить валюту в кабинете можно, пока баланс нулевой и нет ключей и неоплаченных заказов. Дальше — через поддержку: администратор пересчитывает вручную корректировками.
|
||||
3. Цены в кабинете всегда в валюте аккаунта. Тариф без цены в этой валюте недоступен.
|
||||
|
||||
## 3. Покупка ключа (оформление заказа на my.*)
|
||||
|
||||
1. Ссылка «Купить» с сайта ведёт на `my.<домен>/{язык}/order?tariff=…&location=…¤cy=…`.
|
||||
2. Шаг «Почта»: email и код ([05-auth.md](05-auth.md)). Вошедший пользователь шаг пропускает.
|
||||
3. Сервер проверяет: тариф активен, есть цена в валюте аккаунта, локация доступна (dedicated: `available` или `on_demand`).
|
||||
4. Создаётся заказ `pending`: `new_key`, сумма — `firstPeriodAmount`, действует 24 часа. Если у пользователя уже есть такой же неоплаченный заказ, используется он.
|
||||
5. Шаг «Оплата»: сводка заказа, согласие с условиями (обязательная галочка со ссылками на юридические страницы) и способ оплаты.
|
||||
a. Баланса хватает — кнопка «Оплатить с баланса», заказ оплачивается сразу.
|
||||
b. Баланса не хватает — список платёжных систем: активные, в валюте аккаунта, с назначением `order`, видимые пользователю. Создаётся платёж `pending` на сумму «стоимость минус баланс» (показываем: «С баланса спишется X, к оплате Y»), дальше — страница или виджет провайдера (этап 4).
|
||||
6. На этапе 1 видимых платёжных систем нет. Заказ создаётся, пользователь видит «Заказ создан, ожидает оплаты». Оплату проводит администратор ручным платежом, либо пользователь оплачивает с баланса, который пополнил администратор. Так проверяется весь сценарий без эквайринга.
|
||||
7. После оплаты: заказ `paid`, ключ создаётся в статусе `provisioning`, письма `order_paid` пользователю и `admin_provisioning_needed` на почту поддержки.
|
||||
|
||||
## 4. Пополнение баланса
|
||||
|
||||
1. Платёж с назначением `topup`. Суммы — пресеты сайта для валюты (`sites.topupPresets`) или своя сумма в пределах лимитов платёжной системы.
|
||||
2. После зачисления — письмо `topup_succeeded`.
|
||||
3. После любого пополнения система сама оплачивает с баланса неоплаченные заказы на продление этого пользователя, начиная со старого. Неоплаченные заказы на новые ключи сама не оплачивает.
|
||||
4. Криптовалюта (этап 4): счёт в монете, курс фиксируется на 15 минут, зачисление в валюте аккаунта после подтверждения сети.
|
||||
|
||||
## 5. Проведение платежа: `processPayment`
|
||||
|
||||
Выполняется, когда платёж переходит в `succeeded`, в одной транзакции:
|
||||
|
||||
1. Заблокировать строку платежа. Если `processedAt` заполнен — выйти.
|
||||
2. Заблокировать строку пользователя.
|
||||
3. Движение `+amount` (`topup`).
|
||||
4. Если назначение `order` и заказ `pending`: баланса хватает — движение `−order.amount` (`order_charge`), заказ `paid`. Не хватает — заказ остаётся `pending`, деньги остаются на балансе.
|
||||
5. Заказ оплачен — исполнить: `new_key` создаёт ключ `provisioning` с копией цены продления, периода и лимита устройств; `renewal` продлевает ключ (раздел 6).
|
||||
6. Проставить `processedAt`.
|
||||
7. Проверить реферальное вознаграждение (раздел 11).
|
||||
8. Поставить в очередь письма, записать журнал действий.
|
||||
|
||||
Если заказ к моменту оплаты отменён или истёк, деньги остаются на балансе, пользователь получает письмо о зачислении.
|
||||
|
||||
Оплата заказа с баланса без внешнего платежа — те же шаги 2, 4, 5, 7, 8.
|
||||
|
||||
## 6. Продление
|
||||
|
||||
### 6.1 Автоматическое (задача `renewals`, каждые 15 минут)
|
||||
|
||||
Берём ключи в статусе `active` или `grace` с `autoRenew = true`, у которых `expiresAt ≤ сейчас + renewalLeadHours` и нет оплаченного заказа на продление с `renewalFor = expiresAt`. Для каждого, с блокировкой ключа:
|
||||
|
||||
1. Найти или создать заказ `renewal`: `renewalFor = expiresAt`, сумма — `renewalAmount` ключа.
|
||||
2. Баланса хватает — списание, заказ `paid`, `expiresAt = renewalFor + period`, статус `active`, письмо `renewal_succeeded`.
|
||||
3. Не хватает, но есть сохранённая карта с автосписанием (Stripe, этап 4) — платёж на разницу без участия пользователя. Неудача — повтор: за 24 часа до окончания, в момент окончания, через 24 и 48 часов (внутри льготного периода). После первой неудачи — письмо `renewal_failed`.
|
||||
4. Нет ни денег, ни карты — заказ ждёт оплаты. Его оплатит пополнение (раздел 4.3) или кнопка «Продлить сейчас».
|
||||
|
||||
### 6.2 «Продлить сейчас»
|
||||
|
||||
Доступна для ключей `active` и `grace`. Создаёт (или берёт существующий) заказ `renewal` на `renewalFor = expiresAt` и оплачивает его с баланса или через платёжную систему. Ключ продлевается на период от текущего `expiresAt`. Можно продлить заранее несколько раз подряд.
|
||||
|
||||
### 6.3 Автопродление и карта
|
||||
|
||||
1. Галочка автопродления — у каждого ключа.
|
||||
2. «Отвязать карту» — в аккаунте, раздел «Счёт и платежи». После отвязки автопродление списывает только баланс.
|
||||
|
||||
## 7. Льготный период и окончание (задача `expiry`, каждые 15 минут)
|
||||
|
||||
1. `active`, срок истёк, продление не оплачено → `grace`, `graceUntil = expiresAt + graceDays`, письмо `grace_started`. Ключ работает.
|
||||
2. Продление оплачено в льготный период → `active`, новый срок считается от старого `expiresAt`: бесплатных дней нет.
|
||||
3. `grace`, `graceUntil` наступил → `expired`, неоплаченный заказ на продление → `expired`, письмо `key_expired`. На этапе 1 ключ попадает в список «К отключению» в админке; отключение автоматическое — этап 5.
|
||||
4. Правило одинаковое для ключей с автопродлением и без. Без автопродления пользователь сам решает, продлевать ли в льготный период.
|
||||
5. Истёкший ключ не продлевается: покупается новый. Администратор может вернуть ключ вручную.
|
||||
|
||||
## 8. Выдача ключа
|
||||
|
||||
1. Админка, ключ в статусе `provisioning`: администратор добавляет подключения (WG/AWG по устройствам, ссылку Happ, RU-адрес), IP-адреса и нажимает «Выдать ключ».
|
||||
2. Нужно хотя бы одно активное подключение. Затем: `active`, `activatedAt = сейчас`, `expiresAt = сейчас + период`, письмо `key_ready`.
|
||||
3. Список «Очередь выдачи» в админке: ключи `provisioning` по времени создания; локации `on_demand` помечены сроком «24 часа».
|
||||
|
||||
## 9. Ручные операции в админке
|
||||
|
||||
| Операция | Что происходит |
|
||||
|---|---|
|
||||
| Ручной платёж | Платёж через платёжную систему `manual`: пользователь, назначение, заказ (необязательно), сумма в валюте аккаунта, статус, комментарий. Когда статус становится `succeeded`, срабатывает `processPayment` — как у настоящего платежа. Можно завести `pending` (ждём перевод) и позже перевести в `succeeded`. Галочки «провести как реальную» нет: проводится всегда |
|
||||
| Корректировка баланса | Движение `adjustment` ±сумма, комментарий обязателен. Цепочка действий не запускается |
|
||||
| Возврат на баланс | На заказе: сумма (не больше оплаченной минус возвращённое), галочка «Отменить ключ». Движение `refund`, заказ `refunded` или `partially_refunded`, ключ `cancelled`, если отмечено. Письмо `refund_issued` |
|
||||
| Выдать ключ | `provisioning` → `active` (раздел 8) |
|
||||
| Продлить вручную | Продлить ключ на N дней без оплаты (компенсация) |
|
||||
| Заблокировать | Пользователь `blocked`: войти нельзя. Ключ `suspended` |
|
||||
|
||||
Все операции пишутся в журнал действий.
|
||||
|
||||
## 10. Возвраты
|
||||
|
||||
1. Политика: если сервер не заработал по нашей вине — полностью в течение первых 7 дней; если пользователь передумал — остаток неиспользованного периода. Сумму считает администратор, система подсказывает остаток пропорционально дням.
|
||||
2. Этап 1 — только возврат на баланс.
|
||||
3. Этап 4 — возврат на исходный способ оплаты, если провайдер умеет. Запись `refund` (+) на баланс по заказу, затем `payout` (−) — деньги уходят на карту. Итог: баланс не меняется, деньги у пользователя на карте.
|
||||
|
||||
## 11. Реферальное вознаграждение
|
||||
|
||||
1. Привязка: при регистрации, если есть cookie `vx_ref` (ставит `my.<домен>/r/<код>`, живёт 60 дней) или параметр `/start <код>` в боте. Код того же сайта, не свой. Создаётся приглашение `pending`.
|
||||
2. Условие: оплачен первый заказ `new_key` приглашённого. Пополнение баланса условием не считается.
|
||||
3. Проверка: совпадает хэш IP регистрации приглашённого и пригласившего → `review`, решает администратор. Иначе — начисление.
|
||||
4. Начисление: движение `referral_bonus` обоим, в валюте каждого, сумма из `sites.referralBonus`. Нет суммы для валюты — бонус не начисляется, в журнале предупреждение. Приглашение `rewarded`, письма `referral_reward`.
|
||||
|
||||
## 12. Уведомления
|
||||
|
||||
| Когда | Шаблон | Условие |
|
||||
|---|---|---|
|
||||
| Заказ оплачен | `order_paid` | всегда |
|
||||
| Ключ ждёт подготовки (24 часа) | `key_provisioning` | локация `on_demand` |
|
||||
| Ключ выдан | `key_ready` | |
|
||||
| За `reminderDays` до окончания | `renewal_reminder` | автопродление включено, но баланса и карты не хватит |
|
||||
| За `reminderDays` до окончания | `expiry_reminder` | автопродление выключено |
|
||||
| Продление прошло | `renewal_succeeded` | |
|
||||
| Карта не прошла | `renewal_failed` | |
|
||||
| Начался льготный период | `grace_started` | |
|
||||
| Ключ истёк | `key_expired` | |
|
||||
| Баланс пополнен | `topup_succeeded` | |
|
||||
| Реферальный бонус | `referral_reward` | |
|
||||
| Возврат | `refund_issued` | |
|
||||
| Администраторам | `admin_new_order`, `admin_provisioning_needed`, `admin_new_lead` | на почту поддержки сайта |
|
||||
|
||||
С этапа 3 те же уведомления дублируются в основного бота, если Telegram привязан.
|
||||
|
||||
## 13. Статусы
|
||||
|
||||
### 13.1 Ключ
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> provisioning: заказ оплачен
|
||||
provisioning --> active: выдан
|
||||
active --> active: продление оплачено
|
||||
active --> grace: срок истёк, оплаты нет
|
||||
grace --> active: продление оплачено
|
||||
grace --> expired: льготный период закончился
|
||||
provisioning --> cancelled: возврат
|
||||
active --> cancelled: возврат с отменой
|
||||
active --> suspended: блокировка
|
||||
suspended --> active: разблокировка
|
||||
```
|
||||
|
||||
### 13.2 Заказ
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> pending
|
||||
pending --> paid
|
||||
pending --> cancelled: отменён
|
||||
pending --> expired: истёк срок заказа или ключ истёк
|
||||
paid --> partially_refunded
|
||||
paid --> refunded
|
||||
partially_refunded --> refunded
|
||||
```
|
||||
|
||||
### 13.3 Платёж
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> pending
|
||||
pending --> succeeded
|
||||
pending --> failed
|
||||
pending --> cancelled
|
||||
succeeded --> partially_refunded
|
||||
succeeded --> refunded
|
||||
```
|
||||
|
||||
## 14. Конкурентность и идемпотентность
|
||||
|
||||
1. Все денежные операции — в транзакции базы. Баланс меняется только под блокировкой строки пользователя (`SELECT … FOR UPDATE`).
|
||||
2. Уникальные ограничения защищают от двойного проведения: движения (см. [03-data-model.md](03-data-model.md), 6.3), платежи (`adapter` + `providerPaymentId`), заказы (`key` + `renewalFor`).
|
||||
3. Вебхуки: проверка подписи, сохранение id события, идемпотентная обработка, быстрый ответ 200.
|
||||
4. Задачи берут ключи с `FOR UPDATE SKIP LOCKED`, чтобы параллельные запуски не продлили ключ дважды.
|
||||
|
||||
## 15. Как показываем в кабинете
|
||||
|
||||
1. История платежей — одна строка на событие для человека. Платёж картой за заказ (зачисление и списание) показывается одной строкой: «Первый месяц · Нидерланды · Амстердам · Карта ···· 4242 · −4 900 ₽». Списание с баланса — «С баланса». Пополнение, реферальный бонус, возврат, корректировка — отдельными строками.
|
||||
2. «Следующее списание» — ближайшее продление среди ключей с автопродлением: сумма, дата, ключ; ниже — следующие.
|
||||
3. Бейдж «Истекает» — до окончания не больше `expiringBadgeDays` и (автопродление выключено или не хватит ни баланса, ни карты).
|
||||
4. На странице ключа: «Продлится автоматически через N дней», если денег или карты хватает, иначе «Осталось N дней» (предупреждающий цвет).
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
# 05. Вход, сессии, аккаунты
|
||||
|
||||
## 1. Аккаунты
|
||||
|
||||
1. Аккаунты раздельные по сайтам ([D-011](decisions/D-011-accounts.md)). Уникальность: (`site`, `email`) и (`site`, `telegramId`).
|
||||
2. Один человек на двух сайтах — два разных пользователя с разными балансами и ключами.
|
||||
3. Администраторы — отдельная коллекция `admins` с паролем, к пользователям отношения не имеют.
|
||||
|
||||
## 2. Вход по коду из письма
|
||||
|
||||
Одно поле email: если аккаунта нет — он создаётся, если есть — выполняется вход. Ответ всегда одинаковый, по нему нельзя узнать, есть ли аккаунт.
|
||||
|
||||
### 2.1 Запрос кода
|
||||
|
||||
1. Пользователь вводит email и проходит ALTCHA.
|
||||
2. Сервер нормализует email (обрезает пробелы, переводит в нижний регистр) и проверяет лимиты:
|
||||
a. по email — не чаще раза в 60 секунд и не больше 5 кодов в час;
|
||||
b. по IP — не больше 20 запросов в час.
|
||||
3. Прошлые неиспользованные коды для этого email и сайта аннулируются.
|
||||
4. Создаётся код: 6 цифр, действует 10 минут. В базе хранится только HMAC-SHA256 (секрет из `.env`, сайт, email, код).
|
||||
5. Письмо `login_code` на языке текущей страницы. Если в настройках почты включено «Записывать коды входа в журнал», код пишется в журнал писем (раздел 4).
|
||||
6. Ответ: «Код отправлен на {email}. Он действует 10 минут». Кнопка «Отправить ещё раз» — через 60 секунд, «Другая почта» — назад.
|
||||
|
||||
### 2.2 Проверка кода
|
||||
|
||||
1. Берётся последний действующий код для email и сайта. Счётчик попыток +1; после 5 неудачных код аннулируется.
|
||||
2. Сравнение — за постоянное время.
|
||||
3. Код верный:
|
||||
a. найти пользователя (сайт, email) или создать: `registeredVia = email`, язык — текущий, валюта — выбранная (Q-04), реферальная привязка из cookie `vx_ref`, хэш IP регистрации;
|
||||
b. пользователь `blocked` — отказ: «Аккаунт заблокирован, напишите в поддержку»;
|
||||
c. проставить `emailVerifiedAt` и `lastLoginAt`, создать сессию, вернуть на страницу, с которой пришёл (кабинет или оформление заказа).
|
||||
|
||||
## 3. Сессии
|
||||
|
||||
1. Cookie `vx_session`: случайные 32 байта, только для хоста кабинета (`my.veilix.app` и т.д., без атрибута Domain), `HttpOnly`, `Secure`, `SameSite=Lax`.
|
||||
2. В базе (`customer-sessions`) — только SHA-256 токена. Срок — 30 дней, продлевается при активности (не чаще раза в сутки).
|
||||
3. «Выйти» отзывает текущую сессию, «Выйти на всех устройствах» — все.
|
||||
4. Payload: коллекция `customers` — коллекция авторизации с `disableLocalStrategy: true` и своей стратегией. Стратегия читает cookie, находит сессию и возвращает пользователя. Поэтому правила доступа Payload работают и для пользователей.
|
||||
5. Защита от CSRF: `SameSite=Lax`; server actions Next.js сверяют Origin и Host; свои POST-эндпоинты проверяют Origin.
|
||||
6. Cookie хостовые, поэтому dev и prod не пересекаются: `dev.my.veilix.app` и `my.veilix.app` не видят cookie друг друга.
|
||||
|
||||
## 4. Коды в журнале для отладки
|
||||
|
||||
1. Глобальная настройка «Почта» → «Записывать коды входа в журнал».
|
||||
2. Включено: в записи журнала писем видно поле `debugCode` с кодом открытым текстом. В dev-логе процесса код тоже печатается.
|
||||
3. Видят только роли owner и admin. Пока настройка включена, в админке висит предупреждение. Записи с кодом удаляются через сутки.
|
||||
4. Вместе с транспортом «только журнал» (письма не отправляются, только пишутся в журнал) это позволяет тестировать вход без настроенной почты.
|
||||
5. В prod по умолчанию выключено: с кодами из журнала можно войти в любой аккаунт.
|
||||
|
||||
## 5. Оформление заказа
|
||||
|
||||
Шаг «Почта» на странице заказа — тот же вход по коду. После подтверждения пользователь остаётся на шаге «Оплата».
|
||||
|
||||
## 6. Telegram (этап 3)
|
||||
|
||||
### 6.1 Вход из MiniApp
|
||||
|
||||
1. MiniApp передаёт серверу `initData` от Telegram.
|
||||
2. Сервер проверяет подпись: HMAC-SHA256 с ключом `HMAC_SHA256("WebAppData", токен основного бота)`, `auth_date` не старше суток.
|
||||
3. Ищем пользователя (сайт, telegramId). Нет — создаём: `registeredVia = telegram`, язык — из `language_code` Telegram (если он среди включённых), валюта — по умолчанию домена.
|
||||
4. Сессия `kind = miniapp`. Как её хранить (cookie внутри WebView Telegram или токен в памяти) — решаем на этапе 3.
|
||||
|
||||
### 6.2 Привязка почты из Telegram
|
||||
|
||||
1. Пользователь вводит email в боте или MiniApp и получает код с назначением `link_email`.
|
||||
2. Код верный — три случая:
|
||||
a. на этом сайте нет пользователя с такой почтой — почта привязывается, теперь работает вход на сайте по коду;
|
||||
b. есть пользователь с этой почтой без Telegram — аккаунты объединяются (раздел 6.4);
|
||||
c. у пользователя с этой почтой привязан другой Telegram — автоматически не объединяем: «Эта почта привязана к другому Telegram-аккаунту, напишите в поддержку».
|
||||
|
||||
### 6.3 Привязка Telegram с сайта и вход через Telegram
|
||||
|
||||
1. «Подключить Telegram» в кабинете создаёт одноразовый токен (10 минут) и открывает `t.me/<основной бот>?start=link_<токен>`. Бот проверяет токен и привязывает Telegram к аккаунту. Если этот Telegram уже у другого пользователя сайта — правила раздела 6.4.
|
||||
2. «Войти через Telegram» на сайте — официальный виджет Telegram Login. Для основного бота в BotFather указывается домен кабинета. Нет пользователя с этим Telegram — создаётся новый.
|
||||
|
||||
### 6.4 Объединение аккаунтов (в пределах сайта)
|
||||
|
||||
1. Основным остаётся аккаунт с почтой, к нему переходит Telegram второго.
|
||||
2. Ключи, заказы, платежи, сохранённые карты, заявки переназначаются на основной аккаунт.
|
||||
3. Баланс переносится парой движений `adjustment` с комментарием «Объединение аккаунтов»: минус на втором, плюс на основном. Сами движения не меняются.
|
||||
4. Реферальная привязка: если у обоих есть пригласивший — остаётся более ранний. Реферальный код — основного аккаунта.
|
||||
5. Второй аккаунт блокируется со ссылкой «объединён с …». Всё пишется в журнал действий.
|
||||
6. Конфликты переводят объединение в ручной режим, его делает администратор кнопкой «Объединить аккаунты»: разные валюты при ненулевых балансах, у обоих привязан Telegram.
|
||||
|
||||
## 7. Администраторы
|
||||
|
||||
1. Стандартная авторизация Payload: email и пароль, cookie только на хосте админки.
|
||||
2. Админка открывается только на `plc.veilix.cc` и `dev.plc.veilix.cc`; nginx пускает туда только с разрешённых IP.
|
||||
3. 5 неудачных попыток — блокировка на 15 минут.
|
||||
4. Первый администратор каждого окружения создаётся на экране первого входа Payload.
|
||||
|
||||
## 8. Cookies
|
||||
|
||||
| Cookie | Где | Срок | Зачем |
|
||||
|---|---|---|---|
|
||||
| `vx_session` | хост кабинета | 30 дней | сессия пользователя |
|
||||
| `vx_ref` | хост кабинета | 60 дней | реферальный код |
|
||||
| `NEXT_LOCALE` | каждый хост | 1 год | выбранный язык |
|
||||
| `vx_currency` | хост сайта и кабинета | 1 год | выбранная валюта (у вошедшего — валюта аккаунта) |
|
||||
| `vx_consent` | хост сайта и кабинета | 1 год | согласие на cookies (этап 1h) |
|
||||
| `payload-token` | хост админки | сессия админки | вход администратора |
|
||||
|
||||
Тема (светлая, тёмная, как в системе) хранится в localStorage, как в макете, и применяется скриптом до отрисовки, без мигания.
|
||||
@@ -0,0 +1,79 @@
|
||||
# 06. Языки и валюты на сайте
|
||||
|
||||
## 1. Языки
|
||||
|
||||
1. Сейчас: `en` (по умолчанию), `ru`, `de`, `fr`, `es`.
|
||||
2. Позже: `pt-br` (бразильский португальский) и `ja`. Для японского понадобится запасной шрифт: Commissioner и Unbounded не содержат японских знаков.
|
||||
3. Список языков задаётся в конфиге Payload и next-intl (код). У каждого домена в базе — включённые языки и язык по умолчанию.
|
||||
4. Интерфейс админки — `ru` и `en`.
|
||||
|
||||
## 2. Два слоя переводов
|
||||
|
||||
| Что | Где | Кто правит |
|
||||
|---|---|---|
|
||||
| Интерфейс: кнопки, подписи, ошибки, сообщения форм | `src/i18n/messages/{en,ru,de,fr,es}.json`, next-intl | разработка, через git |
|
||||
| Контент: тарифы, локации, FAQ, страницы, шаблоны писем, тексты бота | локализованные поля Payload | админка, без деплоя |
|
||||
|
||||
1. В коде компонентов нет строк на конкретном языке, только ключи переводов.
|
||||
2. Нет перевода на языке — показывается английский. Отдельная проверка выводит список непереведённых ключей.
|
||||
3. Множественные числа — ICU в next-intl: «12 серверов», «3 сервера», «1 сервер».
|
||||
|
||||
## 3. URL
|
||||
|
||||
1. Английский без префикса, остальные — с префиксом: `veilix.app/pricing`, `veilix.app/ru/pricing`, `veilix.app/de/pricing`.
|
||||
2. Так же в кабинете: `my.veilix.app/keys`, `my.veilix.app/ru/keys`.
|
||||
3. MiniApp: адрес в BotFather один, поэтому язык берётся из профиля пользователя или из Telegram; префикс в URL необязателен.
|
||||
4. Пути одинаковые для всех языков (английские): `/pricing`, `/locations`, `/support`, `/legal/terms`, `/keys`, `/billing`, `/referral`, `/order`, `/login`.
|
||||
5. Язык, не включённый на домене, — 404.
|
||||
|
||||
## 4. Определение языка
|
||||
|
||||
1. Порядок: язык из URL → выбранный ранее (cookie `NEXT_LOCALE`, у вошедшего — язык профиля) → язык браузера (`Accept-Language`) среди включённых → страна из заголовка nginx `X-Geo-Country` (MaxMind) → `en`.
|
||||
2. Перенаправление на версию с префиксом — только при первом заходе на адрес без префикса, без cookie, и только если выбран не английский. Поисковых ботов не перенаправляем никогда: так рекомендует Google.
|
||||
3. Переключатель языка ставит cookie `NEXT_LOCALE`. У вошедшего пользователя язык сохраняется ещё и в профиле: на нём уходят письма и сообщения бота.
|
||||
4. Соответствие стран языкам по умолчанию: RU, BY, KZ, KG → ru; DE, AT → de; FR → fr; ES и страны Латинской Америки → es; остальные → en.
|
||||
|
||||
## 5. SEO
|
||||
|
||||
1. `hreflang` на все включённые языки и `x-default` на английскую версию.
|
||||
2. `canonical` на основной домен.
|
||||
3. `sitemap.xml` для каждого маркетингового домена с вариантами всех языков.
|
||||
4. Кабинет и MiniApp не индексируются (`noindex`).
|
||||
5. Атрибут `lang` у `<html>` соответствует языку страницы.
|
||||
|
||||
## 6. Валюта
|
||||
|
||||
1. Переключатель валюты отдельный от языка, стоит рядом с переключателем языка в шапке и подвале. В макете его нет — добавляется в стиле переключателя языка.
|
||||
2. Валюта не определяется по стране: из России часто заходят через VPN.
|
||||
3. По умолчанию — валюта домена для языка (`domains.localeCurrency`: ru → RUB), иначе `domains.defaultCurrency` (USD).
|
||||
4. Выбор хранится в cookie `vx_currency`. Валюта не входит в URL: маркетинговые страницы заранее собраны во всех валютах, proxy выбирает вариант по cookie. Поисковые боты видят валюту по умолчанию.
|
||||
5. Ссылка «Купить» передаёт валюту в оформление заказа (`?currency=`). У вошедшего пользователя в кабинете всегда валюта аккаунта ([04-billing.md](04-billing.md), раздел 2).
|
||||
6. Форматирование — `Intl.NumberFormat` с узким символом валюты: «4 900 ₽» в русском, «₽4,900» в английском, «$49». Неразрывные пробелы ставит Intl. Даты — `Intl.DateTimeFormat`: «14 октября 2026».
|
||||
|
||||
## 7. Как переводим
|
||||
|
||||
1. Источник: русские тексты макетов и английские из словаря макета `design/handoff/project/i18n.js` (около 515 строк). Они ложатся в `ru.json` и `en.json` и в контент Payload.
|
||||
2. Немецкий, французский, испанский — машинный перевод LLM с глоссарием на этапе 1h. Вычитки носителями нет, поэтому глоссарий и контекст обязательны.
|
||||
3. В юридических текстах пункт: «При расхождениях между версиями действует английская».
|
||||
4. Новая строка интерфейса: разработчик добавляет ключ в `en.json` и `ru.json`; de, fr, es переводятся пакетно перед выкладкой.
|
||||
|
||||
## 8. Глоссарий
|
||||
|
||||
| RU | EN | Примечание |
|
||||
|---|---|---|
|
||||
| Вейликс / ВейликсВПН | Veilix / VeilixVPN | не переводится |
|
||||
| ключ | key | купленная единица доступа |
|
||||
| локация | location | |
|
||||
| тариф | plan | |
|
||||
| подключение | connection | |
|
||||
| устройство | device | |
|
||||
| индивидуальный IP | dedicated IP | |
|
||||
| RU-адрес | RU address | |
|
||||
| баланс, счёт | balance | |
|
||||
| пополнить | top up | |
|
||||
| продление, автопродление | renewal, auto-renewal | |
|
||||
| льготный период | grace period | |
|
||||
| реферальная программа | referral program | |
|
||||
| установим за 24 часа | ready within 24 hours | статус локации dedicated |
|
||||
|
||||
Колонки для de, fr, es добавляются на этапе 1h.
|
||||
@@ -0,0 +1,104 @@
|
||||
# 07. Фронтенд
|
||||
|
||||
## 1. Макеты
|
||||
|
||||
1. Источник — [design/handoff/project/](../design/handoff/project/), экспорт Claude Design. Прототипы на HTML: разметка внутри `<x-dc>`, логика в `<script data-dc-script>`, стили — inline плюс CSS-переменные в `<style>`. `support.js` — среда выполнения прототипа, в продукт не переносится.
|
||||
2. Задача — воспроизвести внешний вид точно, читая код макетов: отступы, размеры, цвета, состояния. Внутреннюю структуру прототипа не копируем.
|
||||
3. Тексты макетов — на русском, английские версии — в `i18n.js` (словарь «русская строка → английская»).
|
||||
4. Бриф дизайна — `design/handoff/project/uploads/` (файл «Дизайн VEILIX.md»). Где бриф расходится с прототипами или с [01-product.md](01-product.md), верны прототипы и 01-product.
|
||||
|
||||
## 2. Какой макет куда
|
||||
|
||||
| Макет | Вейликс | ВейликсВПН |
|
||||
|---|---|---|
|
||||
| `Main` / `MassMain` | veilix.app `/` | veilixvpn.com `/` |
|
||||
| `Price` / `MassPrice` | `/pricing` | `/pricing` |
|
||||
| `Locations` / `MassLocations` | `/locations` | `/locations` |
|
||||
| `Support` / `MassSupport` | `/support` | `/support` |
|
||||
| `Order` / `MassOrder` | my.veilix.app `/order` | my.veilixvpn.com `/order` |
|
||||
| `App` / `MassApp` | my.veilix.app: `/login`, `/`, `/keys`, `/keys/[id]`, `/buy`, `/billing`, `/referral` | то же на my.veilixvpn.com |
|
||||
| `AppTG` / `MassAppTG` | tg.veilix.app (этап 3) | tg.veilixvpn.com (этап 3) |
|
||||
|
||||
Плюс страницы без макета: `/legal/terms`, `/legal/privacy`, `/legal/refund`, `/legal/cookies`, 404 и 500 — в стиле сайта.
|
||||
|
||||
## 3. Маршруты кабинета
|
||||
|
||||
| Путь | Что |
|
||||
|---|---|
|
||||
| `/login` | вход по коду |
|
||||
| `/` | главная: ключи, «Купить ключ», FAQ |
|
||||
| `/keys`, `/keys/[id]` | список ключей, страница ключа: срок, автопродление, «Продлить сейчас», WG-конфиги (скачать, QR), ссылка Happ/Xray (копировать, QR) |
|
||||
| `/buy` | покупка внутри кабинета: dedicated — сетка локаций с ценами, mass — «Месяц» и «Год» |
|
||||
| `/order` | оформление заказа: шаги «Почта» и «Оплата», сводка |
|
||||
| `/billing` | баланс, пополнение, карта, ближайшие списания, история |
|
||||
| `/referral` | реферальная программа |
|
||||
| `/r/[код]` | реферальная ссылка: ставит cookie и ведёт на сайт бренда |
|
||||
|
||||
Навигация по макету: на широком экране — боковая панель, на узком — плавающая панель вкладок «Главная, Ключи, Купить, Ещё».
|
||||
|
||||
## 4. Темы и токены
|
||||
|
||||
1. Tailwind v4: токены макета — CSS-переменные, подключённые в `@theme`. Бренд задаёт атрибут `data-site="dedicated|mass"` на `<html>`, тему — `data-theme="light|dark"`. Режим «Как в системе» следует `prefers-color-scheme`.
|
||||
2. Переключатель темы — 3 режима, как в макете. Выбор хранится в localStorage, тема применяется скриптом в `<head>` до отрисовки.
|
||||
3. Токены в OKLCH, как в макете. Ниже — для ориентира; точные значения — в `<style>` прототипов, при расхождении верен прототип.
|
||||
|
||||
| Токен | Вейликс светлая | Вейликс тёмная | ВейликсВПН светлая | ВейликсВПН тёмная |
|
||||
|---|---|---|---|---|
|
||||
| `--bg` | 99% .005 296 (#fcfbff) | 17% .022 296 (#100d18) | 99.6% .0015 215 | 23% .026 210 |
|
||||
| `--surface` | 100% 0 0 | 21% .026 296 | 100% 0 0 | 26.5% .028 210 |
|
||||
| `--surface-2` | 97.5% .01 296 | 24% .03 296 | 98.4% .003 215 | 30% .03 210 |
|
||||
| `--ink` | 24% .04 296 (#211b30) | 96% .01 296 | 24% .04 204 | 96% .012 196 |
|
||||
| `--ink-2` | 45% .03 296 | 79% .02 296 | 45% .03 204 | 80% .02 196 |
|
||||
| `--ink-3` | 60% .025 296 | 63% .025 296 | 60% .025 204 | 65% .025 196 |
|
||||
| `--border` | 92% .014 296 | 31% .032 296 | 92% .014 200 | 36% .04 204 |
|
||||
| `--accent` | 45% .17 296 (#6137a6) | 74% .15 296 (#b495fe) | 48% .105 178 (#00705e) | 76% .105 190 (#52c6c0) |
|
||||
| `--accent-hover` | 38% .17 296 | 80% .13 296 | 41% .1 178 | 83% .09 190 |
|
||||
| `--accent-soft` | 95.5% .028 296 | 27% .05 296 | 95.5% .032 180 | 31% .05 190 |
|
||||
| `--on-accent` | 99% .01 296 | 17% .03 296 | 99% .01 180 | 20% .04 204 |
|
||||
| `--glow-1/2/3` | 86% .1 296 / 89% .09 266 / 91% .08 332 | 45% / 40% / 42% | 86% .085 190 / 88% .075 205 / 89% .07 172 | 52% .09 200 / 47% .085 215 / 50% .08 180 |
|
||||
| `--sel` | — | — | 96.5% .016 190 | 30% .03 195 |
|
||||
|
||||
Общие для брендов: `--warn` (62% .14 62 / 78% .13 62), `--tg` (62% .15 250 / 70% .13 250), `--veil` (белый 55% / 7%), `--shadow`, цвета карты `--land`, `--land-line`, `--ocean`.
|
||||
|
||||
## 5. Шрифты, иконки, логотип
|
||||
|
||||
1. Commissioner 300–600 — весь текст; Unbounded 600 — только надпись логотипа. Подключение через `next/font`: файлы скачиваются при сборке и отдаются с нашего домена, без запросов к Google Fonts. Наборы символов: latin, latin-ext, cyrillic.
|
||||
2. Иконки, флаги и логотип в макете нарисованы CSS-фигурами. Переносим их React-компонентами, сохраняя вид.
|
||||
3. Логотип — вариант «Слои». Фавиконы — `favicon.svg` и `favicon-mass.svg` из макета.
|
||||
|
||||
## 6. Карта мира
|
||||
|
||||
1. В макете карта строится в браузере через d3 и topojson с загрузкой данных с CDN. В продукте SVG-карта рендерится на сервере при сборке: d3-geo, проекция Natural Earth, данные `world-atlas` (countries-110m) из npm.
|
||||
2. В браузере — только интерактив: подсветка, всплывающая карточка, синхронизация с сеткой локаций.
|
||||
3. На узком экране карта прокручивается горизонтально, как в макете.
|
||||
|
||||
## 7. Решения по расхождениям в макетах
|
||||
|
||||
1. Устройств — 5, это поле тарифа. Цена продления dedicated — 2 499 ₽, цены задаются тарифами (3 уровня).
|
||||
2. Оформление заказа получает тариф, локацию и валюту из ссылки. В макете там всегда Нидерланды и 4 900 ₽.
|
||||
3. Поля номера карты из макета `Order` не делаем: карта вводится в виджете провайдера (этап 4). На этапе 1 платёжных систем для пользователя нет ([04-billing.md](04-billing.md), раздел 3.6).
|
||||
4. Mass: в кабинете «Список ключей», без «текущей/запасной локации» и без экрана смены страны. Внутри ключа — ссылка Xray и WG-конфиги по странам.
|
||||
5. Статус локации показываем везде одинаково: в списках, карточках и на карте.
|
||||
6. Кнопка «Получить VPN» на главной ведёт к блоку цен на этой же странице, на остальных страницах — на `/pricing`.
|
||||
7. FAQ — одна коллекция с местами показа: главная, цены, поддержка (3 раздела), кабинет, MiniApp.
|
||||
8. Контакты поддержки берутся из настроек сайта, Telegram — ссылка на бота поддержки.
|
||||
9. Плашки способов оплаты в подвале — по активным платёжным системам сайта.
|
||||
10. Добавляем то, чего нет в макете: переключатель валюты, `<title>` и мета-описания, H1 на каждой странице, поддержку `prefers-reduced-motion` для анимаций.
|
||||
11. Показывать ли на сайте количество свободных серверов (в MiniApp макет его показывает) — решаем на этапе 1b. По умолчанию — только статус.
|
||||
|
||||
## 8. Состояния, которых нет в макетах
|
||||
|
||||
Рисуем в стиле макета на соответствующих этапах:
|
||||
|
||||
1. Вход: ошибка кода, истёкший код, таймер повторной отправки, загрузка.
|
||||
2. Ключ: готовится (до 24 часов), льготный период, истёк, заблокирован; пустой список ключей; ключ без подключений.
|
||||
3. Заказ: «ожидает оплаты» (этап 1), не хватает баланса, оплата не прошла; 3-D Secure и счёт в криптовалюте — этап 4.
|
||||
4. Счёт: своя сумма пополнения, подтверждение отвязки карты, результат пополнения.
|
||||
5. Общие: уведомления (тосты), скелетоны загрузки, страницы 404 и 500, ошибки валидации форм.
|
||||
|
||||
## 9. Производительность и доступность
|
||||
|
||||
1. Маркетинговые страницы: Lighthouse mobile ≥ 90 (цель 95+), LCP ≤ 2 с, CLS ≤ 0.05, INP ≤ 200 мс.
|
||||
2. JavaScript в браузере — только интерактивные островки: переключатели языка, валюты и темы, FAQ, выбор локации, формы, карта.
|
||||
3. Сторонние скрипты при первой загрузке — только Umami (асинхронно). ALTCHA подгружается только на формах.
|
||||
4. Доступность — WCAG 2.1 AA: контраст, видимый фокус, работа с клавиатуры для выпадающих списков, аккордеонов и шитов, области нажатия ≥ 44 px.
|
||||
@@ -0,0 +1,82 @@
|
||||
# 08. Интеграции
|
||||
|
||||
## 1. Почта (этап 1c)
|
||||
|
||||
1. Шаблоны — в Payload (`email-templates`): тема и текст на каждом языке, для каждого сайта свои. Шапка (логотип) и подвал (контакты, юридические ссылки) — в коде, у каждого бренда свои.
|
||||
2. Отправка — через email-адаптер Payload на Nodemailer. Транспорт переключается в админке (глобал «Почта»):
|
||||
a. `smtp` — внешний SMTP-провайдер (данные в настройках, пароль зашифрован);
|
||||
b. `sendmail` — локальный Postfix (`/usr/sbin/sendmail`);
|
||||
c. `log_only` — не отправлять, только записать в журнал (для разработки).
|
||||
3. Письма уходят через фоновую задачу `send-email` с повторами при ошибке. Каждое письмо пишется в журнал писем; коды входа — по флагу ([05-auth.md](05-auth.md), раздел 4).
|
||||
4. Кнопка «Отправить тестовое письмо» проверяет текущий транспорт.
|
||||
5. Чтобы письма через Postfix не попадали в спам (root, [09-infrastructure.md](09-infrastructure.md), раздел 6):
|
||||
a. Postfix отправляет с IP 169.58.76.183 под именем `fr-ub2gs3.veilix.cc` — у этого IP прямая и обратная DNS-записи совпадают;
|
||||
b. для veilix.app и veilixvpn.com нужны SPF, DKIM (OpenDKIM уже установлен) и DMARC;
|
||||
c. проверить, что исходящий порт 25 открыт и IP нет в чёрных списках.
|
||||
6. Новые IP почтовые сервисы поначалу принимают настороженно, поэтому SMTP-провайдер держим запасным вариантом.
|
||||
|
||||
## 2. ALTCHA (этап 1c)
|
||||
|
||||
1. Капча на proof-of-work без сторонних сервисов: браузер решает небольшую вычислительную задачу, сервер проверяет подпись.
|
||||
2. Эндпоинт выдачи задачи — `/svc/altcha/challenge`, ключ подписи — в `.env`.
|
||||
3. Где: запрос кода входа, формы заявок и обратной связи.
|
||||
4. Вместе с лимитами по IP и email ([05-auth.md](05-auth.md)). Провайдер капчи изолирован в `core/`, его можно заменить.
|
||||
|
||||
## 3. MaxMind (этап 1a)
|
||||
|
||||
1. Модуль nginx `geoip2` (уже установлен) определяет страну по базе GeoLite2-Country и передаёт её приложению заголовком `X-Geo-Country`. Клиентский заголовок с таким именем nginx перезаписывает.
|
||||
2. База обновляется `geoipupdate` по таймеру (root, нужен бесплатный аккаунт MaxMind). Модуль nginx перечитывает базу сам (`auto_reload`).
|
||||
3. Страна используется только для выбора языка по умолчанию, не для валюты.
|
||||
4. Если понадобится страна произвольного IP (антифрод), приложение читает тот же `.mmdb` библиотекой `maxmind`.
|
||||
|
||||
## 4. Chatwoot (этап 2)
|
||||
|
||||
1. Один экземпляр на отдельном VPS обслуживает оба бренда. У каждого бренда свои входящие каналы:
|
||||
a. виджет на сайте и в кабинете;
|
||||
b. бот поддержки в Telegram, подключённый к Chatwoot напрямую как Telegram-канал.
|
||||
2. Для вошедших пользователей виджет получает идентификацию с HMAC-подписью: оператор видит, кто пишет, а подделать её нельзя. Передаём атрибуты: сайт, id пользователя, email, число ключей.
|
||||
3. Почему бот поддержки отдельный: Telegram отдаёт обновления одного бота только одному получателю. Основной бот принадлежит нашему приложению, бот поддержки — Chatwoot.
|
||||
|
||||
## 5. Twenty CRM (этап 2)
|
||||
|
||||
1. Заявки из `leads` уходят в Twenty через API: человек и сделка с полями заявки, брендом и страницей. Отправка — фоновой задачей с повторами; id в Twenty сохраняется в `leads.crmId`.
|
||||
2. До этапа 2 заявки видны в админке Payload и приходят письмом `admin_new_lead` на почту поддержки.
|
||||
|
||||
## 6. Telegram (этап 3)
|
||||
|
||||
1. Боты для каждого бренда:
|
||||
a. основной: команды, кнопка MiniApp, уведомления. Код — grammY в нашем приложении, вебхук `/svc/telegram/<сайт>/webhook` с секретным токеном;
|
||||
b. поддержки: подключён к Chatwoot.
|
||||
Для dev — отдельные тестовые боты.
|
||||
2. MiniApp — на tg.*, вход по `initData` ([05-auth.md](05-auth.md), раздел 6). Dev-MiniApp доступна только Telegram-аккаунтам из списка разрешённых.
|
||||
3. Черновик команд основного бота: `/start` (в том числе с реферальным кодом или токеном привязки), `/keys`, `/buy`, `/balance`, `/topup`, `/referral`, `/language`, `/support` (ссылка на бота поддержки), `/help`. Тексты и кнопки — в Payload, локализованные.
|
||||
4. Уведомления из [04-billing.md](04-billing.md) (раздел 12) дублируются в бот, если Telegram привязан.
|
||||
5. Оплата в Telegram (Q-11): по правилам Telegram цифровые товары в ботах и MiniApp оплачиваются Telegram Stars, за нарушения ботов блокируют. Варианты: Stars, внешняя страница оплаты, покупки в Telegram только с баланса. Решаем на старте этапа.
|
||||
|
||||
## 7. Платёжные системы
|
||||
|
||||
1. Каждый провайдер — адаптер в `core/payments/` с единым интерфейсом:
|
||||
a. `createPayment` — создать платёж и вернуть адрес страницы оплаты или данные виджета;
|
||||
b. `handleWebhook` — проверить подпись и вернуть новый статус платежа;
|
||||
c. по возможностям: `chargeSavedMethod` (автосписание), `refund`, `detachMethod`.
|
||||
2. Запись в коллекции `payment-systems` включает адаптер в валютах и для назначений ([03-data-model.md](03-data-model.md), 3.4). На каком сайте система доступна, следует из валют его доменов. Ключи доступа — в `.env`.
|
||||
3. Этап 1: только `manual` — для администраторов.
|
||||
4. Этап 4:
|
||||
a. Stripe — USD: карты, сохранение карты, автосписания без участия пользователя на переменную сумму («цена минус баланс»), без Stripe Subscriptions. Сначала тестовый режим;
|
||||
b. RUB — процессинги, принимающие карты РФ (вероятно, KZ и TR), только пополнения. Провайдеров выбираем на этапе (Q-12);
|
||||
c. криптовалюта — пополнения в RUB и USD, провайдер выбирается на этапе;
|
||||
d. ЮKassa — опционально, только пополнения, если примет категорию.
|
||||
5. Вебхуки — `/svc/payments/<адаптер>/webhook`; обработка идемпотентная ([04-billing.md](04-billing.md), раздел 14).
|
||||
|
||||
## 8. Umami и баннер cookies (этап 1h)
|
||||
|
||||
1. Umami — своя аналитика без cookies, данные не уходят третьим лицам ([D-018](decisions/D-018-analytics.md)).
|
||||
a. Ставится на этот сервер из исходников, без Docker: Node.js и PostgreSQL здесь уже есть. Umami 3.x поддерживает только PostgreSQL.
|
||||
b. Отдельный пользователь ОС `umami`, своя база `umami`, systemd user-юнит, порт 3300.
|
||||
c. Панель — `umi.veilix.cc`, доступ только с разрешённых IP.
|
||||
2. Скрипт и отправка событий проксируются через домены брендов под нейтральными путями (переменные Umami `TRACKER_SCRIPT_NAME` и `COLLECT_API_ENDPOINT`), чтобы их не резали блокировщики рекламы. Реальный IP посетителя Umami берёт из заголовка nginx (`CLIENT_IP_HEADER`). Телеметрия Umami выключена (`DISABLE_TELEMETRY=1`).
|
||||
3. Баннер cookies: обязательные (сессия, язык, валюта) и функциональные (чат Chatwoot). Решение пользователя хранится в cookie `vx_consent`, необязательные скрипты загружаются только после согласия. Библиотека — vanilla-cookieconsent или своя лёгкая реализация.
|
||||
|
||||
## 9. Автоматизация ключей (этап 5)
|
||||
|
||||
Подбор свободного сервера по тарифу (страны, теги) и автоматическая выдача подключений. Обсуждаем при старте этапа.
|
||||
@@ -0,0 +1,108 @@
|
||||
# 09. Инфраструктура
|
||||
|
||||
## 1. Сервер (проверено 18–20.10.2026)
|
||||
|
||||
| Что | Значение |
|
||||
|---|---|
|
||||
| ОС | Ubuntu 26.04.1 LTS |
|
||||
| Ресурсы | 4 vCPU, 7.7 ГБ RAM, 8 ГБ swap, 145 ГБ диска (занято 24 ГБ) |
|
||||
| IP | 169.58.76.183 — сайты (PTR `fr-ub2gs3.veilix.cc`, прямая запись совпадает); 173.212.216.183 — основной, через него по умолчанию идёт исходящий трафик (PTR `vmi3593751.contaboserver.net`) |
|
||||
| Установлено | Node 24.22, PostgreSQL 18.6, nginx 1.28 (модули geoip2 и brotli подключены), Redis 8, Postfix 3.10, OpenDKIM, certbot 4.0, Docker 29 (не используем), git |
|
||||
| Нет | pnpm, база GeoLite2 (`.mmdb`), сертификаты TLS, geoipupdate |
|
||||
| DNS | все 15 хостов проекта (вместе с `umi.veilix.cc`) указывают на 169.58.76.183; DNS-хостинг — Gcore |
|
||||
|
||||
## 2. Папки
|
||||
|
||||
```
|
||||
/home/vx_dev/veilix_dev/ — dev (пользователь vx_dev): рабочая копия git, .env, media/
|
||||
/home/vx_prod/veilix/ — prod (пользователь vx_prod), закрыто для остальных
|
||||
releases/<дата-тег>/ — собранные релизы (храним 5 последних)
|
||||
current -> releases/… — симлинк на работающий релиз
|
||||
shared/.env — секреты prod
|
||||
shared/media/ — загруженные файлы prod
|
||||
/home/umami/umami/ — Umami (пользователь umami)
|
||||
```
|
||||
|
||||
vx — личный пользователь администратора, к проекту отношения не имеет. Под vx_prod нет редактора и ИИ-агентов, VSCode Remote под vx_prod не подключаем ([D-004](decisions/D-004-environments.md)). Домашние папки всех пользователей — `750`, владелец — сам пользователь. Пустые папки `/home/vx/veilix.app`, `/home/vx/my.veilix.app`, `/home/vx/tg.veilix.app` проекту не нужны.
|
||||
|
||||
## 3. Процессы
|
||||
|
||||
1. Каждый сервис — под своим пользователем ОС, как systemd user-юнит. Root один раз включает `loginctl enable-linger` для vx_dev, vx_prod и umami, чтобы юниты работали без входа в систему.
|
||||
2. `veilix-dev.service` (vx_dev): `pnpm dev` на порту 3100 из `/home/vx_dev/veilix_dev`, перезапуск при сбое.
|
||||
3. `veilix.service` (vx_prod): `pnpm start` на порту 3000 из `/home/vx_prod/veilix/current`, `NODE_ENV=production`, перезапуск всегда.
|
||||
4. `umami.service` (umami): Umami на порту 3300.
|
||||
5. Фоновые задачи Payload выполняются внутри процессов приложения ([02-architecture.md](02-architecture.md), раздел 7).
|
||||
6. Логи — journald, у каждого пользователя свои (`journalctl --user -u veilix` под vx_prod). Сессии Claude логи prod не видят: при разборе проблем prod их присылает пользователь.
|
||||
7. Проверка живости — `/svc/health` (с проверкой базы).
|
||||
|
||||
## 4. nginx
|
||||
|
||||
| Хосты | Куда | Доступ |
|
||||
|---|---|---|
|
||||
| veilix.app, veilixvpn.com, my.veilix.app, my.veilixvpn.com, tg.veilix.app, tg.veilixvpn.com | 127.0.0.1:3000 | открыт |
|
||||
| plc.veilix.cc | 127.0.0.1:3000 | только разрешённые IP |
|
||||
| dev.veilix.app, dev.my.veilix.app, dev.veilixvpn.com, dev.my.veilixvpn.com | 127.0.0.1:3100 | только разрешённые IP или пароль; `X-Robots-Tag: noindex` |
|
||||
| dev.tg.veilix.app, dev.tg.veilixvpn.com | 127.0.0.1:3100 | открыт для Telegram; приложение пускает только разрешённые Telegram-аккаунты |
|
||||
| dev.plc.veilix.cc | 127.0.0.1:3100 | только разрешённые IP |
|
||||
| umi.veilix.cc | 127.0.0.1:3300 | только разрешённые IP |
|
||||
|
||||
1. Слушает 169.58.76.183, порты 80 (редирект на HTTPS и проверка certbot) и 443. TLS 1.2 и 1.3, HTTP/2, HSTS.
|
||||
2. Сертификаты — certbot, проверка через HTTP.
|
||||
3. Сжатие — brotli и gzip.
|
||||
4. GeoIP: блок `geoip2` в `/etc/nginx/conf/ip.conf`, база `/var/lib/GeoIP/GeoLite2-Country.mmdb` с `auto_reload`. Переменная `$geoip_country_code` уходит приложению заголовком `X-Geo-Country`; такой же заголовок от клиента nginx затирает. Старый модуль GeoIP v1 (`modules-enabled/50-mod-http-geoip.conf`) отключён: он занимает те же имена переменных, и `nginx -t` падает с ошибкой «the duplicate "geoip_country_code" variable».
|
||||
5. Заголовки к приложению — общий файл `/etc/nginx/snippets/veilix_proxy.conf` (плюс `maps/upgrade.conf` для WebSocket), его подключает каждый `location` с `proxy_pass`: `Host`, `X-Real-IP` и `X-Forwarded-For` (оба — `$remote_addr`: nginx стоит первым, цепочку от клиента не принимаем), `X-Forwarded-Proto`, `X-Geo-Country`, заголовки WebSocket (нужны горячей перезагрузке dev). `proxy_set_header` не наследуется в `location` со своими `proxy_set_header`, поэтому файл подключается в каждый такой `location`.
|
||||
6. В `nginx.conf` подключаются только `sites-enabled/*.conf`: файлы сайтов должны заканчиваться на `.conf`. Сайты: `sites-available/02_vx_veilix-prod.conf`, `02_vx_veilix-dev.conf`, `02_vx_plc-prod.conf`, `02_vx_plc-dev.conf`. Сертификаты Let's Encrypt (certbot, проверка через nginx): `veilix.app` (6 prod-хостов брендов), `dev.veilix.app` (6 dev-хостов), `plc.veilix.cc`, `dev.plc.veilix.cc`.
|
||||
a. Доступ по IP — файл `conf/ip_list_admin.conf` со строками `allow …;`, в `location` после него `deny all;`. Подключается на хостах админки и dev, кроме `dev.tg.*`.
|
||||
b. `dev.tg.*` — отдельный `server` без ограничения по IP, с `X-Robots-Tag: noindex`.
|
||||
c. На порту 80 — `location / { return 301 https://$host$request_uri; }`. Именно внутри `location`, а не на уровне `server`: так certbot при продлении может вставить свой `location` для проверки домена.
|
||||
7. Лимиты запросов (`limit_req`) на эндпоинты входа и формы.
|
||||
8. Для tg.* (этап 3) разрешить показ во фрейме Telegram Web (`frame-ancestors` для доменов telegram.org).
|
||||
9. Пути трекера Umami на доменах брендов проксируются на 127.0.0.1:3300 ([08-integrations.md](08-integrations.md), раздел 8).
|
||||
10. Конфигурацию готовит Claude текстом на согласование, применяет root.
|
||||
|
||||
## 5. PostgreSQL
|
||||
|
||||
1. Базы `veilix_dev`, `veilix_prod` и `umami`. Владелец каждой — роль с именем пользователя ОС: `vx_dev`, `vx_prod`, `umami`.
|
||||
2. Вход по Unix-сокету с проверкой пользователя ОС: стандартное правило Ubuntu `local all all peer` в `pg_hba.conf` пускает пользователя ОС только под ролью с тем же именем. Паролей нет, `pg_hba.conf` и `pg_ident.conf` править не нужно.
|
||||
3. У каждой базы снято право подключения для всех (`REVOKE CONNECT … FROM PUBLIC`): войти может только владелец. vx_dev в `veilix_prod` не попадёт.
|
||||
4. Роль владеет своей базой: Payload нужны права на изменение схемы (push в dev, миграции в prod).
|
||||
5. Строка подключения: `postgresql://vx_dev@/veilix_dev?host=/var/run/postgresql` (для prod — `vx_prod@/veilix_prod`).
|
||||
|
||||
## 6. Почта (Postfix)
|
||||
|
||||
1. `smtp_bind_address = 169.58.76.183`, `myhostname = fr-ub2gs3.veilix.cc`: письма уходят с IP, у которого обратная запись совпадает с прямой.
|
||||
2. OpenDKIM: ключи для veilix.app и veilixvpn.com (селектор, напр. `mail`).
|
||||
3. DNS для каждого из двух доменов:
|
||||
a. SPF: `v=spf1 ip4:169.58.76.183 -all` (плюс SMTP-провайдер, если используем);
|
||||
b. DKIM: TXT `mail._domainkey.<домен>` с открытым ключом;
|
||||
c. DMARC: `_dmarc.<домен>` — сначала `v=DMARC1; p=none; rua=mailto:…`, после проверки — `p=quarantine`.
|
||||
4. Проверить, что хостер не закрывает исходящий порт 25 и что 169.58.76.183 нет в чёрных списках.
|
||||
5. Приём почты поддержки (`help@…`) — где ящик, вопрос Q-16.
|
||||
|
||||
## 7. Бэкапы
|
||||
|
||||
1. Prod-база: каждую ночь `pg_dump -Fc veilix_prod`, хранить 14 ежедневных и 8 еженедельных, копия на другой сервер (куда — Q-14). Раз в месяц — проверочное восстановление в отдельную базу.
|
||||
2. `shared/media` — синхронизация на тот же внешний сервер.
|
||||
3. Dev не бэкапим: код — в git, данные dev — тестовые.
|
||||
|
||||
## 8. Задачи для root по этапам
|
||||
|
||||
| № | Этап | Задача | Статус на 21.10.2026 |
|
||||
|---|---|---|---|
|
||||
| R-00 | 0 | Переезд разработки: перенести `/home/vx/veilix_dev` в `/home/vx_dev/veilix_dev` (владелец vx_dev); SSH-ключ vx_dev для VSCode; Claude Code под vx_dev | не сделано |
|
||||
| R-01 | 0 | Репозиторий в Gitea; SSH-ключ vx_dev с правом push; для vx_prod — ключ только на чтение (deploy key) | не сделано |
|
||||
| R-02 | 1a | PostgreSQL: роли `vx_dev`, `vx_prod`, `umami` и их базы (раздел 5) | сделано и проверено: vx_dev входит в `veilix_dev`, в `veilix_prod` — отказ |
|
||||
| R-03 | 1a | pnpm: `corepack enable pnpm` | сделано |
|
||||
| R-04 | 1a | Пользователи vx_dev, vx_prod, umami; `loginctl enable-linger` для них | сделано |
|
||||
| R-05 | 1a | nginx и сертификаты для всех хостов (раздел 4) | работает; осталось исправить опечатку в `conf/ip_list_admin.conf` и добавить `X-Robots-Tag` для `dev.tg.*` |
|
||||
| R-06 | 1a | MaxMind: `geoipupdate` (ключ в `/etc/GeoIP.conf`, база в `/var/lib/GeoIP`, таймер `geoipupdate.timer`); отключить старый модуль GeoIP v1; блок `geoip2` (раздел 4, пункт 4) | сделано |
|
||||
| R-07 | 1c | Postfix, OpenDKIM, DNS-записи SPF, DKIM, DMARC — или данные SMTP-провайдера | |
|
||||
| R-08 | 1h | PostgreSQL для prod — если не сделано вместе с R-02 | сделано в R-02 |
|
||||
| R-09 | 1h | nginx и сертификаты для prod-хостов | сделано вместе с R-05 |
|
||||
| R-10 | 1h | Umami: nginx и сертификат для `umi.veilix.cc` (DNS уже есть; база — в R-02) | |
|
||||
| R-11 | 1h | Бэкапы prod-базы и media на внешний сервер (Q-14) | |
|
||||
| R-12 | 2 | VPS с Chatwoot и Twenty, ключи API; 2 бота поддержки в BotFather | |
|
||||
| R-13 | 3 | Боты в BotFather: 2 основных и тестовые; домен для Telegram Login; адреса MiniApp | |
|
||||
| R-14 | 4 | Аккаунты платёжных систем (сначала тестовые), адреса вебхуков | |
|
||||
| R-15 | 1h | По желанию (Q-18): sudo-правило, разрешающее vx_dev запускать от имени vx_prod только команду выкладки | |
|
||||
| R-16 | 1h | По желанию: правило nftables — к 127.0.0.1:3000 подключаются только `www-data` (nginx) и vx_prod ([10-security.md](10-security.md), раздел 7) | |
|
||||
@@ -0,0 +1,56 @@
|
||||
# 10. Безопасность и приватность
|
||||
|
||||
## 1. Идентификаторы
|
||||
|
||||
1. Все `id` — UUID. В URL, API и консоли браузера нет последовательных номеров.
|
||||
2. Номера заказов, ключей, платежей и реферальные коды — случайные строки (алфавит Crockford base32, без похожих символов).
|
||||
|
||||
## 2. Секреты
|
||||
|
||||
1. Секреты — только в `.env` своего окружения, права 600, вне git: строка подключения к базе, секрет сессий и кодов, ключ шифрования полей, ключ ALTCHA, ключи платёжных систем, токены ботов.
|
||||
2. Чувствительные поля в базе шифруются приложением (AES-256-GCM): содержимое подключений (конфиги WG, ссылки подписок), SMTP-пароль, id сохранённых карт у провайдера.
|
||||
3. Для prod ключи свои, не такие, как в dev.
|
||||
|
||||
## 3. Админка
|
||||
|
||||
1. Только на `plc.veilix.cc` и `dev.plc.veilix.cc`, вход только с разрешённых IP (nginx).
|
||||
2. Роли owner, admin, support, content ([03-data-model.md](03-data-model.md), раздел 10). Поле `debugCode` журнала писем видят только owner и admin.
|
||||
3. Блокировка после 5 неудачных попыток входа.
|
||||
4. Денежные операции, выдача и блокировка ключей, объединение аккаунтов пишутся в журнал действий. Журнал нельзя изменить.
|
||||
|
||||
## 4. Пользователи
|
||||
|
||||
1. Коды входа хранятся только как HMAC, проверяются за постоянное время, 5 попыток, 10 минут жизни.
|
||||
2. ALTCHA и лимиты по IP и email на запрос кода.
|
||||
3. Ответ на запрос кода не выдаёт, есть ли аккаунт.
|
||||
4. Токены сессий в базе — только SHA-256. Cookie — `HttpOnly`, `Secure`, `SameSite=Lax`, привязаны к хосту.
|
||||
5. CSRF: `SameSite=Lax`, проверка Origin в server actions и своих POST-эндпоинтах.
|
||||
6. Доступ к данным — правила Payload: пользователь видит только свои ключи, заказы, платежи и движения. Кабинет ходит в Local API с `overrideAccess: false`.
|
||||
7. Коды входа в журнале ([05-auth.md](05-auth.md), раздел 4) — только для тестирования. В prod выключено, включение видно в админке.
|
||||
|
||||
## 5. Платежи
|
||||
|
||||
1. Номер карты никогда не попадает на наши серверы: только виджеты и страницы провайдеров.
|
||||
2. Вебхуки проверяются по подписи провайдера, повторная доставка не проводит платёж дважды.
|
||||
3. Деньги — целые числа, операции в транзакциях с блокировками ([04-billing.md](04-billing.md), раздел 14).
|
||||
|
||||
## 6. Приватность
|
||||
|
||||
1. Храним минимум: email, Telegram, язык, валюту, история заказов и платежей.
|
||||
2. IP пользователей в базе не храним в открытом виде — только HMAC для сравнения (самоприглашения, лимиты). Логи nginx — ротация 14 дней.
|
||||
3. В списке приглашённых почта замаскирована: `m···@gmail.com`.
|
||||
4. Аналитика — Umami без cookies, данные не уходят третьим лицам.
|
||||
5. Что и зачем храним — в политике конфиденциальности (этап 1b).
|
||||
|
||||
## 7. Окружения
|
||||
|
||||
1. Dev и prod — разные пользователи ОС (vx_dev и vx_prod), базы, роли и секреты. Dev закрыт от посторонних и от индексации.
|
||||
2. Сессии Claude и редактор работают под vx_dev и не имеют доступа к файлам, секретам, логам и базе prod. Под vx_prod нет ни редактора, ни ИИ-агентов, VSCode Remote под vx_prod не подключаем. Своего SSH-входа у vx_prod нет: под него заходят из root (`sudo -iu vx_prod`). Код попадает в prod только через тег в Gitea и команду выкладки ([02-architecture.md](02-architecture.md), раздел 5).
|
||||
3. Umami работает под своим пользователем: уязвимость в нём не открывает доступ к секретам Veilix.
|
||||
4. В базу каждый пользователь ОС входит только своей ролью (`peer` через Unix-сокет), паролей нет ([09-infrastructure.md](09-infrastructure.md), раздел 5).
|
||||
5. Порты приложений 3000 и 3100 слушают 127.0.0.1, к ним может подключиться любой пользователь сервера в обход ограничения по IP в nginx. Секретов это не открывает: админку защищает пароль с блокировкой после 5 попыток. Строже — правило nftables, которое пускает к порту prod только nginx (`www-data`) и vx_prod (R-16).
|
||||
|
||||
## 8. Обновления
|
||||
|
||||
1. Payload и Next.js выпускают исправления безопасности часто: проверять при каждом этапе, обновлять патч-версии.
|
||||
2. `pnpm audit` перед выкладкой.
|
||||
@@ -0,0 +1,24 @@
|
||||
# D-001. Одно Next.js-приложение на все домены
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Контекст
|
||||
|
||||
Два бренда, у каждого сайт, кабинет и MiniApp — 6 доменов плюс админка. Бизнес-логика общая, дизайны разные.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Одно Next.js-приложение, Payload установлен в него как библиотека.
|
||||
2. Proxy Next.js определяет по Host сайт и тип фронтенда и переписывает запрос во внутренний маршрут.
|
||||
3. Ядро (биллинг, ключи, вход, рефералы, почта) и UI-кит общие, страницы у каждого бренда свои.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Отдельные приложения на каждый фронтенд и отдельный Payload как API — больше деплоев, логика дублируется или ходит по HTTP.
|
||||
2. Один набор страниц с переключением цветов — не подходит: данные в дизайнах представлены по-разному.
|
||||
|
||||
## Последствия
|
||||
|
||||
1. Один процесс на окружение, один деплой.
|
||||
2. Кабинет и сайт читают данные через Local API Payload, без HTTP.
|
||||
3. Нужна дисциплина: бренд-зависимый код — только в `src/brands/`.
|
||||
@@ -0,0 +1,21 @@
|
||||
# D-002. Payload 3, переход на 4 после стабильного релиза
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Контекст
|
||||
|
||||
На 20.10.2026 стабильная версия — Payload 3.91.0. Payload 4 в бете (4.0.0-beta.2 от 16.10.2026, требует Node ≥ 24.15 и Next.js ≥ 16.4). У Payload есть официальный codemod для перехода с 3 на 4.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Стартуем на последней 3.x и Next.js 16.4.x.
|
||||
2. Код пишем совместимо с 4: в Local API всегда явно `overrideAccess`, rich text — только Lexical, без устаревших API.
|
||||
3. Переходим на 4 после стабильного релиза. Если он выйдет до старта этапа 1a — начинаем сразу на 4.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
Начать на бете 4 — нет переезда потом, но риск ошибок и меняющихся API в бете, которой неделя.
|
||||
|
||||
## Последствия
|
||||
|
||||
Один плановый переезд, пока данных в prod мало или нет.
|
||||
@@ -0,0 +1,22 @@
|
||||
# D-003. Без Docker: всё на хосте, процессы под systemd
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Контекст
|
||||
|
||||
На сервере уже работают PostgreSQL 18, nginx и Redis. Пользователь хочет, чтобы dev и prod запускались одинаково и без Docker.
|
||||
|
||||
## Решение
|
||||
|
||||
1. PostgreSQL и nginx — на хосте.
|
||||
2. Приложение — обычный процесс Node: dev — `pnpm dev`, prod — `pnpm build` и `pnpm start`. Оба — systemd user-юниты: dev у пользователя vx_dev, prod у vx_prod ([D-004](D-004-environments.md)).
|
||||
3. Prod выкладывается в отдельную папку релиза и переключается симлинком `current`. Откат — переключить симлинк обратно.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
Docker-образ для prod — воспроизводимая сборка и изоляция, но лишний слой. Тот же откат даёт схема с папками релизов.
|
||||
|
||||
## Последствия
|
||||
|
||||
1. Сборка prod идёт на этом же сервере (нагрузка 2–4 ГБ памяти на время сборки).
|
||||
2. Node и pnpm общие для dev и prod: обновляются вместе.
|
||||
@@ -0,0 +1,41 @@
|
||||
# D-004. Dev и prod на одном сервере под разными пользователями
|
||||
|
||||
Статус: принято, 21.10.2026.
|
||||
|
||||
## Контекст
|
||||
|
||||
Dev и prod живут на одном сервере без Docker. Редактор и сессии Claude работают в dev и не должны иметь доступа к prod.
|
||||
|
||||
## Решение
|
||||
|
||||
| | dev | prod |
|
||||
|---|---|---|
|
||||
| Пользователь ОС | vx_dev | vx_prod |
|
||||
| Папка | `/home/vx_dev/veilix_dev` | `/home/vx_prod/veilix` |
|
||||
| База и роль | `veilix_dev`, роль `vx_dev` | `veilix_prod`, роль `vx_prod` |
|
||||
| Порт | 3100 | 3000 |
|
||||
| Домены | `dev.veilix.app`, `dev.my.veilix.app`, `dev.tg.veilix.app`, так же для veilixvpn.com; `dev.plc.veilix.cc` | основные; `plc.veilix.cc` |
|
||||
|
||||
1. vx — личный пользователь администратора, к проекту отношения не имеет.
|
||||
2. Вся разработка — под vx_dev: VSCode (Remote-SSH), Claude Code и другие ИИ-агенты запускаются только под этим пользователем.
|
||||
3. Под vx_prod — только prod:
|
||||
a. редактора и ИИ-агентов нет, VSCode Remote под vx_prod не подключаем: при подключении VSCode ставит на сервер свою серверную часть и может подтянуть туда расширения, в том числе ИИ-агентов;
|
||||
b. своего SSH-входа нет, под vx_prod заходят из root (`sudo -iu vx_prod`).
|
||||
4. Оба окружения запускаются одинаково — systemd user-юнитами, каждый у своего пользователя.
|
||||
5. Домашние папки — `750`, владелец — сам пользователь: секреты, файлы и логи prod сессиям Claude недоступны.
|
||||
6. В базу каждый пользователь ОС входит только своей ролью (`peer` через Unix-сокет), паролей нет; чужие базы закрыты `REVOKE CONNECT … FROM PUBLIC`.
|
||||
7. Код попадает в prod только через Gitea: из dev публикуется тег, vx_prod забирает его ключом только на чтение и собирает релиз.
|
||||
8. Выкладку запускает пользователь под vx_prod. По желанию — sudo-правило, разрешающее vx_dev запускать от имени vx_prod только команду выкладки (Q-18).
|
||||
9. Домены хранятся в базе, поэтому у dev просто свои записи в своей базе.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Один пользователь для обоих окружений — проще, но сессии Claude и любой процесс dev технически достают до секретов и базы prod.
|
||||
2. Prod под личным пользователем vx — под ним работают редактор и ИИ-агенты, защита держалась бы только на дисциплине.
|
||||
3. Docker-контейнеры для изоляции — пользователь не хочет Docker.
|
||||
|
||||
## Последствия
|
||||
|
||||
1. Ошибки prod сессии Claude не видят напрямую: логи присылает пользователь.
|
||||
2. Отдельные deny-правила Claude Code для prod не нужны.
|
||||
3. Переезд разработки на vx_dev — задача R-00 ([09-infrastructure.md](../09-infrastructure.md), раздел 8).
|
||||
@@ -0,0 +1,18 @@
|
||||
# D-005. Сайты — тенанты, домены в базе, маршрутизация по Host
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Официальный плагин `@payloadcms/plugin-multi-tenant`: тенант — Сайт, поле `site` на коллекциях, привязанных к сайту, переключатель сайта в админке.
|
||||
2. Коллекция «Домены»: хост, сайт, тип фронтенда, языки, валюты.
|
||||
3. Proxy Next.js находит домен по Host (с кэшем в памяти) и переписывает запрос во внутренний маршрут.
|
||||
4. Общие справочники без привязки к сайту: страны, теги, валюты, платёжные системы. Платёжная система привязана к валютам, а сайт выводится через валюты его доменов ([D-010](D-010-currencies.md)).
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
Простое поле «сайт» без плагина — придётся самим писать фильтры и доступ в админке.
|
||||
|
||||
## Последствия
|
||||
|
||||
Определение сайта по домену плагин не делает — это наш код в proxy.
|
||||
@@ -0,0 +1,20 @@
|
||||
# D-006. Переводы: поля Payload и next-intl; 5 языков
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Языки: `en` (по умолчанию, без префикса в URL), `ru`, `de`, `fr`, `es`. Позже — `pt-br` и `ja`.
|
||||
2. Контент — локализованные поля Payload (`localized: true`), откат на английский.
|
||||
3. Интерфейс — next-intl, файлы `messages/*.json` в репозитории.
|
||||
4. Язык всегда в URL (кроме английского). Список языков — в конфиге, у домена — включённые языки. Отдельной сущности «Языки» нет.
|
||||
5. В админке Payload `filterAvailableLocales` скрывает языки, выключенные для сайта.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Языки как записи в базе — Payload задаёт локали в конфиге, сущность дублировала бы конфиг.
|
||||
2. Интерфейсные строки тоже в Payload — нет версионирования и типизации, лишние запросы.
|
||||
|
||||
## Последствия
|
||||
|
||||
Новый язык — правка конфига, переводы и деплой.
|
||||
@@ -0,0 +1,17 @@
|
||||
# D-007. Определение языка; MaxMind через nginx
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Порядок: язык в URL → сохранённый выбор (cookie, профиль) → `Accept-Language` → страна по MaxMind → `en`.
|
||||
2. Перенаправление — только при первом заходе без префикса и только людей, не поисковых ботов. Плюс `hreflang` и `x-default`.
|
||||
3. Страну определяет nginx (модуль geoip2, база GeoLite2-Country) и передаёт заголовком `X-Geo-Country`. Приложение само читает `.mmdb` только там, где нужна страна произвольного IP.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
Язык только по стране — страна не равна языку, а Google не рекомендует перенаправлять по IP.
|
||||
|
||||
## Последствия
|
||||
|
||||
База MaxMind обновляется `geoipupdate` (root).
|
||||
@@ -0,0 +1,16 @@
|
||||
# D-008. UUID и случайные публичные коды
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. `idType: 'uuid'` в адаптере Postgres Payload: все id в базе, REST и URL админки — UUID. Автоинкремента нет.
|
||||
2. Номера для людей — короткие случайные коды: заказы `VX-7K3M9Q`, ключи `K-…`, платежи, реферальные коды.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
Автоинкремент плюс отдельное поле UUID для внешнего мира — нужно следить, чтобы внутренний id нигде не утёк. Штатная настройка Payload решает задачу без кастома.
|
||||
|
||||
## Последствия
|
||||
|
||||
Уникальность случайных кодов обеспечивается уникальным индексом и повтором генерации при коллизии.
|
||||
@@ -0,0 +1,24 @@
|
||||
# D-009. Заказы, платежи, движения по счёту; всё через баланс
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Контекст
|
||||
|
||||
У пользователя есть счёт. Заказ можно оплатить картой, с баланса или частично. Нужна ручная оплата из админки, которая запускает те же действия, что и настоящая.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Три сущности: Заказ (что покупают), Платёж (деньги извне), Движение по счёту (неизменяемые записи «+» и «−»).
|
||||
2. Любой успешный платёж зачисляется на баланс, любой заказ списывается с баланса.
|
||||
3. Одна точка проведения `processPayment`, идемпотентная.
|
||||
4. Ручная оплата — платёжная система `manual`. Платёж проводится, когда получает статус `succeeded`. Галочки «провести как реальную» нет.
|
||||
5. Корректировка баланса — отдельная операция: только движение, без цепочки действий.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Одна сущность «транзакция» — смешивает коммерческий документ, деньги провайдера и баланс; сложно с частичной оплатой и повторами вебхуков.
|
||||
2. Галочка «провести как реальную оплату» — не нужна: проведение запускает смена статуса.
|
||||
|
||||
## Последствия
|
||||
|
||||
Правило «списывается только разница с балансом» получается само. Подробно — [04-billing.md](../04-billing.md).
|
||||
@@ -0,0 +1,22 @@
|
||||
# D-010. Валюты и платёжные системы — сущности; платёжные системы привязаны к валютам
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Контекст
|
||||
|
||||
Из России часто заходят через VPN, поэтому страна по IP не говорит, какая у пользователя валюта и какие карты. Карты РФ принимают одни платёжные системы, зарубежные — другие.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Сущность «Валюты»: RUB и USD.
|
||||
2. Сущность «Платёжные системы»: адаптер, валюты, назначения (оплата заказа, пополнение). RUB — процессинги для карт РФ (вероятно, KZ и TR), USD — Stripe и криптовалюта.
|
||||
3. Платёжная система привязана только к валютам. Где она доступна, следует из цепочки: валюты → домены, на которых они включены → сайты. В оформлении заказа показываются системы в валюте аккаунта.
|
||||
4. У домена — включённые валюты, валюта по умолчанию и валюта для языка: ru → RUB, остальные → USD.
|
||||
5. Переключатель валюты отдельный от языка: любая валюта с любым языком.
|
||||
6. Цены тарифов — строкой на каждую валюту.
|
||||
7. Валюта аккаунта задаётся при регистрации из выбранной и меняется, пока баланс нулевой и нет ключей. Дальше — через поддержку.
|
||||
|
||||
## Последствия
|
||||
|
||||
1. Маркетинговые страницы собираются в вариантах «язык × валюта» ([D-017](D-017-static-pages.md)).
|
||||
2. Платёжная система в RUB доступна сразу на обоих брендах. Если понадобятся разные аккаунты одного провайдера для разных брендов, платёжной системе добавим необязательное поле «только для сайтов» (Q-17).
|
||||
@@ -0,0 +1,17 @@
|
||||
# D-011. Раздельные аккаунты по сайтам
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Пользователь привязан к сайту. Один email или Telegram на двух сайтах — два разных пользователя.
|
||||
2. Внутри сайта: аккаунт можно создать по Telegram, почта привязывается кодом; на сайте есть «Войти через Telegram»; аккаунты объединяются по правилам [05-auth.md](../05-auth.md), раздел 6.4.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Общий аккаунт на оба бренда — бренды смешиваются в кабинете и в балансе.
|
||||
2. Общий вход и отдельный профиль в каждом бренде — без дублей, но сложнее модель. Пользователь выбрал простоту.
|
||||
|
||||
## Последствия
|
||||
|
||||
Переход клиента с mass на dedicated — новая регистрация на veilix.app.
|
||||
@@ -0,0 +1,18 @@
|
||||
# D-012. Вход по коду через свою стратегию Payload; оформление заказа на my.*
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Входа по коду в Payload из коробки нет. Используем штатный механизм своих стратегий: коллекция `customers` с `disableLocalStrategy` и стратегией, которая читает нашу cookie сессии.
|
||||
2. Коды и сессии — свои коллекции; коды и токены хранятся только как хэши.
|
||||
3. Оформление заказа живёт на домене кабинета (my.*), а не на маркетинговом. Cookie сессии — только для хоста кабинета.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Better Auth или Auth.js поверх Payload — ещё одна система со своими таблицами и миграциями.
|
||||
2. Cookie на весь домен бренда (`.veilix.app`) — тогда маркетинговые страницы перестают быть полностью кэшируемыми, а cookie видят dev-поддомены.
|
||||
|
||||
## Последствия
|
||||
|
||||
Маркетинговые сайты остаются без пользовательских cookies и полностью кэшируются.
|
||||
@@ -0,0 +1,16 @@
|
||||
# D-013. Капча ALTCHA
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
ALTCHA — капча на proof-of-work, работает на нашем сервере, без третьих сторон. Вместе с лимитами по IP и email.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Google reCAPTCHA — передаёт данные пользователей в Google и строже проверяет пользователей VPN, то есть нашу аудиторию.
|
||||
2. Cloudflare Turnstile — с июня 2025 российские провайдеры замедляют Cloudflare, пользователи из РФ могут не пройти проверку.
|
||||
|
||||
## Последствия
|
||||
|
||||
Провайдер капчи изолирован в `core/` и заменяется без переделки форм.
|
||||
@@ -0,0 +1,18 @@
|
||||
# D-014. Почта: шаблоны в Payload, переключаемый транспорт, без listmonk
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Шаблоны писем — в Payload, переведены, для каждого сайта свои.
|
||||
2. Транспорт переключается в админке: SMTP, локальный sendmail (Postfix) или «только журнал».
|
||||
3. Все письма пишутся в журнал. Флаг «Записывать коды входа в журнал» — для тестирования.
|
||||
4. listmonk не используем.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
listmonk — прежде всего сервис рассылок; переводов шаблонов в нём нет: на каждое письмо понадобилось бы 10 шаблонов (2 бренда × 5 языков).
|
||||
|
||||
## Последствия
|
||||
|
||||
Для отправки через Postfix нужны SPF, DKIM, DMARC и отправка с IP с правильной обратной записью ([09-infrastructure.md](../09-infrastructure.md), раздел 6).
|
||||
@@ -0,0 +1,19 @@
|
||||
# D-015. Боты на grammY без n8n; два бота на бренд
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Основной бот каждого бренда (команды, кнопка MiniApp, уведомления) написан в коде на grammY и работает внутри нашего приложения через вебхук. Тексты и кнопки — в Payload, переведены, правятся в админке.
|
||||
2. Бот поддержки каждого бренда подключён к Chatwoot напрямую.
|
||||
3. MiniApp — фронтенд в том же приложении, без n8n.
|
||||
4. n8n не используем.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. n8n как движок бота — визуальные схемы для покупок, ключей и баланса на 5 языках × 2 бота трудно поддерживать и тестировать, логика денег всё равно живёт в бэкенде.
|
||||
2. Один бот на всё с пересылкой сообщений поддержки в Chatwoot через API — свой код, который нужно поддерживать.
|
||||
|
||||
## Последствия
|
||||
|
||||
Telegram отдаёт обновления бота только одному получателю, поэтому основной бот и бот поддержки — разные боты.
|
||||
@@ -0,0 +1,15 @@
|
||||
# D-016. Платёжные системы по валютам; автосписания — Stripe
|
||||
|
||||
Статус: принято, 20.10.2026. Провайдеры для RUB и криптовалюты выбираются на этапе 4 (Q-12).
|
||||
|
||||
## Решение
|
||||
|
||||
1. Каждый провайдер — адаптер с единым интерфейсом и флагами возможностей: автосписание, возврат, сохранение карты.
|
||||
2. USD — Stripe (карты, сохранение карты, автосписания на переменную сумму, без Stripe Subscriptions) и криптовалюта.
|
||||
3. RUB — процессинги, принимающие карты РФ (вероятно, KZ и TR), только пополнения, без автосписаний. ЮKassa — опционально, только пополнения.
|
||||
4. Пользователи с картами РФ продлевают ключи с баланса; напоминание — за 3 дня.
|
||||
5. Оплата внутри Telegram (Stars) — решаем на этапе 3.
|
||||
|
||||
## Последствия
|
||||
|
||||
На этапе 1 работает только `manual`. Модель данных готова к любому набору провайдеров.
|
||||
@@ -0,0 +1,14 @@
|
||||
# D-017. Статические маркетинговые страницы с пересборкой по событию
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Маркетинговые страницы собираются статически в вариантах «язык × валюта» и кэшируются.
|
||||
2. Изменение в админке (цены, статус локации, FAQ, страницы) пересобирает затронутые страницы за секунды через пересборку по тегам. Запасной вариант — плановое обновление раз в 5 минут.
|
||||
3. Наличие и цена перепроверяются на сервере при оформлении заказа.
|
||||
4. Кабинет рендерится на сервере для каждого запроса.
|
||||
|
||||
## Последствия
|
||||
|
||||
Высокий балл PageSpeed, а устаревшая страница не может продать недоступную локацию.
|
||||
@@ -0,0 +1,20 @@
|
||||
# D-018. Umami на этом сервере и баннер cookies
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Аналитика — своя Umami без cookies, на этом сервере: Node.js и PostgreSQL здесь уже есть, а трекер проксирует этот же nginx.
|
||||
2. Ставится из исходников, без Docker (официальный способ наряду с Docker-образом): `pnpm install`, `pnpm build`, `pnpm start`. Отдельный пользователь ОС `umami`, своя база `umami`, systemd user-юнит, порт 3300.
|
||||
3. Панель — `umi.veilix.cc`, доступ только с разрешённых IP.
|
||||
4. Скрипт и отправка событий проксируются через домены брендов под нейтральными путями (`TRACKER_SCRIPT_NAME`, `COLLECT_API_ENDPOINT`), чтобы их не резали блокировщики рекламы.
|
||||
5. Баннер cookies: обязательные (сессия, язык, валюта) и функциональные (чат). Необязательные скрипты — только после согласия.
|
||||
|
||||
## Рассмотренные варианты
|
||||
|
||||
1. Umami на VPS с Chatwoot и Twenty — трекеру пришлось бы ходить на другой сервер, где Docker и тяжёлые сервисы.
|
||||
2. Яндекс Метрика и GA4 — передают данные третьим лицам и требуют согласия; нужны, только если будет реклама.
|
||||
|
||||
## Последствия
|
||||
|
||||
У Umami свой пользователь ОС, поэтому уязвимость в ней не открывает доступ к секретам Veilix.
|
||||
@@ -0,0 +1,13 @@
|
||||
# D-019. Chatwoot и Twenty CRM на отдельном VPS
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Chatwoot — один экземпляр на оба бренда: виджеты сайтов и боты поддержки как отдельные входящие каналы.
|
||||
2. Twenty CRM — заявки из Payload уходят в Twenty через API.
|
||||
3. Оба сервиса на отдельном VPS, ставит и обслуживает пользователь. Наше приложение работает с ними только по API.
|
||||
|
||||
## Последствия
|
||||
|
||||
До этапа 2 заявки видны в админке Payload и приходят письмом на почту поддержки.
|
||||
@@ -0,0 +1,16 @@
|
||||
# D-020. Модели ключей dedicated и mass
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Dedicated: ключ — индивидуальный IP в стране, выбранной при покупке. Страна не меняется никогда: нужна другая — покупается новый ключ. В ключ входит RU-адрес. Подключения: WG/AWG-конфиг на каждое устройство (до лимита тарифа), ссылка Happ или Xray, подключения к RU-адресу.
|
||||
2. Mass: ключ включает все страны тарифа, страна не выбирается. Happ — одна ссылка со всеми странами. WG — конфиги любой страны тарифа: у подключения есть поле страны.
|
||||
3. Тип ключа определяется полем `type` тарифа.
|
||||
4. Подключения гибкие: у ключа любой набор подключений разных видов, поэтому ручная выдача на этапе 1 покрывает любой вариант.
|
||||
5. Оплаченный период начинается с момента выдачи ключа.
|
||||
6. На этапе 1 WG-конфиги mass — те, что добавил администратор, по странам. Нужна другая страна — через поддержку. Автоматически — на этапе 5.
|
||||
|
||||
## Последствия
|
||||
|
||||
Автоматическая выдача (этап 5) создаёт те же подключения, что сейчас добавляет администратор.
|
||||
@@ -0,0 +1,15 @@
|
||||
# D-021. Админка на plc.veilix.cc с доступом по IP
|
||||
|
||||
Статус: принято, 20.10.2026.
|
||||
|
||||
## Решение
|
||||
|
||||
1. Админка Payload и её REST API работают только на `plc.veilix.cc` (prod) и `dev.plc.veilix.cc` (dev). На доменах брендов эти пути закрыты.
|
||||
2. nginx пускает на хосты админки только разрешённые IP.
|
||||
3. Свои эндпоинты приложения (ALTCHA, вебхуки, health) — под префиксом `/svc/`.
|
||||
|
||||
## Последствия
|
||||
|
||||
1. Адрес админки не связан с доменами брендов и закрыт от посторонних на уровне nginx.
|
||||
2. Сайты, кабинеты и MiniApp к хосту админки не обращаются, поэтому ограничение по IP касается только администраторов ([02-architecture.md](../02-architecture.md), раздел 2).
|
||||
3. Чтобы заходить в админку откуда угодно, в список разрешённых IP можно внести адреса своих VPN-серверов.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Решения
|
||||
|
||||
По файлу на решение. Формат: контекст, решение, рассмотренные варианты, последствия. Новое решение — новый файл со следующим номером. Пересмотр — новый файл со ссылкой на старый, у старого статус «заменено».
|
||||
|
||||
| № | Решение | Статус |
|
||||
|---|---|---|
|
||||
| [D-001](D-001-single-app.md) | Одно Next.js-приложение на все домены | принято |
|
||||
| [D-002](D-002-payload-version.md) | Payload 3, переход на 4 после стабильного релиза | принято |
|
||||
| [D-003](D-003-hosting.md) | Без Docker: всё на хосте, процессы под systemd | принято |
|
||||
| [D-004](D-004-environments.md) | Dev и prod на одном сервере под разными пользователями | принято |
|
||||
| [D-005](D-005-multitenancy.md) | Сайты — тенанты, домены в базе, маршрутизация по Host | принято |
|
||||
| [D-006](D-006-localization.md) | Переводы: поля Payload и next-intl; 5 языков | принято |
|
||||
| [D-007](D-007-language-detection.md) | Определение языка; MaxMind через nginx | принято |
|
||||
| [D-008](D-008-uuid.md) | UUID и случайные публичные коды | принято |
|
||||
| [D-009](D-009-billing-model.md) | Заказы, платежи, движения по счёту; всё через баланс | принято |
|
||||
| [D-010](D-010-currencies.md) | Валюты и платёжные системы — сущности; платёжные системы привязаны к валютам | принято |
|
||||
| [D-011](D-011-accounts.md) | Раздельные аккаунты по сайтам | принято |
|
||||
| [D-012](D-012-auth.md) | Вход по коду через свою стратегию Payload; оформление заказа на my.* | принято |
|
||||
| [D-013](D-013-captcha.md) | Капча ALTCHA | принято |
|
||||
| [D-014](D-014-email.md) | Почта: шаблоны в Payload, переключаемый транспорт, без listmonk | принято |
|
||||
| [D-015](D-015-telegram-bots.md) | Боты на grammY без n8n; два бота на бренд | принято |
|
||||
| [D-016](D-016-payments.md) | Платёжные системы по валютам; автосписания — Stripe | принято |
|
||||
| [D-017](D-017-static-pages.md) | Статические маркетинговые страницы с пересборкой по событию | принято |
|
||||
| [D-018](D-018-analytics.md) | Umami на этом сервере и баннер cookies | принято |
|
||||
| [D-019](D-019-external-services.md) | Chatwoot и Twenty CRM на отдельном VPS | принято |
|
||||
| [D-020](D-020-key-models.md) | Модели ключей dedicated и mass | принято |
|
||||
| [D-021](D-021-admin-host.md) | Админка на plc.veilix.cc с доступом по IP | принято |
|
||||
@@ -0,0 +1,226 @@
|
||||
# Общее
|
||||
|
||||
Сервис продажи VPN. Ключи для приложений Happ (vless, histeria) и Wiregueard/Amnezia WG.
|
||||
|
||||
|
||||
|
||||
# Своими словами
|
||||
|
||||
Есть 2 сайта.
|
||||
|
||||
Один - продаёт Dedicated VPN, где каждому пользователю даётся свой IP.
|
||||
|
||||
Второй - более классический вариант, где на 1 IP множество пользователей.
|
||||
|
||||
Фундаментально они работают одинаково, это привязанные к пользователю ключи. Ключи уже открывают доступ либо к dedicated либо mass.
|
||||
|
||||
Скрипты - которыми обрабатывается оба сайта - общие.
|
||||
|
||||
Поэтому это одна база, один бэк, но разные домены. Отличаться они будут по доменам.
|
||||
|
||||
Дизайн отличается по цветам, но в общей концепции, но дизайны всё таки разные, тк по разному представлены данные и делать именно общий смысла нет.
|
||||
- Фиолетовый - это dedicated.
|
||||
- Зелёный - mass.
|
||||
- Дизайн сделал в Claude Design лежит в файле VEILIX-handoff.zip.
|
||||
|
||||
Сайт x2
|
||||
Личный кабинет x2
|
||||
Личный кабинет в Telegram (MiniApp) x2
|
||||
Бот в TG x2
|
||||
|
||||
veilix.app - dedicated
|
||||
veilixvpn.com - mass
|
||||
|
||||
my.veilix.app - личный кабинет dedicated
|
||||
my.veilixvpn.com - личный кабинет mass
|
||||
|
||||
tg.veilix.app - tg miniapp dedicated
|
||||
tg.veilixvpn.com - tg miniapp mass
|
||||
|
||||
Сайты многоязычные.
|
||||
|
||||
Боты чтобы и миниапп были или в самом tg можно было командами всё делать.
|
||||
|
||||
|
||||
|
||||
# Стэк
|
||||
|
||||
- CMS Payload
|
||||
- Next
|
||||
- React
|
||||
- SSR
|
||||
- Postgres
|
||||
|
||||
Вопрос. Payload через докер ставится? Нужно чтобы он с базой и nginx которые на хосте работал, чтобы не ставить базу в докер. Это нормально или лучше чтобы база в докере тоже была?
|
||||
|
||||
|
||||
|
||||
# Сущности
|
||||
|
||||
- Сайты
|
||||
- Домены
|
||||
- Языки
|
||||
- Страны
|
||||
- Пользователи
|
||||
- Тарифы
|
||||
- Ключи
|
||||
- Подключения
|
||||
- Заказы
|
||||
- Транзации счёта пользователя
|
||||
|
||||
|
||||
|
||||
# Сайты и Домены
|
||||
|
||||
Каждая из сущностей привязывается к Сайту.
|
||||
|
||||
Языки привязываются к Домену, Домены привязываются к сайтам.
|
||||
|
||||
Вопрос.
|
||||
В Payload связка сайты/языки может какой то нативный мехнизм есть?
|
||||
И в целом где-то переводы всего в базе тоже нужно иметь на разных языках или это есть тоже нативное?
|
||||
|
||||
|
||||
|
||||
# Языки
|
||||
|
||||
По умолчанию язык EN без постфикса в URL, остальные языки с postfix /de /es /jp /ru
|
||||
|
||||
Английский, немецкий, французский, испанский, португальский, японский и дальше RU.
|
||||
|
||||
Язык именно через URL везде проводить, те запомнить выбор можно в настройках пользователя или куки или localstorage, но в URL показывать именно с постфиксом.
|
||||
|
||||
Определять по MAXMIND LITE в Nginx.
|
||||
|
||||
|
||||
|
||||
# Telegram боты
|
||||
|
||||
Команды в ботах через n8n, чтобы можно было править логику ответов.
|
||||
|
||||
Вопрос.
|
||||
|
||||
n8n для этого подходит или есть другие варианты?
|
||||
Miniapp я думаю имеет смысл напрямую делать без n8n?
|
||||
|
||||
|
||||
|
||||
# Авторизация и регистрация
|
||||
|
||||
По email по коду без паролей.
|
||||
|
||||
Одно поле email, проверять есть или нет. Либо регистрировать либо авторизовывать.
|
||||
|
||||
Защить Google Captcha.
|
||||
|
||||
Отправка будет через локально установленный listmonk.
|
||||
|
||||
|
||||
|
||||
# Оплата, заказы, транзации
|
||||
|
||||
У пользователя есть счёт.
|
||||
|
||||
Его можно пополнить криптой и картой, может ещё как то в итоге.
|
||||
|
||||
Привязка карты к счёту и рекурентная оплата в зависимости от суммы всех ключей.
|
||||
|
||||
Если положить на счёт деньги заранее, то ежемесячно списывается только сумма за вычетом того что есть на счёте.
|
||||
|
||||
Понять как лучше сделать в плане связки Заказы и Транзакции.
|
||||
|
||||
Оплачивает клиент Заказ, к заказу справочно привязывается Тариф и Ключ и Пользователь.
|
||||
|
||||
Юкасса Страйп (демо режимы пока)
|
||||
|
||||
Вопрос.
|
||||
|
||||
Заказы и Транзакции - не понятно, может одной сущности достаточно.
|
||||
|
||||
Нужна возможность делать оплату вручную, те то что генерируется при оплате с карточки или при рекурентном списании, чтобы любые из этих действий я мог добавить в админке payload и иницировались остальные действия, пользователь получил ключ или начисления на счёт и тп. Может по какой то галочке - "Провести как реальную оплату", тогда это будет не просто запись в коллекции, а произойдёт весь набор действий, который при реальной оплате.
|
||||
|
||||
|
||||
|
||||
|
||||
# Ключи VPN
|
||||
|
||||
Страны привязыны к Тарифам.
|
||||
|
||||
Тэги привязаны к Тарифам.
|
||||
|
||||
Страны, чтобы на сайте отображать и возможно для получения с бэка в дальнейшем ключей.
|
||||
|
||||
Тэги, нужно будут для получения с бэка ключей для фильтраций каких-то произвольных.
|
||||
|
||||
Пользователь выбирает Тариф, на основе Тарифа ему подбирается свободный сервер и с бэка прилетают Подключения для ключа.
|
||||
|
||||
Подключения могут быть вручную добавлены, ссылка на подписку для happ, и ключ для wg.
|
||||
|
||||
Срок истечения у Ключа. Те следующий заказ продлевает этот же ключ.
|
||||
|
||||
К Ключу тоже привязывается Тариф, чтобы при достижении срока оплата списалась.
|
||||
|
||||
|
||||
|
||||
# ID и UUID
|
||||
|
||||
Не должно быть явных ID нигде, которые идут подряд, чтобы не было понятно, сколько и чего есть в базе.
|
||||
|
||||
Нужно использовать UUID, в тч при работе с API, чтобы и в консоли браузера нельзя было увидеть реальные ID.
|
||||
|
||||
Если payload предполагает автоикремент ID обычный, возможно не стоит городить кастом, и пусть так будет. Но нужно тогда всё равно всё обыграть все обращения по UUID.
|
||||
|
||||
Вопрос. Обсудить.
|
||||
|
||||
|
||||
|
||||
# Дополнительный софт
|
||||
|
||||
На этом же vps:
|
||||
|
||||
- N8N - для TG бота
|
||||
- Listmonk - отправка транзакционных писем
|
||||
|
||||
|
||||
На отдельном vps:
|
||||
|
||||
- Chatwoot - чат на сайте
|
||||
- 12CRM - лиды собирать
|
||||
|
||||
|
||||
|
||||
# Этапы
|
||||
|
||||
Составить всю .md документацию и план. Далее, чтобы я мог запускать по этапам в отдельных сессиях.
|
||||
|
||||
В первую очередь собрать полноценный сайт с дизайном, всем функционалом (регистрация, авторизация, реферальная программа). Без эквайринга и ключей по внешним api.
|
||||
|
||||
При этом чтобы вручную я мог и добавить ключи и оплаты.
|
||||
|
||||
Далее интеграция с chatwoot, 12crm.
|
||||
|
||||
Далее listmonk.
|
||||
|
||||
Далее TG боты.
|
||||
|
||||
Далее эквайринг и ключи. Но ключи автоматизация пока с вопросами, это нужно оставить обсуждение к моменту запуска этапа.
|
||||
|
||||
Эквайринг тестовый режим страйпа и юкассы, я зарегистриуюсь, если они дают песочницу, то сначала так.
|
||||
|
||||
|
||||
|
||||
# Общее
|
||||
|
||||
- Отраслевые стандарты. Стараться придерживаться отраслевых стандартов и использовать максимально современные решения. Если я предлагаю решения, которые плохо связываются друг с другом или кастом, который может решается иными методами, то обсудить со мной. Я могу не знать, но с другой стороны может быть что нужно и делать кастом, в любом случае это повод для обсуждения.
|
||||
|
||||
- Нумерация пунктов в документации и чатаха с тобой - цифрами и латинскими буквами, чтобы не было путаницы, где А русская, где анлийская, гда B - толи БЭ латинская, толи ВЭ русская.
|
||||
|
||||
- Не писать никаких скриптов пока не будет всё утверждено, сначала обсуждаем.
|
||||
|
||||
- Если есть противоречия на любом этапе, сначала обсуждаем, потом делаем.
|
||||
|
||||
- Софт, который не достаёт и требуется установки от root - говоришь, я сделаю. Не нужно пытаться какие то обходные пути делать внутри пользователя, чтобы меня не беспокоить. Что нужно и правильно установить глобально на сервере - я установлю.
|
||||
|
||||
- Настройки на сервере - также как и софт, что правильнее сделать глобально, делаю глобально я.
|
||||
|
||||
- Доступ к root у меня есть, я в этом разбираюсь, нет никаких проблем и софт и настройки сделать.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Этап 0. Документация
|
||||
|
||||
Статус: готово 20.10.2026.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Зафиксировать продукт, архитектуру, модель данных и план этапов так, чтобы каждый этап можно было запускать в отдельной сессии.
|
||||
|
||||
## 2. Сделано
|
||||
|
||||
1. Обсуждение исходной задачи ([history/HUMAN_IDEA.md](../history/HUMAN_IDEA.md)) и макетов, решения [D-001…D-021](../decisions/README.md).
|
||||
2. Документы `docs/01…10`, файлы этапов, [CLAUDE.md](../../CLAUDE.md).
|
||||
3. Макеты скопированы в `design/handoff/`.
|
||||
4. Репозиторий инициализирован локально, без коммитов.
|
||||
|
||||
## 3. Осталось
|
||||
|
||||
1. Переезд разработки под пользователя vx_dev (R-00), репозиторий в Gitea и SSH-доступ для vx_dev (R-01), затем первый коммит и push по просьбе пользователя.
|
||||
2. Вопросы к следующим этапам — в [00-index.md](../00-index.md), раздел 4.2. Этап 1a они не блокируют.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Этап 1a. Фундамент
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Рабочий каркас: одно приложение отвечает на все dev-домены, определяет сайт, язык и валюту, показывает оболочку обоих брендов в своих цветах; админка с базовыми справочниками.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Проект: Next.js 16.4, Payload 3.x (или 4, если вышел стабильный), TypeScript strict, pnpm, ESLint, Prettier. Структура папок — [02-architecture.md](../02-architecture.md), раздел 3.
|
||||
2. PostgreSQL: адаптер с `idType: 'uuid'`, push в dev, настроенные миграции для prod.
|
||||
3. Payload: админка только на хосте админки; `admins` с ролями; плагин multi-tenant (поле `site`); локализация `en, ru, de, fr, es` с откатом на `en`; интерфейс админки `ru` и `en`; `filterAvailableLocales`.
|
||||
4. Коллекции: `sites`, `domains`, `currencies`, `countries`, `locations`, `tags`, `tariffs`, `payment-systems` (пока только адаптер `manual`), `media` — по [03-data-model.md](../03-data-model.md).
|
||||
5. Proxy: Host → домен → сайт и тип фронтенда; префиксы языков (next-intl, английский без префикса); определение языка по [06-i18n.md](../06-i18n.md), раздел 4; cookie валюты; внутренние маршруты; закрыть `/admin` и REST Payload на доменах брендов.
|
||||
6. Темы: токены брендов ([07-frontend.md](../07-frontend.md), раздел 4), Tailwind v4, `data-site`, `data-theme`, три режима темы без мигания; шрифты через `next/font`.
|
||||
7. Оболочки: шапка и подвал маркетинговых сайтов обоих брендов с переключателями языка, валюты и темы; оболочка кабинета (боковая панель и мобильные вкладки) с заглушками страниц.
|
||||
8. Начальные данные (seed): 2 сайта, dev-домены, валюты, 30 стран и RU, 30 локаций на каждом сайте, тарифы dedicated (3 уровня) и mass (месяц, год) с ценами в RUB и USD, платёжная система `manual`.
|
||||
9. `/svc/health`.
|
||||
10. systemd user-юнит `veilix-dev.service`; текст конфигурации nginx для dev-хостов на согласование.
|
||||
11. Раздел «Команды» в [CLAUDE.md](../../CLAUDE.md).
|
||||
|
||||
## 3. Не входит
|
||||
|
||||
Страницы по макетам (1b), вход (1c), биллинг (1d).
|
||||
|
||||
## 4. Перед стартом
|
||||
|
||||
Root: R-00…R-06 ([09-infrastructure.md](../09-infrastructure.md), раздел 8). Открытых решений к этапу нет.
|
||||
|
||||
## 5. Готово, когда
|
||||
|
||||
1. `dev.veilix.app` и `dev.veilixvpn.com` открываются по HTTPS: шапка и подвал в цветах бренда, три режима темы.
|
||||
2. Работают `/ru`, `/de`, `/fr`, `/es`; первый заход без префикса перенаправляет по правилам определения языка; поисковые боты не перенаправляются.
|
||||
3. Переключатель валюты запоминает выбор.
|
||||
4. `dev.plc.veilix.cc/admin`: переключение сайтов, справочники редактируются, локализованные поля переключаются по языкам, у сайта видны только его языки.
|
||||
5. В URL админки — UUID.
|
||||
6. `dev.my.*` показывает оболочку кабинета; `/admin` на доменах брендов — 404.
|
||||
7. `pnpm build`, линтер и проверка типов проходят.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Этап 1b. Маркетинговые страницы
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Сайты обоих брендов по макетам, с контентом из Payload, формами и SEO.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Страницы по макетам для обоих брендов: главная, цены, локации (карта, отрисованная на сервере), поддержка ([07-frontend.md](../07-frontend.md), раздел 2). Юридические страницы `/legal/*`, 404 и 500.
|
||||
2. Коллекции `pages`, `faq` (с местами показа), `leads`; поля контактов в `sites`.
|
||||
3. Контент из макетов на RU и EN: тексты, FAQ, локации, тарифы.
|
||||
4. Черновики юридических текстов на RU и EN с заглушками вместо данных юрлица: условия, конфиденциальность, возвраты, cookies.
|
||||
5. ALTCHA ([08-integrations.md](../08-integrations.md), раздел 2). Формы «Индивидуальные условия» и обратной связи с ALTCHA пишут в `leads`. Письмо администратору появится в 1c.
|
||||
6. SEO: `<title>` и описания на каждом языке, `hreflang`, `canonical`, `sitemap.xml` и `robots.txt` на каждый домен, картинки для соцсетей.
|
||||
7. Статическая сборка «язык × валюта» и пересборка по событию из Payload ([D-017](../decisions/D-017-static-pages.md)).
|
||||
8. Решить, показывать ли количество свободных серверов.
|
||||
|
||||
## 3. Не входит
|
||||
|
||||
Оформление заказа (1d), кабинет (1f), переводы на de, fr, es (1h; до этого — английский запасной).
|
||||
|
||||
## 4. Перед стартом
|
||||
|
||||
Q-02 (цены mass в USD), Q-08 (email поддержки), Q-09 (часовой пояс), Q-16 (ящик поддержки).
|
||||
|
||||
## 5. Готово, когда
|
||||
|
||||
1. Страницы обоих брендов совпадают с макетами: светлая и тёмная тема, десктоп и мобильный, RU и EN.
|
||||
2. Правка цены, статуса локации или FAQ в админке видна на сайте в течение нескольких секунд.
|
||||
3. Lighthouse mobile ≥ 90 на главной и странице цен обоих брендов.
|
||||
4. Формы создают заявки, ALTCHA работает.
|
||||
5. `sitemap.xml` и `hreflang` проходят проверку.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Этап 1c. Вход по коду и почта
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Регистрация и вход по коду из письма в кабинетах обоих брендов; почтовая подсистема с журналом.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Почта ([08-integrations.md](../08-integrations.md), раздел 1): глобал «Почта» (транспорты `smtp`, `sendmail`, `log_only`), email-адаптер, `email-templates` с шапкой и подвалом бренда, `email-log`, задача `send-email`, тестовое письмо, флаг «Записывать коды входа в журнал».
|
||||
2. Шаблоны на RU и EN: `login_code`, `admin_new_lead` (и подключить его к формам из 1b).
|
||||
3. `customers` со своей стратегией, `customer-sessions`, `auth-codes`; запрос и проверка кода, лимиты, ALTCHA ([05-auth.md](../05-auth.md), разделы 2–4).
|
||||
4. Страница `/login` по макету с состояниями: код отправлен, ошибка, истёк, повторная отправка через 60 секунд.
|
||||
5. Выход, выход на всех устройствах.
|
||||
6. Язык и валюта аккаунта при регистрации ([04-billing.md](../04-billing.md), раздел 2).
|
||||
7. Задача `cleanup` для кодов, сессий и журнала.
|
||||
|
||||
## 3. Не входит
|
||||
|
||||
Telegram (этап 3), реферальная привязка при регистрации (1g).
|
||||
|
||||
## 4. Перед стартом
|
||||
|
||||
Root: R-07 (Postfix, OpenDKIM, DNS) или данные SMTP-провайдера. Без них этап делается на транспорте `log_only`.
|
||||
|
||||
## 5. Готово, когда
|
||||
|
||||
1. Регистрация и вход работают на `dev.my.veilix.app` и `dev.my.veilixvpn.com`; аккаунты сайтов независимы.
|
||||
2. С флагом коды видны в журнале; на `log_only` письма не уходят.
|
||||
3. Через `sendmail` или `smtp` письмо доходит до Gmail не в спам; оценка mail-tester ≥ 9/10.
|
||||
4. Лимиты и счётчик попыток работают; сессии отзываются.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Этап 1d. Биллинг без эквайринга
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Деньги и заказы работают целиком: оформление заказа, оплата с баланса, ручные платежи, продления, льготный период — без внешних платёжных систем.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Коллекции `orders`, `payments`, `ledger-entries`, `payment-methods` (структура), `keys` (поля и статусы), `audit-log`.
|
||||
2. `core/billing`: `processPayment`, оплата с баланса, задачи `renewals`, `expiry`, `reminders`, `reconcile`; блокировки и уникальные ограничения ([04-billing.md](../04-billing.md)).
|
||||
3. `/order` по макетам обоих брендов: шаг «Почта» из 1c, шаг «Оплата» (с баланса; «Заказ создан, ожидает оплаты», когда платёжных систем нет), согласие с условиями, сводка из тарифа, локации и валюты.
|
||||
4. `/buy` в кабинете: dedicated — сетка локаций с ценами, mass — «Месяц» и «Год».
|
||||
5. Админка: ручной платёж, корректировка баланса, возврат на баланс, продление вручную, блокировка, «Очередь выдачи»; всё — в журнал действий.
|
||||
6. Шаблоны писем на RU и EN: `order_paid`, `key_provisioning`, `renewal_reminder`, `expiry_reminder`, `renewal_succeeded`, `renewal_failed`, `grace_started`, `key_expired`, `topup_succeeded`, `refund_issued`, `admin_new_order`, `admin_provisioning_needed`.
|
||||
|
||||
## 3. Не входит
|
||||
|
||||
Подключения и выдача ключа (1e), реальные платёжные системы (этап 4). Для проверок ключ переводится в `active` вручную в админке.
|
||||
|
||||
## 4. Перед стартом
|
||||
|
||||
Этап 1c готов.
|
||||
|
||||
## 5. Готово, когда
|
||||
|
||||
Автотесты `core/billing` проходят сценарии:
|
||||
|
||||
1. Ручное пополнение → оплата заказа с баланса → заказ оплачен → ключ `provisioning`.
|
||||
2. Частичный баланс + ручной платёж на разницу → заказ оплачен.
|
||||
3. Продление с баланса за 24 часа; денег нет → заказ ждёт → пополнение само оплачивает продление.
|
||||
4. Переходы в льготный период и «истёк» с письмами.
|
||||
5. Частичный и полный возврат на баланс, с отменой ключа и без.
|
||||
6. Повторный `processPayment` и параллельные запуски задач не списывают дважды.
|
||||
7. `reconcile` не находит расхождений.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Этап 1e. Ключи и подключения
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Администратор выдаёт ключи вручную; пользователь получает конфиги и ссылки на странице ключа.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Коллекция `connections` с шифрованием содержимого ([03-data-model.md](../03-data-model.md), 5.2).
|
||||
2. Админка: на странице ключа — добавление подключений (WG/AWG — файлом или текстом, ссылка Happ, ссылка Xray, RU-адрес, номер устройства, страна), действие «Выдать ключ», список «К отключению».
|
||||
3. Страница ключа в кабинете: срок, галочка автопродления, «Продлить сейчас», WG-конфиги (скачать, QR), Happ/Xray (копировать, QR); для mass — WG по странам: конфиги, которые добавил администратор; нужна другая страна — ссылка на поддержку.
|
||||
4. Состояния: готовится, льготный период, истёк, нет подключений.
|
||||
5. Содержимое конфигов отдаётся отдельным запросом только владельцу, в HTML страницы не попадает. QR строится в браузере из полученного содержимого.
|
||||
6. Шаблон `key_ready` на RU и EN.
|
||||
|
||||
## 3. Перед стартом
|
||||
|
||||
Этап 1d готов.
|
||||
|
||||
## 4. Готово, когда
|
||||
|
||||
1. Администратор выдал ключ — пользователь видит его активным, срок пошёл с момента выдачи.
|
||||
2. Скачанный `.conf` импортируется в WireGuard; QR сканируется в WireGuard и Happ.
|
||||
3. Чужой пользователь получает 404 на конфиг (тест).
|
||||
@@ -0,0 +1,24 @@
|
||||
# Этап 1f. Веб-кабинеты
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Кабинеты обоих брендов по макетам `App` и `MassApp`.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Главная: ключи, «Купить ключ», FAQ.
|
||||
2. Список ключей; бейдж «Истекает» по правилу [04-billing.md](../04-billing.md), раздел 15.
|
||||
3. «Счёт и платежи»: баланс, пополнение (пресеты и своя сумма; пока платёжных систем нет — понятное сообщение), сохранённая карта и «Отвязать карту» (видно с этапа 4), ближайшие списания, история.
|
||||
4. Настройки: язык, валюта (правило — [04-billing.md](../04-billing.md), раздел 2), тема; выход.
|
||||
5. Мобильная навигация: вкладки «Главная, Ключи, Купить, Ещё».
|
||||
6. Различия брендов: у mass «Список ключей» без «текущей/запасной локации».
|
||||
|
||||
## 3. Не входит
|
||||
|
||||
Реферальная страница (1g), MiniApp (этап 3).
|
||||
|
||||
## 4. Готово, когда
|
||||
|
||||
Кабинеты обоих брендов совпадают с макетами: светлая и тёмная тема, десктоп и мобильный, RU и EN. Все данные — реальные, из базы.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Этап 1g. Реферальная программа
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
«Приведите друга — вы оба получите по месяцу в подарок» по правилам [04-billing.md](../04-billing.md), раздел 11.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Реферальные коды пользователей.
|
||||
2. Маршрут `my.<домен>/r/<код>`: cookie `vx_ref` на 60 дней и переход на сайт бренда.
|
||||
3. Привязка при регистрации; коллекция `referrals`.
|
||||
4. Бонус обоим после первой оплаты заказа `new_key` приглашённого; статус `review` при совпадении хэша IP.
|
||||
5. Суммы бонуса по валютам в `sites`.
|
||||
6. Страница `/referral` по макету: приглашено, начислено, ожидает; ссылка с копированием; приглашённые с замаскированной почтой.
|
||||
7. Шаблон `referral_reward` на RU и EN; список приглашений в админке.
|
||||
|
||||
## 3. Готово, когда
|
||||
|
||||
1. A делится ссылкой → B регистрируется → B оплачивает первый заказ → обоим начислен бонус.
|
||||
2. Самоприглашение с того же IP попадает в `review`.
|
||||
3. Ссылка другого бренда не засчитывается.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Этап 1h. Переводы, аналитика, запуск prod
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Сайты и кабинеты на 5 языках работают на основных доменах.
|
||||
|
||||
## 2. Входит
|
||||
|
||||
1. Переводы de, fr, es: интерфейс и контент Payload, машинный перевод LLM с глоссарием ([06-i18n.md](../06-i18n.md), раздел 7); проверка непереведённых ключей.
|
||||
2. Umami на этом сервере ([D-018](../decisions/D-018-analytics.md)) с проксированием через домены брендов; баннер cookies ([08-integrations.md](../08-integrations.md), раздел 8).
|
||||
3. Проверка производительности (Lighthouse mobile ≥ 90) и доступности.
|
||||
4. Prod под пользователем vx_prod: `.env`, команда выкладки с папками релизов и откатом ([02-architecture.md](../02-architecture.md), раздел 5), первая выкладка, начальные данные prod, первый администратор, smoke-тесты.
|
||||
5. Бэкапы базы и media с проверкой восстановления.
|
||||
6. Юридические черновики на сайтах (Q-13 — пока с заглушками).
|
||||
|
||||
## 3. Перед стартом
|
||||
|
||||
Root: R-08…R-11, по желанию R-15. Решения: Q-14 (куда бэкапы), Q-18 (кто запускает выкладку).
|
||||
|
||||
## 4. Готово, когда
|
||||
|
||||
1. Основные домены работают по HTTPS, dev и prod разделены.
|
||||
2. Выкладка и откат отрепетированы.
|
||||
3. Восстановление из бэкапа проверено.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Этап 2. Chatwoot и Twenty CRM
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Входит
|
||||
|
||||
1. Chatwoot ([08-integrations.md](../08-integrations.md), раздел 4): виджет на сайтах и в кабинетах, отдельные входящие каналы для каждого бренда; идентификация вошедших пользователей с HMAC; категория в баннере cookies.
|
||||
2. Боты поддержки обоих брендов подключены к Chatwoot как Telegram-каналы.
|
||||
3. Почта поддержки (Q-16) — по решению, например email-канал Chatwoot.
|
||||
4. Twenty ([08-integrations.md](../08-integrations.md), раздел 5): заявки уходят через API фоновой задачей с повторами, `leads.crmId`.
|
||||
|
||||
## 2. Перед стартом
|
||||
|
||||
Root: R-12 — VPS, Chatwoot, Twenty, ключи API, боты поддержки в BotFather.
|
||||
|
||||
## 3. Готово, когда
|
||||
|
||||
1. Сообщения из виджетов и ботов поддержки приходят в Chatwoot с указанием бренда; вошедший пользователь опознан.
|
||||
2. Заявки появляются в Twenty.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Этап 3. Telegram
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Входит
|
||||
|
||||
1. Основные боты обоих брендов на grammY: вебхук, команды, меню, тексты в Payload на всех языках ([08-integrations.md](../08-integrations.md), раздел 6).
|
||||
2. MiniApp по макетам `AppTG` и `MassAppTG` с Telegram SDK: BackButton, MainButton, тема Telegram, отклик вибрацией; вход по `initData`.
|
||||
3. Привязка почты из Telegram, «Подключить Telegram» в кабинете, «Войти через Telegram» на сайте, объединение аккаунтов ([05-auth.md](../05-auth.md), раздел 6).
|
||||
4. Уведомления в бот; реферальный код через `/start`.
|
||||
5. Тестовые боты для dev с доступом по списку Telegram-аккаунтов.
|
||||
6. Оплата в Telegram — по решению Q-11.
|
||||
|
||||
## 2. Перед стартом
|
||||
|
||||
1. Root: R-13 — основные и тестовые боты, домен для Telegram Login, адреса MiniApp.
|
||||
2. Решения: Q-10 (имена ботов), Q-11 (оплата в Telegram).
|
||||
|
||||
## 3. Готово, когда
|
||||
|
||||
1. Из Telegram можно зарегистрироваться, купить ключ (по Q-11), получить конфиги, пополнить баланс, пригласить друга.
|
||||
2. Уведомления приходят в бот.
|
||||
3. Сценарии привязки и объединения аккаунтов проходят.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Этап 4. Платёжные системы
|
||||
|
||||
Статус: не начат.
|
||||
|
||||
## 1. Входит
|
||||
|
||||
1. Адаптеры ([08-integrations.md](../08-integrations.md), раздел 7):
|
||||
a. Stripe: Payment Element, сохранение карты, автосписания на переменную сумму, возвраты, отвязка карты, вебхуки;
|
||||
b. RUB: процессинги для карт РФ — пополнения (провайдеров выбираем на старте, Q-12);
|
||||
c. криптовалюта — пополнения, экран счёта с адресом, суммой и таймером курса;
|
||||
d. ЮKassa — опционально, пополнения.
|
||||
2. Состояния оформления и пополнения: 3-D Secure, отказ, ожидание подтверждения сети.
|
||||
3. Повторы автосписаний ([04-billing.md](../04-billing.md), 6.1); возврат на карту (`refund` + `payout`).
|
||||
4. Тексты согласия на автосписания в оформлении заказа и в юридических документах.
|
||||
5. Настройка платёжных систем в админке: валюты, назначения, лимиты, тестовый или боевой режим. Если бренды получат разные аккаунты одного провайдера — Q-17.
|
||||
|
||||
## 2. Перед стартом
|
||||
|
||||
1. Root: R-14 — аккаунты провайдеров (сначала тестовые).
|
||||
2. Решения: Q-12; для боевого Stripe нужны данные юрлица (Q-13).
|
||||
|
||||
## 3. Готово, когда
|
||||
|
||||
1. Каждый адаптер в тестовом режиме проходит путь от оплаты до зачисления, включая повторную доставку вебхуков.
|
||||
2. Автопродление через Stripe работает на тестовых картах. Если карта требует 3-D Secure, пользователь получает письмо оплатить вручную.
|
||||
3. Возвраты на карту работают.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Этап 5. Автоматизация выдачи ключей
|
||||
|
||||
Статус: обсуждение при старте этапа.
|
||||
|
||||
## 1. Что известно
|
||||
|
||||
1. По тарифу (страны, теги) подбирается свободный сервер, бэкенд возвращает подключения для ключа.
|
||||
2. Автоматика создаёт те же подключения, что на этапе 1 добавляет администратор ([D-020](../decisions/D-020-key-models.md)), и отключает их, когда ключ истекает.
|
||||
3. Наличие в локациях обновляется автоматически, сайты пересобираются по событию.
|
||||
|
||||
## 2. Обсудить при старте
|
||||
|
||||
Источник серверов и подключений, способ интеграции, как получать WG-конфиги mass по странам, обработка ошибок выдачи.
|
||||
Reference in new issue
Block a user