From 1b9cc8ac57f109afed237b0ca448990b1cd3e53a Mon Sep 17 00:00:00 2001 From: Kirill Date: Sat, 23 May 2026 10:56:08 +0500 Subject: [PATCH] docs: add IP-gate access control spec --- ...026-05-23-ip-gate-access-control-design.md | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-23-ip-gate-access-control-design.md diff --git a/docs/superpowers/specs/2026-05-23-ip-gate-access-control-design.md b/docs/superpowers/specs/2026-05-23-ip-gate-access-control-design.md new file mode 100644 index 0000000..3251152 --- /dev/null +++ b/docs/superpowers/specs/2026-05-23-ip-gate-access-control-design.md @@ -0,0 +1,93 @@ +# IP-gate: ограничение доступа на время разработки + +## Задача + +Сайт доступен на реальном домене (через VPS + NPM + Netbird), но находится в активной разработке/тестировании. Нужно ограничить доступ, не мешая разработке и полному тестированию функционала (включая OAuth и webhook-и). + +## Решение + +IP-whitelist на уровне Fastify (`onRequest` хук). Только запросы с разрешённых IP проходят. Внешние webhook-и и OAuth callback-и исключены из проверки. + +## Конфигурация + +### `.env` + +```env +SITE_ACCESS_IPS=1.2.3.4,5.6.7.8 +``` + +- Не задана или пуста — защита **отключена** +- IP через запятую, пробелы игнорируются (трим) +- `request.ip` возвращает реальный IP клиента благодаря `trustProxy: true` + +## Архитектура + +### Новый плагин: `server/src/plugins/ip-gate.js` + +Регистрируется в `server/src/index.js` **перед** всеми маршрутами. + +```js +fastify.register(async function ipGate(fastify, opts) { + fastify.addHook('onRequest', async (request, reply) => { + // защита выключена + // путь в исключениях + // ip в списке + // иначе 403 + }) +}) +``` + +### Логика `onRequest` + +1. `SITE_ACCESS_IPS` пуст → `return` (пропустить) +2. Путь запроса в списке исключений → `return` +3. `request.ip` есть в `SITE_ACCESS_IPS` → `return` +4. Иначе → `reply.code(403).type('text/html').send(htmlPage)` + +### Исключения + +Маршруты, которые должны работать всегда (их вызывают внешние сервисы, а не браузер тестировщика): + +| Путь | Причина | +|---|---| +| `/api/auth/oauth/vk/callback` | VK OAuth callback | +| `/api/auth/oauth/yandex/callback` | Yandex OAuth callback | +| `/api/webhooks/yookassa` | YooKassa payment webhook | +| `/api/admin/notifications/telegram/webhook` | Telegram webhook | + +Статика (загружается браузером тестировщика, поэтому тоже проверяется): +- `/uploads/*` и `/uploads-resized/*` — **не** исключаем, блокируются вместе со всем остальным + +### 403-страница + +HTML-страница с информацией о магазине и статусе разработки: + +- Название: «Любимый Креатив» +- Подзаголовок: «Изделия ручной работы: вещи с характером и вниманием к деталям» +- Сообщение: «Сайт находится в разработке и скоро будет доступен» +- Показывает IP посетителя (чтобы можно было сообщить для добавления в whitelist) +- Минимальная стилизация (чистый HTML + inline CSS, без внешних ресурсов) + +## Точки регистрации + +В `server/src/index.js`: + +```js +// после trustProxy, перед маршрутами +await fastify.register(require('./plugins/ip-gate')) +``` + +## Тестирование + +- **Юнит-тесты**: `server/src/plugins/__tests__/ip-gate.test.js` + - IP в списке → запрос проходит + - IP не в списке → 403 + - Путь-исключение → проходит с любым IP + - `SITE_ACCESS_IPS` не задан → защита выключена + - Пробелы в списке IP → корректная работа + +## Включение/выключение + +- **Включить**: задать `SITE_ACCESS_IPS` в `.env` +- **Выключить**: удалить `SITE_ACCESS_IPS` или оставить пустым +- Перезапуск сервера не требуется если используется `node --watch`