Web Client Server

Цель

Нужно построить серверный слой для веб-клиентов, который умеет:

  1. авторизовать пользователя;
  2. отдавать только доступные этому пользователю приложения;
  3. запускать или подключать веб-клиент к выбранному приложению платформы;
  4. отдавать метаданные приложения и модель UI;
  5. держать живой двунаправленный канал для событий, команд, изменений данных и состояния интерфейса.

Новый сервер не должен быть продолжением старого HTML-интерфейса из plugin_webinterface.cpp. Старый слой рендерит окна в HTML и принимает POST-формы. Новая схема должна дать стабильный REST/WSS-контракт, поверх которого React UI сможет работать с метаданными платформы и runtime-событиями.

Общая Схема

Browser / React UI
        |
        | HTTPS REST + WSS
        v
Web Client Server
  - HTTP API
  - WebSocket gateway
  - auth/session middleware
  - application catalog
  - runtime session manager
        |
        | internal RPC / in-process adapter / process bridge
        v
EasyControl platform runtime
  - MetaProgramm
  - формы и контролы
  - главное меню
  - права пользователя
  - DB connection и бизнес-методы

Внешним протоколом, токенами, клиентскими сессиями и совместимостью API владеет web-сервер. Бизнес-логикой, интерпретацией метаданных, доступом к БД и проверкой прав владеет runtime платформы.

Основные Понятия

Principal

Авторизованный пользователь. Содержит стабильный userId, логин, отображаемое имя, роли, права, tenant/database context и опциональные claims внешнего провайдера.

Application

Запись доступного приложения платформы. Минимально содержит appId, название, иконку, версию/ревизию и runtime target. Может указывать на конкретную БД, MetaProgramm, экземпляр платформы или пул runtime-процессов.

Application catalog

Серверный список приложений, отфильтрованный по пользователю. Клиент не должен получать приложения, которые пользователь не может открыть.

Client session

Сессия логина браузера или устройства. Одна client session может создать несколько runtime session, например при открытии двух приложений в разных вкладках.

Runtime session

Живая серверная сессия выбранного приложения. Содержит runtimeSessionId, appId, principal, привязку к runtime платформы, открытые формы, UI state и WSS-канал.

Архитектурные Решения

  1. REST используется для логина, получения каталога приложений, bootstrap выбранного приложения и одноразовых запросов.
  2. WSS используется для live runtime-трафика после создания runtime session.
  3. Авторизация остается на сервере. Список приложений, меню, формы, поля и действия отдаются уже с учетом прав.
  4. Браузер не получает DB credentials и внутренние указатели платформы.
  5. Метаданные приложения версионируются. Bootstrap возвращает appRevision, WSS-сообщения содержат protocol version.
  6. Загрузка приложения создает или присоединяет runtime session. Это не просто скачивание JSON.
  7. Для браузера предпочтительны HttpOnly Secure cookies. Bearer token допустим для технических клиентов, но его нельзя хранить в небезопасном JS-хранилище.

Поток Запросов

1. Server Info

Запрос до логина, чтобы web shell понял возможности сервера.

GET /api/v1/server-info

Ответ:

{
  "serverVersion": "1.0.0",
  "protocolVersion": 1,
  "auth": {
    "password": true,
    "sso": false
  }
}

2. Login

POST /api/v1/auth/login
Content-Type: application/json

Запрос:

{
  "login": "ivan",
  "password": "secret",
  "tenant": "main"
}

Ответ:

{
  "accessToken": "opaque-or-jwt",
  "expiresInSec": 900,
  "refreshToken": "opaque-refresh-token",
  "principal": {
    "userId": "u-123",
    "login": "ivan",
    "displayName": "Ivan Petrov",
    "roles": ["manager"]
  }
}

Для браузерного сценария сервер может не возвращать токены в JSON, а выставлять access и refresh cookies с флагами HttpOnly, Secure, SameSite=Lax или строже.

3. Список Доступных Приложений

GET /api/v1/apps
Authorization: Bearer <accessToken>

Ответ:

{
  "items": [
    {
      "appId": "sales",
      "title": "Sales",
      "description": "Orders and customers",
      "iconUrl": "/api/v1/apps/sales/icon",
      "revision": "2026.06.02.1",
      "defaultRoute": "main-menu"
    }
  ]
}

Правила:

  • список фильтруется по пользователю, tenant/database, лицензии и доступности runtime;
  • клиент не может выбрать приложение, которого нет в этом ответе;
  • ответ кэшируется только в рамках пользователя/сессии, не глобально.

4. Создание Runtime Session

POST /api/v1/apps/{appId}/sessions
Authorization: Bearer <accessToken>
Content-Type: application/json

Запрос:

{
  "client": {
    "clientId": "browser-tab-uuid",
    "timezone": "Asia/Tomsk",
    "locale": "ru-RU"
  }
}

Ответ:

{
  "runtimeSessionId": "rs-8e9f",
  "appId": "sales",
  "appRevision": "2026.06.02.1",
  "wsUrl": "wss://host/ws/v1/runtime/rs-8e9f",
  "bootstrap": {
    "mainMenuUrl": "/api/v1/runtime/rs-8e9f/main-menu",
    "assetsBaseUrl": "/api/v1/apps/sales/assets/"
  }
}

На этом шаге сервер повторно проверяет доступ к приложению. runtimeSessionId привязан к principal и не может использоваться другим пользователем.

5. Загрузка Bootstrap-Данных

GET /api/v1/runtime/{runtimeSessionId}/main-menu
Authorization: Bearer <accessToken>

Ответ:

{
  "items": [
    {
      "id": "orders",
      "title": "Orders",
      "icon": "orders.png",
      "enabled": true,
      "visible": true,
      "command": "OpenOrders",
      "children": []
    }
  ]
}

Меню уже должно быть отфильтровано по правам и локализовано. По смыслу это web-кодировка того же набора, который desktop использует через mainWindow_menuItems, но без C++ указателей и process-local id.

6. Подключение WSS

wss://host/ws/v1/runtime/{runtimeSessionId}

WebSocket авторизуется через ту же cookie-сессию или через короткоживущий одноразовый wsTicket, выданный при создании runtime session. Долгоживущий bearer token в query string использовать нельзя.

Первое сообщение клиента:

{
  "type": "hello",
  "protocolVersion": 1,
  "runtimeSessionId": "rs-8e9f",
  "clientId": "browser-tab-uuid"
}

Ответ сервера:

{
  "type": "ready",
  "serverTime": "2026-06-02T11:00:00Z",
  "appRevision": "2026.06.02.1"
}

WSS-События

Каждое сообщение имеет общий envelope:

{
  "type": "message-type",
  "id": "optional-request-id",
  "payload": {}
}

Базовый набор сообщений:

Direction Type Назначение
client -> server command.execute Выполнить команду меню или формы.
client -> server form.open Открыть форму/модуль по id.
client -> server form.event Отправить UI event: click/change/submit.
client -> server data.query Запросить страницу данных для grid/list.
server -> client form.patch Изменить дерево формы/контролов.
server -> client data.patch Отправить изменения данных.
server -> client notification Сообщение, toast, ошибка.
server -> client session.expiring Предупредить об истечении сессии.
both ping / pong Keepalive и latency.

Клиент может скрывать недоступные действия, но сервер всегда остается источником истины и повторно проверяет права на каждую команду.

Модель Авторизации

Авторизация делится на три уровня:

  1. Server access: пользователь может войти и пользоваться web-сервером.
  2. Application access: пользователь может видеть и запускать конкретный appId.
  3. Runtime permissions: пользователь может видеть пункт меню, открыть форму, читать поле, редактировать поле, выполнить метод, запросить таблицу или запустить бизнес-действие.

Каталог приложений отвечает за уровни 1 и 2. Runtime платформы отвечает за уровень 3 через существующую модель метаданных и прав. Если web-сервер кэширует вычисленные права, ключ кэша должен включать ревизию приложения и пользователя/роль.

Источник Каталога Приложений

Каталог хранится на сервере, не в браузере. На первом этапе это может быть XML/YAML/DB config:

applications:
  - appId: sales
    title: Sales
    tenant: main
    runtime:
      type: platform-process
      executable: TmaPlatform.exe
      database: sales_db
    access:
      roles: [manager, admin]

REST API не должен зависеть от конкретного формата хранения каталога.

Runtime Loading Contract

Загрузка приложения должна вернуть нормализованный web manifest:

{
  "appId": "sales",
  "revision": "2026.06.02.1",
  "ui": {
    "navigation": "side-menu",
    "theme": "system"
  },
  "capabilities": {
    "forms": true,
    "reports": true,
    "files": true,
    "realtime": true
  }
}

Правила manifest:

  • содержит стабильные id, а не C++ указатели или process-local handles;
  • ссылается на assets через URL или asset id;
  • может кэшироваться по appId + revision;
  • user-specific visibility остается в runtime-ответах, не в публичном manifest.

Ошибки

REST и WSS command replies используют один envelope:

{
  "error": {
    "code": "APP_ACCESS_DENIED",
    "message": "Application is not available for this user.",
    "details": {
      "appId": "sales"
    }
  }
}

Начальный набор кодов:

Code Значение
AUTH_REQUIRED Нет токена или он истек.
AUTH_INVALID Неверные credentials или token.
APP_ACCESS_DENIED Приложение недоступно пользователю.
APP_NOT_FOUND Неизвестный appId.
RUNTIME_NOT_FOUND Runtime session не существует или принадлежит другому пользователю.
RUNTIME_BUSY Runtime платформы временно не может принять команду.
COMMAND_DENIED Команда существует, но запрещена.
VALIDATION_ERROR Невалидный payload запроса.
INTERNAL_ERROR Неожиданная ошибка сервера или runtime.

Security Requirements

  • HTTPS и WSS обязательны вне local development.
  • Password login имеет rate limit и audit log.
  • Access token живет 5-15 минут.
  • Refresh token revocable и привязан к client session.
  • Runtime session привязана к principal, app, tenant и browser client id.
  • WSS проверяет origin.
  • Сервер логирует login, app session creation, denied command, runtime errors.
  • DB credentials и внутренние handles платформы не уходят в браузер.
  • File/image endpoints требуют app/runtime authorization.

MVP

Первый этап:

  1. password login через текущую базу пользователей платформы;
  2. GET /api/v1/apps;
  3. POST /api/v1/apps/{appId}/sessions;
  4. GET /api/v1/runtime/{sessionId}/main-menu;
  5. WSS hello, ready, command.execute, notification, error;
  6. audit log для login, app start, command execute, close session.

Не включаем в первый этап:

  • offline mode;
  • SSO;
  • runtime migration между серверами;
  • push updates между несколькими web-серверами;
  • полный протокол form diff.

Открытые Решения

  1. Где хранить application catalog: XML config, DB table или admin UI.
  2. Делать browser auth только на cookies или сохранить bearer-token режим для части клиентов.
  3. Может ли один process платформы безопасно держать несколько runtime session.
  4. Как версионировать сериализованные формы и контролы.
  5. Как сопоставить существующие MetaProgramm и menu command ids со стабильными web id.
  6. Как инвалидировать активные сессии при изменении ролей или метаданных приложения.