133 lines
3.5 KiB
JavaScript
133 lines
3.5 KiB
JavaScript
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 `<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Любимый Креатив</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #faf8f5;
|
|
color: #3d322b;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 100vh;
|
|
padding: 24px;
|
|
}
|
|
.card {
|
|
max-width: 520px;
|
|
width: 100%;
|
|
background: #fff;
|
|
border: 1px solid #e8e0d8;
|
|
border-radius: 16px;
|
|
padding: 48px 40px;
|
|
text-align: center;
|
|
box-shadow: 0 2px 16px rgb(0 0 0 / 4%);
|
|
}
|
|
.card h1 {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.3px;
|
|
color: #4a3a2e;
|
|
margin-bottom: 8px;
|
|
}
|
|
.card .tagline {
|
|
font-size: 14px;
|
|
color: #8c8177;
|
|
margin-bottom: 32px;
|
|
line-height: 1.5;
|
|
}
|
|
.card .status {
|
|
font-size: 16px;
|
|
color: #6b5e52;
|
|
margin-bottom: 24px;
|
|
line-height: 1.6;
|
|
}
|
|
.card .ip {
|
|
font-size: 12px;
|
|
color: #b8a99b;
|
|
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<h1>Любимый Креатив</h1>
|
|
<p class="tagline">Изделия ручной работы: вещи с характером и вниманием к деталям</p>
|
|
<p class="status">Сайт находится в разработке и скоро будет доступен</p>
|
|
<p class="ip">Ваш IP: ${safeIp}</p>
|
|
</div>
|
|
</body>
|
|
</html>`
|
|
}
|
|
|
|
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))
|
|
})
|
|
}
|