diff --git a/Makefile b/Makefile index 684e6dbc..c924492e 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ install: configs install -d -m 0777 $(DESTDIR)/var/www/images install -d -m 0777 $(DESTDIR)/var/www/uploads install -d -m 0777 $(DESTDIR)/var/www/scripts/i18n + install -d -m 0755 $(DESTDIR)/var/www/fonts cp -a dist/css/*.css $(DESTDIR)/var/www/css cp -a dist/images/* $(DESTDIR)/var/www/images @@ -52,13 +53,12 @@ install: configs cp -a dist/*.js $(DESTDIR)/var/www/ cp -a dist/*.svg $(DESTDIR)/var/www/ cp -a dist/*.png $(DESTDIR)/var/www/ - cp -a dist/*.ttf $(DESTDIR)/var/www/ - cp -a dist/*.woff $(DESTDIR)/var/www/ - cp -a dist/*.woff2 $(DESTDIR)/var/www/ || : + cp -a dist/fonts/* $(DESTDIR)/var/www/fonts install -m 0644 dist/404.html $(DESTDIR)/var/www/ install -m 0644 dist/robots.txt $(DESTDIR)/var/www/ install -m 0644 dist/index.html $(DESTDIR)/var/www/ + install -m 0644 login/login.html $(DESTDIR)/var/www/ install -Dm0644 dist/configs/*.json -t $(DESTDIR)/usr/share/wb-mqtt-homeui install -Dm0755 convert_config_v1v2.py $(DESTDIR)/usr/lib/wb-mqtt-homeui/convert_config_v1v2 diff --git a/app/index.ejs b/app/index.ejs index 00fd9d0e..d089adbe 100644 --- a/app/index.ejs +++ b/app/index.ejs @@ -24,6 +24,10 @@
+
@@ -102,7 +112,7 @@
  • {{'navigation.menu.channels' | translate}}
  • -
  • +
  • {{'navigation.menu.access' | translate}}
  • diff --git a/app/scripts/app.js b/app/scripts/app.js index 1febe99f..9cb98ce7 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -67,7 +67,6 @@ import HomeCtrl from './controllers/homeController'; import NavigationCtrl from './controllers/navigationController'; import LoginCtrl from './controllers/loginController'; import MQTTCtrl from './controllers/MQTTChannelsController'; -import AccessLevelCtrl from './controllers/accessLevelController'; import DateTimePickerModalCtrl from './controllers/dateTimePickerModalController'; import DiagnosticCtrl from './controllers/diagnosticController'; import BackupCtrl from './controllers/backupController'; @@ -95,6 +94,7 @@ import onResizeDirective from './directives/resize'; import confirmDirective from './directives/confirm'; import fullscreenToggleDirective from './directives/fullscreenToggle'; import expCheckMetaDirective from './react-directives/exp-check/exp-check'; +import usersPageDirective from './react-directives/users/users'; // Angular routes import routingModule from './app.routes'; @@ -178,7 +178,6 @@ module .controller('HomeCtrl', HomeCtrl) .controller('LoginCtrl', LoginCtrl) .controller('MQTTCtrl', MQTTCtrl) - .controller('AccessLevelCtrl', AccessLevelCtrl) .controller('DateTimePickerModalCtrl', DateTimePickerModalCtrl) .controller('DiagnosticCtrl', DiagnosticCtrl) .controller('BackupCtrl', BackupCtrl) @@ -266,7 +265,8 @@ module .directive('onResize', ['$parse', onResizeDirective]) .directive('ngConfirm', confirmDirective) .directive('fullscreenToggle', fullscreenToggleDirective) - .directive('expCheckWidget', expCheckMetaDirective); + .directive('expCheckWidget', expCheckMetaDirective) + .directive('usersPage', usersPageDirective); module .config([ @@ -277,7 +277,6 @@ module 'app', 'console', 'help', - 'access', 'mqtt', 'system', 'ui', diff --git a/app/scripts/app.routes.js b/app/scripts/app.routes.js index fb951e39..ac04c8dc 100644 --- a/app/scripts/app.routes.js +++ b/app/scripts/app.routes.js @@ -53,7 +53,6 @@ function routing($stateProvider, $locationProvider, $urlRouterProvider) { .state('accessLevel', { url: '/access-level', template: require('../views/access-level.html'), - controller: 'AccessLevelCtrl as $ctrl', }) .state('scan', { url: '/scan', diff --git a/app/scripts/components/loginForm/loginForm.controller.js b/app/scripts/components/loginForm/loginForm.controller.js index 71bbcf51..433a2423 100644 --- a/app/scripts/components/loginForm/loginForm.controller.js +++ b/app/scripts/components/loginForm/loginForm.controller.js @@ -3,11 +3,11 @@ class LoginFormCtrl { constructor($window, $rootScope, $state, $location, rolesFactory) { 'ngInject'; - var currentURL = new URL("/mqtt", $window.location.href); + var currentURL = new URL('/mqtt', $window.location.href); currentURL.protocol = currentURL.protocol.replace('http', 'ws'); this.rootScope = $rootScope; - this.isDev = ($window.location.host === 'localhost:8080'); // FIXME: find more beautiful way to detect local dev + this.isDev = $window.location.host === 'localhost:8080'; // FIXME: find more beautiful way to detect local dev this.localStorage = $window.localStorage; this.state = $state; this.rolesFactory = rolesFactory; @@ -62,8 +62,9 @@ class LoginFormCtrl { //........................................................................... updateLoginSettings() { // Update settings in Local Storage - if (this.isDev) + if (this.isDev) { this.localStorage.setItem('url', this.url); + } this.localStorage.setItem('prefix', this.prefix); @@ -84,7 +85,6 @@ class LoginFormCtrl { isDev: this.isDev, }; - this.rolesFactory.setRole(1); this.rootScope.requestConfig(loginData); location.reload(); } diff --git a/app/scripts/controllers/accessLevelController.js b/app/scripts/controllers/accessLevelController.js deleted file mode 100644 index 11f65152..00000000 --- a/app/scripts/controllers/accessLevelController.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Created by ozknemoy on 21.06.2017. - */ - -export default class accessLevelController { - constructor($timeout, rolesFactory) { - 'ngInject'; - - this.$timeout = $timeout; - this.rolesFactory = rolesFactory; - - this.ok = false; - this.isLevelUp = false; - this.one = rolesFactory.ROLES[0]; - this.two = rolesFactory.ROLES[1]; - this.three = rolesFactory.ROLES[2]; - this.activeRole = rolesFactory.current.role; - this.level = '' + rolesFactory.getRole(); - this.type = { id: +this.level }; - } - - select(newType) { - if (newType.id > this.activeRole) { - this.ok = false; - this.isLevelUp = true; - } else if (newType.id == this.activeRole) { - this.ok = false; - this.isLevelUp = false; - } else { - this.ok = true; - this.isLevelUp = false; - this.type = newType; - } - } - - apply() { - this.rolesFactory.setRole(this.level); - this.ok = false; - this.isLevelUp = false; - this.activeRole = this.level; - } -} diff --git a/app/scripts/controllers/navigationController.js b/app/scripts/controllers/navigationController.js index 6a7d1653..f22ddaa3 100644 --- a/app/scripts/controllers/navigationController.js +++ b/app/scripts/controllers/navigationController.js @@ -8,11 +8,13 @@ class NavigationCtrl { whenMqttReady, errors, uiConfig, - rolesFactory + rolesFactory, + $rootScope ) { 'ngInject'; $scope.roles = rolesFactory; + $rootScope.roles = rolesFactory; $scope.isActive = function (viewLocation) { return viewLocation === $location.path(); @@ -84,6 +86,18 @@ class NavigationCtrl { ? pageWrapperClassList.remove(overlayClass) : pageWrapperClassList.add(overlayClass); }; + + $scope.showAccessControl = function () { + return rolesFactory.current.roles.isAdmin || rolesFactory.notConfiguredAdmin; + }; + + $scope.logout = function () { + fetch('/logout', { + method: 'POST', + }).then(() => { + window.location.href = '/login'; + }); + }; } //----------------------------------------------------------------------------- diff --git a/app/scripts/i18n/access/en.json b/app/scripts/i18n/access/en.json deleted file mode 100644 index ec3b084c..00000000 --- a/app/scripts/i18n/access/en.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "access": { - "title": "Access level", - "labels": { - "active": "active", - "confirm": "I take full responsibility for my actions" - }, - "buttons": { - "apply": "Apply" - }, - "user": { - "name": "User", - "short": "U", - "description": "Can view dashboards and history" - }, - "operator": { - "name": "Operator", - "short": "O", - "description": "Can create and edit dashboards" - }, - "admin": { - "name": "Administrator", - "short": "A", - "description": "Has full access to device settings and rules" - } - } -} diff --git a/app/scripts/i18n/access/ru.json b/app/scripts/i18n/access/ru.json deleted file mode 100644 index 6f65fd61..00000000 --- a/app/scripts/i18n/access/ru.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "access": { - "title": "Права доступа", - "labels": { - "active": "активно", - "confirm": "Я принимаю всю ответственность за свои действия" - }, - "buttons": { - "apply": "Применить" - }, - "user": { - "name": "Пользователь", - "short": "П", - "description": "Может просматривать панели и историю" - }, - "operator": { - "name": "Оператор", - "short": "O", - "description": "Может создавать и редактировать панели" - }, - "admin": { - "name": "Администратор", - "short": "A", - "description": "Имеет полный доступ к настройкам и правилам" - } - } -} diff --git a/app/scripts/i18n/app/en.json b/app/scripts/i18n/app/en.json index e92d61f3..f2e81be1 100644 --- a/app/scripts/i18n/app/en.json +++ b/app/scripts/i18n/app/en.json @@ -4,11 +4,20 @@ "load": "Cannot load WebUI config.", "save": "Config saving failed", "overflow": "Config saving failed. Try to clear page's localStorage and restart the browser. If problem remains, try to reduce overall size of SVG images.", - "stop-scan": "The controller started searching for Modbus devices. This could lead to slow polling of already configured devices. The search process is forcibly stopped" + "stop-scan": "The controller started searching for Modbus devices. This could lead to slow polling of already configured devices. The search process is forcibly stopped", + "not-configured-admin": "The administrator password is not set. Please set it in the settings" }, "prompt": { "dirty": "The page has unsaved changes. Are you sure you want to leave?", "serial-config-leave": "Scanning will be canceled. Do you really want to leave the page?" + }, + "buttons": { + "logout": "Logout" + }, + "roles": { + "user": "User", + "operator": "Operator", + "admin": "Administrator" } }, "home": { diff --git a/app/scripts/i18n/app/ru.json b/app/scripts/i18n/app/ru.json index 83cfb9fc..96d01640 100644 --- a/app/scripts/i18n/app/ru.json +++ b/app/scripts/i18n/app/ru.json @@ -4,11 +4,20 @@ "load": "Не удалось загрузить настройки WebUI.", "save": "Не удалось сохранить настройки", "overflow": "Не удалось сохранить настройки. Попробуйте очистить localStorage страницы и перезапустить браузер. Если не помогло — попробуйте уменьшить суммарный размер SVG-изображений.", - "stop-scan": "В контроллере был запущен процесс поиска Modbus устройств. Это могло приводить к медленному опросу уже настроенных устройств. Процесс поиска принудительно остановлен" + "stop-scan": "В контроллере был запущен процесс поиска Modbus устройств. Это могло приводить к медленному опросу уже настроенных устройств. Процесс поиска принудительно остановлен", + "not-configured-admin": "Пароль администратора не установлен. Пожалуйста, установите его в настройках" }, "prompt": { "dirty": "На странице остались несохранённые изменения. Вы действительно хотите покинуть страницу?", "serial-config-leave": "Процесс поиска устройств будет остановлен. Вы действительно хотите перейти на другую страницу?" + }, + "buttons": { + "logout": "Выйти" + }, + "roles": { + "user": "Пользователь", + "operator": "Оператор", + "admin": "Администратор" } }, "home": { diff --git a/app/scripts/i18n/configurations/en.json b/app/scripts/i18n/configurations/en.json index 794525f7..960cba66 100644 --- a/app/scripts/i18n/configurations/en.json +++ b/app/scripts/i18n/configurations/en.json @@ -6,8 +6,7 @@ "file": "File", "title": "Title", "description": "Description", - "notice": "You cannot view this page. You can change", - "access": "access level" + "access-notice": "You don't have enough permissions to view this page" }, "buttons": { "save": "Save" diff --git a/app/scripts/i18n/configurations/ru.json b/app/scripts/i18n/configurations/ru.json index 6e9d22fb..cf2e4584 100644 --- a/app/scripts/i18n/configurations/ru.json +++ b/app/scripts/i18n/configurations/ru.json @@ -6,8 +6,7 @@ "file": "Файл", "title": "Название", "description": "Описание", - "notice": "Для просмотра этой страницы необходимо получить соответствующие ", - "access": "права доступа" + "access-notice": "У вас недостаточно прав для просмотра этой страницы" }, "buttons": { "save": "Записать" diff --git a/app/scripts/i18n/devices/en.json b/app/scripts/i18n/devices/en.json index fe4b560f..87776bb2 100644 --- a/app/scripts/i18n/devices/en.json +++ b/app/scripts/i18n/devices/en.json @@ -3,8 +3,7 @@ "labels": { "nothing": "No devices available for this moment.", "delete": "Delete device", - "notice": "You cannot view this page. You can change", - "access": "access level" + "access-notice": "You don't have enough permissions to view this page" }, "prompt": { "delete": "Remove {{name}}?" diff --git a/app/scripts/i18n/devices/ru.json b/app/scripts/i18n/devices/ru.json index 72484250..c5996d8e 100644 --- a/app/scripts/i18n/devices/ru.json +++ b/app/scripts/i18n/devices/ru.json @@ -3,8 +3,7 @@ "labels": { "nothing": "Нет устройств, доступных для отображения.", "delete": "Удалить устройство", - "notice": "Для просмотра этой страницы необходимо получить соответствующие ", - "access": "права доступа" + "access-notice": "У вас недостаточно прав для просмотра этой страницы" }, "prompt": { "delete": "Удалить {{name}}?" diff --git a/app/scripts/i18n/react/locales/en/translations.json b/app/scripts/i18n/react/locales/en/translations.json index cf66dbf3..df5b94c2 100644 --- a/app/scripts/i18n/react/locales/en/translations.json +++ b/app/scripts/i18n/react/locales/en/translations.json @@ -233,8 +233,7 @@ } }, "errors": { - "access-failed": "You cannot view this page. You can change ", - "access-failed-link-text": "access level" + "access-failed": "You cannot view this page. You can change access level" }, "forms": { "default-text-prefix": "If the value is not set, ", @@ -326,5 +325,27 @@ "search-device": "Search device", "search-control": "Search channel" } + }, + "users": { + "title": "Users", + "errors": { + "forbidden": "You don't have permission to view this page", + "old-backend": "The backend is outdated. Please update it", + "unknown": "Error: {{msg}}" + }, + "labels":{ + "login": "Login", + "password": "Password", + "type": "Type", + "admin": "Admin", + "user": "User", + "operator": "Operator", + "confirm-delete":"Do you really want to delete user \"{{name}}\"?" + }, + "buttons": { + "save": "Save", + "add": "Add", + "delete": "Delete" + } } } diff --git a/app/scripts/i18n/react/locales/ru/translations.json b/app/scripts/i18n/react/locales/ru/translations.json index 80b21ba4..8a1bb1ee 100644 --- a/app/scripts/i18n/react/locales/ru/translations.json +++ b/app/scripts/i18n/react/locales/ru/translations.json @@ -231,8 +231,7 @@ } }, "errors": { - "access-failed": "Для просмотра этой страницы необходимо получить соответствующие ", - "access-failed-link-text": "права доступа" + "access-failed": "Для просмотра этой страницы необходимо получить соответствующие права доступа" }, "forms": { "default-text-prefix": "Если значение не указано, используется ", @@ -324,5 +323,27 @@ "search-device": "Найти устройство", "search-control": "Найти канал" } + }, + "users": { + "title": "Пользователи", + "errors": { + "forbidden": "У вас нет прав на изменение настроек", + "old-backend": "ПО на контроллере устарело. Пожалуйста, обновите его", + "unknown": "Ошибка: {{msg}}" + }, + "labels":{ + "login": "Имя", + "password": "Пароль", + "type": "Тип", + "admin": "Администратор", + "user": "Пользователь", + "operator": "Оператор", + "confirm-delete": "Вы действительно хотите удалить пользователя \"{{name}}\"?" + }, + "buttons": { + "save": "Сохранить", + "add": "Добавить", + "delete": "Удалить" + } } } diff --git a/app/scripts/i18n/rules/en.json b/app/scripts/i18n/rules/en.json index 0d9ae7b9..66382c6b 100644 --- a/app/scripts/i18n/rules/en.json +++ b/app/scripts/i18n/rules/en.json @@ -3,8 +3,7 @@ "title": "Rules", "labels": { "loading": "Loading...", - "notice": "You cannot view this page. You can change", - "access": "access level" + "access-notice": "You don't have enough permissions to view this page" }, "buttons": { "new": "New...", diff --git a/app/scripts/i18n/rules/ru.json b/app/scripts/i18n/rules/ru.json index 2534b35f..2b271094 100644 --- a/app/scripts/i18n/rules/ru.json +++ b/app/scripts/i18n/rules/ru.json @@ -3,8 +3,7 @@ "title": "Правила", "labels": { "loading": "Загрузка...", - "notice": "Для просмотра этой страницы необходимо получить соответствующие ", - "access": "права доступа" + "access-notice": "У вас недостаточно прав для просмотра этой страницы" }, "buttons": { "new": "Создать...", diff --git a/app/scripts/react-directives/components/access-level/accessLevel.jsx b/app/scripts/react-directives/components/access-level/accessLevel.jsx index 3b1e58a6..10b62b14 100644 --- a/app/scripts/react-directives/components/access-level/accessLevel.jsx +++ b/app/scripts/react-directives/components/access-level/accessLevel.jsx @@ -12,7 +12,6 @@ const AccessLevelErrorBanner = observer(({ store, children }) => { return (
    {t('errors.access-failed')} - {t('errors.access-failed-link-text')}
    ); }); diff --git a/app/scripts/react-directives/components/select/select.jsx b/app/scripts/react-directives/components/select/select.jsx index 00a0fb1d..61013b71 100644 --- a/app/scripts/react-directives/components/select/select.jsx +++ b/app/scripts/react-directives/components/select/select.jsx @@ -10,6 +10,7 @@ const BootstrapLikeSelect = ({ onChange, isClearable, className, + disabled, }) => { const withGroups = options.some(el => 'options' in el); const customStyles = { @@ -37,6 +38,7 @@ const BootstrapLikeSelect = ({ onChange={onChange} className={'wb-react-select' + (className ? ' ' + className : '')} classNames={customClasses} + isDisabled={disabled} /> ); }; diff --git a/app/scripts/react-directives/forms/formStore.js b/app/scripts/react-directives/forms/formStore.js index 29934975..7fa19f8f 100644 --- a/app/scripts/react-directives/forms/formStore.js +++ b/app/scripts/react-directives/forms/formStore.js @@ -3,10 +3,10 @@ import { makeAutoObservable } from 'mobx'; export class FormStore { - constructor(name) { + constructor(name, params) { this.type = 'object'; this.name = name; - this.params = {}; + this.params = params || {}; makeAutoObservable(this); } diff --git a/app/scripts/react-directives/forms/forms.jsx b/app/scripts/react-directives/forms/forms.jsx index 0ae2376b..54e92236 100644 --- a/app/scripts/react-directives/forms/forms.jsx +++ b/app/scripts/react-directives/forms/forms.jsx @@ -85,6 +85,7 @@ export const FormSelect = observer(({ store, isClearable }) => { setSelectedOption={store.selectedOption} placeholder={store.placeholder} onChange={value => store.setSelectedOption(value)} + disabled={store.readOnly} /> ); diff --git a/app/scripts/react-directives/forms/optionsStore.js b/app/scripts/react-directives/forms/optionsStore.js index 5e8a3881..c0412d4e 100644 --- a/app/scripts/react-directives/forms/optionsStore.js +++ b/app/scripts/react-directives/forms/optionsStore.js @@ -19,6 +19,7 @@ export class OptionsStore { this.selectedOption = null; this.formColumns = null; this.initialValue = value; + this.readOnly = false; this.setValue(value); makeObservable(this, { @@ -73,6 +74,10 @@ export class OptionsStore { this.formColumns = columns; } + setReadOnly(value) { + this.readOnly = value; + } + get isDirty() { return this.value !== this.initialValue; } @@ -83,5 +88,6 @@ export class OptionsStore { reset() { this.setValue(this.initialValue); + this.setReadOnly(false); } } diff --git a/app/scripts/react-directives/users/pageStore.js b/app/scripts/react-directives/users/pageStore.js new file mode 100644 index 00000000..7dac7cb0 --- /dev/null +++ b/app/scripts/react-directives/users/pageStore.js @@ -0,0 +1,232 @@ +'use strict'; + +import { makeAutoObservable, runInAction } from 'mobx'; +import PageWrapperStore from '../components/page-wrapper/pageWrapperStore'; +import { StringStore } from '../forms/stringStore'; +import i18n from '../../i18n/react/config'; +import { FormStore } from '../forms/formStore'; +import { makeNotEmptyValidator } from '../forms/stringValidators'; +import FormModalState from '../components/modals/formModalState'; +import { OptionsStore } from '../forms/optionsStore'; +import ConfirmModalState from '../components/modals/confirmModalState'; + +function sortUsers(users) { + users.sort((a, b) => { + if (a.type === b.type) { + return a.login.localeCompare(b.login); + } + return a.type.localeCompare(b.type); + }); +} + +class UsersPageAccessLevelStore { + constructor() { + this.notConfiguredAdmin = false; + this.accessGranted = true; + + makeAutoObservable(this); + } + + setNotConfiguredAdmin() { + this.notConfiguredAdmin = true; + this.accessGranted = true; + } + + setAccessNotGranted() { + this.accessGranted = false; + } +} + +class UsersPageStore { + constructor(rolesFactory) { + this.pageWrapperStore = new PageWrapperStore(i18n.t('users.title')); + this.accessLevelStore = new UsersPageAccessLevelStore(); + this.userParamsStore = new FormStore('userParams', { + login: new StringStore({ + name: i18n.t('users.labels.login'), + validator: makeNotEmptyValidator(), + }), + password: new StringStore({ + name: i18n.t('users.labels.password'), + validator: makeNotEmptyValidator(), + }), + type: new OptionsStore({ + name: i18n.t('users.labels.type'), + options: [ + { value: 'user', label: i18n.t('users.labels.user') }, + { value: 'operator', label: i18n.t('users.labels.operator') }, + { value: 'admin', label: i18n.t('users.labels.admin') }, + ], + value: 'user', + }), + }); + this.formModalState = new FormModalState(); + this.confirmModalState = new ConfirmModalState(); + this.users = []; + + makeAutoObservable(this); + + rolesFactory.notConfiguredAdminPromise.then(() => { + if (rolesFactory.notConfiguredAdmin) { + this.accessLevelStore.setNotConfiguredAdmin(); + this.pageWrapperStore.setLoading(false); + return; + } + if (rolesFactory.current.roles.isAdmin) { + this.loadUsers(); + } else { + this.accessLevelStore.setAccessNotGranted(); + this.pageWrapperStore.setLoading(false); + } + }); + } + + processFetchError(fetchResponse) { + switch (fetchResponse.status) { + case 403: + this.pageWrapperStore.setError(i18n.t('users.errors.forbidden')); + break; + case 404: + this.pageWrapperStore.setError(i18n.t('users.errors.old-backend')); + break; + default: + fetchResponse + .text() + .then(text => + this.pageWrapperStore.setError( + i18n.t('users.errors.unknown', { msg: text, interpolation: { escapeValue: false } }) + ) + ); + } + } + + async showUserEditModal() { + return await this.formModalState.show( + i18n.t('users.labels.user'), + this.userParamsStore, + i18n.t('users.buttons.save') + ); + } + + async loadUsers() { + this.pageWrapperStore.setLoading(true); + try { + const res = await fetch('/auth/users'); + if (res.ok) { + this.setUsers(await res.json()); + } else { + this.processFetchError(res); + } + } catch (error) { + this.pageWrapperStore.setError(error); + this.setUsers([]); + } finally { + this.pageWrapperStore.setLoading(false); + } + } + + setUsers(users) { + sortUsers(users); + this.users = users; + } + + async execRequest(url, request) { + try { + this.pageWrapperStore.clearError(); + this.pageWrapperStore.setLoading(true); + const res = await fetch(url, request); + if (res.ok) { + this.pageWrapperStore.setLoading(false); + return res; + } + this.processFetchError(res); + } catch (error) { + this.pageWrapperStore.setError(error); + } + this.pageWrapperStore.setLoading(false); + return null; + } + + async addUser() { + this.userParamsStore.reset(); + if (this.accessLevelStore.notConfiguredAdmin) { + this.userParamsStore.params.type.setValue('admin'); + this.userParamsStore.params.type.setReadOnly(true); + } + const user = await this.showUserEditModal(); + if (!user) { + return; + } + const res = await this.execRequest('/auth/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(user), + }); + if (res === null) { + return; + } + if (this.accessLevelStore.notConfiguredAdmin) { + window.location.href = '/login'; + return; + } + res.text().then(text => { + runInAction(() => { + user.id = text; + this.users.push(user); + sortUsers(this.users); + }); + }); + } + + async editUser(user) { + this.userParamsStore.reset(); + this.userParamsStore.setValue(user); + let modifiedUser = await this.showUserEditModal(); + if (!modifiedUser) { + return; + } + const res = await this.execRequest(`/auth/users/${user.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(modifiedUser), + }); + if (res == null) { + return; + } + user.name = modifiedUser.name; + user.type = modifiedUser.type; + sortUsers(this.users); + } + + async showDeleteConfirmModal(user) { + return this.confirmModalState.show( + i18n.t('users.labels.confirm-delete', { name: user.login }), + [ + { + label: i18n.t('users.buttons.delete'), + type: 'danger', + }, + ] + ); + } + + async deleteUser(user) { + if ((await this.showDeleteConfirmModal(user)) == 'ok') { + const res = await this.execRequest(`/auth/users/${user.id}`, { + method: 'DELETE', + }); + if (res === null) { + return; + } + runInAction(() => { + this.users = this.users.filter(u => u.id !== user.id); + }); + } + } +} + +export default UsersPageStore; diff --git a/app/scripts/react-directives/users/users.js b/app/scripts/react-directives/users/users.js new file mode 100644 index 00000000..9909d803 --- /dev/null +++ b/app/scripts/react-directives/users/users.js @@ -0,0 +1,27 @@ +'use strict'; + +import ReactDOM from 'react-dom/client'; +import CreateUsersPage from './usersPage'; +import UsersStore from './pageStore'; +import { setReactLocale } from '../locale'; + +function usersDirective(rolesFactory) { + 'ngInject'; + + setReactLocale(); + return { + restrict: 'E', + scope: {}, + link: function (scope, element) { + scope.store = new UsersStore(rolesFactory); + scope.root = ReactDOM.createRoot(element[0]); + scope.root.render(CreateUsersPage({ store: scope.store })); + + element.on('$destroy', function () { + scope.root.unmount(); + }); + }, + }; +} + +export default usersDirective; diff --git a/app/scripts/react-directives/users/usersPage.jsx b/app/scripts/react-directives/users/usersPage.jsx new file mode 100644 index 00000000..4fd9c402 --- /dev/null +++ b/app/scripts/react-directives/users/usersPage.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { observer } from 'mobx-react-lite'; +import { PageWrapper, PageTitle, PageBody } from '../components/page-wrapper/pageWrapper'; +import { useTranslation } from 'react-i18next'; +import FormModal from '../components/modals/formModal'; +import { Button } from '../common'; +import ConfirmModal from '../components/modals/confirmModal'; + +const UsersPage = observer(({ store }) => { + const { t } = useTranslation(); + return ( + + + + +
    +
    +
    + + + + + + + + + + + {store.users.map(user => ( + + + + + + ))} + +
    {t('users.labels.login')}{t('users.labels.type')}
    {user.login}{t('users.labels.' + user.type)} +
    +
    +
    +
    +
    + ); +}); + +function CreateUsersPage({ store }) { + return ; +} + +export default CreateUsersPage; diff --git a/app/scripts/services/roles.factory.js b/app/scripts/services/roles.factory.js index 1e3dbdab..641a4b70 100644 --- a/app/scripts/services/roles.factory.js +++ b/app/scripts/services/roles.factory.js @@ -6,25 +6,21 @@ export default function rolesFactory() { 'ngInject'; var roles = {}; + const DEFAULT_ROLE = 1; + roles._ROLE_ONE = { id: 1, - name: 'access.user.name', - shortName: 'access.user.short', - description: 'access.user.description', + name: 'app.roles.user', isAdmin: false, }; roles._ROLE_TWO = { id: 2, - name: 'access.operator.name', - shortName: 'access.operator.short', - description: 'access.operator.description', + name: 'app.roles.operator', isAdmin: false, }; roles._ROLE_THREE = { id: 3, - name: 'access.admin.name', - shortName: 'access.admin.short', - description: 'access.admin.description', + name: 'app.roles.admin', isAdmin: true, }; @@ -33,36 +29,31 @@ export default function rolesFactory() { roles.ROLE_THREE = roles._ROLE_THREE.id; roles.ROLES = [roles._ROLE_ONE, roles._ROLE_TWO, roles._ROLE_THREE]; - const setDefaultRole = (defaultRole = 1) => { - localStorage.setItem('role', defaultRole); - return defaultRole; - }; - - roles.current = { - role: localStorage.getItem('role') || setDefaultRole(), - roles: roles.ROLES[(localStorage.getItem('role') || 1) - 1], - }; - - roles.getRole = () => { - roles.current.role = localStorage.getItem('role'); - roles.current.roles = roles.ROLES[roles.current.role - 1]; - return roles.current.role; - }; - - roles.setRole = n => { - roles.current = { role: n, roles: roles.ROLES[n - 1] }; - localStorage.setItem('role', n); - }; - - roles.resetRole = n => { - localStorage.setItem('role', n); + const typeToRoleId = { + admin: roles.ROLE_THREE, + operator: roles.ROLE_TWO, + user: roles.ROLE_ONE, }; + const roleId = typeToRoleId[localStorage.getItem('user_type')] || DEFAULT_ROLE; + roles.current = { role: roleId, roles: roles.ROLES[roleId - 1] }; + roles.notConfiguredAdminResolve = null; + roles.notConfiguredAdmin = false; + roles.notConfiguredAdminPromise = new Promise(resolve => { + roles.notConfiguredAdminResolve = resolve; + }); // проверяет есть ли права доступа/просмотра // принимает значение минимально возможного статуса для доступа/просмотра roles.checkRights = onlyRoleGreatThanOrEqual => { - return roles.getRole() >= onlyRoleGreatThanOrEqual; + return roles.current.role >= onlyRoleGreatThanOrEqual; }; + fetch('/auth/check_config').then(response => { + if (response.ok) { + roles.notConfiguredAdmin = true; + } + roles.notConfiguredAdminResolve(); + }); + return roles; } diff --git a/app/styles/css/new.css b/app/styles/css/new.css index 05ad6625..4cae4ae3 100644 --- a/app/styles/css/new.css +++ b/app/styles/css/new.css @@ -369,3 +369,24 @@ a[ng-click]:hover { word-wrap: break-word; white-space: break-spaces; } + +.user-menu span { + font-size: var(--bar-font-size); + padding: 3px 20px; + display: block; + color: black; +} + +.user-menu .divider { + margin: 4px 0; +} + +.user-menu .glyphicon-user { + font-size: 20px; + color: lightgray; + transition: color 0.2s ease; +} + +.user-menu .glyphicon-user:hover { + color: gray; +} diff --git a/app/styles/main.css b/app/styles/main.css index ebe9e85a..d7a51cb4 100644 --- a/app/styles/main.css +++ b/app/styles/main.css @@ -170,24 +170,26 @@ ul.alert-dropdown { /* connection status */ .connection-status{ - float: right; - padding-right: 20px; - display: none; + float: right; + padding-right: 22px; + display: none; + margin-right: 0px; + margin-top: 13px; + margin-bottom: 0px; + display: flex; + align-items: center; + gap: 10px; } -@media(min-width:360px) { - .connection-status{ - display: inherit; - } + +.connection-status .glyphicon-user { + font-size: 20px; + cursor: pointer; } @media(min-width:768px) { .navbar-header { float: none; } - - .connection-status{ - padding-right: 40px; - } } @@ -933,19 +935,6 @@ body > double-bounce-spinner .double-bounce-spinner .double-bounce2 { background: #5cb300; } -@media (max-width: 500px) { - .big-screen-access-level { - display: none; - } -} - -@media (min-width: 501px) { - .mobile-screen-access-level { - display: none; - } -} - - /* Для совместимости с wb-mqtt-serial >= 2.9.0 */ h3.je-header { margin-top: 0px; diff --git a/app/views/access-level.html b/app/views/access-level.html index c1c6d306..3b9aeb2b 100644 --- a/app/views/access-level.html +++ b/app/views/access-level.html @@ -1,48 +1 @@ -

    {{'access.title'}}

    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -

    -
    -
    - -
    -
    - - + diff --git a/app/views/config.html b/app/views/config.html index c5ff3cf7..91b3beb0 100644 --- a/app/views/config.html +++ b/app/views/config.html @@ -1,6 +1,5 @@
    diff --git a/app/views/configs.html b/app/views/configs.html index 2e6f2b10..e15f3a04 100644 --- a/app/views/configs.html +++ b/app/views/configs.html @@ -1,8 +1,7 @@

    {{'configurations.title'}}

    diff --git a/app/views/devices.html b/app/views/devices.html index de075168..3bc5bbfc 100644 --- a/app/views/devices.html +++ b/app/views/devices.html @@ -1,6 +1,5 @@
    {{'devices.labels.nothing'}}
    diff --git a/app/views/logs.html b/app/views/logs.html index b03a385a..186b08b1 100644 --- a/app/views/logs.html +++ b/app/views/logs.html @@ -5,8 +5,7 @@

    diff --git a/app/views/network-connections.html b/app/views/network-connections.html index 411612ce..353e7e01 100644 --- a/app/views/network-connections.html +++ b/app/views/network-connections.html @@ -1,6 +1,5 @@
    diff --git a/app/views/scripts.html b/app/views/scripts.html index 7149b580..ff526323 100644 --- a/app/views/scripts.html +++ b/app/views/scripts.html @@ -3,8 +3,7 @@

    {{'rules.title'}}

    diff --git a/app/views/system.html b/app/views/system.html index cd6865ef..b9f099a0 100644 --- a/app/views/system.html +++ b/app/views/system.html @@ -1,8 +1,7 @@

    {{'system.title'}}

    diff --git a/debian/changelog b/debian/changelog index 1b7b3455..ffb8521b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +wb-mqtt-homeui (2.108.0) stable; urgency=medium + + * Add authentication + + -- Petr Krasnoshchekov Wed, 18 Dec 2024 15:49:56 +0500 + wb-mqtt-homeui (2.107.4) stable; urgency=medium * Fix enum translations in text dashboards diff --git a/debian/control b/debian/control index 9d539b82..08a0a57f 100644 --- a/debian/control +++ b/debian/control @@ -1,5 +1,5 @@ Source: wb-mqtt-homeui -Maintainer: Evgeny Boger +Maintainer: Wiren Board team Section: misc Priority: optional Standards-Version: 4.5.1 @@ -10,7 +10,7 @@ Package: wb-mqtt-homeui Architecture: all Conflicts: wb-homa-webinterface Depends: ${shlibs:Depends}, ${misc:Depends}, mosquitto, mqtt-wss, mqtt-tools, nginx-extras, diffutils, wb-utils (>= 4.20.1), - wb-configs (>= 3.26.0) + wb-configs (>= 3.35.0~~) Recommends: wb-mqtt-logs, wb-device-manager Suggests: wb-mqtt-confed (>= 1.4.0), Breaks: wb-mqtt-confed (<< 1.0.3), wb-mqtt-db (<< 1.5), wb-mqtt-serial (<< 2.116.0~~), wb-device-manager (<< 1.4.0~~), diff --git a/login/login.html b/login/login.html new file mode 100644 index 00000000..068645d4 --- /dev/null +++ b/login/login.html @@ -0,0 +1,185 @@ + + + + + + + Login Page + + + +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 490e8145..97360b05 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -87,13 +87,19 @@ module.exports = (function makeWebpackConfig() { type: 'asset/resource', }, { - // without hash - test: /\.(svg|woff|woff2|ttf|eot)$/, + test: /\.(svg)$/, type: 'asset/resource', generator: { filename: '[name][ext]', }, }, + { + test: /\.(woff|woff2|ttf|eot)$/, + type: 'asset/resource', + generator: { + filename: 'fonts/[name][ext]', + }, + }, { test: /\.html$/, type: 'asset/source', @@ -305,6 +311,12 @@ module.exports = (function makeWebpackConfig() { }, port: 8080, hot: true, + proxy: [ + { + context: ['/auth/check_config', '/auth/users', '/login'], + target: 'http://10.200.200.1', + }, + ], }; return config;