import express from 'express'; import helmet from 'helmet'; import cors from 'cors'; import cookieParser from 'cookie-parser'; import swaggerUi from 'swagger-ui-express'; import { env } from './config/env'; import { swaggerSpec } from './config/swagger'; import { traceMiddleware } from './middleware/trace'; import { authMiddleware } from './middleware/auth'; import { csrfMiddleware } from './middleware/csrf'; import { globalLimiter, mutateLimiter, sensitiveLimiter, mnemonicRevealLimiter } from './middleware/rate-limit'; import { errorHandler } from './middleware/error-handler'; import { WalletController } from './controllers/wallet.controller'; import walletRoutes from './routes/wallet.routes'; import relayProxyRoutes from './routes/relay-proxy.routes'; import tronProxyRoutes from './routes/tron-proxy.routes'; import solSwapProxyRoutes from './routes/sol-swap-proxy.routes'; import tronSwapProxyRoutes from './routes/tron-swap-proxy.routes'; import btcProxyRoutes from './routes/btc-proxy.routes'; import bscSwapProxyRoutes from './routes/bsc-swap-proxy.routes'; const app = express(); // Trust proxy для корректного req.ip за reverse proxy / load balancer app.set('trust proxy', 1); app.use(helmet()); app.use( cors({ origin: env.cors.origins.length > 0 ? env.cors.origins : false, credentials: env.cors.allowCredentials, }), ); app.use(express.json({ limit: '64kb' })); // защита от больших payload-DoS app.use(cookieParser()); app.use(traceMiddleware); // ── PUBLIC endpoints ───────────────────────────────────────────────────────── // H11 — /api/health with DB probe (не возвращает OK если DB down) import { db } from './config/database'; app.get('/api/health', async (_req, res) => { try { await Promise.race([ db.raw('select 1'), new Promise((_, reject) => setTimeout(() => reject(new Error('db-timeout')), 1000)), ]); res.json({ success: true, data: { status: 'ok' } }); } catch (err: any) { res.status(503).json({ success: false, error: 'db_unavailable' }); } }); // ── Глобальный rate limit на /api/* — ДО docs чтобы не было unauthenticated DoS на swagger.json app.use('/api', globalLimiter); // H1 — Swagger gated. В production требуется basic-auth ИЛИ NODE_ENV != production. // JSON endpoint ОБЯЗАТЕЛЬНО до app.use('/api/docs', ...) — иначе swagger-ui-express // перехватывает все /api/docs/* и возвращает HTML вместо JSON. const docsGate = (req: express.Request, res: express.Response, next: express.NextFunction) => { if (process.env.NODE_ENV !== 'production' || process.env.SWAGGER_PUBLIC === 'true') { return next(); } // Production без SWAGGER_PUBLIC=true → require basic auth (operator credentials) const auth = req.headers.authorization || ''; const expected = process.env.SWAGGER_BASIC_AUTH; // "user:pass" if (!expected || !auth.startsWith('Basic ')) { res.set('WWW-Authenticate', 'Basic realm="docs"'); res.status(401).json({ success: false, error: 'Docs auth required' }); return; } const decoded = Buffer.from(auth.slice(6), 'base64').toString('utf8'); if (decoded !== expected) { res.set('WWW-Authenticate', 'Basic realm="docs"'); res.status(401).json({ success: false, error: 'Invalid docs credentials' }); return; } return next(); }; app.get('/api/docs/swagger.json', docsGate, (_req, res) => { res.json(swaggerSpec); }); app.use('/api/docs', docsGate, swaggerUi.serve, swaggerUi.setup(swaggerSpec)); // ── PROTECTED endpoints (JWT + CSRF) ───────────────────────────────────────── const protect = [authMiddleware, csrfMiddleware]; app.post('/api/wallets/create', sensitiveLimiter, WalletController.createWallet); app.use('/api/wallets/mnemonic/reveal', ...protect, mnemonicRevealLimiter); app.use('/api/wallets/:chain/send', ...protect, sensitiveLimiter); // Mutating (proxy + read endpoints) — повышенный лимит app.use('/api/wallets', ...protect, mutateLimiter, walletRoutes); app.use('/api/relay', ...protect, mutateLimiter, relayProxyRoutes); app.use('/api/tron', ...protect, mutateLimiter, tronProxyRoutes); app.use('/api/sol/swap', ...protect, mutateLimiter, solSwapProxyRoutes); app.use('/api/tron/swap', ...protect, mutateLimiter, tronSwapProxyRoutes); app.use('/api/btc', ...protect, mutateLimiter, btcProxyRoutes); app.use('/api/bsc/swap', ...protect, mutateLimiter, bscSwapProxyRoutes); // 404 для всего что не сматчилось выше — единый JSON-ответ, не express default text app.use((_req, res) => { res.status(404).json({ success: false, error: 'Not found' }); }); app.use(errorHandler); export default app;