README: модуль «Дзвінки», JWT, env, бічна панель CRM, API, обмеження
This commit is contained in:
152
README.md
152
README.md
@@ -1,40 +1,128 @@
|
||||
# crm-calls
|
||||
|
||||
Jitsi Meet для Tvorymo CRM: https://crm-calls.volcanic.tes.gd
|
||||
Модуль «Дзвінки» для CRM Tvorymo і власний Jitsi Meet з авторизацією через JWT.
|
||||
|
||||
Розгортання: Coolify Вулканика (`http://coolify.volcanic.tes.gd:8000`), VPS `164.90.233.156`
|
||||
(DigitalOcean, `*.volcanic.tes.gd`), проєкт і застосунок `crm-calls`, build pack `dockercompose`,
|
||||
файл `docker-compose.yml`. Основа: офіційний [docker-jitsi-meet](https://github.com/jitsi/docker-jitsi-meet), образи `stable-11248`.
|
||||
| Що | Адреса |
|
||||
|---|---|
|
||||
| Модуль (сайт в iframe CRM, вікно дзвінка, гостьова сторінка) | https://crm-calls.volcanic.tes.gd |
|
||||
| Jitsi Meet | https://meet.volcanic.tes.gd |
|
||||
| CRM (тестова) | https://test.units.tvorymo.com |
|
||||
| Supabase CRM | https://yjxbkcqsquwbsyoonfkn.supabase.co |
|
||||
|
||||
## Сервіси
|
||||
Розгортання: Coolify Вулканика (`http://coolify.volcanic.tes.gd:8000`), VPS `164.90.233.156` (DigitalOcean), проєкт `crm-calls`
|
||||
(`fdzlvs0ox6tb58i8ezwpca00`), два застосунки з одного репозиторію:
|
||||
|
||||
| Сервіс | Що робить | Як доступний ззовні |
|
||||
|---------|-----------------------------------|---------------------|
|
||||
| web | nginx + веб-застосунок Jitsi | HTTPS 443 через Traefik (Coolify) → `web:8000` |
|
||||
| prosody | XMPP-сервер | лише `wss://crm-calls.volcanic.tes.gd/xmpp-websocket` (проксі через web) |
|
||||
| jicofo | фокус конференцій | ззовні не доступний |
|
||||
| jvb | відеоміст (медіа) | **UDP 10000 напряму**, повз Traefik |
|
||||
| Застосунок Coolify | uuid | Compose | Домен (`docker_compose_domains`) |
|
||||
|---|---|---|---|
|
||||
| `crm-calls` (Jitsi) | `ibugx0ujnub2pbvtfr3euk9f` | `docker-compose.yml` | `web` → `https://meet.volcanic.tes.gd:8000` |
|
||||
| `crm-calls-module` (модуль) | `g2mi10g1rcuukibu6yy3tf5u` | `docker-compose.calls.yml` | `calls` → `https://crm-calls.volcanic.tes.gd:8080` |
|
||||
|
||||
Colibri-websocket у цій версії Jitsi не використовується: канал «клієнт ↔ JVB» іде через SCTP
|
||||
усередині того самого медіа-з'єднання (UDP 10000). Тому через 443 ходять лише веб і XMPP.
|
||||
`:8000` / `:8080` у домені — порт контейнера, куди Traefik веде трафік; публічні адреси — звичайний HTTPS 443.
|
||||
Застосунки окремі, щоб деплой модуля не перезапускав Jitsi і не обривав дзвінки.
|
||||
|
||||
## Як додати модуль у бічну панель CRM
|
||||
|
||||
1. CRM → налаштування бічної панелі → додати посилання.
|
||||
2. URL: `https://crm-calls.volcanic.tes.gd` (саме так, без шляху й редіректів: CRM надсилає токен лише на origin цієї адреси).
|
||||
3. Назва: «Дзвінки».
|
||||
4. Увімкнути галочку **«Передавати авторизацію»** (без неї модуль відкриється, але чекатиме авторизацію).
|
||||
5. Відкрити модуль у CRM: має з'явитися ім'я користувача і назва воркспейсу, кнопка «Приєднатися» і гостьове посилання.
|
||||
|
||||
## Як це працює
|
||||
|
||||
1. **Модуль у iframe CRM** надсилає `crm:ready`, отримує `crm:auth` (токен Supabase і `workspace: { id, kind, title }`),
|
||||
сервер перевіряє токен (`exp`, `aud = authenticated`, `GET /auth/v1/user` з publishable-ключем).
|
||||
Ім'я та email — з відповіді Supabase. Зміна воркспейсу в CRM (`crm:workspace`) підхоплюється одразу.
|
||||
Якщо CRM не передала воркспейс (`workspace: null` або поля немає), модуль показує список: «Мій», «Загальний»
|
||||
і воркспейси на людей, де користувач власник або учасник (таблиця `workspaces` з його токеном, RLS). Вибір запам'ятовується.
|
||||
2. **Доступ до воркспейсу** сервер перевіряє сам: `shared` — усім користувачам CRM, `personal:<uid>` — лише власнику,
|
||||
інші id — запитом `GET /rest/v1/workspaces?id=eq.<id>` з токеном користувача (RLS). Інакше `403`.
|
||||
3. **Кімната:** одна постійна на воркспейс, ім'я `ws-` + 20 випадкових символів `[a-z0-9]`, створюється при першому зверненні,
|
||||
зберігається в SQLite. Id воркспейсу в імені немає.
|
||||
4. **«Приєднатися»:** модуль одразу відкриває нове вікно (`window.open` по кліку, у sandbox CRM є `allow-popups`),
|
||||
бере в сервера одноразовий код (60 секунд) і веде вікно на `/call?code=…`. У iframe CRM немає доступу до камери,
|
||||
тому дзвінок іде в окремому вікні.
|
||||
5. **Вікно `/call`** одразу прибирає код з адреси, міняє його на **сесію вікна** (12 годин, у `sessionStorage`) і по ній
|
||||
бере **пропуск Jitsi**. Звонок — через `external_api.js` (`new JitsiMeetExternalAPI('meet.volcanic.tes.gd', { roomName, jwt, parentNode })`).
|
||||
Токен CRM у вікно не передається взагалі.
|
||||
6. **Пропуск Jitsi:** HS256, `iss = tvorymo-crm`, `aud = jitsi`, `sub = meet.volcanic.tes.gd`, `room` — одна кімната, 15 хвилин,
|
||||
`context.user = { id, name, email, moderator }`. Учасник воркспейсу — `moderator: true`, гість — `false`.
|
||||
7. **Після 15 хвилин** розпочатий дзвінок не обривається (пропуск перевіряється лише при підключенні).
|
||||
Якщо Jitsi не зміг сам відновити з'єднання (фатальна помилка), вікно бере новий пропуск і входить знову (до 6 спроб із паузою).
|
||||
8. **Телефон:** `/call` і гостьова сторінка показують «Відкрити в додатку» і «Продовжити в браузері».
|
||||
«Відкрити в додатку» бере пропуск лише по натисканню й відкриває `org.jitsi.meet://meet.volcanic.tes.gd/<кімната>?jwt=<пропуск>`
|
||||
(тут пропуск неминуче в адресі). Посилання на App Store і Google Play — поруч. Додаток сам новий пропуск не бере:
|
||||
якщо зв'язок зник надовго, людина натискає кнопку ще раз. Власну сторінку Jitsi «завантажте додаток» вимкнено (`DISABLE_DEEP_LINKING`).
|
||||
9. **Гостьове посилання** `https://crm-calls.volcanic.tes.gd/g/<токен>`: постійне, без лобі. Гість вводить ім'я і входить без прав модератора.
|
||||
Будь-який учасник воркспейсу може згенерувати нове (підтвердження своїм вікном, `confirm()` в iframe не працює):
|
||||
старе одразу перестає пускати нових гостей, ті, хто вже в дзвінку, лишаються. Токен посилання = HMAC(`GUEST_LINK_SECRET`, id посилання);
|
||||
у базі — id, SHA-256 токена, хто й коли згенерував.
|
||||
|
||||
### Jitsi (`docker-compose.yml`)
|
||||
|
||||
`ENABLE_AUTH=1`, `AUTH_TYPE=jwt`, `ENABLE_GUESTS=0`, `JWT_APP_ID=tvorymo-crm`, `JWT_ACCEPTED_ISSUERS=tvorymo-crm`,
|
||||
`JWT_ACCEPTED_AUDIENCES=jitsi`, `JWT_APP_SECRET` (env Coolify), `XMPP_MUC_MODULES=token_affiliation` (модератор з пропуску),
|
||||
`JICOFO_ENABLE_AUTH=0` і `ENABLE_AUTO_OWNER=false` (інакше jicofo робить модератором кожного з токеном, гостей теж),
|
||||
`DISABLE_DEEP_LINKING=true`, `CSP_HEADER: frame-ancestors https://crm-calls.volcanic.tes.gd` (вбудовувати Jitsi може лише модуль).
|
||||
|
||||
### Заголовки модуля
|
||||
|
||||
`Content-Security-Policy: frame-ancestors <CRM_ORIGINS>; script-src 'self' https://meet.volcanic.tes.gd; frame-src https://meet.volcanic.tes.gd; …`
|
||||
(без `X-Frame-Options`), `Referrer-Policy: no-referrer`, `Permissions-Policy` дозволяє камеру й мікрофон собі й Jitsi.
|
||||
|
||||
## Змінні оточення (Coolify)
|
||||
|
||||
Секрети задаються **лише** в env застосунку в Coolify, у репозиторії їх немає:
|
||||
Секрети — **лише** в env Coolify, у репозиторії їх немає.
|
||||
|
||||
- `JICOFO_AUTH_PASSWORD` — пароль jicofo у prosody
|
||||
- `JVB_AUTH_PASSWORD` — пароль jvb у prosody
|
||||
**Модуль (`g2mi10g1rcuukibu6yy3tf5u`):**
|
||||
|
||||
Мережеві (змінюються в env Coolify без правок у репозиторії, після зміни — Redeploy):
|
||||
| Змінна | Що це | Значення / за замовчуванням |
|
||||
|---|---|---|
|
||||
| `JWT_APP_SECRET` | секрет підпису пропусків Jitsi, **такий самий, як у Jitsi** | секрет |
|
||||
| `GUEST_LINK_SECRET` | секрет токенів гостьових посилань; зміна робить усі гостьові посилання недійсними | секрет |
|
||||
| `SUPABASE_PUBLISHABLE_KEY` | publishable-ключ Supabase CRM (публічний за дизайном) | `sb_publishable_…` |
|
||||
| `CRM_ORIGINS` | origin CRM через пробіл: звідки приймати `crm:auth`, вони ж у `frame-ancestors` | `https://test.units.tvorymo.com` |
|
||||
| `CRM_ASSETS_ORIGIN` | звідки стилі CRM | `https://test.units.tvorymo.com` |
|
||||
| `SUPABASE_URL` | | `https://yjxbkcqsquwbsyoonfkn.supabase.co` |
|
||||
| `JITSI_DOMAIN` | | `meet.volcanic.tes.gd` |
|
||||
| `JITSI_TOKEN_TTL_MINUTES` | строк пропуску | `15` |
|
||||
| `CALLS_SESSION_HOURS` | скільки живе сесія вікна дзвінка / гостя | `12` |
|
||||
| `CALLS_PUBLIC_URL` | | `https://crm-calls.volcanic.tes.gd` |
|
||||
|
||||
- `JVB_PORT` — UDP-порт JVB, публікується на хості (`10000`)
|
||||
- `JVB_ADVERTISE_IPS` — публічні адреси, які JVB повідомляє клієнтам, через кому (`164.90.233.156`)
|
||||
Для бойової CRM: `CRM_ORIGINS="https://test.units.tvorymo.com https://units.tvorymo.com"` (і `CRM_ASSETS_ORIGIN`, якщо стилі звідти) → Redeploy.
|
||||
|
||||
Інші: `PUBLIC_URL` (`https://crm-calls.volcanic.tes.gd`), `TZ` (`Europe/Kyiv`), `DEFAULT_LANGUAGE` (`uk`),
|
||||
`JITSI_IMAGE_VERSION` (`stable-11248`).
|
||||
**Jitsi (`ibugx0ujnub2pbvtfr3euk9f`):** `JWT_APP_SECRET` (секрет, той самий), `JICOFO_AUTH_PASSWORD`, `JVB_AUTH_PASSWORD` (секрети),
|
||||
`PUBLIC_URL=https://meet.volcanic.tes.gd`, `JVB_PORT=10000`, `JVB_ADVERTISE_IPS=164.90.233.156`, `TZ`, `DEFAULT_LANGUAGE`.
|
||||
|
||||
Домен у Coolify (`docker_compose_domains`): `web` → `https://crm-calls.volcanic.tes.gd:8000`.
|
||||
`:8000` тут — порт контейнера `web`, куди Traefik веде трафік; публічна адреса — звичайний HTTPS 443.
|
||||
Заміна `JWT_APP_SECRET`: однакове нове значення в обох застосунках → Redeploy обох.
|
||||
|
||||
## REST API модуля
|
||||
|
||||
| Метод | Шлях | Авторизація | Що робить |
|
||||
|---|---|---|---|
|
||||
| `GET` | `/health` | — | `{ status, dbCreatedAt }` |
|
||||
| `GET` | `/api/config` | — | `{ crmOrigins, meetDomain, publicUrl, appStoreUrl, playStoreUrl }` |
|
||||
| `POST` | `/api/verify` | — | `{ access_token }` → `{ ok, id, email, name, exp }` |
|
||||
| `GET` | `/api/workspaces` | токен CRM | воркспейси користувача (для вибору) |
|
||||
| `GET` | `/api/room` | токен CRM + `X-Calls-Workspace` | `{ workspace, guestLink: { url, updatedAt, updatedByName } }` |
|
||||
| `POST` | `/api/room/guest-link` | токен CRM + `X-Calls-Workspace` | нове гостьове посилання |
|
||||
| `POST` | `/api/join` | токен CRM + `X-Calls-Workspace` | `{ code, expiresIn: 60, url }` — одноразовий код входу |
|
||||
| `POST` | `/api/call/exchange` | — | `{ code }` → `{ session, kind, name }`; код одноразовий (`410`, якщо використаний/застарів) |
|
||||
| `POST` | `/api/call/pass` | — | `{ session }` → `{ jwt, exp, room, domain, name, kind }`; `401` — сесія завершилась |
|
||||
| `GET` | `/api/guest/{token}` | — | `200` — посилання дійсне, `404` — ні |
|
||||
| `POST` | `/api/guest/{token}/join` | — | `{ name }` → `{ session, kind: "guest", name }` |
|
||||
|
||||
Токен CRM: `Authorization: Bearer <access_token>`. Помилки: `{ "error": "…" }`, `401` — токен не прийнято, `403` — немає доступу до воркспейсу.
|
||||
Дані: SQLite `/data/calls.db` на томі `calls-data` (таблиці `rooms`, `guest_links`, `join_codes`, `call_sessions`; секрети — лише хешами).
|
||||
|
||||
## Відомі обмеження
|
||||
|
||||
- `JitsiMeetExternalAPI` сам кладе пропуск у адресу вкладеного iframe Jitsi (`?jwt=…`). В адресному рядку й історії вікна його немає
|
||||
(там `/call`), але він потрапляє в адресу запиту до `meet.volcanic.tes.gd` (і в журнал nginx Jitsi). Пропуск — на одну кімнату й 15 хвилин.
|
||||
- Вікно дзвінка, відкрите з iframe CRM, успадковує його sandbox: `alert`/`confirm` там не працюють (у коді їх немає).
|
||||
- Сесія вікна дзвінка живе 12 годин і не перевіряє повторно членство у воркспейсі: людина, яку прибрали з воркспейсу, може
|
||||
перепідключитися до кінця сесії. Новий вхід з CRM уже не пройде.
|
||||
- Гості, які увійшли за старим посиланням, після перегенерації лишаються в дзвінку й можуть перепідключатися до кінця своєї сесії.
|
||||
|
||||
## Мережа
|
||||
|
||||
@@ -55,7 +143,7 @@ UDP 10000 має бути дозволений і в ufw/iptables на VPS, і
|
||||
`164.90.233.156:10000` (udp). Або на VPS: `sudo tcpdump -ni any udp port 10000` і ззовні
|
||||
`echo test | nc -u -w1 164.90.233.156 10000`.
|
||||
|
||||
## TURN (на майбутнє, `turn.volcanic.tes.gd`)
|
||||
## TURN (не робимо — рішення власника 2026-10-14; довідка на майбутнє, `turn.volcanic.tes.gd`)
|
||||
|
||||
Потрібен клієнтам, у яких заблокований UDP (корпоративні мережі, частина мобільних операторів).
|
||||
DNS `turn.volcanic.tes.gd` уже веде на `164.90.233.156` (wildcard).
|
||||
@@ -76,12 +164,12 @@ coturn: `use-auth-secret`, `static-auth-secret=<секрет у Coolify env>`, `
|
||||
`TURN_HOST=turn.volcanic.tes.gd`, `TURN_PORT=3478`, `TURN_TRANSPORT=udp,tcp`.
|
||||
Prosody (`mod_external_services`) роздає клієнтам тимчасові TURN-облікові дані.
|
||||
|
||||
## Вбудовування (iframe) у вікі / CRM
|
||||
|
||||
- Jitsi nginx не ставить `X-Frame-Options`; framing задається через
|
||||
`Content-Security-Policy: frame-ancestors 'self' https://*.tes.gd https://test.units.tvorymo.com`
|
||||
(змінна `CSP_HEADER` у compose, сервіс `web`). Щоб додати домен — дописати його туди.
|
||||
- Вбудовувати через `https://crm-calls.volcanic.tes.gd/external_api.js` (`JitsiMeetExternalAPI`), iframe має
|
||||
мати `allow="camera; microphone; display-capture; autoplay; clipboard-write; fullscreen"`.
|
||||
- Авторизація (postMessage → JWT) поки не налаштована: кімнати відкриті для всіх, хто знає URL.
|
||||
Для CRM планується `ENABLE_AUTH=1`, `AUTH_TYPE=jwt`, `JWT_APP_ID`, `JWT_APP_SECRET` (секрет — лише в Coolify).
|
||||
## Розробка
|
||||
|
||||
```sh
|
||||
cd calls/backend/Calls.Api && Jitsi__AppSecret=<32+ символи> Calls__LinkSecret=<32+ символи> \
|
||||
ASPNETCORE_URLS=http://127.0.0.1:5090 dotnet run # база в ./data
|
||||
cd calls/frontend && npm i && npm run dev # Vite, /api → 127.0.0.1:5090
|
||||
dotnet test calls/backend/Calls.Api.Tests # кімнати, коди, пропуски, гостьові посилання, доступ до воркспейсу (фальшивий Supabase)
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user