From 3be00d18872e15f2c7a7a0d07d57aa2a78c8bba2 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 20 Jan 2022 10:24:07 +0100 Subject: [PATCH] Finishing touches --- libnymea-app/jsonrpc/jsonrpcclient.cpp | 20 +- libnymea-app/jsonrpc/jsonrpcclient.h | 2 +- libnymea-app/types/tokeninfo.h | 5 + libnymea-app/types/tokeninfos.cpp | 8 + libnymea-app/types/tokeninfos.h | 2 + libnymea-app/types/userinfo.cpp | 26 ++ libnymea-app/types/userinfo.h | 12 + libnymea-app/usermanager.cpp | 48 ++- libnymea-app/usermanager.h | 6 +- nymea-app/images.qrc | 1 + nymea-app/resources.qrc | 1 + nymea-app/ui/MainMenu.qml | 4 +- nymea-app/ui/SettingsPage.qml | 24 +- nymea-app/ui/components/NymeaTextField.qml | 19 ++ nymea-app/ui/components/PasswordTextField.qml | 11 +- nymea-app/ui/connection/LoginPage.qml | 169 +++++----- .../ui/devicepages/GenericDevicePage.qml | 2 + nymea-app/ui/images/contact-group.svg | 193 ++++++++++++ nymea-app/ui/system/UsersSettingsPage.qml | 291 +++++++++++++++--- nymea-app/ui/utils/NymeaUtils.qml | 4 +- 20 files changed, 706 insertions(+), 142 deletions(-) create mode 100644 nymea-app/ui/components/NymeaTextField.qml create mode 100644 nymea-app/ui/images/contact-group.svg diff --git a/libnymea-app/jsonrpc/jsonrpcclient.cpp b/libnymea-app/jsonrpc/jsonrpcclient.cpp index 4822d768..7a76dbc5 100644 --- a/libnymea-app/jsonrpc/jsonrpcclient.cpp +++ b/libnymea-app/jsonrpc/jsonrpcclient.cpp @@ -379,11 +379,15 @@ QVariantMap JsonRpcClient::experiences() const return m_experiences; } -int JsonRpcClient::createUser(const QString &username, const QString &password) +int JsonRpcClient::createUser(const QString &username, const QString &password, const QString &displayName, const QString &email) { QVariantMap params; params.insert("username", username); params.insert("password", password); + if (ensureServerVersion("6.0")) { + params.insert("displayName", displayName); + params.insert("email", email); + } JsonRpcReply* reply = createReply("JSONRPC.CreateUser", params, this, "processCreateUser"); m_replies.insert(reply->commandId(), reply); m_connection->sendData(QJsonDocument::fromVariant(reply->requestMap()).toJson()); @@ -639,7 +643,7 @@ void JsonRpcClient::dataReceived(const QByteArray &data) JsonRpcReply *reply = m_replies.take(commandId); if (reply) { reply->deleteLater(); - // qDebug() << QString("JsonRpc: got response for %1.%2: %3").arg(reply->nameSpace(), reply->method(), QString::fromUtf8(jsonDoc.toJson(QJsonDocument::Indented))) << reply->callback() << reply->callback(); + qWarning() << QString("JsonRpc: got response for %1.%2: %3").arg(reply->nameSpace(), reply->method(), QString::fromUtf8(jsonDoc.toJson(QJsonDocument::Indented))) << reply->callback() << reply->callback(); if (dataMap.value("status").toString() == "unauthorized") { qWarning() << "Something's off with the token"; @@ -767,6 +771,18 @@ void JsonRpcClient::helloReply(int /*commandId*/, const QVariantMap ¶ms) // qDebug() << "Caches:" << m_cacheHashes; if (m_jsonRpcVersion.majorVersion() >= 6 && m_authenticationRequired) { + if (!params.value("authenticated").toBool()) { + qCWarning(dcJsonRpc) << "Seems our token is not valid!"; + m_token.clear(); + QSettings settings; + settings.beginGroup("jsonTokens"); + settings.setValue(m_serverUuid, m_token); + settings.endGroup(); + emit authenticationRequiredChanged(); + m_authenticated = false; + emit authenticatedChanged(); + return; + } m_permissionScopes = UserInfo::listToScopes(params.value("permissionScopes").toStringList()); } else { m_permissionScopes = UserInfo::PermissionScopeAdmin; diff --git a/libnymea-app/jsonrpc/jsonrpcclient.h b/libnymea-app/jsonrpc/jsonrpcclient.h index e55a3ad3..035f459d 100644 --- a/libnymea-app/jsonrpc/jsonrpcclient.h +++ b/libnymea-app/jsonrpc/jsonrpcclient.h @@ -116,7 +116,7 @@ public: Q_INVOKABLE bool ensureServerVersion(const QString &jsonRpcVersion); - Q_INVOKABLE int createUser(const QString &username, const QString &password); + Q_INVOKABLE int createUser(const QString &username, const QString &password, const QString &displayName, const QString &email); Q_INVOKABLE int authenticate(const QString &username, const QString &password, const QString &deviceName); Q_INVOKABLE int requestPushButtonAuth(const QString &deviceName); Q_INVOKABLE int setupRemoteAccess(const QString &idToken, const QString &userId); diff --git a/libnymea-app/types/tokeninfo.h b/libnymea-app/types/tokeninfo.h index c27f10f3..10c57419 100644 --- a/libnymea-app/types/tokeninfo.h +++ b/libnymea-app/types/tokeninfo.h @@ -8,6 +8,11 @@ class TokenInfo : public QObject { Q_OBJECT + Q_PROPERTY(QUuid id READ id CONSTANT) + Q_PROPERTY(QString username READ username CONSTANT) + Q_PROPERTY(QString deviceName READ deviceName CONSTANT) + Q_PROPERTY(QDateTime creationTime READ creationTime CONSTANT) + public: explicit TokenInfo(const QUuid &id, const QString &username, const QString &deviceName, const QDateTime &creationTime, QObject *parent = nullptr); diff --git a/libnymea-app/types/tokeninfos.cpp b/libnymea-app/types/tokeninfos.cpp index e66fb3a6..ba31ee6f 100644 --- a/libnymea-app/types/tokeninfos.cpp +++ b/libnymea-app/types/tokeninfos.cpp @@ -58,3 +58,11 @@ void TokenInfos::removeToken(const QUuid &tokenId) } } } + +TokenInfo *TokenInfos::get(int index) const +{ + if (index < 0 || index >= m_list.count()) { + return nullptr; + } + return m_list.at(index); +} diff --git a/libnymea-app/types/tokeninfos.h b/libnymea-app/types/tokeninfos.h index ed666253..58b0e463 100644 --- a/libnymea-app/types/tokeninfos.h +++ b/libnymea-app/types/tokeninfos.h @@ -26,6 +26,8 @@ public: void addToken(TokenInfo *tokenInfo); void removeToken(const QUuid &tokenId); + Q_INVOKABLE TokenInfo* get(int index) const; + signals: void countChanged(); diff --git a/libnymea-app/types/userinfo.cpp b/libnymea-app/types/userinfo.cpp index 2a8afdd1..c3112e15 100644 --- a/libnymea-app/types/userinfo.cpp +++ b/libnymea-app/types/userinfo.cpp @@ -29,6 +29,32 @@ void UserInfo::setUsername(const QString &username) } } +QString UserInfo::email() const +{ + return m_email; +} + +void UserInfo::setEmail(const QString &email) +{ + if (m_email != email) { + m_email = email; + emit emailChanged(); + } +} + +QString UserInfo::displayName() const +{ + return m_displayName; +} + +void UserInfo::setDisplayName(const QString &displayName) +{ + if (m_displayName != displayName) { + m_displayName = displayName; + emit displayNameChanged(); + } +} + UserInfo::PermissionScopes UserInfo::scopes() const { return m_scopes; diff --git a/libnymea-app/types/userinfo.h b/libnymea-app/types/userinfo.h index 6aaf4109..98373d18 100644 --- a/libnymea-app/types/userinfo.h +++ b/libnymea-app/types/userinfo.h @@ -7,6 +7,8 @@ class UserInfo : public QObject { Q_OBJECT Q_PROPERTY(QString username READ username NOTIFY usernameChanged) + Q_PROPERTY(QString email READ email NOTIFY emailChanged) + Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged) Q_PROPERTY(PermissionScopes scopes READ scopes NOTIFY scopesChanged) public: enum PermissionScope { @@ -26,6 +28,12 @@ public: QString username() const; void setUsername(const QString &username); + QString email() const; + void setEmail(const QString &email); + + QString displayName() const; + void setDisplayName(const QString &displayName); + PermissionScopes scopes() const; void setScopes(PermissionScopes scopes); @@ -34,10 +42,14 @@ public: signals: void usernameChanged(); + void emailChanged(); + void displayNameChanged(); void scopesChanged(); private: QString m_username; + QString m_email; + QString m_displayName; PermissionScopes m_scopes = PermissionScopeNone; }; diff --git a/libnymea-app/usermanager.cpp b/libnymea-app/usermanager.cpp index 044943a5..3689f660 100644 --- a/libnymea-app/usermanager.cpp +++ b/libnymea-app/usermanager.cpp @@ -70,12 +70,15 @@ Users *UserManager::users() const return m_users; } -int UserManager::createUser(const QString &username, const QString &password, int permissionScopes) + +int UserManager::createUser(const QString &username, const QString &password, const QString &displayName, const QString &email, int permissionScopes) { QVariantMap params; params.insert("username", username); params.insert("password", password); - if (m_engine->jsonRpcClient()->ensureServerVersion("5.6")) { + if (m_engine->jsonRpcClient()->ensureServerVersion("6.0")) { + params.insert("displayName", displayName); + params.insert("email", email); params.insert("scopes", UserInfo::scopesToList((UserInfo::PermissionScopes)permissionScopes)); } qCDebug(dcUserManager()) << "Creating user" << username << permissionScopes; @@ -115,6 +118,16 @@ int UserManager::setUserScopes(const QString &username, int scopes) return m_engine->jsonRpcClient()->sendCommand("Users.SetUserScopes", params, this, "setUserScopesResponse"); } +int UserManager::setUserInfo(const QString &username, const QString &displayName, const QString &email) +{ + QVariantMap params; + params.insert("username", username); + params.insert("displayName", displayName); + params.insert("email", email); + qCDebug(dcUserManager()) << "Setting new info for user" << username << displayName << email; + return m_engine->jsonRpcClient()->sendCommand("Users.SetUserInfo", params, this, "setUserInfoResponse"); +} + void UserManager::notificationReceived(const QVariantMap &data) { qCDebug(dcUserManager()) << "Users notification" << data; @@ -122,6 +135,8 @@ void UserManager::notificationReceived(const QVariantMap &data) if (notification == "Users.UserAdded") { QVariantMap userMap = data.value("params").toMap().value("userInfo").toMap(); UserInfo *info = new UserInfo(userMap.value("username").toString()); + info->setDisplayName(userMap.value("displayName").toString()); + info->setEmail(userMap.value("email").toString()); info->setScopes(UserInfo::listToScopes(userMap.value("scopes").toStringList())); m_users->insertUser(info); } else if (notification == "Users.UserRemoved") { @@ -129,12 +144,24 @@ void UserManager::notificationReceived(const QVariantMap &data) } else if (notification == "Users.UserChanged") { QVariantMap userMap = data.value("params").toMap().value("userInfo").toMap(); QString username = userMap.value("username").toString(); + QString displayName = userMap.value("displayName").toString(); + QString email = userMap.value("email").toString(); + UserInfo::PermissionScopes scopes = UserInfo::listToScopes(userMap.value("scopes").toStringList()); + // Update current user info + if (m_userInfo && m_userInfo->username() == username) { + m_userInfo->setDisplayName(displayName); + m_userInfo->setEmail(email); + m_userInfo->setScopes(scopes); + } + // Update user info in the list of all users. UserInfo *info = m_users->getUserInfo(username); if (!info) { qCWarning(dcUserManager()) << "Received a change notification for a user we don't know:" << username; return; } - info->setScopes(UserInfo::listToScopes(userMap.value("scopes").toStringList())); + info->setDisplayName(displayName); + info->setEmail(email); + info->setScopes(scopes); } } @@ -145,6 +172,8 @@ void UserManager::getUsersResponse(int commandId, const QVariantMap &data) foreach (const QVariant &userVariant, data.value("users").toList()) { QVariantMap userMap = userVariant.toMap(); UserInfo *userInfo = new UserInfo(userMap.value("username").toString()); + userInfo->setDisplayName(userMap.value("displayName").toString()); + userInfo->setEmail(userMap.value("email").toString()); userInfo->setScopes(UserInfo::listToScopes(userMap.value("scopes").toStringList())); m_users->insertUser(userInfo); } @@ -155,6 +184,8 @@ void UserManager::getUserInfoResponse(int commandId, const QVariantMap &data) qCDebug(dcUserManager()) << "User info reply" << commandId << data; QVariantMap userMap = data.value("userInfo").toMap(); m_userInfo->setUsername(userMap.value("username").toString()); + m_userInfo->setEmail(userMap.value("email").toString()); + m_userInfo->setDisplayName(userMap.value("displayName").toString()); m_userInfo->setScopes(UserInfo::listToScopes(userMap.value("scopes").toStringList())); } @@ -203,6 +234,9 @@ void UserManager::changePasswordResponse(int commandId, const QVariantMap ¶m void UserManager::createUserResponse(int commandId, const QVariantMap ¶ms) { qCDebug(dcUserManager()) << "Create user response:" << commandId << params; + QMetaEnum metaEnum = QMetaEnum::fromType(); + UserError error = static_cast(metaEnum.keyToValue(params.value("error").toString().toUtf8())); + emit createUserReply(commandId, error); } void UserManager::removeUserResponse(int commandId, const QVariantMap ¶ms) @@ -221,6 +255,14 @@ void UserManager::setUserScopesResponse(int commandId, const QVariantMap ¶ms emit setUserScopesReply(commandId, error); } +void UserManager::setUserInfoResponse(int commandId, const QVariantMap ¶ms) +{ + qCDebug(dcUserManager()) << "Set user info response:" << commandId << params; + QMetaEnum metaEnum = QMetaEnum::fromType(); + UserError error = static_cast(metaEnum.keyToValue(params.value("error").toString().toUtf8())); + emit setUserInfoReply(commandId, error); +} + Users::Users(QObject *parent): QAbstractListModel(parent) { diff --git a/libnymea-app/usermanager.h b/libnymea-app/usermanager.h index cae16a96..3d380940 100644 --- a/libnymea-app/usermanager.h +++ b/libnymea-app/usermanager.h @@ -46,21 +46,24 @@ public: Users *users() const; // NOTE: Q_FLAG from another QObject (UserInfo::PermissionScopes) doesn't seem to work in certain Qt versions. Using int instead - Q_INVOKABLE int createUser(const QString &username, const QString &password, int permissionScopes = UserInfo::PermissionScopeAdmin); + Q_INVOKABLE int createUser(const QString &username, const QString &password, const QString &displayName, const QString &email, int permissionScopes = UserInfo::PermissionScopeAdmin); Q_INVOKABLE int changePassword(const QString &newPassword); Q_INVOKABLE int removeToken(const QUuid &id); Q_INVOKABLE int removeUser(const QString &username); // NOTE: Q_FLAG from another QObject (UserInfo::PermissionScopes) doesn't seem to work in certain Qt versions. Using int instead Q_INVOKABLE int setUserScopes(const QString &username, int permissionScopes); + Q_INVOKABLE int setUserInfo(const QString &username, const QString &displayName, const QString &email); signals: void engineChanged(); void loadingChanged(); + void createUserReply(int id, UserError error); void removeTokenReply(int id, UserError error); void changePasswordReply(int id, UserError error); void removeUserReply(int id, UserError error); void setUserScopesReply(int id, UserError error); + void setUserInfoReply(int id, UserError error); private slots: void notificationReceived(const QVariantMap &data); @@ -73,6 +76,7 @@ private slots: void createUserResponse(int commandId, const QVariantMap ¶ms); void removeUserResponse(int commandId, const QVariantMap ¶ms); void setUserScopesResponse(int commandId, const QVariantMap ¶ms); + void setUserInfoResponse(int commandId, const QVariantMap ¶ms); private: Engine *m_engine = nullptr; diff --git a/nymea-app/images.qrc b/nymea-app/images.qrc index 8ba84fd5..c3497c45 100644 --- a/nymea-app/images.qrc +++ b/nymea-app/images.qrc @@ -270,5 +270,6 @@ ui/images/sensors/orp.svg ui/images/sensors/co.svg ui/images/sensors/gas.svg + ui/images/contact-group.svg diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index f88c2ac2..74f77410 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -273,5 +273,6 @@ ui/mainviews/energy/ConsumersPieChart.qml ui/components/NymeaToolTip.qml ui/components/SettingsTile.qml + ui/components/NymeaTextField.qml diff --git a/nymea-app/ui/MainMenu.qml b/nymea-app/ui/MainMenu.qml index a58b6be3..55318bf4 100644 --- a/nymea-app/ui/MainMenu.qml +++ b/nymea-app/ui/MainMenu.qml @@ -140,7 +140,7 @@ Drawer { text: qsTr("Configure things") iconName: "../images/things.svg" visible: root.currentEngine && root.currentEngine.jsonRpcClient.currentHost - && NymeaUtils.hasPermissionScope(root.currentEngine, UserInfo.PermissionScopeConfigureThings) + && NymeaUtils.hasPermissionScope(root.currentEngine.jsonRpcClient.permissions, UserInfo.PermissionScopeConfigureThings) && root.currentEngine.jsonRpcClient.connected progressive: false onClicked: { @@ -154,7 +154,7 @@ Drawer { iconName: "../images/magic.svg" progressive: false visible: root.currentEngine && root.currentEngine.jsonRpcClient.currentHost - && NymeaUtils.hasPermissionScope(root.currentEngine, UserInfo.PermissionScopeConfigureRules) + && NymeaUtils.hasPermissionScope(root.currentEngine.jsonRpcClient.permissions, UserInfo.PermissionScopeConfigureRules) && root.currentEngine.jsonRpcClient.connected && Configuration.magicEnabled onClicked: { root.openMagicSettings(); diff --git a/nymea-app/ui/SettingsPage.qml b/nymea-app/ui/SettingsPage.qml index 82d7ac35..cdcff4d5 100644 --- a/nymea-app/ui/SettingsPage.qml +++ b/nymea-app/ui/SettingsPage.qml @@ -61,7 +61,7 @@ Page { iconSource: "../images/configure.svg" text: qsTr("General") subText: qsTr("Change system name and time zone") - visible: NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) + visible: NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) onClicked: pageStack.push(Qt.resolvedUrl("system/GeneralSettingsPage.qml")) } @@ -80,7 +80,7 @@ Page { iconSource: "../images/connections/network-wifi.svg" text: qsTr("Networking") subText: qsTr("Configure the system's network connection") - visible: NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) && Configuration.networkSettingsEnabled + visible: NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) && Configuration.networkSettingsEnabled onClicked: pageStack.push(Qt.resolvedUrl("system/NetworkSettingsPage.qml")) } @@ -89,7 +89,7 @@ Page { iconSource: "../images/connections/cloud.svg" text: qsTr("Cloud") subText: qsTr("Connect this %1 system to %1:cloud").arg(Configuration.systemName) - visible: NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) + visible: NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) onClicked: pageStack.push(Qt.resolvedUrl("system/CloudSettingsPage.qml")) } @@ -98,7 +98,7 @@ Page { iconSource: "../images/connections/network-vpn.svg" text: qsTr("API interfaces") subText: qsTr("Configure how clients interact with this system") - visible: NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) && Configuration.apiSettingsEnabled + visible: NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) && Configuration.apiSettingsEnabled onClicked: pageStack.push(Qt.resolvedUrl("system/ConnectionInterfacesPage.qml")) } @@ -107,7 +107,7 @@ Page { iconSource: "../images/mqtt.svg" text: qsTr("MQTT broker") subText: qsTr("Configure the MQTT broker") - visible: engine.jsonRpcClient.ensureServerVersion("1.11") && NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) && Configuration.mqttSettingsEnabled + visible: engine.jsonRpcClient.ensureServerVersion("1.11") && NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) && Configuration.mqttSettingsEnabled onClicked: pageStack.push(Qt.resolvedUrl("system/MqttBrokerSettingsPage.qml")) } @@ -116,7 +116,7 @@ Page { iconSource: "../images/stock_website.svg" text: qsTr("Web server") subText: qsTr("Configure the web server") - visible: NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) + visible: NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) && Configuration.webServerSettingsEnabled onClicked: pageStack.push(Qt.resolvedUrl("system/WebServerSettingsPage.qml")) } @@ -126,7 +126,7 @@ Page { iconSource: "../images/zigbee.svg" text: qsTr("ZigBee") subText: qsTr("Configure ZigBee networks") - visible: engine.jsonRpcClient.ensureServerVersion("5.3") && NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) && Configuration.zigbeeSettingsEnabled + visible: engine.jsonRpcClient.ensureServerVersion("5.3") && NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) && Configuration.zigbeeSettingsEnabled onClicked: pageStack.push(Qt.resolvedUrl("system/ZigbeeSettingsPage.qml")) } @@ -135,7 +135,7 @@ Page { iconSource: "../images/modbus.svg" text: qsTr("Modbus RTU") subText: qsTr("Configure Modbus RTU master interfaces") - visible: engine.jsonRpcClient.ensureServerVersion("5.6") && NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) && Configuration.modbusSettingsEnabled + visible: engine.jsonRpcClient.ensureServerVersion("5.6") && NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) && Configuration.modbusSettingsEnabled onClicked: pageStack.push(Qt.resolvedUrl("system/ModbusRtuSettingsPage.qml")) } @@ -144,7 +144,7 @@ Page { iconSource: "../images/plugin.svg" text: qsTr("Plugins") subText: qsTr("List and cofigure installed plugins") - visible: NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) && Configuration.pluginSettingsEnabled + visible: NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) && Configuration.pluginSettingsEnabled onClicked:pageStack.push(Qt.resolvedUrl("system/PluginsPage.qml")) } @@ -153,7 +153,7 @@ Page { iconSource: "../images/sdk.svg" text: qsTr("Developer tools") subText: qsTr("Access tools for debugging and error reporting") - visible: NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) + visible: NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) onClicked: pageStack.push(Qt.resolvedUrl("system/DeveloperTools.qml")) } @@ -163,7 +163,7 @@ Page { text: qsTr("System update") subText: qsTr("Update your %1 system").arg(Configuration.systemName) visible: engine.systemController.updateManagementAvailable && - NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) + NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) onClicked: pageStack.push(Qt.resolvedUrl("system/SystemUpdatePage.qml")) } @@ -172,7 +172,7 @@ Page { iconSource: "../images/logs.svg" text: qsTr("Log viewer") subText: qsTr("View system log") - visible: NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) + visible: NymeaUtils.hasPermissionScope(engine.jsonRpcClient.permissions, UserInfo.PermissionScopeAdmin) onClicked: pageStack.push(Qt.resolvedUrl("system/LogViewerPage.qml")) } diff --git a/nymea-app/ui/components/NymeaTextField.qml b/nymea-app/ui/components/NymeaTextField.qml new file mode 100644 index 00000000..556e8a3f --- /dev/null +++ b/nymea-app/ui/components/NymeaTextField.qml @@ -0,0 +1,19 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.0 +import QtQuick.Controls.Material 2.0 +import Nymea 1.0 + +TextField { + id: control + + property bool error: false + + background: Rectangle { + y: control.height - height - control.bottomPadding + 8 + implicitWidth: 120 + height: control.activeFocus || control.hovered ? 2 : 1 + color: control.error ? Style.red : control.activeFocus ? Style.accentColor + : (control.hovered ? control.Material.primaryTextColor : control.Material.hintTextColor) + } +} + diff --git a/nymea-app/ui/components/PasswordTextField.qml b/nymea-app/ui/components/PasswordTextField.qml index 3f7fc0c9..286d6e31 100644 --- a/nymea-app/ui/components/PasswordTextField.qml +++ b/nymea-app/ui/components/PasswordTextField.qml @@ -65,15 +65,19 @@ ColumnLayout { property bool hiddenPassword: true + property bool showErrors: false + RowLayout { Layout.fillWidth: true - TextField { + NymeaTextField { id: passwordTextField Layout.fillWidth: true echoMode: root.hiddenPassword ? TextInput.Password : TextInput.Normal placeholderText: root.signup ? qsTr("Pick a password") : qsTr("Password") + error: root.showErrors && !root.isValidPassword + palette.toolTipBase: Style.tooltipBackgroundColor ToolTip.visible: root.signup && focus && !root.isValidPassword ToolTip.delay: 1000 ToolTip.onVisibleChanged: print("Tooltip visible changed:", ToolTip.visible, focus, root.isValidPassword) @@ -100,7 +104,7 @@ ColumnLayout { } var ret = [] for (var i = 0; i < texts.length; i++) { - var entry = "• ".arg(checks[i] ? "#ffffff" : Style.accentColor) + var entry = "• ".arg(checks[i] ? "#ffffff" : Style.red) entry += texts[i] entry += "" ret.push(entry) @@ -126,11 +130,12 @@ ColumnLayout { RowLayout { visible: root.signup - TextField { + NymeaTextField { id: confirmationPasswordTextField Layout.fillWidth: true echoMode: root.hiddenPassword ? TextInput.Password : TextInput.Normal placeholderText: qsTr("Confirm password") + error: root.showErrors && (!root.isValidPassword || !root.confirmationMatches) } } } diff --git a/nymea-app/ui/connection/LoginPage.qml b/nymea-app/ui/connection/LoginPage.qml index 529f958c..b63e229a 100644 --- a/nymea-app/ui/connection/LoginPage.qml +++ b/nymea-app/ui/connection/LoginPage.qml @@ -34,7 +34,7 @@ import QtQuick.Layouts 1.1 import Nymea 1.0 import "../components" -Page { +SettingsPageBase { id: root signal backPressed(); @@ -82,85 +82,116 @@ Page { } } - Flickable { - anchors.fill: parent - contentHeight: contentColumn.implicitHeight + ColumnLayout { + id: contentColumn + width: parent.width - ColumnLayout { - id: contentColumn - width: parent.width + spacing: app.margins + RowLayout { + Layout.margins: app.margins spacing: app.margins - RowLayout { - Layout.margins: app.margins - spacing: app.margins - - ColorIcon { - Layout.preferredHeight: Style.iconSize * 2 - Layout.preferredWidth: Style.iconSize * 2 - name: "../images/lock-closed.svg" - color: Style.accentColor - } - - Label { - Layout.fillWidth: true - text: engine.jsonRpcClient.initialSetupRequired ? - qsTr("In order to use your %1 system, please create an account.").arg(Configuration.systemName) - : qsTr("In order to use your %1 system, please log in.").arg(Configuration.systemName) - wrapMode: Text.WordWrap - } + ColorIcon { + Layout.preferredHeight: Style.iconSize * 2 + Layout.preferredWidth: Style.iconSize * 2 + name: "../images/lock-closed.svg" + color: Style.accentColor } - - GridLayout { + Label { Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins - columns: app.width > 500 ? 2 : 1 - columnSpacing: app.margins + text: engine.jsonRpcClient.initialSetupRequired ? + qsTr("In order to use your %1 system, please create an account.").arg(Configuration.systemName) + : qsTr("In order to use your %1 system, please log in.").arg(Configuration.systemName) + wrapMode: Text.WordWrap + } + } - Label { - text: engine.jsonRpcClient.ensureServerVersion("7.0") ? qsTr("Username:") : qsTr("Your e-mail address:") - Layout.fillWidth: true - Layout.minimumWidth: implicitWidth - } - TextField { - id: usernameTextField - Layout.fillWidth: true - inputMethodHints: engine.jsonRpcClient.ensureServerVersion("7.0") - ? Qt.ImhEmailCharactersOnly | Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText - : Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText -// placeholderText: "john.smith@cooldomain.com" - } - Label { - Layout.fillWidth: true - text: qsTr("Password:") - } - PasswordTextField { - id: passwordTextField - Layout.fillWidth: true - minPasswordLength: 8 - requireLowerCaseLetter: true - requireUpperCaseLetter: true - requireNumber: true - requireSpecialChar: false - signup: engine.jsonRpcClient.initialSetupRequired + + GridLayout { + id: loginForm + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins + columns: app.width > 500 ? 2 : 1 + columnSpacing: app.margins + + property bool showErrors: false + + Label { + text: (engine.jsonRpcClient.ensureServerVersion("6.0") ? qsTr("Username") : qsTr("Your e-mail address")) + Layout.fillWidth: true + Layout.minimumWidth: implicitWidth + } + NymeaTextField { + id: usernameTextField + Layout.fillWidth: true + placeholderText: qsTr("Required") + inputMethodHints: engine.jsonRpcClient.ensureServerVersion("6.0") + ? Qt.ImhEmailCharactersOnly | Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText + : Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText + error: loginForm.showErrors && !acceptableInput + validator: RegExpValidator { + regExp: /[a-zA-Z0-9_\\.+-@]{3,}/ } } - - Button { + Label { Layout.fillWidth: true - Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.bottomMargin: app.margins - text: qsTr("OK") - enabled: usernameTextField.displayText.length >= 3 && passwordTextField.isValid - onClicked: { - if (engine.jsonRpcClient.initialSetupRequired) { - print("create user") - engine.jsonRpcClient.createUser(usernameTextField.text, passwordTextField.password); - } else { - print("authenticate", usernameTextField.text, passwordTextField.text, "nymea-app") - engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app (" + PlatformHelper.deviceModel + ")"); - } + text: qsTr("Password") + } + PasswordTextField { + id: passwordTextField + Layout.fillWidth: true + minPasswordLength: 8 + requireLowerCaseLetter: true + requireUpperCaseLetter: true + requireNumber: true + requireSpecialChar: false + signup: engine.jsonRpcClient.initialSetupRequired + showErrors: loginForm.showErrors + } + + Label { + text: qsTr("Your name") + Layout.fillWidth: true + visible: engine.jsonRpcClient.ensureServerVersion("6.0") && engine.jsonRpcClient.initialSetupRequired + } + TextField { + id: displayNameTextField + Layout.fillWidth: true + placeholderText: qsTr("Optional") + visible: engine.jsonRpcClient.ensureServerVersion("6.0") && engine.jsonRpcClient.initialSetupRequired + } + + Label { + text: qsTr("Email") + Layout.fillWidth: true + visible: engine.jsonRpcClient.ensureServerVersion("6.0") && engine.jsonRpcClient.initialSetupRequired + } + TextField { + id: emailTextField + Layout.fillWidth: true + placeholderText: qsTr("Optional") + visible: engine.jsonRpcClient.ensureServerVersion("6.0") && engine.jsonRpcClient.initialSetupRequired + } + } + + Button { + Layout.fillWidth: true + Layout.leftMargin: app.margins; Layout.rightMargin: app.margins; Layout.bottomMargin: app.margins + text: qsTr("OK") + onClicked: { + loginForm.showErrors = true + if (!usernameTextField.acceptableInput || !passwordTextField.isValid) { + return; + } + + if (engine.jsonRpcClient.initialSetupRequired) { + print("create user") + engine.jsonRpcClient.createUser(usernameTextField.text, passwordTextField.password, displayNameTextField.text, emailTextField.text); + } else { + print("authenticate", usernameTextField.text, passwordTextField.text, "nymea-app") + engine.jsonRpcClient.authenticate(usernameTextField.text, passwordTextField.password, "nymea-app (" + PlatformHelper.deviceModel + ")"); } } } diff --git a/nymea-app/ui/devicepages/GenericDevicePage.qml b/nymea-app/ui/devicepages/GenericDevicePage.qml index 64249d5c..88001b53 100644 --- a/nymea-app/ui/devicepages/GenericDevicePage.qml +++ b/nymea-app/ui/devicepages/GenericDevicePage.qml @@ -272,11 +272,13 @@ ThingPageBase { target: stateDelegateLoader.item property: "from" value: stateDelegate.thingState.minValue + when: stateDelegateLoader.item.hasOwnProperty("from") } Binding { target: stateDelegateLoader.item property: "to" value: stateDelegate.thingState.maxValue + when: stateDelegateLoader.item.hasOwnProperty("to") } Binding { target: stateDelegateLoader.item.hasOwnProperty("unit") ? stateDelegateLoader.item : null diff --git a/nymea-app/ui/images/contact-group.svg b/nymea-app/ui/images/contact-group.svg new file mode 100644 index 00000000..3592a3e1 --- /dev/null +++ b/nymea-app/ui/images/contact-group.svg @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/nymea-app/ui/system/UsersSettingsPage.qml b/nymea-app/ui/system/UsersSettingsPage.qml index db92aa52..6ac5404c 100644 --- a/nymea-app/ui/system/UsersSettingsPage.qml +++ b/nymea-app/ui/system/UsersSettingsPage.qml @@ -14,7 +14,7 @@ SettingsPageBase { engine: _engine onChangePasswordReply: { - if (error != UserManager.UserErrorNoError) { + if (error !== UserManager.UserErrorNoError) { var component = Qt.createComponent("../components/ErrorDialog.qml") var text; switch (error) { @@ -36,22 +36,37 @@ SettingsPageBase { popup.open() } } + } - SettingsPageSectionHeader { - text: qsTr("User info") + RowLayout { + Layout.margins: Style.margins + spacing: Style.margins + ColorIcon { + size: Style.hugeIconSize + source: "../images/account.svg" + color: Style.accentColor + } + ColumnLayout { + Label { + Layout.fillWidth: true + text: userManager.userInfo.displayName || userManager.userInfo.username + font: Style.bigFont + } + Label { + Layout.fillWidth: true + text: userManager.userInfo.username + visible: userManager.userInfo.displayName !== "" + } + Label { + Layout.fillWidth: true + text: userManager.userInfo.email + font: Style.smallFont + } + } } - NymeaSwipeDelegate { - Layout.fillWidth: true - text: userManager.userInfo.username - subText: qsTr("Username") - progressive: false - prominentSubText: false - iconName: "../images/account.svg" - } - - NymeaSwipeDelegate { + NymeaItemDelegate { Layout.fillWidth: true text: qsTr("Change password") iconName: "../images/key.svg" @@ -63,15 +78,17 @@ SettingsPageBase { } } - SettingsPageSectionHeader { - text: qsTr("Device access") + NymeaItemDelegate { + Layout.fillWidth: true + text: qsTr("Edit user information") + iconName: "../images/edit.svg" + onClicked: pageStack.push(editUserInfoComponent) } - Button { + NymeaItemDelegate { Layout.fillWidth: true - Layout.leftMargin: Style.margins - Layout.rightMargin: Style.margins text: qsTr("Manage authorized devices") + iconName: "../images/smartphone.svg" onClicked: { pageStack.push(manageTokensComponent) } @@ -82,17 +99,67 @@ SettingsPageBase { visible: userManager.userInfo.scopes & UserInfo.PermissionScopeAdmin } - Button { + NymeaItemDelegate { Layout.fillWidth: true - Layout.leftMargin: Style.margins - Layout.rightMargin: Style.margins text: qsTr("Manage users") visible: userManager.userInfo.scopes & UserInfo.PermissionScopeAdmin + iconName: "../images/contact-group.svg" onClicked: { pageStack.push(manageUsersComponent) } } + Component { + id: editUserInfoComponent + SettingsPageBase { + id: editUserInfoPage + title: qsTr("Edit user information") + GridLayout { + Layout.margins: Style.margins + columnSpacing: Style.margins + columns: 2 + Label { + text: qsTr("Your name") + } + NymeaTextField { + id: displayNameTextField + Layout.fillWidth: true + text: userManager.userInfo.displayName + } + Label { + text: qsTr("Email") + } + NymeaTextField { + id: emailTextField + Layout.fillWidth: true + text: userManager.userInfo.email + } + } + Button { + Layout.fillWidth: true + Layout.margins: Style.margins + text: qsTr("OK") + onClicked: { + editUserInfoPage.busy = true + userManager.setUserInfo(userManager.userInfo.username, displayNameTextField.text, emailTextField.text) + } + } + Connections { + target: userManager + onSetUserInfoReply: { + editUserInfoPage.busy = false + if (error != UserManager.UserErrorNoError) { + var component = Qt.createComponent("../components/ErrorDialog.qml") + var text = qsTr("Un unexpected error happened when creating the user. We're sorry for this. (Error code: %1)").arg(error); + var popup = component.createObject(app, {text: text}); + popup.open() + } else { + pageStack.pop() + } + } + } + } + } Component { id: changePasswordComponent @@ -146,6 +213,20 @@ SettingsPageBase { id: manageTokensPage title: qsTr("Device access") + Component { + id: confirmTokenDeletionComponent + MeaDialog { + headerIcon: "../images/lock-closed.svg" + title: qsTr("Remove device access") + text: qsTr("Are you sure you want to remove %1 from accessing your %2 system?").arg("" + tokenInfo.deviceName + "").arg(Configuration.systemName) + property TokenInfo tokenInfo: null + standardButtons: Dialog.Yes | Dialog.No + onAccepted: { + userManager.removeToken(tokenInfo.id) + } + } + } + SettingsPageSectionHeader { text: qsTr("Devices / Apps accessing %1").arg(Configuration.systemName) } @@ -160,9 +241,12 @@ SettingsPageBase { prominentSubText: false progressive: false canDelete: true + iconName: "../images/smartphone.svg" + onClicked: deleteClicked() onDeleteClicked: { - userManager.removeToken(model.id) + var popup = confirmTokenDeletionComponent.createObject(manageTokensPage, {tokenInfo: userManager.tokenInfos.get(index)}) + popup.open() } } } @@ -192,9 +276,10 @@ SettingsPageBase { Repeater { model: userManager.users - delegate: NymeaSwipeDelegate { + delegate: NymeaItemDelegate { Layout.fillWidth: true - text: model.username + text: engine.jsonRpcClient.ensureServerVersion("6.0") && model.displayName ? model.displayName : model.username + subText: engine.jsonRpcClient.ensureServerVersion("6.0") && model.displayName ? model.username : "" iconName: "/ui/images/account.svg" iconColor: userManager.userInfo.scopes & UserInfo.PermissionScopeAdmin ? Style.accentColor : Style.iconColor @@ -202,10 +287,6 @@ SettingsPageBase { onClicked: { pageStack.push(userDetailsComponent, {userInfo: userManager.users.get(index)}) } - - onDeleteClicked: { - userManager.removeUser(model.username) - } } } } @@ -215,18 +296,59 @@ SettingsPageBase { id: userDetailsComponent SettingsPageBase { id: userDetailsPage - title: qsTr("Manage user") + title: qsTr("Manage %1").arg(userInfo.username) property UserInfo userInfo: null - SettingsPageSectionHeader { - text: qsTr("User info") + Component { + id: confirmUserDeletionComponent + MeaDialog { + headerIcon: "../images/lock-closed.svg" + title: qsTr("Remove user") + text: qsTr("Are you sure you want to remove %1 from accessing your %2 system?").arg("" + userInfo.username + "").arg(Configuration.systemName) + property UserInfo userInfo: null + standardButtons: Dialog.Yes | Dialog.No + onAccepted: { + userDetailsPage.busy = true + userManager.removeUser(userInfo.username) + } + } } - NymeaItemDelegate { + SettingsPageSectionHeader { + text: qsTr("User information for %1").arg(userDetailsPage.userInfo.username) + } + + GridLayout { + Layout.leftMargin: Style.margins + Layout.rightMargin: Style.margins + columnSpacing: Style.margins + columns: 2 + Label { + text: qsTr("Name") + } + NymeaTextField { + id: displayNameTextField + Layout.fillWidth: true + text: userDetailsPage.userInfo.displayName + } + Label { + text: qsTr("Email") + } + NymeaTextField { + id: emailTextField + Layout.fillWidth: true + text: userDetailsPage.userInfo.email + } + } + + Button { Layout.fillWidth: true - text: userDetailsPage.userInfo.username - progressive: false + Layout.margins: Style.margins + text: qsTr("Save") + onClicked: { + userManager.setUserInfo(userDetailsPage.userInfo.username, displayNameTextField.text, emailTextField.text) + } } SettingsPageSectionHeader { @@ -268,7 +390,23 @@ SettingsPageBase { Layout.rightMargin: Style.margins text: qsTr("Remove this user") onClicked: { - userManager.removeUser(userDetailsPage.userInfo.username) + var popup = confirmUserDeletionComponent.createObject(userDetailsPage, {userInfo: userDetailsPage.userInfo}) + popup.open() + } + } + + Connections { + target: userManager + onRemoveUserReply: { + userDetailsPage.busy = false + if (error !== UserManager.UserErrorNoError) { + var component = Qt.createComponent("../components/ErrorDialog.qml") + var text = qsTr("Un unexpected error happened when creating the user. We're sorry for this. (Error code: %1)").arg(error); + var popup = component.createObject(app, {text: text}); + popup.open() + } else { + pageStack.pop(); + } } } } @@ -284,24 +422,50 @@ SettingsPageBase { property var permissionScopes: UserInfo.PermissionScopeNone SettingsPageSectionHeader { - text: qsTr("Login information") + text: qsTr("USer information") } - TextField { - id: usernameTextField - Layout.fillWidth: true - Layout.leftMargin: Style.margins - Layout.rightMargin: Style.margins - placeholderText: qsTr("Username") - inputMethodHints: Qt.ImhEmailCharactersOnly | Qt.ImhNoAutoUppercase - } - PasswordTextField { - id: passwordTextField + GridLayout { Layout.fillWidth: true Layout.leftMargin: Style.margins Layout.rightMargin: Style.margins + columns: 2 + Label { + text: qsTr("Username:") + "*" + } + TextField { + id: usernameTextField + Layout.fillWidth: true + inputMethodHints: Qt.ImhNoAutoUppercase + } + + Label { + text: qsTr("Password:") + "*" + Layout.alignment: Qt.AlignTop + Layout.topMargin: Style.smallMargins + } + PasswordTextField { + id: passwordTextField + Layout.fillWidth: true + } + + Label { + text: qsTr("Full name:") + } + TextField { + id: displayNameTextField + Layout.fillWidth: true + } + Label { + text: qsTr("e-mail:") + } + TextField { + id: emailTextField + Layout.fillWidth: true + } } + SettingsPageSectionHeader { text: qsTr("Permissions") } @@ -335,7 +499,40 @@ SettingsPageBase { Layout.rightMargin: Style.margins enabled: usernameTextField.displayText.length >= 3 && passwordTextField.isValid onClicked: { - userManager.createUser(usernameTextField.displayText, passwordTextField.password, createUserPage.permissionScopes) + createUserPage.busy = true + userManager.createUser(usernameTextField.displayText, passwordTextField.password, displayNameTextField.text, emailTextField.text, createUserPage.permissionScopes) + } + } + Connections { + target: userManager + onCreateUserReply: { + createUserPage.busy = false + if (error !== UserManager.UserErrorNoError) { + var component = Qt.createComponent("../components/ErrorDialog.qml") + var text; + switch (error) { + case UserManager.UserErrorInvalidUserId: + text = qsTr("The given username is not valid. It needs to be at least three characters long and not contain special characters."); + break; + case UserManager.UserErrorDuplicateUserId: + text = qsTr("The given username is already in use. Please choose a different username."); + break; + case UserManager.UserErrorBadPassword: + text = qsTr("The given password is not valid."); + break; + case UserManager.UserErrorPermissionDenied: + text = qsTr("Permission denied."); + break; + default: + text = qsTr("Un unexpected error happened when creating the user. We're sorry for this. (Error code: %1)").arg(error); + break; + } + + var popup = component.createObject(app, {text: text}); + popup.open() + } else { + pageStack.pop(); + } } } } diff --git a/nymea-app/ui/utils/NymeaUtils.qml b/nymea-app/ui/utils/NymeaUtils.qml index aab60709..e511af55 100644 --- a/nymea-app/ui/utils/NymeaUtils.qml +++ b/nymea-app/ui/utils/NymeaUtils.qml @@ -142,7 +142,7 @@ Item { ListElement { text: qsTr("Configure magic"); scope: UserInfo.PermissionScopeConfigureRules; resetOnUnset: UserInfo.PermissionScopeExecuteRules } } - function hasPermissionScope(engine, requestedScope) { - return (engine.jsonRpcClient.permissions & requestedScope) === requestedScope; + function hasPermissionScope(permissions, requestedScope) { + return (permissions & requestedScope) === requestedScope; } }