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» по env CRM_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>), картинка: ![alt](/api/images/<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://shotam.tvorymo.com https://units.tvorymo.com; … (без X-Frame-Options). Список origin CRM задаётся env CRM_ORIGINS или 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__Origins), Crm__AssetsOrigin, Supabase__Url, Supabase__PublishableKey.

Description
No description provided
Readme 330 KiB
Languages
TypeScript 49.6%
C# 45%
CSS 4.3%
Dockerfile 0.7%
HTML 0.4%