From 8d91dbeb146dbc1d8c5f118533ba7f289212094a Mon Sep 17 00:00:00 2001 From: ZOMBIIIIIII <120676065+Metaton241@users.noreply.github.com> Date: Mon, 11 May 2026 18:36:44 +0300 Subject: [PATCH] remove /api/vault endpoints --- Dockerfile | 9 --- apps/api/.eslintrc.json | 20 ------ apps/api/src/app.ts | 2 - apps/api/src/controllers/vault.controller.ts | 70 -------------------- apps/api/src/models/user.model.ts | 17 ----- apps/api/src/routes/vault.routes.ts | 9 --- apps/api/swagger.json | 45 ------------- cryptowallet-schema.sql | 29 +++----- docker-compose.yml | 3 - start.sh | 19 ++---- 10 files changed, 12 insertions(+), 211 deletions(-) delete mode 100644 apps/api/.eslintrc.json delete mode 100644 apps/api/src/controllers/vault.controller.ts delete mode 100644 apps/api/src/routes/vault.routes.ts diff --git a/Dockerfile b/Dockerfile index c62e173..692edc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ diff --git a/apps/api/.eslintrc.json b/apps/api/.eslintrc.json deleted file mode 100644 index 9223e0a..0000000 --- a/apps/api/.eslintrc.json +++ /dev/null @@ -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/"] -} diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 3046c66..79e6e55 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -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); diff --git a/apps/api/src/controllers/vault.controller.ts b/apps/api/src/controllers/vault.controller.ts deleted file mode 100644 index 7a2ddce..0000000 --- a/apps/api/src/controllers/vault.controller.ts +++ /dev/null @@ -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' }); - } - }, -}; diff --git a/apps/api/src/models/user.model.ts b/apps/api/src/models/user.model.ts index 175ae22..69a61d7 100644 --- a/apps/api/src/models/user.model.ts +++ b/apps/api/src/models/user.model.ts @@ -61,23 +61,6 @@ export const UserModel = { return user; }, - async setVault(id: string, encryptedVault: string, vaultSalt: string): Promise { - 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, diff --git a/apps/api/src/routes/vault.routes.ts b/apps/api/src/routes/vault.routes.ts deleted file mode 100644 index df3481a..0000000 --- a/apps/api/src/routes/vault.routes.ts +++ /dev/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; diff --git a/apps/api/swagger.json b/apps/api/swagger.json index 4813c93..04f3143 100644 --- a/apps/api/swagger.json +++ b/apps/api/swagger.json @@ -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": { diff --git a/cryptowallet-schema.sql b/cryptowallet-schema.sql index def3f5e..c87aa8b 100644 --- a/cryptowallet-schema.sql +++ b/cryptowallet-schema.sql @@ -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, diff --git a/docker-compose.yml b/docker-compose.yml index 0bba249..aa2a7e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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"] diff --git a/start.sh b/start.sh index 7424c6a..b4b4b93 100644 --- a/start.sh +++ b/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"