chore: initial deploy bundle

This commit is contained in:
ZOMBIIIIIII
2026-04-21 15:01:05 +03:00
parent 9329b76e9b
commit 59a7d1d9ca
6 changed files with 23 additions and 150 deletions

View File

@@ -6,7 +6,9 @@ VAULT_MOUNT_POINT=dev-secrets
VAULT_SECRET_PATH=database VAULT_SECRET_PATH=database
VAULT_JWT_KID_PATH=jwt/kid VAULT_JWT_KID_PATH=jwt/kid
VAULT_JWT_KIDS_PREFIX=jwt/kids VAULT_JWT_KIDS_PREFIX=jwt/kids
VAULT_CSRF_PATH=csrf
# CSRF загружается если указан путь (оставь пустым чтобы отключить)
VAULT_CSRF_PATH=
# ── JWT ──────────────────────────────────────────────────────────── # ── JWT ────────────────────────────────────────────────────────────
JWT_ALGORITHM=RS256 JWT_ALGORITHM=RS256
@@ -17,17 +19,3 @@ JWT_AUDIENCE=elcsa
API_PORT=3001 API_PORT=3001
CORS_ORIGINS=http://localhost:3000 CORS_ORIGINS=http://localhost:3000
LOG_LEVEL=INFO 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=

View File

@@ -11,8 +11,6 @@ deployserver/
├── .env.example # Шаблон переменных окружения ├── .env.example # Шаблон переменных окружения
├── .dockerignore ├── .dockerignore
├── start.sh # Автоматический deploy скрипт ├── start.sh # Автоматический deploy скрипт
├── db/
│ └── schema.sql # DDL таблиц (users, wallets, sessions) — идемпотентный
├── apps/api/ # Исходник API ├── apps/api/ # Исходник API
│ ├── src/ │ ├── src/
│ ├── package.json │ ├── package.json
@@ -30,48 +28,7 @@ deployserver/
- Docker Compose plugin (`docker compose` команда) - Docker Compose plugin (`docker compose` команда)
- Исходящий HTTPS на Vault (`corp.vault.elcsa.ru:443`) - Исходящий HTTPS на Vault (`corp.vault.elcsa.ru:443`)
- Сетевой доступ к PostgreSQL (адрес приходит из Vault) - Сетевой доступ к PostgreSQL (адрес приходит из Vault)
- БД должна быть **инициализирована отдельно** (таблицы `users`, `wallets`, `sessions` — создаются вручную DBA)
## Быстрый старт
```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 <host> -U <user> -d <db> -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://<server-ip>:3001/api/health # извне
```
Swagger UI: `http://<server-ip>:3001/api/docs`
## Порты ## Порты
@@ -85,22 +42,12 @@ Swagger UI: `http://<server-ip>:3001/api/docs`
```bash ```bash
docker compose logs -f api # смотреть логи docker compose logs -f api # смотреть логи
docker compose restart api # рестарт (например после смены .env) docker compose restart api # рестарт (после смены .env)
docker compose down # остановить docker compose down # остановить
docker compose ps # статус 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 ...`. 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 - Проверь VAULT_ROLE_ID / VAULT_SECRET_ID в .env
- Проверь доступ: `curl -v https://corp.vault.elcsa.ru/v1/sys/health` - Проверь доступ: `curl -v https://corp.vault.elcsa.ru/v1/sys/health`
**`password authentication failed for user "postgres_user"`** **`relation "users" does not exist`**
- Креды в `.env` не совпадают с тем что в Vault (или с реальной БД) - БД не инициализирована — попроси DBA создать таблицы (`users`, `wallets`, `sessions`)
- Решение: либо подставь пароль из Vault в `.env`, либо оставь пустыми — Vault перекроет при логине
**Таблицы не существуют (relation does not exist)**
- Не применён `db/schema.sql` — см. шаг 4 в Quick Start
**Port 3001 занят** **Port 3001 занят**
- `sudo lsof -i :3001` - `sudo lsof -i :3001`
- Измени порт: в `docker-compose.yml` `"3002:3001"` и в `ufw allow 3002` - Измени порт: в `docker-compose.yml` `"3002:3001"` и в `ufw allow 3002`
**Нет места на диске**
- `docker system prune -a` — удалит старые образы
## Автозапуск при reboot ## Автозапуск при reboot
Restart policy `unless-stopped` уже настроен. Убедись что Docker стартует: Restart policy `unless-stopped` уже настроен. Убедись что Docker стартует:

View File

@@ -28,7 +28,7 @@ export let env = {
secretPath: p.VAULT_SECRET_PATH || 'database', secretPath: p.VAULT_SECRET_PATH || 'database',
jwtKidPath: p.VAULT_JWT_KID_PATH || 'jwt/kid', jwtKidPath: p.VAULT_JWT_KID_PATH || 'jwt/kid',
jwtKidsPrefix: p.VAULT_JWT_KIDS_PREFIX || 'jwt/kids', jwtKidsPrefix: p.VAULT_JWT_KIDS_PREFIX || 'jwt/kids',
csrfPath: p.VAULT_CSRF_PATH || 'csrf', csrfPath: p.VAULT_CSRF_PATH || '',
}, },
cors: { cors: {
origins: (p.CORS_ORIGINS || 'http://localhost:3000').split(','), origins: (p.CORS_ORIGINS || 'http://localhost:3000').split(','),

View File

@@ -1,5 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { verifyCsrfToken, isCsrfConfigured } from '../services/csrf.service'; import { verifyCsrfToken, isCsrfConfigured } from '../services/csrf.service';
import { env } from '../config/env';
import { logger } from '../lib/logger'; import { logger } from '../lib/logger';
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']); const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
@@ -10,8 +11,13 @@ export function csrfMiddleware(req: Request, res: Response, next: NextFunction):
return; return;
} }
// If CSRF is not configured (Vault down при старте) — пропускаем, чтобы не блокировать сервис. // CSRF отключён если VAULT_CSRF_PATH не задан
// В логах будет warning — легко заметить. if (!env.vault.csrfPath) {
next();
return;
}
// Секрет не загрузился (Vault недоступен) — пропускаем чтобы не блокировать сервис
if (!isCsrfConfigured()) { if (!isCsrfConfigured()) {
logger.warn('CSRF check skipped: secret not loaded'); logger.warn('CSRF check skipped: secret not loaded');
next(); next();

View File

@@ -40,10 +40,12 @@ export async function refreshAllKeys(): Promise<void> {
logger.error(`Failed to refresh JWT keys: ${err.message}`); logger.error(`Failed to refresh JWT keys: ${err.message}`);
} }
try { if (csrfPath) {
await loadCsrfSecret(addr, token, mount, csrfPath); try {
} catch (err: any) { await loadCsrfSecret(addr, token, mount, csrfPath);
logger.error(`Failed to refresh CSRF secret: ${err.message}`); } catch (err: any) {
logger.error(`Failed to refresh CSRF secret: ${err.message}`);
}
} }
} }

View File

@@ -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);