deploy: POST /api/wallets + full swagger
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
42
apps/api/src/middleware/rate-limit.ts
Normal file
42
apps/api/src/middleware/rate-limit.ts
Normal 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' },
|
||||
});
|
||||
Reference in New Issue
Block a user