Веб‑приложение на Next.js 15 для выдачи временных ключей доступа (accessUrl) к Outline VPN. Пользователь нажимает «Получить VPN‑ключ», сервер создаёт/выдаёт ключ через Outline API и возвращает ссылку доступа. Используется Postgres + Prisma для учёта устройств (по IP) и выданных ключей, есть ограничение по частоте запросов и служебный endpoint для очистки всех ключей.
- Next.js (App Router), React 19, Tailwind CSS.
- Prisma ORM + PostgreSQL 16.
- Интеграция с Outline API (outlinevpn-api).
- Rate limit: 3 запроса на IP в минуту, 10 за 6 часов и 30 за 24 часа для
POST /api/keys. - Защищённый endpoint
DELETE /api/keysс заголовкомx-api-key. - Dockerfile со сборкой в режиме standalone, docker-compose с nginx, certbot, registry, postgres.
- CI/CD: GitHub Actions собирает образ и деплоит на self‑hosted сервер (см.
.github/workflows/workflow.yml).
- zod — валидация и типобезопасное парсинг конфигурации/данных. Применяется для переменных окружения в
src\lib\env.ts(жёсткая проверка URL и обязательных полей). - usehooks-ts — набор готовых React‑хуков (например,
useLocalStorage,useEventListener,useIsClient) для упрощения работы с состоянием и браузерными API. - sonner — лёгкие toast‑уведомления для UI.
- shadcn/ui + Radix UI — UI‑примитивы и компоненты (в проекте используются диалоги и алерты:
@radix-ui/react-dialog,@radix-ui/react-alert-dialog). - class-variance-authority + tailwind-merge — удобное управление вариациями классов и корректное слияние Tailwind‑классов.
- lucide-react — иконки.
- next-themes — переключение темы (light/dark) на стороне клиента.
- @serwist/next — сервис‑воркер и PWA‑инфраструктура (см.
src\app\sw.ts). - axios — HTTP‑клиент для серверных и клиентских запросов.
- react-if — декларативный условный рендеринг компонентов.
- vaul — анимационный Drawer/Sheet, поверх Radix‑примитивов.
- outlinevpn-api — обёртка над Outline Manager API для создания/удаления access‑ключей.
Проект использует next-intl (App Router) без URL-префиксов языков.
- Поддерживаемые локали: ru (по умолчанию), en, fr, uk, uz, de, pl, zh, ja, ko, es, ar, pt, id, hi, tr, vi, it.
- Сообщения хранятся в JSON-файлах:
messages/<locale>.json. - Определение языка на сервере:
- Парсится заголовок
Accept-Language; выбирается первый поддерживаемый язык из списка, иначеru. - Если в cookies есть валидный
localeиз поддерживаемых — используется он. Логика находится вsrc\i18n\request.ts.
- Парсится заголовок
- Подключение next-intl:
- Плагин в
next.config.ts(createNextIntlPlugin). - Провайдер в
src\app\layout.tsx:<NextIntlClientProvider messages={messages}>.
- Плагин в
Как переключить язык
- Клиентом: достаточно установить cookie
localeв один из поддерживаемых значений (см. список выше) и обновить страницу. - Сервером (рекомендуется): используйте готовый server action
setLocale:import {setLocale} from '@/i18n/set-locale'; import type {Locale} from '@/i18n/request'; // Пример вызова из Server Action или Route Handler export async function changeLang(locale: Locale) { await setLocale(locale); // при необходимости сделайте редирект }
Как добавлять/изменять переводы
- Добавьте ключи во все файлы
messages/*.json, сохраняя одинаковую структуру. - В компонентах используйте хук
useTranslationsили форматтеры next-intl. Пример:import {useTranslations} from 'next-intl'; export default function Button() { const t = useTranslations('Common'); // соответствует секции в JSON return <button>{t('getKey')}</button>; }
- Для серверных компонентов можно пользоваться
getMessages()(см. layout) и передавать их черезNextIntlClientProvider.
Замечания
- В данный момент язык не зашит в URL и не влияет на маршрутизацию. SEO-метки
hreflangне настроены. - Если потребуется дополнительный язык, добавьте
messages/<locale>.jsonи включите его в массивSUPPORTEDвsrc\i18n\request.ts(типLocaleвыводится автоматически).
Задаются через .env в разработке и через секреты/ENV в продакшне. Значения ниже примерные — подставьте свои.
- DATABASE_URL — строка подключения к Postgres (например,
postgresql://user:pass@host:5432/db) - POSTGRES_USER — имя пользователя Postgres (для docker-compose)
- POSTGRES_PASSWORD — пароль Postgres (для docker-compose)
- POSTGRES_DB — база данных Postgres (для docker-compose)
- OUTLINE_API_URL — URL API сервера Outline (включая секретный сегмент)
- OUTLINE_FINGERPRINT — TLS fingerprint сервера Outline
- HOST_IP — IP сервера, с которого идут запросы изнутри VPN; такие запросы блокируются
- API_KEY — секрет для
DELETE /api/keys
Примечание: не коммитьте реальные секреты в репозиторий. Используйте .env.local и секреты GitHub/CI.
-
Установить зависимости:
npm ci
-
Подготовить .env:
- Скопируйте
.env.example(если нет — создайте) в.envи заполните переменные.
- Скопируйте
-
Запустить Postgres (варианты):
- Локально установленный Postgres 16, либо
- docker-compose только с postgres:
docker compose up -d postgres
-
Применить Prisma:
npx prisma generatenpx prisma migrate dev
-
Запустить dev‑сервер:
npm run dev
Откройте http://localhost:3000. Главная страница — кнопка «Получить VPN‑ключ». Внизу может отображаться версия сборки, если проброшена в процесс.
-
POST /api/keys
- Возвращает
{ uuid: string, accessUrl: string }. - Rate limit: 3 в минуту, 10 за 6 часов и 30 за 24 часа на IP (заголовки X-RateLimit-*, Retry-After).
- Внутри: привязка устройства по IP, повторная выдача свободного ключа либо создание нового через Outline API.
- Ошибки: 400 (неверный IP), 403 (запрос из VPN — совпадает с HOST_IP), 429, 500 и т.д.
- Возвращает
-
GET /api/keys/{uuid}
- Возвращает
{ accessUrl: string, createdAt: string }(ISO 8601). - Ошибки: 400 (неверный UUID).
- Возвращает
-
DELETE /api/keys
- Требует заголовок
x-api-key: <API_KEY>. - Удаляет все access keys на Outline и синхронизирует базу (удаляет записи по accessUrl).
- Для внутренних задач/обслуживания; не предназначен для публичного вызова.
- Требует заголовок
- Клиент создаётся в
src/lib/db.tsс переиспользованием в dev. - Схема в
schema.prisma, миграции — вmigrations/. - Применение в CI/build:
prisma generateзапускается в Dockerfile на этапе deps (требуется DATABASE_URL через секрет BuildKit).
Предварительные условия:
- DNS A‑записи: lonadels.ru → IP сервера; www.lonadels.ru → туда же; registry.lonadels.ru → туда же
- Порты 80 и 443 открыты на сервере
Из корня проекта на целевом сервере Запускаем только nginx (HTTP‑01), registry и одноразовые задачи certbot init
-
Запустить nginx и registry, чтобы на 80 обслуживался ACME webroot
docker compose up -d nginx registry -
Выпустить сертификаты для lonadels.ru и www
docker compose --profile init up --exit-code-from certbot_init_main certbot_init_main
-
Выпустить сертификат для registry.lonadels.ru
docker compose --profile init up --exit-code-from certbot_init_registry certbot_init_registry
-
Запустить остальные сервисы и перезагрузить nginx
docker compose up -d
- Продление выполняется автоматически в контейнере
certbot(каждые ~12 часов). При обновлении сертификатов перезагрузите nginx при необходимости.
Docker Registry:
- Доступен по адресу https://registry.lonadels.ru.
- Аутентификация htpasswd включена; файл монтируется из
./docker/registry/auth/htpasswd. - В GitHub Actions логин выполняется по
DOCKER_USERNAME/DOCKER_PASSWORD.
CI/CD (GitHub Actions):
- Запускается на пуши в
mainна self‑hosted раннере. - Сборка и push образа:
registry.lonadels.ru/lonadels/lonadels.ru:latest. - Деплой:
docker compose pull main-appиdocker compose up -d main-app --force-recreateна сервере. - Build args/секреты: в workflow пробрасываются
POSTGRES_*,OUTLINE_*,HOST_IP,DATABASE_URL,API_KEYи т.д. ( см..github/workflows/workflow.yml).
Ручной деплой без CI (на сервере):
docker build -t registry.lonadels.ru/lonadels/lonadels.ru:latest .
docker push registry.lonadels.ru/lonadels/lonadels.ru:latest
docker compose pull main-app
docker compose up -d main-app --force-recreate- При ошибках Outline проверьте корректность
OUTLINE_API_URLиOUTLINE_FINGERPRINT. - Убедитесь, что
HOST_IPсовпадает с внешним IP сервера — это нужно, чтобы блокировать запросы изнутри VPN. - Для локальной разработки избегайте хранения реальных секретов в репозитории; используйте
.env.localи переменные окружения.
- Живая документация (Swagger UI): /api/docs
- Локально: http://localhost:3000/api/docs
- JSON‑спецификация OpenAPI: /openapi.json
- Локально: http://localhost:3000/openapi.json
- Спецификация лежит в
public\openapi.json. - Swagger UI отдаётся маршрутом
src\app\api\docs\route.tsи использует CDN (swagger-ui-dist) с ссылкой на /openapi.json. - Никаких дополнительных зависимостей в проект не добавлено.
- Добавьте/измените endpoint в
public\openapi.jsonв секции paths. Пример для закрытого эндпоинта с заголовкомx-api-key:{ "paths": { "/api/clearAllProxyKeys": { "post": { "security": [ { "ApiKeyAuth": [] } ], "responses": { "204": { "description": "No Content" } } } } }, "components": { "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-api-key" } } } } - При добавлении новых ручек опишите запрос/ответ (schemas) в components/schemas при необходимости.
- Для разных окружений можно задать servers в корне openapi.json, например:
{ "servers": [ { "url": "http://localhost:3000", "description": "Local" }, { "url": "https://lonadels.ru", "description": "Production" } ] }