94 lines
4.0 KiB
JavaScript
94 lines
4.0 KiB
JavaScript
// Тестовая страница встраивания в 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 || "";
|
|
// Разрешённые origin CRM: встраивание (frame-ancestors) и приём postMessage.
|
|
const CRM_ORIGINS = (process.env.CRM_ORIGINS || "https://shotam.tvorymo.com,https://units.tvorymo.com")
|
|
.split(",").map((s) => s.trim().replace(/\/$/, "")).filter(Boolean);
|
|
|
|
const INDEX = fs.readFileSync(path.join(__dirname, "public", "index.html"), "utf8")
|
|
.replace("__CRM_ORIGINS__", JSON.stringify(CRM_ORIGINS).replace(/</g, "\\u003c"));
|
|
|
|
const BASE_HEADERS = {
|
|
"Content-Security-Policy": `frame-ancestors ${CRM_ORIGINS.join(" ")}`,
|
|
"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}`));
|