From f56d6a79fb0c738e6d69fbaf214ca3629ce76994 Mon Sep 17 00:00:00 2001 From: "@kirill.komarov" Date: Sun, 10 May 2026 14:38:32 +0500 Subject: [PATCH] base commit --- README.md | 18 ++++++++- docs/test-deploy-proxmox.md | 78 +++++++++++++++++++++++++++++++++++++ server/.dev_env | 20 ++++++++++ server/.env.example | 4 ++ server/package.json | 10 ++++- server/src/lib/auth.js | 15 +++++++ 6 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 docs/test-deploy-proxmox.md create mode 100644 server/.dev_env diff --git a/README.md b/README.md index 15cfe42..4a29498 100644 --- a/README.md +++ b/README.md @@ -50,17 +50,31 @@ ### Бэкенд +**Вариант A — типовой `.env`** + ```bash cd server cp .env.example .env # укажите ADMIN_EMAIL npm install npx prisma migrate dev # если база ещё не создана -npx prisma db seed # опционально: тестовые категории и товары -npm run dev +npx prisma db seed # опционально: тестовые категории и товары +npm run dev:classic # загрузка из `.env` ``` +**Вариант B — файл [`server/.dev_env`](server/.dev_env)** (то, что уже лежит в репозитории для локального стенда; нужен **Node.js 20.6+** из‑за `node --env-file`): + +```bash +cd server +npm install +npm run dev # переменные из `.dev_env` +``` + +Очистка БД до «чистого» тестового состояния (SQLite + миграции + seed): в `server/` выполните `npm run db:reset:test`. + Сервер: `http://127.0.0.1:3333`. Проверка: `GET /health`. +Черновик деплоя на Proxmox: [docs/test-deploy-proxmox.md](docs/test-deploy-proxmox.md). + ### Фронтенд В другом терминале: diff --git a/docs/test-deploy-proxmox.md b/docs/test-deploy-proxmox.md new file mode 100644 index 0000000..71a5f61 --- /dev/null +++ b/docs/test-deploy-proxmox.md @@ -0,0 +1,78 @@ +# Тестовый деплой на Proxmox (локально у вас в ЦОД) + +Цель — один недорогой стенд: витрина + API + SQLite, без отказоустойчивого кластера. + +## Серверов и ресурсов + +Рекомендация для **минимального тестового** инстанса на Proxmox: + +| Вариант | Ресурс | Комментарий | +|---------|--------|---------------| +| **1× LXC Debian 12 или Ubuntu 22.04** | CPU: **2 vCPU**, RAM: **1–2 GB**, Диск: **15–25 GB** (SSD/pool SSD) | Один контейнер: Node (API), при желании статика фронта с того же узла через nginx | + +Для сборки (`npm ci` + `vite build`) и запасов по памяти лучше **2 GB RAM**; на **1 GB** возможны OOM при параллельной сборке — собирать фронт можно на машине разработчика и копировать `client/dist`. + +Отделять приложение на **два LXC** (фронт / бэкенд) для теста обычно не нужно; имеет смысл только если хотите явно ограничить поверхность API. + +### Сеть и доступ + +- Одна виртуальная сеть VM/LAN вашего хостинга до Proxmox. +- Открыть снаружи (если нужен доступ из браузера): **80/tcp** и **443/tcp** на reverse proxy; прямой порт Fastify (**3333**) наружу не публиковать. +- Сервер приложения слушает **опционально** `127.0.0.1:3333`, наружу отдаёт **nginx** или **caddy**: + + - статика из `client/dist` (SPA `try_files`); + + - `location /api/` → proxy на `http://127.0.0.1:3333`; + + - `location /uploads/` → те же файлы с диска, что использует сервер (`server/uploads/` рядом с процессом) или через тот же origin с proxy на Fastify `@fastify/static`. + +## Очистка БД локально перед тестом + +В каталоге `server/` (нужны Node **20.6+** или **22+** для флага `--env-file`; иначе скопируйте переменные из `.dev_env` в `.env` и используйте обычные команды Prisma): + +```bash +npm run db:reset:test +``` + +Удалит данные SQLite, заново применит миграции и выполнит `prisma`-seed из `server/package.json`. + +## Программный стек на ВМ/LXC + +- **Node.js** LTS (20.6+, лучше 22; скрипты `dev`, `start:dev_env`, `db:*:test` читают `server/.dev_env` через `node --env-file=.dev_env`). +- Приложение ставится так: склонировать репозиторий, на сервере в `server/` — `npm ci`, `npm run db:migrate:test` (или `migrate deploy`), при необходимости `npm run db:seed:test`. +- Переменные окружения: скопировать `server/.dev_env` или `server/.env.example`, задать **сильный** `JWT_SECRET`, свой `ADMIN_EMAIL`, `DATABASE_URL=file:./dev.db` или путь под персистентный раздел (`/var/lib/craftshop/data.db`). +- Для прод-подобного теста: **`IS_DEFAULT_CODE_ENABLED=false`** (не оставлять общий код на поставку наружу). + +## Сервис (systemd, пример) + +Один юнит `craftshop-api.service`: + +- `WorkingDirectory=/opt/craftshop/server` +- `ExecStart=/usr/bin/node src/index.js` (перед этим экспортировать env через `EnvironmentFile=` к вашему `.env`) +- После деплоя: `systemctl daemon-reload && systemctl enable --now craftshop-api` + +Персистентность: файл SQLite и каталог **`uploads/`** должны жить на диске, который не теряется при пересборке образа контейнера. + +## Контрольный чеклист перед «тестом в бою» + +1. Прогон миграций на чистую БД: `npm run db:reset:test` (только в **закрытом** окружении — стирает данные). +2. Сборка фронта и выставление `VITE_API_URL` (если API не на том же origin, что SPA). +3. `CORS_ORIGIN` — URL публичного фронта. +4. Выключить `DEFAULT_CODE` на внешнем стенде. + +## Краткая схема + +```mermaid +flowchart LR + User[Браузер] + Px[nginx на LXC] + Api[Fastify Node :3333] + Db[(SQLite файл)] + Up[диск uploads] + + User --> Px + Px -->|"/api"| Api + Px -->|"статика /"| Ui[client dist] + Api --> Db + Api --> Up +``` diff --git a/server/.dev_env b/server/.dev_env new file mode 100644 index 0000000..0bf153d --- /dev/null +++ b/server/.dev_env @@ -0,0 +1,20 @@ +DATABASE_URL="file:./dev.db" +ADMIN_EMAIL=admin@example.com +# Default code for login +DEFAULT_CODE=123456 +IS_DEFAULT_CODE_ENABLED=true +PORT=3333 +# Токен для админ-запросов с фронта: Authorization: Bearer <значение> +ADMIN_API_TOKEN=dev-secret-change-me +# JWT для пользовательской авторизации (замени в проде) +JWT_SECRET=dev-jwt-secret-change-me +# Опционально: список origin для CORS через запятую (в dev можно не задавать) +# CORS_ORIGIN=http://localhost:5173 + +# SMTP для отправки кода (если не задано — код логируется в консоль как [DEV]) +# SMTP_HOST=smtp.example.com +# SMTP_PORT=587 +# SMTP_SECURE=false +# SMTP_USER=user@example.com +# SMTP_PASS=password +# MAIL_FROM="Craftshop " \ No newline at end of file diff --git a/server/.env.example b/server/.env.example index 5f5ca09..0ddd0e9 100644 --- a/server/.env.example +++ b/server/.env.example @@ -3,6 +3,10 @@ PORT=3333 ADMIN_EMAIL=admin@example.com JWT_SECRET=замените-на-секрет-jwt +# Только приватный стенд: фиксированный код входа (без письма), см. server/.dev_env и npm run dev/start:dev_env +# IS_DEFAULT_CODE_ENABLED=true +# DEFAULT_CODE=123456 + # Разрешённый Origin фронта (через запятую при нескольких) # CORS_ORIGIN=http://127.0.0.1:5173 diff --git a/server/package.json b/server/package.json index a10b991..99a7540 100644 --- a/server/package.json +++ b/server/package.json @@ -4,11 +4,17 @@ "private": true, "type": "module", "scripts": { - "dev": "node --watch src/index.js", + "dev": "node --env-file=.dev_env --watch src/index.js", + "dev:classic": "node --watch src/index.js", "start": "node src/index.js", + "start:dev_env": "node --env-file=.dev_env src/index.js", "db:migrate": "prisma migrate dev", + "db:migrate:test": "node --env-file=.dev_env ./node_modules/prisma/build/index.js migrate deploy", "db:seed": "prisma db seed", - "db:studio": "prisma studio" + "db:seed:test": "node --env-file=.dev_env prisma/seed.js", + "db:reset:test": "node --env-file=.dev_env ./node_modules/prisma/build/index.js migrate reset --force", + "db:studio": "prisma studio", + "db:studio:test": "node --env-file=.dev_env ./node_modules/prisma/build/index.js studio" }, "prisma": { "seed": "node prisma/seed.js" diff --git a/server/src/lib/auth.js b/server/src/lib/auth.js index 510b5e3..19a47c4 100644 --- a/server/src/lib/auth.js +++ b/server/src/lib/auth.js @@ -29,7 +29,22 @@ export async function issueEmailCode({ email, purpose, userId = null }) { await sendLoginCodeEmail({ to: email, code }) } +function parseEnvBool(raw) { + const v = String(raw ?? '').trim().toLowerCase() + return v === 'true' || v === '1' || v === 'yes' +} + +/** Тестовые стенды: принять код из переменной DEFAULT_CODE без записи в БД. */ +export function isDefaultLoginCodeAccepted(codeInput) { + if (!parseEnvBool(process.env.IS_DEFAULT_CODE_ENABLED)) return false + const expected = String(process.env.DEFAULT_CODE ?? '').trim() + if (!expected || expected.length < 4) return false + return String(codeInput ?? '').trim() === expected +} + export async function verifyEmailCode({ email, purpose, code, userId = null }) { + if (purpose === 'login' && isDefaultLoginCodeAccepted(code)) return true + const now = new Date() const codeHash = sha256(`${email}:${purpose}:${code}:${userId ?? ''}`)