265 lines
13 KiB
HTML
265 lines
13 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>CRM embed test</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<!-- Стили CRM Tvorymo: сначала дизайн-система, затем стили самой CRM (так требует шапка crm.css). -->
|
||
<link rel="stylesheet" href="https://test.units.tvorymo.com/tvorymo-crm/css/tvorymo-ui.css">
|
||
<link rel="stylesheet" href="https://test.units.tvorymo.com/tvorymo-crm/css/crm.css">
|
||
</head>
|
||
<body class="side-off">
|
||
<main id="main">
|
||
<div class="page">
|
||
|
||
<div class="head">
|
||
<div>
|
||
<h1>Тест встраивания в CRM</h1>
|
||
<div class="sub">Оформлено стилями CRM: <b>tvorymo-ui.css</b> + <b>crm.css</b></div>
|
||
</div>
|
||
<span class="chip"><span class="syncdot off" id="dot"></span><span id="mode">—</span></span>
|
||
</div>
|
||
|
||
<div class="wsinfo">
|
||
<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16h.01"/></svg>
|
||
<div class="grow">
|
||
<span id="status" class="chip gold">жду авторизацию от CRM</span>
|
||
<div class="small mt-6">Страница получает сессию CRM через <span class="code">postMessage</span> и проверяет токен в Supabase на сервере.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sec"><h2>Пользователь CRM <b id="count">0</b></h2><span class="small mute">токенов получено</span></div>
|
||
<div id="info" hidden>
|
||
<div class="pcard">
|
||
<div class="t1">
|
||
<span class="mono s44" id="initials">?</span>
|
||
<div class="min-w-0">
|
||
<div class="ttl" id="name"></div>
|
||
<div class="ds" id="email"></div>
|
||
</div>
|
||
<div class="n"><b id="mins">—</b><small>мин до exp</small></div>
|
||
</div>
|
||
<div class="mt">
|
||
<span class="chip fill">Supabase ✓</span>
|
||
<span class="chip gold">aud: authenticated</span>
|
||
<span class="chip" id="changed">токен первый</span>
|
||
</div>
|
||
<div>
|
||
<div class="mrow"><div class="t">ID<small class="code" id="uid"></small></div></div>
|
||
<div class="mrow"><div class="t">Origin отправителя<small id="origin"></small></div></div>
|
||
<div class="mrow"><div class="t">expires_at<small id="expires"></small></div></div>
|
||
</div>
|
||
<div class="row wrap">
|
||
<button class="btn gold" id="refresh" hidden>
|
||
<svg class="ic" viewBox="0 0 24 24"><path d="M20 11a8 8 0 1 0-2.3 5.7M20 4v7h-7"/></svg>
|
||
Обновить токен
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="empty" id="noinfo">Пока нет данных. Откройте страницу из боковой панели CRM — она пришлёт <b>crm:auth</b>.</div>
|
||
|
||
<div class="sec"><h2>Журнал</h2></div>
|
||
<div id="log"><div class="empty">событий нет</div></div>
|
||
|
||
<hr>
|
||
|
||
<div class="sec"><h2>Демо компонентов CRM</h2><span class="small mute">не функциональны</span></div>
|
||
|
||
<div class="stat">
|
||
<div><b>12</b><small>задач</small></div>
|
||
<div><b class="gold">5</b><small>в работе</small></div>
|
||
<div><b class="danger">2</b><small>просрочено</small></div>
|
||
<div><b class="ok">7</b><small>готово</small></div>
|
||
</div>
|
||
|
||
<div class="f"><label>Кнопки</label></div>
|
||
<div class="row wrap mt-6">
|
||
<button class="btn gold">Главная</button>
|
||
<button class="btn">Обычная</button>
|
||
<button class="btn ghost">Ghost</button>
|
||
<button class="btn dng">Удалить</button>
|
||
<button class="btn sm">Маленькая</button>
|
||
<button class="btn" disabled>Disabled</button>
|
||
<button class="icon-btn" title="Настройки"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19 12h2M3 12h2M12 3v2M12 19v2M5.6 5.6l1.4 1.4M17 17l1.4 1.4M5.6 18.4 7 17M17 7l1.4-1.4"/></svg></button>
|
||
<button class="planbtn on"><svg class="ic" viewBox="0 0 24 24"><path d="M5 12l5 5 9-10"/></svg>В плане</button>
|
||
<button class="timebtn est">~1ч 30м</button>
|
||
</div>
|
||
|
||
<div class="f"><label>Бейджи и теги</label></div>
|
||
<div class="row wrap mt-6">
|
||
<span class="chip">chip</span>
|
||
<span class="chip gold">gold</span>
|
||
<span class="chip dng">dng</span>
|
||
<span class="chip fill">fill</span>
|
||
<span class="chip"><span class="dot" style="background:#7DD3FC"></span>с точкой</span>
|
||
<span class="tag" style="background:rgba(74,222,128,.16);color:#4ADE80"><span class="d" style="background:#4ADE80"></span>Frontend</span>
|
||
<span class="tag" style="background:rgba(255,90,78,.16);color:#FF5A4E"><span class="d" style="background:#FF5A4E"></span>Срочно</span>
|
||
</div>
|
||
|
||
<div class="f"><label>Переключатели</label></div>
|
||
<div class="row wrap mt-6 gap-14">
|
||
<div class="seg"><button class="on">Мои</button><button>Все</button><button>Архив</button></div>
|
||
<button class="switch on" role="switch" aria-checked="true"></button>
|
||
<span class="cbx on"><svg class="ic" viewBox="0 0 24 24"><path d="M5 12l5 5 9-10"/></svg></span>
|
||
<span class="cbx"><svg class="ic" viewBox="0 0 24 24"><path d="M5 12l5 5 9-10"/></svg></span>
|
||
</div>
|
||
|
||
<div class="f2">
|
||
<div class="f"><label>Поле</label><div class="in"><input placeholder="Название задачи" value="Проверить встраивание"></div></div>
|
||
<div class="f"><label>Список</label><div class="in"><select><option>Tvorymo shotam</option><option>Другой проект</option></select></div></div>
|
||
</div>
|
||
<div class="qa mt-14">
|
||
<input placeholder="Быстро добавить задачу…">
|
||
<button class="go ok"><svg class="ic" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></button>
|
||
</div>
|
||
|
||
<div class="sec"><h2>Канбан</h2></div>
|
||
<div class="board">
|
||
<div class="col">
|
||
<div class="ch"><h2>В работе</h2><span class="n">2</span><span class="sum">3ч</span></div>
|
||
<div class="cards">
|
||
<div class="tcard plan">
|
||
<div class="t">Передавати авторизацію CRM сайтам в iframe через postMessage</div>
|
||
<div class="m"><span class="chip gold">Tvorymo shotam</span><span>сегодня</span></div>
|
||
<div class="sb"><div class="pbar"><i style="width:66%"></i></div><span class="cnt">2/3</span></div>
|
||
<div class="ft">
|
||
<span class="by"><i class="av" style="background:#FFCA40">О</i></span>
|
||
<span class="to"><i class="av" style="background:#7DD3FC">М</i><i class="av" style="background:#4ADE80">Л</i></span>
|
||
</div>
|
||
</div>
|
||
<div class="tcard over-due">
|
||
<div class="t">Подключить стили CRM на тестовой странице</div>
|
||
<div class="m"><span class="ov">просрочено</span><span class="chip dng">блокирует</span></div>
|
||
</div>
|
||
</div>
|
||
<button class="add">+ Задача</button>
|
||
</div>
|
||
<div class="col">
|
||
<div class="ch"><h2>Готово</h2><span class="n">1</span></div>
|
||
<div class="cards">
|
||
<div class="tcard done">
|
||
<div class="t">Задеплоить test-crm.tes.gd</div>
|
||
<div class="sb"><div class="pbar"><i style="width:100%"></i></div><span class="cnt full">3/3</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</main>
|
||
<script>
|
||
(() => {
|
||
// Сессия только в памяти, токен не выводится и не логируется.
|
||
let session = null; // { token, expiresAt }
|
||
let crmSource = null, crmOrigin = null, count = 0, logged = false;
|
||
const $ = (id) => document.getElementById(id);
|
||
const inIframe = window.parent !== window;
|
||
|
||
// kind: "wait" | "ok" | "err"
|
||
function setStatus(text, kind) {
|
||
const s = $("status");
|
||
s.textContent = text;
|
||
s.className = "chip " + (kind === "ok" ? "fill" : kind === "err" ? "dng" : "gold");
|
||
$("dot").className = "syncdot" + (kind === "ok" ? "" : " off");
|
||
}
|
||
function log(text) {
|
||
if (!logged) { $("log").textContent = ""; logged = true; }
|
||
const row = document.createElement("div");
|
||
row.className = "arcitem";
|
||
const s = document.createElement("span"); s.className = "s";
|
||
s.textContent = new Date().toLocaleTimeString().slice(0, 5);
|
||
const t = document.createElement("span"); t.className = "t"; t.textContent = text;
|
||
row.append(s, t);
|
||
$("log").prepend(row);
|
||
}
|
||
function toDate(v) {
|
||
if (v == null) return null;
|
||
const n = Number(v);
|
||
const d = new Date(Number.isFinite(n) ? (n < 1e12 ? n * 1000 : n) : v);
|
||
return isNaN(d) ? null : d;
|
||
}
|
||
function fmtTime(v) {
|
||
const d = toDate(v);
|
||
return d ? d.toLocaleString() + " (" + d.toISOString() + ")" : (v == null ? "—" : String(v));
|
||
}
|
||
function initials(s) {
|
||
return (s || "?").split(/[\s@._-]+/).filter(Boolean).slice(0, 2).map((w) => w[0]).join("").toUpperCase() || "?";
|
||
}
|
||
|
||
async function onAuth(data, ev) {
|
||
crmSource = ev.source; crmOrigin = ev.origin;
|
||
$("origin").textContent = ev.origin;
|
||
const token = data.access_token;
|
||
if (typeof token !== "string" || !token) { setStatus("crm:auth без access_token", "err"); return; }
|
||
const changed = session && session.token !== token;
|
||
const prevExp = session && session.expiresAt;
|
||
session = { token, expiresAt: data.expires_at };
|
||
count++;
|
||
setStatus("проверяю токен в Supabase…", "wait");
|
||
try {
|
||
const r = await fetch("/api/verify", {
|
||
method: "POST", headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ access_token: token }),
|
||
});
|
||
const res = await r.json();
|
||
if (!res.ok) { setStatus("Токен не принят: " + res.error, "err"); log("crm:auth от " + ev.origin + " — отклонён"); return; }
|
||
$("name").textContent = res.name || "имя не задано в Supabase";
|
||
$("email").textContent = res.email || "—";
|
||
$("initials").textContent = initials(res.name || res.email);
|
||
$("uid").textContent = res.id;
|
||
const exp = data.expires_at != null ? data.expires_at : res.exp;
|
||
$("expires").textContent = fmtTime(exp);
|
||
const d = toDate(exp);
|
||
$("mins").textContent = d ? String(Math.max(0, Math.round((d - Date.now()) / 60000))) : "—";
|
||
$("count").textContent = String(count);
|
||
$("changed").textContent = changed ? "токен сменился" : count > 1 ? "тот же токен" : "токен первый";
|
||
$("changed").className = changed ? "chip fill" : "chip";
|
||
$("info").hidden = false; $("noinfo").hidden = true; $("refresh").hidden = false;
|
||
setStatus(changed ? "Токен сменился, пользователь подтверждён Supabase" : "Пользователь подтверждён Supabase", "ok");
|
||
log("crm:auth от " + ev.origin + (changed
|
||
? " — новый токен, expires_at: " + fmtTime(prevExp) + " → " + fmtTime(data.expires_at)
|
||
: count > 1 ? " — тот же токен" : " — принят"));
|
||
} catch (e) {
|
||
setStatus("Ошибка проверки токена", "err");
|
||
}
|
||
}
|
||
|
||
window.addEventListener("message", (ev) => {
|
||
const data = ev.data;
|
||
if (!data || typeof data !== "object") return;
|
||
// Для теста принимаем от любого origin (решение владельца).
|
||
if (data.type === "crm:auth") onAuth(data, ev);
|
||
else if (data.type === "crm:theme") onTheme(data);
|
||
});
|
||
|
||
// Тема и палитра CRM: дальше всё делают селекторы tvorymo-ui.css ([data-theme][data-pal]).
|
||
function onTheme(data) {
|
||
const theme = data.theme === "dark" ? "dark" : "light";
|
||
const pal = typeof data.palette === "string" && data.palette ? data.palette : "classic";
|
||
document.documentElement.dataset.theme = theme;
|
||
document.documentElement.dataset.pal = pal;
|
||
log("crm:theme " + theme + "/" + pal);
|
||
}
|
||
|
||
$("refresh").addEventListener("click", () => {
|
||
const target = crmSource || window.parent;
|
||
target.postMessage({ type: "crm:refresh" }, crmOrigin || "*");
|
||
setStatus("запросил новый токен (crm:refresh)…", "wait");
|
||
log("отправлен crm:refresh → " + (crmOrigin || "*"));
|
||
});
|
||
|
||
$("mode").textContent = inIframe ? "в iframe" : "без iframe";
|
||
if (inIframe) {
|
||
window.parent.postMessage({ type: "crm:ready", version: 1 }, "*");
|
||
log("отправлен crm:ready");
|
||
} else {
|
||
log("страница открыта не в iframe");
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|