diff --git a/proxy/server.js b/proxy/server.js new file mode 100644 index 0000000..0fab46c --- /dev/null +++ b/proxy/server.js @@ -0,0 +1,74 @@ +import http from 'node:http'; +import httpProxy from 'http-proxy'; + +const PORT = Number(process.env.PORT || 8080); +const TARGET = process.env.SUPABASE_TARGET || 'https://cksuehzcimitsxmeloes.supabase.co'; +const allowedOrigins = new Set([ + 'https://app.caterium.ru', + 'https://caterium.ru', + 'https://www.caterium.ru' +]); + +const proxy = httpProxy.createProxyServer({ + target: TARGET, + changeOrigin: true, + xfwd: true, + secure: true, + ws: true +}); + +proxy.on('proxyReq', (proxyReq) => { + proxyReq.setHeader('host', new URL(TARGET).host); +}); + +proxy.on('error', (err, req, res) => { + console.error('[proxy:error]', err?.message || err); + if (res && !res.headersSent) { + res.writeHead(502, {'content-type':'application/json; charset=utf-8'}); + } + try { res?.end(JSON.stringify({error:'upstream_unavailable'})); } catch (_) {} +}); + +function cors(req, res) { + const origin = req.headers.origin; + if (origin && !allowedOrigins.has(origin)) return false; + if (origin) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } + res.setHeader('Access-Control-Allow-Credentials', 'true'); + res.setHeader('Access-Control-Allow-Headers', req.headers['access-control-request-headers'] || 'authorization,apikey,content-type,x-client-info,prefer,range'); + res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS'); + return true; +} + +const server = http.createServer((req, res) => { + if (!cors(req, res)) { + res.writeHead(403, {'content-type':'application/json; charset=utf-8'}); + return res.end(JSON.stringify({error:'origin_not_allowed'})); + } + if (req.method === 'OPTIONS') { + res.writeHead(204); + return res.end(); + } + if (req.url === '/health' || req.url === '/healthz') { + res.writeHead(200, {'content-type':'application/json; charset=utf-8'}); + return res.end(JSON.stringify({ok:true,target:'supabase'})); + } + if (!/^\/(rest|auth|storage|functions|realtime)\/v1(?:\/|\?|$)/.test(req.url || '')) { + res.writeHead(404, {'content-type':'application/json; charset=utf-8'}); + return res.end(JSON.stringify({error:'route_not_allowed'})); + } + proxy.web(req, res); +}); + +server.on('upgrade', (req, socket, head) => { + const origin = req.headers.origin; + if (origin && !allowedOrigins.has(origin)) return socket.destroy(); + if (!/^\/realtime\/v1(?:\/|\?|$)/.test(req.url || '')) return socket.destroy(); + proxy.ws(req, socket, head); +}); + +server.listen(PORT, '0.0.0.0', () => { + console.log(`[caterium-proxy] listening on ${PORT} -> ${TARGET}`); +});