remove /api/vault endpoints
This commit is contained in:
@@ -1,31 +1,24 @@
|
||||
# Multi-stage build: base → deps → build → prod-deps → runtime
|
||||
# Build context: deployserver/ root.
|
||||
|
||||
FROM node:20-alpine AS base
|
||||
RUN corepack enable && corepack prepare pnpm@10.28.2 --activate \
|
||||
&& apk add --no-cache python3 make g++
|
||||
WORKDIR /app
|
||||
|
||||
# ── deps: install ВСЕ зависимости (включая dev) для сборки TS ───────────────
|
||||
FROM base AS deps
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
COPY apps/api/package.json apps/api/
|
||||
RUN pnpm install --frozen-lockfile --prod=false
|
||||
|
||||
# ── build: компилируем TypeScript ───────────────────────────────────────────
|
||||
FROM base AS build
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules
|
||||
COPY . .
|
||||
RUN cd apps/api && pnpm build
|
||||
|
||||
# ── prod-deps: только production node_modules ──────────────────────────────
|
||||
FROM base AS prod-deps
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
|
||||
COPY apps/api/package.json apps/api/
|
||||
RUN pnpm install --frozen-lockfile --prod
|
||||
|
||||
# ── runtime: минимальный образ для прода ───────────────────────────────────
|
||||
FROM node:20-alpine AS runtime
|
||||
RUN apk add --no-cache tini wget \
|
||||
&& addgroup -S app -g 1001 \
|
||||
@@ -39,11 +32,9 @@ COPY --from=build --chown=app:app /app/apps/api/dist ./dist
|
||||
COPY --from=build --chown=app:app /app/apps/api/swagger.json ./swagger.json
|
||||
COPY --from=build --chown=app:app /app/apps/api/package.json ./package.json
|
||||
|
||||
# logs/ создаётся для audit-log
|
||||
RUN mkdir -p /app/logs && chown -R app:app /app/logs
|
||||
|
||||
USER app
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2022,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-namespace": "off",
|
||||
"no-console": "off"
|
||||
},
|
||||
"ignorePatterns": ["dist/", "node_modules/"]
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import { csrfMiddleware } from './middleware/csrf';
|
||||
import { globalLimiter, mutateLimiter, sensitiveLimiter, mnemonicRevealLimiter } from './middleware/rate-limit';
|
||||
import { errorHandler } from './middleware/error-handler';
|
||||
import walletRoutes from './routes/wallet.routes';
|
||||
import vaultRoutes from './routes/vault.routes';
|
||||
import relayProxyRoutes from './routes/relay-proxy.routes';
|
||||
import tronProxyRoutes from './routes/tron-proxy.routes';
|
||||
import solSwapProxyRoutes from './routes/sol-swap-proxy.routes';
|
||||
@@ -55,7 +54,6 @@ const protect = [authMiddleware, csrfMiddleware];
|
||||
app.use('/api/wallets/create', ...protect, sensitiveLimiter);
|
||||
app.use('/api/wallets/mnemonic/reveal', ...protect, mnemonicRevealLimiter);
|
||||
app.use('/api/wallets/:chain/send', ...protect, sensitiveLimiter);
|
||||
app.use('/api/vault', ...protect, sensitiveLimiter, vaultRoutes);
|
||||
|
||||
// Mutating (proxy + read endpoints) — повышенный лимит
|
||||
app.use('/api/wallets', ...protect, mutateLimiter, walletRoutes);
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { UserModel } from '../models/user.model';
|
||||
import { logger } from '../lib/logger';
|
||||
|
||||
const MAX_VAULT_SIZE = 8192; // base64 encrypted blob upper limit
|
||||
const MAX_SALT_LEN = 128;
|
||||
|
||||
/**
|
||||
* Encrypted vault — opaque blob (зашифрованный mnemonic, AES-GCM на клиенте).
|
||||
* Сервис хранит как есть; никогда не расшифровывает. Ключ только у клиента
|
||||
* (PBKDF2(password+pin) или аналог).
|
||||
*/
|
||||
export const VaultController = {
|
||||
/**
|
||||
* GET /api/vault — вернуть encrypted_vault + vault_salt пользователя.
|
||||
*/
|
||||
async getVault(req: Request, res: Response) {
|
||||
const userId = req.auth!.userId;
|
||||
try {
|
||||
const row = await UserModel.getVault(userId);
|
||||
if (!row || !row.encrypted_vault || !row.vault_salt) {
|
||||
res.status(404).json({ success: false, error: 'Vault not found' });
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
encryptedVault: row.encrypted_vault,
|
||||
vaultSalt: row.vault_salt,
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
logger.error(`getVault failed for user ${userId}: ${err.stack || err.message}`);
|
||||
res.status(500).json({ success: false, error: 'Internal error' });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT /api/vault — сохранить новый encrypted_vault + vault_salt.
|
||||
* Создаёт user-row если её ещё нет.
|
||||
*/
|
||||
async putVault(req: Request, res: Response) {
|
||||
const userId = req.auth!.userId;
|
||||
const { encryptedVault, vaultSalt } = req.body ?? {};
|
||||
|
||||
if (typeof encryptedVault !== 'string' || encryptedVault.length === 0 || encryptedVault.length > MAX_VAULT_SIZE) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `encryptedVault must be a non-empty string (max ${MAX_VAULT_SIZE} chars)`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (typeof vaultSalt !== 'string' || vaultSalt.length === 0 || vaultSalt.length > MAX_SALT_LEN) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `vaultSalt must be a non-empty string (max ${MAX_SALT_LEN} chars)`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await UserModel.ensureExists(userId);
|
||||
await UserModel.setVault(userId, encryptedVault, vaultSalt);
|
||||
res.json({ success: true });
|
||||
} catch (err: any) {
|
||||
logger.error(`putVault failed for user ${userId}: ${err.stack || err.message}`);
|
||||
res.status(500).json({ success: false, error: 'Internal error' });
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -61,23 +61,6 @@ export const UserModel = {
|
||||
return user;
|
||||
},
|
||||
|
||||
async setVault(id: string, encryptedVault: string, vaultSalt: string): Promise<void> {
|
||||
await db('users')
|
||||
.where({ id })
|
||||
.update({
|
||||
encrypted_vault: encryptedVault,
|
||||
vault_salt: vaultSalt,
|
||||
updated_at: db.fn.now(),
|
||||
});
|
||||
},
|
||||
|
||||
async getVault(id: string): Promise<{ encrypted_vault: string | null; vault_salt: string | null } | undefined> {
|
||||
return db('users')
|
||||
.where({ id, is_deleted: false })
|
||||
.select('encrypted_vault', 'vault_salt')
|
||||
.first();
|
||||
},
|
||||
|
||||
/**
|
||||
* Custodial: атомарно записать зашифрованную мнемонику.
|
||||
* Используется set-once семантика: UPDATE WHERE encrypted_mnemonic IS NULL,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Router } from 'express';
|
||||
import { VaultController } from '../controllers/vault.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', VaultController.getVault);
|
||||
router.put('/', VaultController.putVault);
|
||||
|
||||
export default router;
|
||||
@@ -12,7 +12,6 @@
|
||||
{ "name": "System", "description": "Health & service info" },
|
||||
{ "name": "Wallets", "description": "User wallet records" },
|
||||
{ "name": "Wallet Ops", "description": "Per-chain balance / transactions / send" },
|
||||
{ "name": "Vault", "description": "Encrypted mnemonic blob storage (opaque)" },
|
||||
{ "name": "BTC", "description": "Bitcoin RPC proxy (Blockstream)" },
|
||||
{ "name": "TRON", "description": "TRON RPC proxy (TronGrid)" },
|
||||
{ "name": "Solana", "description": "Solana swap proxy (Jupiter)" },
|
||||
@@ -144,27 +143,6 @@
|
||||
"description": "Unsigned tx — формат зависит от chain (kind: btc | tron | evm | solana). Клиент подписывает приватом и broadcast'ит через соответствующий /api/{btc,tron}/broadcast endpoint"
|
||||
}
|
||||
}
|
||||
},
|
||||
"VaultResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": { "type": "boolean" },
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"encryptedVault": { "type": "string", "description": "AES-GCM encrypted mnemonic, base64" },
|
||||
"vaultSalt": { "type": "string", "description": "PBKDF2 salt, hex" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"VaultPutRequest": {
|
||||
"type": "object",
|
||||
"required": ["encryptedVault", "vaultSalt"],
|
||||
"properties": {
|
||||
"encryptedVault": { "type": "string", "maxLength": 8192 },
|
||||
"vaultSalt": { "type": "string", "maxLength": 128 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -285,29 +263,6 @@
|
||||
}
|
||||
},
|
||||
|
||||
"/vault": {
|
||||
"get": {
|
||||
"summary": "Get user's encrypted mnemonic vault",
|
||||
"tags": ["Vault"],
|
||||
"responses": {
|
||||
"200": { "description": "Encrypted vault blob", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VaultResponse" } } } },
|
||||
"404": { "description": "Vault not yet stored" }
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"summary": "Save / replace encrypted mnemonic vault",
|
||||
"description": "Vault — opaque blob (AES-GCM на стороне клиента). Сервер хранит как есть, не расшифровывает.",
|
||||
"tags": ["Vault"],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": { "application/json": { "schema": { "$ref": "#/components/schemas/VaultPutRequest" } } }
|
||||
},
|
||||
"responses": {
|
||||
"200": { "description": "Saved", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SuccessEmpty" } } } },
|
||||
"400": { "description": "Invalid input" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"/btc/utxos/{address}": {
|
||||
"get": {
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
-- CryptoWallet API — DB schema (idempotent)
|
||||
-- Применить один раз на внешнюю БД при первом деплое.
|
||||
-- Версия 3.0 (custodial: AES-GCM encrypted_mnemonic на сервере).
|
||||
-- ─────────────────────────────────────────────────────────────────────────
|
||||
-- CryptoWallet API — DB schema (idempotent, custodial v3.0)
|
||||
|
||||
-- ── USERS ────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(26) PRIMARY KEY, -- ULID из JWT.sub
|
||||
id VARCHAR(26) PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL, -- "EXTERNAL_AUTH" (auth у BITOK)
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
last_name VARCHAR(255),
|
||||
first_name VARCHAR(255),
|
||||
middle_name VARCHAR(255),
|
||||
@@ -22,14 +17,13 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
kyc_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
kyc_verified_at TIMESTAMPTZ,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
encrypted_vault TEXT, -- legacy client-side AES blob (opt)
|
||||
vault_salt VARCHAR(128), -- legacy client KDF salt (opt)
|
||||
encrypted_mnemonic TEXT, -- AES-256-GCM blob (custodial)
|
||||
encrypted_vault TEXT,
|
||||
vault_salt VARCHAR(128),
|
||||
encrypted_mnemonic TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Добавляем encrypted_mnemonic если таблица существует с прошлой версии
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
@@ -40,11 +34,6 @@ BEGIN
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- CHECK: encrypted_mnemonic при значении должен иметь разумный размер.
|
||||
-- AES-GCM blob: 12 IV + plaintext + 16 tag.
|
||||
-- 12-word mnemonic ≈ 11*7 + 11 = 88 байт → 12+88+16 = 116 байт → ~156 base64 chars.
|
||||
-- 24-word mnemonic ≈ 23*7 + 23 = 184 байт → 12+184+16 = 212 байт → ~284 base64 chars.
|
||||
-- Минимум 140 (12 слов с маленькими словами), максимум 512 запас на 24 слова + padding.
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
@@ -56,11 +45,10 @@ BEGIN
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ── WALLETS ──────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS wallets (
|
||||
id VARCHAR(26) PRIMARY KEY, -- ULID
|
||||
id VARCHAR(26) PRIMARY KEY,
|
||||
user_id VARCHAR(26) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
chain VARCHAR(16) NOT NULL, -- ETH / BTC / SOL / TRX / BSC
|
||||
chain VARCHAR(16) NOT NULL,
|
||||
address VARCHAR(256) NOT NULL,
|
||||
derivation_path VARCHAR(64) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
@@ -69,7 +57,6 @@ CREATE TABLE IF NOT EXISTS wallets (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_wallets_user_id ON wallets(user_id);
|
||||
|
||||
-- ── SESSIONS (placeholder, не используется в текущей версии) ─────────────
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id VARCHAR(26) PRIMARY KEY,
|
||||
user_id VARCHAR(26) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
@@ -11,10 +11,7 @@ services:
|
||||
- .env
|
||||
environment:
|
||||
API_PORT: "3001"
|
||||
# Внешняя БД (72.56.9.76) — postgres-сервис не нужен.
|
||||
# DB-creds + master-key читаются из Vault через AppRole.
|
||||
volumes:
|
||||
# Audit-log: mnemonic reveal / wallet create / send — для compliance/forensics
|
||||
- ./logs:/app/logs
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost:3001/api/health"]
|
||||
|
||||
19
start.sh
19
start.sh
@@ -3,15 +3,9 @@ set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "=========================================="
|
||||
echo " CryptoWallet API — Docker Deploy"
|
||||
echo "=========================================="
|
||||
|
||||
# 1. Docker check
|
||||
command -v docker >/dev/null 2>&1 || { echo "[ERROR] Docker not installed"; exit 1; }
|
||||
docker compose version >/dev/null 2>&1 || { echo "[ERROR] docker compose plugin missing"; exit 1; }
|
||||
|
||||
# 2. .env check
|
||||
if [ ! -f .env ]; then
|
||||
if [ -f .env.example ]; then
|
||||
cp .env.example .env
|
||||
@@ -23,11 +17,9 @@ if [ ! -f .env ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Build & start
|
||||
echo "[INFO] Building and starting containers..."
|
||||
docker compose up -d --build
|
||||
|
||||
# 4. Wait healthy
|
||||
echo "[INFO] Waiting for API to become healthy..."
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://localhost:3001/api/health >/dev/null 2>&1; then
|
||||
@@ -43,10 +35,7 @@ for i in $(seq 1 30); do
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Up!"
|
||||
echo " API: http://localhost:3001"
|
||||
echo " Health: http://localhost:3001/api/health"
|
||||
echo " Docs: http://localhost:3001/api/docs"
|
||||
echo " Logs: docker compose logs -f api"
|
||||
echo "=========================================="
|
||||
echo "API: http://localhost:3001"
|
||||
echo "Health: http://localhost:3001/api/health"
|
||||
echo "Docs: http://localhost:3001/api/docs"
|
||||
echo "Logs: docker compose logs -f api"
|
||||
|
||||
Reference in New Issue
Block a user