Files
crm-wiki/README.md

98 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# crm-wiki
Вики (база знаний) для CRM Tvorymo. Открывается в iframe из боковой панели CRM, пользователя узнаёт по токену CRM (postMessage `crm:ready` → `crm:auth`).
- Сайт: https://wiki-crm.tes.gd
- Спецификация: [SPEC.md](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`, а другим — по выдаче доступа CRM («Доступ для акаунтів», таблица `access_grants`, строка `<owner>:<grantee>`): `GET /rest/v1/access_grants?owner_uid=eq.<uid>&grantee_uid=eq.<я>` с токеном пользователя, личную вики открывает `projects = all` («Мої особисті проєкти: Усі»), права — `level` (`read` — лишь просмотр, `full` — полные); «Вибрані» проекты и один лишь план вики не открывают. Id пользовательского workspace проверяется запросом `GET /rest/v1/workspaces?id=eq.<id>&select=id,owner_uid,members` с токеном пользователя (RLS Supabase): владелец и участники — полные права; строка видна, но я не владелец и не участник — значит, её открыли через `access_grants.shared = all` владельцем или участником, права по `level`. Результат проверки workspace кэшируется на 1 минуту, поэтому выданный или отозванный в CRM доступ вступает в силу в пределах минуты. В workspace «лишь просмотр» любой запрос, кроме `GET`/`HEAD`, получает `403`; `/api/me` и `/api/tree` возвращают `access: "read" | "full"`, интерфейс в режиме `read` прячет кнопки изменения. 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` (в том числе `personal:<uid владельца>` с `kind: "granted"` — чужое личное, открытое мне). Сообщение о смене 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 и `access` (`read` / `full`) |
| `GET` | `/api/tree` | `{ workspace, access, 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 картинки |
| `DELETE` | `/api/images/{id}` | удалить картинку своего workspace (`X-Wiki-Workspace`): строку в базе и файл → `200 { id, deleted: true }`. Нет такой картинки в этом workspace — `404`. Картинка упомянута на странице (`/api/images/{id}` в тексте любой страницы любого workspace) — `409 { error, pages: [{ id, workspace, title }] }`, ничего не удаляется; `title` = `null` для страниц чужого workspace |
```ts
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 не ломаются при переименовании и переносе.
Пример (агент или скрипт):
```sh
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` — подсказка. Интерфейс на украинском.
## Разработка
```sh
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`.