Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
51 lines
1.9 KiB
TypeScript
51 lines
1.9 KiB
TypeScript
import { crmFetch } from '../../../../lib/flats';
|
|
|
|
// Заявка на просмотр со страницы /ru/apartments → CRM (клиент «BarevClub» и сообщение в группу AREV).
|
|
|
|
/** Не больше 5 заявок с одного адреса за 10 минут — от случайных повторов и ботов */
|
|
const hits = new Map<string, number[]>();
|
|
function limited(ip: string) {
|
|
const now = Date.now();
|
|
const recent = (hits.get(ip) ?? []).filter((t) => now - t < 10 * 60_000);
|
|
recent.push(now);
|
|
hits.set(ip, recent);
|
|
if (hits.size > 5000) hits.clear();
|
|
return recent.length > 5;
|
|
}
|
|
|
|
const json = (body: object, status = 200) => Response.json(body, { status });
|
|
|
|
export async function POST(req: Request) {
|
|
const ip = (req.headers.get('x-forwarded-for') ?? '').split(',')[0]!.trim() || 'unknown';
|
|
if (limited(ip)) return json({ code: 'TOO_MANY' }, 429);
|
|
let body: Record<string, unknown>;
|
|
try {
|
|
body = (await req.json()) as Record<string, unknown>;
|
|
} catch {
|
|
return json({ code: 'VALIDATION' }, 400);
|
|
}
|
|
// скрытое поле: его заполняют только боты
|
|
if (typeof body.website === 'string' && body.website) return json({ ok: true });
|
|
const str = (v: unknown) => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
|
|
const payload = {
|
|
leadId: Number(body.leadId),
|
|
day: str(body.day),
|
|
time: str(body.time),
|
|
name: str(body.name),
|
|
phone: str(body.phone),
|
|
telegram: str(body.telegram),
|
|
};
|
|
try {
|
|
const res = await crmFetch('/api/barev/requests', {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const data = (await res.json().catch(() => ({}))) as { code?: string };
|
|
if (!res.ok) return json({ code: data.code ?? 'FAILED' }, res.status === 401 ? 503 : res.status);
|
|
return json({ ok: true });
|
|
} catch {
|
|
return json({ code: 'FAILED' }, 502);
|
|
}
|
|
}
|