Модуль "Профиль пользователя"

Описание

Страница профиля пользователя: бэкенд на Kotlin в пакете ru.bgcrm.core.profile, интерфейс на Vue + TypeScript в components/user/profile.

Открывается из меню пользователя (пункт "Профиль", адрес /user/profile) и по ссылкам вида userProfile#<id>, которые ведут на /user/userProfile.do?userId=<id>.

Состав страницы

Свой профиль:

  • "Учётная запись" — фото, значки связи, имя, логин, комментарий, смена пароля с правилами политики и индикатором сложности;

  • "Параметры" — параметры пользователя с поиском по названию и ID и постраничным выводом, сохраняются сразу; вкладка "История" — журнал изменений;

  • "Опции интерфейса" — персональные настройки интерфейса и уведомлений ядра, с поиском и сворачиваемыми разделами;

  • "Настройки плагинов" — опции плагинов, по разделу на плагин; карточки нет, если у плагинов нет опций;

  • "Сеансы" — свои сеансы на текущем сервере: браузер, IP, время входа и активности, завершение остальных;

  • карточки плагинов, например "Telegram" (см. [profile-cards]).

Учётная запись

Чужой профиль (/user/userProfile.do?userId=<id>):

  • карточка пользователя — фото, имя, под ним отметки "Заблокирован" и отсутствия (отпуск, больничный и т. п.) с датой окончания или смена на сегодня, справа ID и значки связи, справа после значков связи значок "Послать персональную новость", действующие группы с периодами (по 5 на странице);

  • "Открытые процессы" — отдельная карточка со списком процессов, где пользователь исполнитель, с постраничным выводом;

  • "Параметры" — только просмотр.

Чужой профиль

Если userId не передан или совпадает с текущим пользователем, открывается свой профиль.

Структура модуля

ru.bgcrm.core.profile
├── IProfileOptionService       опции интерфейса
├── IProfileAccountService      учётная запись
├── IProfileCardService         карточка чужого профиля
├── ProfilePluginSettings       настройки плагина: префикс ключей и опции
├── ProfileOptionCatalog        регистрация настроек плагинов
├── model/                      ProfileOption, ProfileAccount, ProfileCard
├── impl/                       реализации сервисов, опции ядра (CoreProfileOptionProvider)
├── impl/repository/            запись в таблицу user
└── struts/
    ├── action/UserProfileAction
    └── model/                  DTO ответов

Сервисы зарегистрированы в ServiceModule, репозитории — в RepositoryModule. Действие зарегистрировано в struts-config.xml по пути /user/userProfile.

Действия и права

Действие Право Что делает

без action

UserProfileAction:null, всем

Отдаёт страницу

options

всем

Опции ядра (options) и опции плагинов по группам (plugins: id, title, options) с текущими значениями

updateOptions

всем

Сохраняет опции. Тело запроса — JSON-словарь "ключ → значение"

account

всем

Имя, логин и комментарий текущего пользователя

updateAccount

по праву "Изменение учётной записи"

Сохраняет учётную запись. Тело — JSON с полями title, login, description, password, passwordConfirm

card

всем

Карточка пользователя userId: имя, признак блокировки, действующие группы

contacts

всем

Почта и телефоны пользователя userId для значков связи

processes

всем

Открытые процессы пользователя userId, где он исполнитель: общее число и страница page.pageIndex (с 1) размером page.pageSize (по умолчанию 10, не больше 100), новые сверху

photo

всем

Своё фото и признак, что фото можно загружать

uploadPhoto

всем

Загружает своё фото из поля формы file, прежнее удаляется

removePhoto

всем

Удаляет своё фото

sessions

всем

Свои сеансы на текущем сервере

revokeOtherSessions

всем

Завершает свои сеансы на текущем сервере, кроме текущего

updatePersonalization

всем

Сохраняет произвольные ключи персонализации плагинов, см. Персонализация плагинов

Права описаны в kernel.xml в разделе "Профиль пользователя".

Право "Изменение учётной записи" объявлено в kernel.xml синонимом ru.bgcrm.struts.action.ProfileAction:updateSettings;ru.bgcrm.core.profile.struts.action.UserProfileAction:updateAccount: выдача хранится под первым именем, в таблицах user_permission и user_permset_permission его нужно искать по нему.

Учётная запись

Пароль меняется, только если передан непустой password. Без него сохраняются имя, логин и комментарий, а пароль не меняется.

Проверки на сервере:

  • пустое имя — "Не указано имя.";

  • пустой логин — "Не указан логин.";

  • логин занят другим активным пользователем — отказ с текстом про занятый логин;

  • пароль проверяется политикой PswdUtil с префиксом user., включая совпадение двух полей.

Запись идёт только в поля title, login, description и отдельно в password (через EncryptorFactory), наборы прав, группы и конфиг пользователя не трогаются. После записи сбрасывается UserCache.

Опции интерфейса

Опции хранятся в поле user.personalization, JSP читают их через u:getFromPers.

Каждая опция описывается моделью ProfileOption: ключ, раздел, подпись, тип (switch, select, number), значение по умолчанию, варианты выбора, границы для чисел и значения переключателя (onValue/offValue, по умолчанию 1/0). Значение по умолчанию задаётся в одном месте — в описании опции. Сохранённое значение переключателя приводится к его onValue/offValue, поэтому true, 1 и yes читаются одинаково.

Сервер принимает только известные ключи и проверяет значение по типу опции.

У опции есть способ применения (ProfileOptionApply):

  • immediate — опцию читает JS из bgcrm.pers, после сохранения страница обновляет значение сама;

  • nextOpen — опцию читают JSP при отрисовке, она применится при следующем открытии экрана;

  • reload — опцию читает shell.jsp при загрузке оболочки, нужна перезагрузка страницы.

Опция iface.startPage задаёт стартовую страницу: если в адресе только /user/, оболочка открывает выбранный пункт меню, при условии что он пользователю разрешён.

Опции интерфейса

Уведомления о процессах

Опции раздела "Уведомления" (notify.process.telegram, notify.process.email, notify.process.news) по умолчанию включены. Отправки уведомлений о процессах пользователям проверяют их через isProcessNotificationEnabled / filterProcessNotificationRecipients из ru.bgcrm.core.profile:

  • команды telegramNotify*, emailNotify*, newsNotifyExecutors в ProcessCommandExecutor;

  • действия конструктора правил: Telegram исполнителям, группам и пользователям, e-mail пользователям, новость исполнителям.

Отправки в произвольный чат и рассылки контрагентам не фильтруются.

Настройки плагинов

Плагин описывает свои настройки одним классом — наследником ProfilePluginSettings — и регистрирует его при инициализации:

ProfileOptionCatalog.register(HolidaysProfileSettings())

ProfilePluginSettings задаёт:

  • pluginId и title — id плагина и название раздела в карточке "Настройки плагинов";

  • keyPrefix — префикс ключей плагина, по умолчанию plugin.<pluginId>.;

  • options() — опции плагина, описанные так же, как опции ядра; по умолчанию опций нет;

  • legacyKeys — ключи вне префикса, которые плагину разрешены для совместимости.

Плагин владеет ключами под своим префиксом и ключами из legacyKeys (owns). Опцию с чужим ключом сервер не показывает и пишет предупреждение в лог. Повторная регистрация с тем же pluginId заменяет предыдущую, разделы плагинов идут по названию.

Новые ключи плагинов заводятся только под plugin.<pluginId>.. Единственное исключение — iface.bgbilling.contractOpenMode у BGBilling: ключ появился раньше правила, переименование потребовало бы переноса персонализации всех пользователей.

Примеры:

  • ru.bgcrm.plugin.bgbilling.BgbillingProfileSettings — одна опция-список и ключ в legacyKeys;

  • ru.bgcrm.plugin.holidays.HolidaysProfileSettings — эффекты текущего праздника как переключатели. Праздник берётся из plugin.holidays.event, эффекты и их значения по умолчанию — из webapps/plugin/holidays/events.json (файл перечитывается, когда меняется). Значения переключателей — true/false, как их пишет меню праздника, поэтому профиль и меню показывают одно и то же. Применяются после перезагрузки страницы: эффекты читает script.jsp при отрисовке оболочки. Если праздник не задан или не найден, опций нет.

  • ru.bgcrm.plugin.oktell.OktellProfileSettings — два переключателя, оба по умолчанию включены. plugin.oktell.profile.call — кнопка звонка у телефонов в профиле: oktell-loader.js после загрузки оболочки читает bgcrm.pers и регистрирует PhoneCall.vue на пути user.profile.card.phone.action, поэтому применяется после перезагрузки страницы. plugin.oktell.call.confirm — окно подтверждения номера перед звонком. PhoneCall.vue проверяет его при каждом нажатии, и в профиле, и в списке параметров, поэтому применяется сразу.

Настройки плагинов

Персонализация плагинов

Данные, которые не описать опцией (положение перетаскиваемых элементов и подобное), плагин пишет через /user/userProfile.do?action=updatePersonalization. Значения передаются параметрами запроса, подчёркивания в именах заменяются точками: ключ plugin_holidays_event_newyear_effect_tree_position сохранится как plugin.holidays.event.newyear.effect.tree.position. Сервер принимает только ключи, которыми владеет какой-либо зарегистрированный плагин (см. Настройки плагинов); отдельно регистрировать префикс не нужно.

Параметры

Карточка "Параметры" использует общий компонент ParametersWidget.vue с типом объекта user и отдаёт данные через существующий /user/parameter.do. В чужом профиле компонент открывается с disabled: редакторы параметров не раскрываются, кроме параметров типа file, которые можно открыть для просмотра.

Параметры

Вкладка "История" берёт журнал из /user/parameter.do?action=parameterLog без разбиения на страницы. Нужно право "Просмотр истории изменения параметров", без него вкладка показывает сообщение об ошибке.

История параметров

Расширения списка параметров

ParametersWidget передаёт в компонент расширения контекст с полями object, user, parameter и parameters — весь список параметров объекта. Расширение регистрируется через $loader.load({ extensions }) на путь parameter.list.<тип>.item.action.

Так сделан значок Telegram у параметров "Telegram chat id" (1799) и "Telegram username" (6507): компонент components/ufanet/TelegramParameterLink.vue, регистрация в js/ufanet.js. Значок показывается только у пользователя и только у непустого параметра. У username ссылка ведёт на https://t.me/<username>;, у chat id — туда же, если username заполнен, иначе на tg://user?id=<chat id>.

Чужой профиль: процессы, смена, отсутствие, фото

  • Открытые процессы — те, где пользователь исполнитель и close_dt пуст, сортировка по id по убыванию. Карточка "Открытые процессы" (ProfileProcesses.vue) запрашивает их постранично отдельным вызовом processes, карточка пользователя их не загружает.

  • Группы приходят в карточке целиком, ProfileCard.vue листает их на клиенте по 5 на странице.

  • Смена — записи callboard_shift_user на текущую дату приложения, время в минутах от начала суток.

  • Отсутствие — тоже из графика смен. Смены, которые означают отсутствие, перечисляются ключом user.profile.absenceShiftIds в конфигурации через запятую (id из callboard_shift, например отпуска, больничные, командировки). Без ключа отметки нет. Если на сегодня у пользователя такая смена, период расширяется в обе стороны по дням с такими же сменами: дни без записей в графике (выходные) период не прерывают, день с другой сменой прерывает. Поиск ограничен 62 днями в каждую сторону. Подпись — название сегодняшней смены без хвоста с временем вида 0:00-23:59, карточка показывает её как "<подпись> по <дата окончания>" и в этот день не выводит строку смены.

    Отметка отсутствия
  • Фото — параметр-файл пользователя, его id задаётся ключом user.profile.photoParamId в конфигурации. Без ключа показываются инициалы.

Значки связи

ProfileContacts.vue показывает круглые значки справа вверху: в чужом профиле под ID, в карточке учётной записи — в строке с фото. Данные отдаёт contacts:

  • почта — поле email пользователя и значения всех параметров пользователя типа email, одинаковые адреса без учёта регистра выводятся один раз, значок открывает mailto:;

  • телефоны — значения всех параметров пользователя типа phone, в подсказке 11-значный номер на 7 или 8 выводится как +7. Ядро не звонит: в значок встраиваются компоненты на пути user.profile.card.phone.action, им передаётся context в формате кнопок телефонных параметров (object, user, parameter.value). Плагин oktell регистрирует там PhoneCall.vue, если у пользователя включена опция plugin.oktell.profile.call. Без такого компонента значок только показывает номер.

Плагины добавляют свои значки компонентами на пути user.profile.card.contacts, им передаётся user-id. Так ufanet добавляет Telegram (components/ufanet/TelegramContactButton.vue): ссылку отдаёт /user/plugin/ufanet/telegramLink.do?action=contact&userId=<id> — https://t.me/<username>; по параметру 6507, иначе tg://user?id=<chat id> по параметру 1799. Без обоих значка нет.

Значки связи

Своё фото

Фото загружается в карточке учётной записи (ProfilePhoto.vue) и хранится в том же параметре-файле user.profile.photoParamId, поэтому сразу видно в чужом профиле. Если ключ не задан или параметр не типа file, кнопок нет.

Сервер (ProfilePhotoService) принимает JPG, PNG, GIF и WebP до 5 МБ и проверяет и расширение, и сигнатуру содержимого. Перед записью все прежние версии удаляются, файл сохраняется под именем photo.<расширение>. Удаление версии параметра удаляет и сам файл, восстановить прежнее фото нельзя.

Сеансы

Сеансы берутся из LoginStat того сервера, на который пришёл запрос. На других серверах кластера сеансы не видны и не завершаются, интерфейс прямо об этом пишет. Завершённый сеанс выходит из системы при следующем запросе, в списке он не показывается сразу после завершения.

IP и User-Agent запоминаются при входе (AuthFilter → LoginStat.userLoggedIn). IP берётся из заголовка X-Real-IP, который ставит nginx, иначе — адрес соединения. Название браузера и системы фронт разбирает из User-Agent сам.

Сеансы

Карточки плагинов

UserProfile.vue выводит в своём профиле компоненты, зарегистрированные на путь расширения user.profile.cards, и передаёт им user-id:

Vue.prototype.$loader.load({
    extensions: {user: {profile: {cards: [{name: 'my-card', component: '/path/MyCard.vue'}]}}}
});

Telegram в один клик

Карточка components/ufanet/TelegramLinkCard.vue, регистрация в js/ufanet.js, бэкенд ru.bgcrm.plugin.ufanet.telegram.link.

  1. Кнопка "Подключить Telegram" вызывает /user/plugin/ufanet/telegramLink.do?action=link. Сервер выдаёт одноразовый токен на 15 минут и ссылку https://t.me/<бот>?start=<server.instance.id>-<токен>;.

  2. Пользователь нажимает в боте "Запустить". telegram-service превращает /start в CallbackRequest с командой 0;linkTelegram:<токен> и кладёт в очередь tg.telegrab с to=<server.instance.id> — на тот сервер, который выдал токен.

  3. TeleGrabAPI обрабатывает команду до поиска пользователя по Telegram id: по токену находит пользователя ERP и записывает параметры 1799 (chat id) и 6507 (username). Если этот Telegram уже привязан к другому пользователю, привязка отклоняется.

  4. Бот уведомлений присылает подтверждение, карточка в профиле видит новый chat id и показывает "Telegram подключён".

Имя бота задаётся ключом telegram.link.botUsername. Без него кнопки нет, карточка подсказывает команду бота /get_id. Токены хранятся в памяти сервера, после перезапуска ссылку нужно получить заново.

Telegram

Фронтенд

Файл Назначение

WEB-INF/jspf/user/userProfile/default.jsp

Обёртка страницы, передаёт id текущего и запрошенного пользователя, заголовок оболочки

components/user/profile/UserProfile.vue

Выбор режима: свой или чужой профиль

components/user/profile/ProfileAccount.vue

Карточка "Учётная запись"

components/user/profile/ProfilePhoto.vue

Своё фото: загрузка, замена, удаление

components/user/profile/ProfileContacts.vue

Значки почты, телефона и плагинов

components/user/profile/ProfileParameters.vue

Карточка "Параметры"

components/user/profile/ProfileOptions.vue

Карточка "Опции интерфейса"

components/user/profile/ProfileCard.vue

Карточка чужого профиля

components/user/profile/ProfileProcesses.vue

Открытые процессы в чужом профиле

components/user/profile/ProfileSessions.vue

Карточка "Сеансы"

js/thesaurus/repositories/user/UserProfileSettingsRepository.js

Репозиторий запросов к /user/userProfile.do

ts/types/repositories/user/profile/

Типы ответов и запросов

Заголовок оболочки задаётся через shell_title.jsp, который вставляет его в JS-строку в двойных кавычках. Поэтому в заголовке нельзя использовать разметку с атрибутами в двойных кавычках, имя пользователя выводится через c:out.

Версия скриптов плагинов

js/ufanet.js и другие скрипты плагинов, подключённые через endpoint js с путём под /js/, получают в адрес суффикс версии по времени изменения файла (script_css.jsp). Суффикс снимает UrlRewriteFilter, который замаплен только на /js/*. Скрипты вне /js/ (например, /plugin/oktell/js/oktell-loader.js) подключаются без версии.