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 @@
+
+
+ {{'app.errors.not-configured-admin'}}
+
@@ -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 (
);
});
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 (
+
+
+
+
+
+
+
+
+
+
+
+ {t('users.labels.login')} |
+ {t('users.labels.type')} |
+ |
+
+
+
+ {store.users.map(user => (
+
+ {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.buttons.apply'}}
+
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 @@
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 @@
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 @@
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;