Files
cryptowallet/apps/api/src/app.ts
2026-05-14 02:14:45 +03:00

108 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;