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_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=
|
|
||||||
|
|||||||
70
README.md
70
README.md
@@ -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 стартует:
|
||||||
|
|||||||
@@ -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(','),
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -40,12 +40,14 @@ export async function refreshAllKeys(): Promise<void> {
|
|||||||
logger.error(`Failed to refresh JWT keys: ${err.message}`);
|
logger.error(`Failed to refresh JWT keys: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (csrfPath) {
|
||||||
try {
|
try {
|
||||||
await loadCsrfSecret(addr, token, mount, csrfPath);
|
await loadCsrfSecret(addr, token, mount, csrfPath);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error(`Failed to refresh CSRF secret: ${err.message}`);
|
logger.error(`Failed to refresh CSRF secret: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function startKeyRotation(intervalMs: number = DEFAULT_INTERVAL_MS): void {
|
export function startKeyRotation(intervalMs: number = DEFAULT_INTERVAL_MS): void {
|
||||||
if (timer) return;
|
if (timer) return;
|
||||||
|
|||||||
@@ -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