crm-wiki
Вики (база знаний) для CRM Tvorymo. Открывается в iframe из боковой панели CRM, пользователя узнаёт по токену CRM (postMessage crm:ready → crm:auth).
- Сайт: https://wiki-crm.tes.gd
- Спецификация: SPEC.md
- Стек: ASP.NET Core 10 (minimal API) + SQLite (FTS5), фронтенд Vite + React + TipTap.
- Данные:
/data/wiki.dbи/data/images/на постоянном томеwiki-data(Coolify).
Авторизация
Каждый запрос к /api/* (кроме /api/verify и /api/config) требует:
| Заголовок | Значение |
|---|---|
Authorization |
Bearer <access_token Supabase из CRM> |
X-Wiki-Workspace |
shared (по умолчанию), personal:<uid> или id пользовательского workspace CRM (например, wsmv5l7d425re160, как отдаёт crm workspaces list) |
Сервер проверяет токен: exp, aud = "authenticated" и GET <SUPABASE_URL>/auth/v1/user с publishable-ключом (результат кэшируется на 5 минут по SHA-256 токена). Workspace из запроса не доверяется: shared доступен всем, personal:<uid> только владельцу uid, id пользовательского workspace проверяется запросом GET /rest/v1/workspaces?id=eq.<id> с токеном пользователя (RLS Supabase: строку видят только владелец и участники). Id в таблице workspaces CRM текстовые: shared, personal:<uid> и для пользовательских ws + base36 (createWorkspace в CRM). Принимается любой id из [A-Za-z0-9_-]{1,64} (в том числе uuid), другие символы отклоняются ещё до запроса к Supabase. Ответы: 401 — токен не принят или истёк, 403 — нет доступа к workspace.
Пока CRM не передаёт workspace (задача Олега mv6p5b92slw1an), фронтенд работает в shared. Когда в crm:auth появится workspace: { id, kind, title }, он подхватится автоматически: kind: "shared" → shared, kind: "personal" → personal:<uid>, иначе id. Сообщение о смене workspace тоже обрабатывается ({ type: "crm:workspace", workspace: {…} }), если Олег выберет другое имя — поправить в frontend/src/crm.ts.
Ссылки на страницы в CRM
Спецификация «Ссылки на страницы вики в CRM» от 2026-10-14 (часть CRM — задача Олега mv7z4jkhgzaelh).
- CRM → вики
crm:context:{ type: "crm:context", version: 1, link_template: "https://<crm>/#/module/<id>?path={path}&ws={ws}" }. Принимается только с origin изCrm__Origins. Формат терпимый:link_template(илиlinkTemplate) ищется в самом сообщенииcrm:contextилиcrm:auth, а также во вложенныхcontext/crm_context/crm. Шаблон без{path}или не http(s) игнорируется. - Кнопка «Скопіювати посилання» (страница и папка): ссылка по
link_template,{path}=/p/<id>или/f/<id>,{ws}=workspace.idиз CRM (оба черезencodeURIComponent). Пока шаблона нет — прямая ссылкаhttps://wiki-crm.tes.gd/p/<id>. Копирование с запасным путём (textarea +execCommand). - CRM → вики
crm:theme:{ type: "crm:theme", version: 1, theme: "light"|"dark", palette: "<имя>" }— в ответ наcrm:ready/crm:refreshи при каждой смене темы или палитры. Только с origin изCrm__Originsи отwindow.parent. Вики ставит<html data-theme data-pal>(paletteпо умолчаниюclassic), остальное делают стилиtvorymo-ui.css. Последняя тема запоминается вlocalStorage, чтобы при загрузке не мигала системная. - Вики → CRM
crm:navigate:{ type: "crm:navigate", version: 1, path }при каждом переходе (страница, папка, главная, назад/вперёд) и один раз после первогоcrm:auth. Только в iframe и только на origin CRM. - Вне iframe: для
/p/<id>и/f/<id>кнопка «Відкрити в CRM» по envCRM_LINK_TEMPLATE({ws}пустой).CRM_AUTO_REDIRECT=true— сразуlocation.replaceтуда. Без шаблона всё как раньше. Оба значения отдаётGET /api/config(crmLinkTemplate,crmAutoRedirect).
REST API
Все ответы JSON (кроме /markdown и картинок). Ошибки: { "error": "…" }.
| Метод | Путь | Что делает |
|---|---|---|
GET |
/health |
{ status, dbCreatedAt } — без авторизации; dbCreatedAt не меняется, пока жив том с базой |
GET |
/api/config |
{ crmOrigins: [...] } — без авторизации |
POST |
/api/verify |
{ access_token } → { ok, id, email, name, exp } — без авторизации |
GET |
/api/me |
текущий пользователь и workspace |
GET |
/api/tree |
{ workspace, nodes: [Node] } — все папки и страницы workspace без текста |
GET |
/api/nodes/{id} |
папка или страница (Page) |
GET |
/api/pages/{id} |
страница (Page), 404 для папки |
GET |
/api/pages/{id}/markdown |
текст страницы, text/markdown |
POST |
/api/pages |
{ title, parentId?, markdown? } → 201 Page |
POST |
/api/folders |
{ title, parentId? } → 201 Page (kind = folder) |
PUT |
/api/pages/{id} |
{ title?, markdown?, version } → Page; если version устарела — 409 { error, current } |
PATCH |
/api/nodes/{id} |
переименовать/переместить: { title?, parentId?, version? }; parentId: null — в корень. Перенос в себя/потомка или в страницу — 400 |
DELETE |
/api/nodes/{id} |
удалить узел со всем содержимым → { deleted: N } |
GET |
/api/search?q=…&limit=30 |
{ results: [{ id, kind, title, snippet, path }] } — FTS5 по названию и тексту, префиксный поиск по словам; совпадения в snippet обрамлены \u0002…\u0003 |
POST |
/api/images |
multipart/form-data: file (PNG/JPEG/GIF/WebP, до 10 МБ), pageId? → 201 { id, url } |
GET |
/api/images/{id} |
картинка; нужен токен и доступ к workspace картинки |
type Node = { id, parentId: string|null, kind: "folder"|"page", title, version: number,
createdAt, updatedAt, updatedBy /* uid */, updatedByName };
type Page = Node & { markdown: string, path: { id, kind, title }[] /* папки от корня */,
author: Person | null /* кто создал (createdAt) */,
lastEditor: Person | null /* кто последний правил (updatedAt) */ };
type Person = { id /* uid Supabase */, name: string|null, email: string|null,
avatarUrl: string|null /* публичный https-URL или data:image */, color: string|null /* кружок с инициалами */ };
Автор
У каждой страницы и папки хранятся автор (created_by = uid из проверенного токена, createdAt) и последний редактор (updated_by, updatedAt); перенос и переименование тоже считаются правкой. Агенты (бот «Лана» и т. п.) — такие же авторы, как люди. Поля author и lastEditor есть в ответах GET /api/pages/{id}, GET /api/nodes/{id} и в ответах на создание и сохранение; в /api/tree их нет. Если автор неизвестен, поле null; если uid известен, а профиля нет, name/avatarUrl будут null.
Профиль берётся там же, где его берёт CRM: довідник виконавців в app_config (id = 'main', data.assignees[]: { id, name, email, uid, avatar, color }), запрос с токеном пользователя (RLS), сопоставление по uid, затем по email. Обновляется не чаще раза в 10 минут на пользователя и хранится в таблице users вики. Аватар — URL из avatar: CRM кладёт фото в публичный бакет Storage logos (avatars/<id записи>) и сохраняет getPublicUrl, поэтому картинка грузится без токена. Принимаются только https:// и data:image/. Нет фото — кружок с инициалами на цвете color (свой цвет записи или colorOf(id), как в CRM). Нет записи в довіднику (например, бот) — имя из токена (user_metadata.name → email), инициалы.
Формат текста — Markdown (GFM: таблицы, списки задач). Ссылка на другую страницу: [текст](/p/<id>), картинка: . Ссылки по id не ломаются при переименовании и переносе.
Пример (агент или скрипт):
curl -H "Authorization: Bearer $TOKEN" https://wiki-crm.tes.gd/api/tree
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"title":"Регламент","markdown":"# Привет"}' https://wiki-crm.tes.gd/api/pages
Заголовки
Content-Security-Policy: frame-ancestors https://test.units.tvorymo.com https://units.tvorymo.com; … (без X-Frame-Options). Список origin CRM задаётся Crm__Origins (через пробел или запятую): из них принимается crm:auth (плюс ev.source === window.parent) и они же попадают в frame-ancestors. Origin родителя фронтенд определяет по location.ancestorOrigins или document.referrer и шлёт crm:ready только ему; если родитель не определён — каждому origin из списка (браузер доставит лишь совпавшему). Домен стилей CRM — Crm__AssetsOrigin.
На экране ожидания есть диагностика: origin родителя, куда ушёл crm:ready, какие сообщения пришли (без токенов); через 10 секунд без crm:auth — подсказка. Интерфейс на украинском.
Разработка
cd backend/Wiki.Api && ASPNETCORE_URLS=http://127.0.0.1:5080 dotnet run # база в ./data
cd frontend && npm i && npm run dev # Vite, /api проксируется на 127.0.0.1:5080
dotnet test backend/Wiki.Api.Tests # тесты проверки доступа к workspace
Настройки (env): Storage__DataDir, Crm__Origins, Crm__AssetsOrigin, Supabase__Url, Supabase__PublishableKey.