108 lines
4.9 KiB
TypeScript
108 lines
4.9 KiB
TypeScript
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;
|