deploy: POST /api/wallets + full swagger

This commit is contained in:
ZOMBIIIIIII
2026-05-03 20:01:58 +03:00
parent 59a7d1d9ca
commit 295c3a9d6d
27 changed files with 1994 additions and 430 deletions

View File

@@ -11,16 +11,18 @@ export function csrfMiddleware(req: Request, res: Response, next: NextFunction):
return;
}
// CSRF отключён если VAULT_CSRF_PATH не задан
// Если CSRF полностью отключён через ENV (csrfPath пустой) — пропускаем.
// Это явная конфигурация, не fail-open.
if (!env.vault.csrfPath) {
next();
return;
}
// Секрет не загрузился (Vault недоступен) — пропускаем чтобы не блокировать сервис
// CSRF включён, но секрет не загружен (Vault недоступен) → fail-secure: 503.
// НИКОГДА не пропускаем mutating запросы при не-валидном состоянии.
if (!isCsrfConfigured()) {
logger.warn('CSRF check skipped: secret not loaded');
next();
logger.error('CSRF check unavailable: secret not loaded — rejecting mutating request');
res.status(503).json({ success: false, error: 'CSRF protection unavailable, retry later' });
return;
}

View File

@@ -1,7 +1,39 @@
import { Request, Response, NextFunction } from 'express';
import { logger } from '../lib/logger';
export function errorHandler(err: Error, _req: Request, res: Response, _next: NextFunction): void {
logger.error(err.message);
interface HttpError extends Error {
status?: number;
statusCode?: number;
type?: string;
}
/**
* Generic error handler. Sanitization of err.message происходит в logger.
* Клиенту НИКОГДА не отдаём raw err.message (может содержать sensitive data).
*/
export function errorHandler(err: HttpError, _req: Request, res: Response, _next: NextFunction): void {
const status = err.status || err.statusCode || 500;
// Standard Express body-parser errors
if (err.type === 'entity.too.large') {
logger.warn(`Payload too large: ${err.message}`);
res.status(413).json({ success: false, error: 'Payload too large' });
return;
}
if (err.type === 'entity.parse.failed') {
logger.warn(`Invalid JSON: ${err.message}`);
res.status(400).json({ success: false, error: 'Invalid JSON' });
return;
}
// Известные клиентские ошибки (4xx) — отдаём safe сообщение
if (status >= 400 && status < 500) {
logger.warn(`Client error ${status}: ${err.message}`);
res.status(status).json({ success: false, error: 'Bad request' });
return;
}
// Серверные ошибки (5xx) — generic message, детали только в логи
logger.error(`Server error: ${err.message}`);
res.status(500).json({ success: false, error: 'Internal server error' });
}

View File

@@ -0,0 +1,42 @@
import rateLimit, { ipKeyGenerator } from 'express-rate-limit';
import { Request } from 'express';
/**
* Per-user rate limiting (если auth есть, то по userId; иначе по IP).
* Защищает от brute force / DoS / API quota exhaustion.
*/
function keyByUserOrIp(req: Request, res: any): string {
if (req.auth?.userId) return `user:${req.auth.userId}`;
// ipKeyGenerator корректно нормализует IPv6 (по /64 префиксу)
return ipKeyGenerator(req.ip || '');
}
// Глобальный лимит на любые API запросы — защита от мусорного трафика
export const globalLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 120,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: keyByUserOrIp,
message: { success: false, error: 'Too many requests' },
});
// Жёсткий лимит на mutating операции с балансами/wallet
export const mutateLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 30,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: keyByUserOrIp,
message: { success: false, error: 'Too many mutating requests' },
});
// Самый строгий — для send / vault PUT (anti-abuse / spam tx prevention)
export const sensitiveLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 10,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: keyByUserOrIp,
message: { success: false, error: 'Too many sensitive requests' },
});