const EXCLUDED_PATHS = [ '/api/auth/oauth/vk/callback', '/api/auth/oauth/yandex/callback', '/api/webhooks/yookassa', '/api/admin/notifications/telegram/webhook', ] export function normalizeIp(ip) { if (ip && ip.startsWith('::ffff:')) { return ip.slice(7) } return ip } export function ipToInt(ip) { const parts = ip.split('.') if (parts.length !== 4) return null return parts.reduce((acc, octet) => { const num = parseInt(octet, 10) if (isNaN(num) || num < 0 || num > 255) return null return acc !== null ? (acc << 8) + num : null }, 0) } export function cidrMatch(ip, cidr) { const slashIdx = cidr.indexOf('/') if (slashIdx === -1) return false const baseIp = cidr.slice(0, slashIdx) const prefix = parseInt(cidr.slice(slashIdx + 1), 10) if (isNaN(prefix) || prefix < 0 || prefix > 32) return false const ipInt = ipToInt(normalizeIp(ip)) const baseInt = ipToInt(normalizeIp(baseIp)) if (ipInt === null || baseInt === null) return false const mask = prefix === 0 ? 0 : ~(2 ** (32 - prefix) - 1) >>> 0 return (ipInt & mask) === (baseInt & mask) } export function build403Html(ip) { const safeIp = ip || 'не определён' return ` Любимый Креатив

Любимый Креатив

Изделия ручной работы: вещи с характером и вниманием к деталям

Сайт находится в разработке и скоро будет доступен

Ваш IP: ${safeIp}

` } export async function registerIpGate(fastify) { fastify.addHook('onRequest', async (request, reply) => { const allowed = process.env.SITE_ACCESS_IPS if (!allowed) return const allowedIps = allowed .split(',') .map((s) => normalizeIp(s.trim())) .filter(Boolean) if (allowedIps.length === 0) return const urlPath = request.url.split('?')[0] if (EXCLUDED_PATHS.includes(urlPath)) return const normalizedIp = normalizeIp(request.ip) if (allowedIps.includes(normalizedIp)) return const isInCidr = allowedIps.some((entry) => cidrMatch(normalizedIp, entry)) if (isInCidr) return return reply.code(403).type('text/html').send(build403Html(request.ip)) }) }