diff --git a/.env.example b/.env.example index bfe8b4d..8618836 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,9 @@ VAULT_MOUNT_POINT=dev-secrets VAULT_SECRET_PATH=database VAULT_JWT_KID_PATH=jwt/kid VAULT_JWT_KIDS_PREFIX=jwt/kids -VAULT_CSRF_PATH=csrf + +# CSRF загружается если указан путь (оставь пустым чтобы отключить) +VAULT_CSRF_PATH= # ── JWT ──────────────────────────────────────────────────────────── JWT_ALGORITHM=RS256 @@ -17,17 +19,3 @@ JWT_AUDIENCE=elcsa API_PORT=3001 CORS_ORIGINS=http://localhost:3000 LOG_LEVEL=INFO - -# ── External API keys (fallback, обычно приходят из Vault) ───────── -RELAY_API_KEY= -TRON_API_KEY= -JUPITER_API_KEY= -JUPITER_REFERRAL_ACCOUNT= -JUPITER_FEE_BPS=70 - -# ── DB fallback (используется если Vault недоступен при старте) ──── -DB_HOST= -DB_PORT=5432 -DB_USER= -DB_PASSWORD= -DB_NAME= diff --git a/README.md b/README.md index 2b52b22..1b42b1b 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,6 @@ deployserver/ ├── .env.example # Шаблон переменных окружения ├── .dockerignore ├── start.sh # Автоматический deploy скрипт -├── db/ -│ └── schema.sql # DDL таблиц (users, wallets, sessions) — идемпотентный ├── apps/api/ # Исходник API │ ├── src/ │ ├── package.json @@ -30,48 +28,7 @@ deployserver/ - Docker Compose plugin (`docker compose` команда) - Исходящий HTTPS на Vault (`corp.vault.elcsa.ru:443`) - Сетевой доступ к PostgreSQL (адрес приходит из Vault) - -## Быстрый старт - -```bash -# 1. Скопировать папку на сервер -scp -r deployserver user@server:/opt/cryptowallet -ssh user@server -cd /opt/cryptowallet - -# 2. Установить Docker (если нет) -curl -fsSL https://get.docker.com | sudo sh -sudo usermod -aG docker $USER -newgrp docker - -# 3. Настроить .env -cp .env.example .env -nano .env # заполнить VAULT_ROLE_ID, VAULT_SECRET_ID - -# 4. Применить схему БД (один раз, на пустой БД) -sudo apt install -y postgresql-client -PGPASSWORD=<пароль_из_Vault> psql -h -U -d -f db/schema.sql - -# 5. Запустить -chmod +x start.sh -./start.sh - -# 6. Открыть порт наружу -sudo ufw allow 22/tcp -sudo ufw allow 3001/tcp -sudo ufw enable -``` - -## Проверка - -```bash -curl http://localhost:3001/api/health -# → {"success":true,"data":{"status":"ok"}} - -curl http://:3001/api/health # извне -``` - -Swagger UI: `http://:3001/api/docs` +- БД должна быть **инициализирована отдельно** (таблицы `users`, `wallets`, `sessions` — создаются вручную DBA) ## Порты @@ -85,22 +42,12 @@ Swagger UI: `http://:3001/api/docs` ```bash docker compose logs -f api # смотреть логи -docker compose restart api # рестарт (например после смены .env) +docker compose restart api # рестарт (после смены .env) docker compose down # остановить docker compose ps # статус -docker compose up -d --build # пересобрать и запустить (после обновления кода) +docker compose up -d --build # пересобрать и запустить ``` -## Обновление - -```bash -# Скопировать новую версию deployserver/ (или git pull) -docker compose build --pull api -docker compose up -d -``` - -Схема БД не применяется автоматически — если добавились новые таблицы/колонки, выполни `schema.sql` вручную (он идемпотентный, безопасно запускать повторно). - ## Ротация ключей JWT public keys и CSRF secret читаются из Vault при старте и **каждый час** обновляются автоматически (см. `key-rotation.service.ts`). При ошибках Vault сервис продолжает работать со старыми ключами — в логах будет `ERROR: Failed to refresh ...`. @@ -120,20 +67,13 @@ JWT public keys и CSRF secret читаются из Vault при старте - Проверь VAULT_ROLE_ID / VAULT_SECRET_ID в .env - Проверь доступ: `curl -v https://corp.vault.elcsa.ru/v1/sys/health` -**`password authentication failed for user "postgres_user"`** -- Креды в `.env` не совпадают с тем что в Vault (или с реальной БД) -- Решение: либо подставь пароль из Vault в `.env`, либо оставь пустыми — Vault перекроет при логине - -**Таблицы не существуют (relation does not exist)** -- Не применён `db/schema.sql` — см. шаг 4 в Quick Start +**`relation "users" does not exist`** +- БД не инициализирована — попроси DBA создать таблицы (`users`, `wallets`, `sessions`) **Port 3001 занят** - `sudo lsof -i :3001` - Измени порт: в `docker-compose.yml` `"3002:3001"` и в `ufw allow 3002` -**Нет места на диске** -- `docker system prune -a` — удалит старые образы - ## Автозапуск при reboot Restart policy `unless-stopped` уже настроен. Убедись что Docker стартует: diff --git a/apps/api/src/config/env.ts b/apps/api/src/config/env.ts index 7ab06c6..33dd782 100644 --- a/apps/api/src/config/env.ts +++ b/apps/api/src/config/env.ts @@ -28,7 +28,7 @@ export let env = { secretPath: p.VAULT_SECRET_PATH || 'database', jwtKidPath: p.VAULT_JWT_KID_PATH || 'jwt/kid', jwtKidsPrefix: p.VAULT_JWT_KIDS_PREFIX || 'jwt/kids', - csrfPath: p.VAULT_CSRF_PATH || 'csrf', + csrfPath: p.VAULT_CSRF_PATH || '', }, cors: { origins: (p.CORS_ORIGINS || 'http://localhost:3000').split(','), diff --git a/apps/api/src/middleware/csrf.ts b/apps/api/src/middleware/csrf.ts index aead5db..6a46ddb 100644 --- a/apps/api/src/middleware/csrf.ts +++ b/apps/api/src/middleware/csrf.ts @@ -1,5 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import { verifyCsrfToken, isCsrfConfigured } from '../services/csrf.service'; +import { env } from '../config/env'; import { logger } from '../lib/logger'; const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']); @@ -10,8 +11,13 @@ export function csrfMiddleware(req: Request, res: Response, next: NextFunction): return; } - // If CSRF is not configured (Vault down при старте) — пропускаем, чтобы не блокировать сервис. - // В логах будет warning — легко заметить. + // CSRF отключён если VAULT_CSRF_PATH не задан + if (!env.vault.csrfPath) { + next(); + return; + } + + // Секрет не загрузился (Vault недоступен) — пропускаем чтобы не блокировать сервис if (!isCsrfConfigured()) { logger.warn('CSRF check skipped: secret not loaded'); next(); diff --git a/apps/api/src/services/key-rotation.service.ts b/apps/api/src/services/key-rotation.service.ts index ae86cb9..361db9e 100644 --- a/apps/api/src/services/key-rotation.service.ts +++ b/apps/api/src/services/key-rotation.service.ts @@ -40,10 +40,12 @@ export async function refreshAllKeys(): Promise { logger.error(`Failed to refresh JWT keys: ${err.message}`); } - try { - await loadCsrfSecret(addr, token, mount, csrfPath); - } catch (err: any) { - logger.error(`Failed to refresh CSRF secret: ${err.message}`); + if (csrfPath) { + try { + await loadCsrfSecret(addr, token, mount, csrfPath); + } catch (err: any) { + logger.error(`Failed to refresh CSRF secret: ${err.message}`); + } } } diff --git a/db/schema.sql b/db/schema.sql deleted file mode 100644 index d0a40f5..0000000 --- a/db/schema.sql +++ /dev/null @@ -1,63 +0,0 @@ --- ============================================================================ --- CryptoWallet API — Database Schema --- Idempotent: safe to run multiple times (CREATE IF NOT EXISTS). --- --- Применение: --- psql "postgresql://user:pass@host:5432/db" -f deployserver/db/schema.sql --- ============================================================================ - --- ── users ─────────────────────────────────────────────────────────────────── -CREATE TABLE IF NOT EXISTS users ( - id VARCHAR(26) PRIMARY KEY, - email VARCHAR(255) NOT NULL UNIQUE, - password_hash VARCHAR(255) NOT NULL, - last_name VARCHAR(128), - first_name VARCHAR(128), - middle_name VARCHAR(128), - birth_date DATE, - crypto_wallet VARCHAR(255), - phone VARCHAR(16), - bik VARCHAR(9), - account_number VARCHAR(20), - card_number VARCHAR(19), - inn VARCHAR(12), - kyc_verified BOOLEAN NOT NULL DEFAULT FALSE, - kyc_verified_at TIMESTAMPTZ, - is_deleted BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP -); - --- ── wallets ───────────────────────────────────────────────────────────────── -CREATE TABLE IF NOT EXISTS wallets ( - id VARCHAR(26) PRIMARY KEY, - user_id VARCHAR(26) NOT NULL REFERENCES users(id) ON DELETE CASCADE, - chain VARCHAR(10) NOT NULL, - address VARCHAR(256) NOT NULL, - derivation_path VARCHAR(64) NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT wallets_user_id_chain_unique UNIQUE (user_id, chain) -); - -CREATE INDEX IF NOT EXISTS idx_wallets_user_id ON wallets(user_id); -CREATE INDEX IF NOT EXISTS idx_wallets_address ON wallets(address); - --- ── sessions ──────────────────────────────────────────────────────────────── -CREATE TABLE IF NOT EXISTS sessions ( - id VARCHAR(26) PRIMARY KEY, - sid VARCHAR(26) NOT NULL UNIQUE, - user_id VARCHAR(26) NOT NULL REFERENCES users(id) ON DELETE CASCADE, - device_id VARCHAR(26), - user_agent VARCHAR(500), - first_ip VARCHAR(64), - last_ip VARCHAR(64), - last_seen_at TIMESTAMPTZ, - revoked_at TIMESTAMPTZ, - refresh_jti_hash VARCHAR(255), - refresh_expires_at TIMESTAMPTZ, - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); -CREATE INDEX IF NOT EXISTS idx_sessions_sid ON sessions(sid);