chore: initial deploy bundle
This commit is contained in:
18
.env.example
18
.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=
|
||||
|
||||
70
README.md
70
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 <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`
|
||||
- БД должна быть **инициализирована отдельно** (таблицы `users`, `wallets`, `sessions` — создаются вручную DBA)
|
||||
|
||||
## Порты
|
||||
|
||||
@@ -85,22 +42,12 @@ Swagger UI: `http://<server-ip>: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 стартует:
|
||||
|
||||
@@ -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(','),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -40,11 +40,13 @@ export async function refreshAllKeys(): Promise<void> {
|
||||
logger.error(`Failed to refresh JWT keys: ${err.message}`);
|
||||
}
|
||||
|
||||
if (csrfPath) {
|
||||
try {
|
||||
await loadCsrfSecret(addr, token, mount, csrfPath);
|
||||
} catch (err: any) {
|
||||
logger.error(`Failed to refresh CSRF secret: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function startKeyRotation(intervalMs: number = DEFAULT_INTERVAL_MS): void {
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user