import { NextRequest, NextResponse } from "next/server"; export const runtime = "nodejs"; const MAX_LENGTHS = { name: 100, contact: 150, message: 2000, }; // --- Rate limiting (in-memory, per IP) --- // Простой sliding-window лимитер без внешних зависимостей. // Для продакшена с несколькими инстансами замените на Redis/внешний стор. const RATE_LIMIT_WINDOW_MS = 10 * 60 * 1000; // 10 минут const RATE_LIMIT_MAX = 5; // максимум 5 запросов за окно const rateStore = new Map(); function getClientIp(req: NextRequest): string { const forwarded = req.headers.get("x-forwarded-for"); if (forwarded) { return forwarded.split(",")[0].trim(); } return req.headers.get("x-real-ip") ?? "unknown"; } function isRateLimited(ip: string): boolean { const now = Date.now(); const entry = rateStore.get(ip); if (!entry || now - entry.windowStart >= RATE_LIMIT_WINDOW_MS) { rateStore.set(ip, { count: 1, windowStart: now }); return false; } entry.count += 1; return entry.count > RATE_LIMIT_MAX; } // Периодическая очистка устаревших записей, чтобы Map не рос бесконечно. setInterval(() => { const now = Date.now(); for (const [ip, entry] of rateStore) { if (now - entry.windowStart >= RATE_LIMIT_WINDOW_MS) { rateStore.delete(ip); } } }, RATE_LIMIT_WINDOW_MS).unref?.(); // Убираем HTML-теги и обрезаем длину, чтобы нельзя было сломать // форматирование сообщения в Telegram или протащить лишний код. function sanitize(value: unknown, maxLength: number): string { if (typeof value !== "string") return ""; return value.replace(/<[^>]*>/g, "").trim().slice(0, maxLength); } // Экранируем спецсимволы Telegram MarkdownV2. function escapeMarkdown(text: string): string { return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, (m) => `\\${m}`); } export async function POST(req: NextRequest) { const ip = getClientIp(req); if (isRateLimited(ip)) { console.warn(`Rate limit exceeded for ${ip}`); return NextResponse.json( { ok: false, error: "Слишком много запросов. Попробуйте позже" }, { status: 429 } ); } const token = process.env.TELEGRAM_BOT_TOKEN; const chatId = process.env.TELEGRAM_CHAT_ID; if (!token || !chatId) { console.error("TELEGRAM_BOT_TOKEN или TELEGRAM_CHAT_ID не заданы"); return NextResponse.json( { ok: false, error: "Сервис временно недоступен, попробуйте позже" }, { status: 500 } ); } let body: unknown; try { body = await req.json(); } catch { return NextResponse.json( { ok: false, error: "Некорректный запрос" }, { status: 400 } ); } const { name, contact, message } = (body ?? {}) as Record; const cleanName = sanitize(name, MAX_LENGTHS.name); const cleanContact = sanitize(contact, MAX_LENGTHS.contact); const cleanMessage = sanitize(message, MAX_LENGTHS.message); if (!cleanName || !cleanContact || !cleanMessage) { return NextResponse.json( { ok: false, error: "Заполните имя, контакт и сообщение" }, { status: 400 } ); } const text = [ "*Новая заявка с сайта LabNetHub*", "", `*Имя:* ${escapeMarkdown(cleanName)}`, `*Контакт:* ${escapeMarkdown(cleanContact)}`, `*Сообщение:*`, escapeMarkdown(cleanMessage), ].join("\n"); try { const telegramRes = await fetch( `https://api.telegram.org/bot${token}/sendMessage`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chat_id: chatId, text, parse_mode: "MarkdownV2", }), } ); if (!telegramRes.ok) { const details = await telegramRes.text(); console.error("Telegram API error:", details); return NextResponse.json( { ok: false, error: "Не удалось отправить сообщение" }, { status: 502 } ); } return NextResponse.json({ ok: true }); } catch (err) { console.error("Ошибка отправки в Telegram:", err); return NextResponse.json( { ok: false, error: "Не удалось отправить сообщение" }, { status: 500 } ); } }