CRM переехала на shotam.tvorymo.com: allowlist origin (CRM_ORIGINS) для postMessage и frame-ancestors, стили CRM от корня

This commit is contained in:
EugeneTes
2026-10-16 07:40:40 +00:00
parent f6b9f03ed7
commit ec2f094f48
5 changed files with 24 additions and 11 deletions

View File

@@ -9,7 +9,7 @@
- Деплой: Coolify на сервере storage — env `COOLIFY_URL`, `COOLIFY_KEY` (через профиль «Storage coolify»). - Деплой: Coolify на сервере storage — env `COOLIFY_URL`, `COOLIFY_KEY` (через профиль «Storage coolify»).
Деплоить через Coolify v4 REST API (скилл `deploying-to-coolify-via-api`), fire-and-forget. Деплоить через Coolify v4 REST API (скилл `deploying-to-coolify-via-api`), fire-and-forget.
- Домен страницы: `https://test-crm.tes.gd` (HTTPS обязателен; DNS и домен на том же сервере). - Домен страницы: `https://test-crm.tes.gd` (HTTPS обязателен; DNS и домен на том же сервере).
- CRM: `https://test.units.tvorymo.com`. Supabase: `https://yjxbkcqsquwbsyoonfkn.supabase.co`. - CRM: `https://shotam.tvorymo.com`, `https://units.tvorymo.com` (env `CRM_ORIGINS`). CSS/JS CRM — от корня: `/css/crm.css`, `/css/tvorymo-ui.css`, `/js/pagebridge.js`. Supabase: `https://yjxbkcqsquwbsyoonfkn.supabase.co`.
- Publishable-ключ Supabase пришлёт Лана (секретарь владельца) напрямую — запиши его сюда, в раздел ниже (ключ публичный по дизайну). - Publishable-ключ Supabase пришлёт Лана (секретарь владельца) напрямую — запиши его сюда, в раздел ниже (ключ публичный по дизайну).
## Supabase publishable key ## Supabase publishable key

View File

@@ -5,8 +5,13 @@
Supabase `GET /auth/v1/user` (+ `exp`, `aud = authenticated`) и показывает имя/email/id. Supabase `GET /auth/v1/user` (+ `exp`, `aud = authenticated`) и показывает имя/email/id.
- Node 22, без зависимостей: `server.js` + `public/index.html`. - Node 22, без зависимостей: `server.js` + `public/index.html`.
- Заголовок `Content-Security-Policy: frame-ancestors *`, без `X-Frame-Options`. - Разрешённые origin CRM — env `CRM_ORIGINS` (через запятую; по умолчанию
- Env: `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY`, `PORT` (по умолчанию 3000). `https://shotam.tvorymo.com,https://units.tvorymo.com`). Используются в
`Content-Security-Policy: frame-ancestors …` (без `X-Frame-Options`) и как allowlist для `postMessage`:
сообщения от других origin игнорируются (пишется в журнал).
- Стили CRM берутся с `https://shotam.tvorymo.com/css/tvorymo-ui.css` и `/css/crm.css` (от корня;
старые пути `/tvorymo-crm/css/...` дают 404).
- Env: `SUPABASE_URL`, `SUPABASE_PUBLISHABLE_KEY`, `CRM_ORIGINS`, `PORT` (по умолчанию 3000).
- Деплой: Coolify (Dockerfile), домен `https://test-crm.tes.gd`. - Деплой: Coolify (Dockerfile), домен `https://test-crm.tes.gd`.
Подробности — `SPEC.md`. Подробности — `SPEC.md`.

View File

@@ -12,13 +12,13 @@
## Адреса ## Адреса
- Страница: `https://test-crm.tes.gd` (HTTPS обязателен). - Страница: `https://test-crm.tes.gd` (HTTPS обязателен).
- CRM: `https://test.units.tvorymo.com`. - CRM: `https://shotam.tvorymo.com` и `https://units.tvorymo.com` (с 2026-10-16; `test.units.tvorymo.com` больше не используется — 403). Та же база Supabase.
- Supabase: `https://yjxbkcqsquwbsyoonfkn.supabase.co`. - Supabase: `https://yjxbkcqsquwbsyoonfkn.supabase.co`.
## Протокол (со стороны страницы) ## Протокол (со стороны страницы)
1. После загрузки: `window.parent.postMessage({ type: "crm:ready", version: 1 }, "*")`. 1. После загрузки: `window.parent.postMessage({ type: "crm:ready", version: 1 }, "*")`.
2. Принимать `crm:auth` `{ access_token, expires_at, user: { id, email, name } }` от любого origin (решение владельца для теста). Origin отправителя показывать на странице. 2. Принимать `crm:auth` `{ access_token, expires_at, user: { id, email, name } }` только от разрешённых origin CRM (env `CRM_ORIGINS`, решение владельца 2026-10-16). Origin отправителя показывать на странице.
3. `access_token` отправить на сервер страницы; сервер проверяет его через `GET /auth/v1/user` (заголовки `apikey: <publishable key>`, `Authorization: Bearer <token>`) и сверяет `exp`, `aud = "authenticated"`. 3. `access_token` отправить на сервер страницы; сервер проверяет его через `GET /auth/v1/user` (заголовки `apikey: <publishable key>`, `Authorization: Bearer <token>`) и сверяет `exp`, `aud = "authenticated"`.
4. Имя и email брать из ответа Supabase, не из `user` в сообщении (имя из `user_metadata`; если пусто, показать «имя не задано в Supabase» и email). 4. Имя и email брать из ответа Supabase, не из `user` в сообщении (имя из `user_metadata`; если пусто, показать «имя не задано в Supabase» и email).
5. Кнопка «Обновить токен»: отправляет `{ type: "crm:refresh" }`, CRM отвечает новым `crm:auth`; страница показывает, что токен сменился (время `expires_at`, без самого токена). 5. Кнопка «Обновить токен»: отправляет `{ type: "crm:refresh" }`, CRM отвечает новым `crm:auth`; страница показывает, что токен сменился (время `expires_at`, без самого токена).
@@ -26,7 +26,7 @@
## Требования ## Требования
- Заголовок `Content-Security-Policy: frame-ancestors *` (встраивание откуда угодно, для теста), без `X-Frame-Options`. - Заголовок `Content-Security-Policy: frame-ancestors <origin CRM>` (только разрешённые origin CRM), без `X-Frame-Options`.
- Токен не логировать ни в браузере, ни на сервере. Сессию держать в памяти. - Токен не логировать ни в браузере, ни на сервере. Сессию держать в памяти.
- Publishable-ключ Supabase агент получает от Ланы и записывает в свой CLAUDE.md (ключ публичный по дизайну). - Publishable-ключ Supabase агент получает от Ланы и записывает в свой CLAUDE.md (ключ публичный по дизайну).
- Без открытого токена в ответе страницы: показываем только имя, email, id, origin, `expires_at`. - Без открытого токена в ответе страницы: показываем только имя, email, id, origin, `expires_at`.

View File

@@ -7,8 +7,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Стили CRM Tvorymo: сначала дизайн-система, затем стили самой CRM (так требует шапка crm.css). --> <!-- Стили 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://shotam.tvorymo.com/css/tvorymo-ui.css">
<link rel="stylesheet" href="https://test.units.tvorymo.com/tvorymo-crm/css/crm.css"> <link rel="stylesheet" href="https://shotam.tvorymo.com/css/crm.css">
</head> </head>
<body class="side-off"> <body class="side-off">
<main id="main"> <main id="main">
@@ -151,6 +151,7 @@
</div> </div>
</main> </main>
<script> <script>
const CRM_ORIGINS = __CRM_ORIGINS__;
(() => { (() => {
// Сессия только в памяти, токен не выводится и не логируется. // Сессия только в памяти, токен не выводится и не логируется.
let session = null; // { token, expiresAt } let session = null; // { token, expiresAt }
@@ -230,7 +231,10 @@
window.addEventListener("message", (ev) => { window.addEventListener("message", (ev) => {
const data = ev.data; const data = ev.data;
if (!data || typeof data !== "object") return; if (!data || typeof data !== "object") return;
// Для теста принимаем от любого origin (решение владельца). if (!CRM_ORIGINS.includes(ev.origin)) {
if (data.type === "crm:auth" || data.type === "crm:theme") log(data.type + " от " + ev.origin + " — origin не разрешён, игнорирую");
return;
}
if (data.type === "crm:auth") onAuth(data, ev); if (data.type === "crm:auth") onAuth(data, ev);
else if (data.type === "crm:theme") onTheme(data); else if (data.type === "crm:theme") onTheme(data);
}); });

View File

@@ -7,11 +7,15 @@ const path = require("path");
const PORT = Number(process.env.PORT || 3000); const PORT = Number(process.env.PORT || 3000);
const SUPABASE_URL = (process.env.SUPABASE_URL || "").replace(/\/$/, ""); const SUPABASE_URL = (process.env.SUPABASE_URL || "").replace(/\/$/, "");
const SUPABASE_KEY = process.env.SUPABASE_PUBLISHABLE_KEY || ""; 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")); const INDEX = fs.readFileSync(path.join(__dirname, "public", "index.html"), "utf8")
.replace("__CRM_ORIGINS__", JSON.stringify(CRM_ORIGINS).replace(/</g, "\\u003c"));
const BASE_HEADERS = { const BASE_HEADERS = {
"Content-Security-Policy": "frame-ancestors *", "Content-Security-Policy": `frame-ancestors ${CRM_ORIGINS.join(" ")}`,
"Cache-Control": "no-store", "Cache-Control": "no-store",
"X-Content-Type-Options": "nosniff", "X-Content-Type-Options": "nosniff",
}; };