// Тестовая страница встраивания в CRM Tvorymo. // Токены не логируются; сервер ничего не хранит, кроме конфигурации. const http = require("http"); const fs = require("fs"); const path = require("path"); const PORT = Number(process.env.PORT || 3000); const SUPABASE_URL = (process.env.SUPABASE_URL || "").replace(/\/$/, ""); const SUPABASE_KEY = process.env.SUPABASE_PUBLISHABLE_KEY || ""; const INDEX = fs.readFileSync(path.join(__dirname, "public", "index.html")); const BASE_HEADERS = { "Content-Security-Policy": "frame-ancestors *", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", }; function send(res, status, body, type) { res.writeHead(status, { ...BASE_HEADERS, "Content-Type": type }); res.end(body); } const json = (res, status, obj) => send(res, status, JSON.stringify(obj), "application/json; charset=utf-8"); function decodeJwtPayload(token) { const parts = token.split("."); if (parts.length !== 3) throw new Error("not a JWT"); return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")); } function readBody(req, limit = 16384) { return new Promise((resolve, reject) => { let size = 0; const chunks = []; req.on("data", (c) => { size += c.length; if (size > limit) { reject(new Error("too large")); req.destroy(); return; } chunks.push(c); }); req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); req.on("error", reject); }); } async function verify(req, res) { let token; try { token = JSON.parse(await readBody(req)).access_token; } catch { return json(res, 400, { ok: false, error: "некорректный запрос" }); } if (typeof token !== "string" || !token) return json(res, 400, { ok: false, error: "нет access_token" }); let claims; try { claims = decodeJwtPayload(token); } catch { return json(res, 401, { ok: false, error: "токен не является JWT" }); } const now = Math.floor(Date.now() / 1000); if (typeof claims.exp !== "number" || claims.exp <= now) return json(res, 401, { ok: false, error: "токен истёк (exp)" }); if (claims.aud !== "authenticated") return json(res, 401, { ok: false, error: "aud ≠ authenticated" }); let r; try { r = await fetch(`${SUPABASE_URL}/auth/v1/user`, { headers: { apikey: SUPABASE_KEY, Authorization: `Bearer ${token}` }, }); } catch { return json(res, 502, { ok: false, error: "Supabase недоступен" }); } if (!r.ok) return json(res, 401, { ok: false, error: `Supabase отклонил токен (HTTP ${r.status})` }); const user = await r.json(); if (user.aud !== "authenticated") return json(res, 401, { ok: false, error: "aud пользователя ≠ authenticated" }); const md = user.user_metadata || {}; const name = md.name || md.full_name || md.display_name || null; json(res, 200, { ok: true, id: user.id, email: user.email || null, name, exp: claims.exp }); } http.createServer((req, res) => { const url = new URL(req.url, "http://localhost"); if ((req.method === "GET" || req.method === "HEAD") && (url.pathname === "/" || url.pathname === "/index.html")) return send(res, 200, INDEX, "text/html; charset=utf-8"); if (req.method === "GET" && url.pathname === "/healthz") return send(res, 200, "ok", "text/plain"); if (req.method === "POST" && url.pathname === "/api/verify") return verify(req, res).catch(() => json(res, 500, { ok: false, error: "ошибка сервера" })); send(res, 404, "not found", "text/plain"); }).listen(PORT, () => console.log(`listening on ${PORT}`));