diff --git a/libnymea-app/jsonrpc/jsonrpcclient.cpp b/libnymea-app/jsonrpc/jsonrpcclient.cpp index 3487d2da..174e9b59 100644 --- a/libnymea-app/jsonrpc/jsonrpcclient.cpp +++ b/libnymea-app/jsonrpc/jsonrpcclient.cpp @@ -333,6 +333,11 @@ QHash JsonRpcClient::cacheHashes() const return m_cacheHashes; } +UserInfo::PermissionScopes JsonRpcClient::permissions() const +{ + return m_permissionScopes; +} + QString JsonRpcClient::serverVersion() const { return m_serverVersion; @@ -428,6 +433,12 @@ void JsonRpcClient::processAuthenticate(int /*commandId*/, const QVariantMap &da if (data.value("success").toBool()) { qDebug() << "authentication successful"; m_token = data.value("token").toByteArray(); + m_username = data.value("username").toString(); + if (m_jsonRpcVersion.majorVersion() >= 6) { + m_permissionScopes = UserInfo::listToScopes(data.value("scopes").toStringList()); + } else { + m_permissionScopes = UserInfo::PermissionScopeAdmin; + } QSettings settings; settings.beginGroup("jsonTokens"); settings.setValue(m_serverUuid, m_token); @@ -435,7 +446,7 @@ void JsonRpcClient::processAuthenticate(int /*commandId*/, const QVariantMap &da emit authenticationRequiredChanged(); m_authenticated = true; - emit authenticated(); + emit authenticatedChanged(); setNotificationsEnabled(); } else { @@ -489,6 +500,11 @@ void JsonRpcClient::setNotificationsEnabled() return; } + // We always want the Users notification to check for changed permissions + if (!namespaces.contains("Users")) { + namespaces.append("Users"); + } + QVariantMap params; if (ensureServerVersion("3.1")) { @@ -555,6 +571,14 @@ void JsonRpcClient::onInterfaceConnectedChanged(bool connected) qCInfo(dcJsonRpc()) << "JsonRpcClient: Transport connected. Starting handshake."; // Clear anything that might be left in the buffer from a previous connection. m_receiveBuffer.clear(); + + // Load token for this host + QSettings settings; + settings.beginGroup("jsonTokens"); + m_token = settings.value(currentHost()->uuid().toString()).toByteArray(); + settings.endGroup(); + + QVariantMap params; params.insert("locale", QLocale().name()); sendCommand("JSONRPC.Hello", params, this, "helloReply"); @@ -591,7 +615,17 @@ void JsonRpcClient::dataReceived(const QByteArray &data) // check if this is a notification if (dataMap.contains("notification")) { - // qDebug() << "Incoming notification:" << jsonDoc.toJson(); + qCDebug(dcJsonRpc()) << "Incoming notification:" << qUtf8Printable(jsonDoc.toJson()); + // Check if our permissions changed + if (dataMap.value("notification").toString() == "Users.UserChanged") { + QVariantMap userMap = dataMap.value("params").toMap().value("userInfo").toMap(); + if (userMap.value("username").toString() == m_username) { + m_permissionScopes = UserInfo::listToScopes(userMap.value("scopes").toStringList()); + qCritical() << "Permissions changed for" << userMap.value("username") << userMap.value("scopes").toStringList().join(",") << m_permissionScopes; + qCritical() << "***" << (m_permissionScopes & UserInfo::PermissionScopeConfigureThings); + emit permissionsChanged(); + } + } QStringList notification = dataMap.value("notification").toString().split("."); QString nameSpace = notification.first(); foreach (QObject *handler, m_notificationHandlers.values(nameSpace)) { @@ -678,10 +712,10 @@ void JsonRpcClient::helloReply(int /*commandId*/, const QVariantMap ¶ms) m_jsonRpcVersion = QVersionNumber::fromString(protoVersionString); - qCInfo(dcJsonRpc()) << "Handshake reply:" << "Protocol version:" << protoVersionString << "InitRequired:" << m_initialSetupRequired << "AuthRequired:" << m_authenticationRequired << "PushButtonAvailable:" << m_pushButtonAuthAvailable;; + qCInfo(dcJsonRpc()) << "Handshake reply:" << "Protocol version:" << protoVersionString << "InitRequired:" << m_initialSetupRequired << "AuthRequired:" << m_authenticationRequired << "PushButtonAvailable:" << m_pushButtonAuthAvailable; QVersionNumber minimumRequiredVersion = QVersionNumber(5, 0); - QVersionNumber maximumMajorVersion = QVersionNumber(5); + QVersionNumber maximumMajorVersion = QVersionNumber(6); if (m_jsonRpcVersion < minimumRequiredVersion) { qCWarning(dcJsonRpc()) << "Nymea core doesn't support minimum required version. Required:" << minimumRequiredVersion << "Found:" << m_jsonRpcVersion; emit invalidMinimumVersion(m_jsonRpcVersion.toString(), minimumRequiredVersion.toString()); @@ -732,6 +766,15 @@ void JsonRpcClient::helloReply(int /*commandId*/, const QVariantMap ¶ms) } // qDebug() << "Caches:" << m_cacheHashes; + if (m_jsonRpcVersion.majorVersion() >= 6) { + m_permissionScopes = UserInfo::listToScopes(params.value("permissionScopes").toStringList()); + } else { + m_permissionScopes = UserInfo::PermissionScopeAdmin; + } + m_username = params.value("username").toString(); + qCInfo(dcJsonRpc()) << "User:" << m_username << "Permissions:" << UserInfo::scopesToList(m_permissionScopes); + emit permissionsChanged(); + emit handshakeReceived(); if (m_connection->currentHost()->uuid().isNull()) { @@ -746,6 +789,7 @@ void JsonRpcClient::helloReply(int /*commandId*/, const QVariantMap ¶ms) } if (m_authenticationRequired) { + // Reload the token, now that we're certain about the server uuid. QSettings settings; settings.beginGroup("jsonTokens"); m_token = settings.value(m_serverUuid).toByteArray(); diff --git a/libnymea-app/jsonrpc/jsonrpcclient.h b/libnymea-app/jsonrpc/jsonrpcclient.h index 55748341..e55a3ad3 100644 --- a/libnymea-app/jsonrpc/jsonrpcclient.h +++ b/libnymea-app/jsonrpc/jsonrpcclient.h @@ -37,6 +37,7 @@ #include #include "connection/nymeaconnection.h" +#include "types/userinfo.h" class JsonRpcReply; class Param; @@ -63,6 +64,7 @@ class JsonRpcClient : public QObject Q_PROPERTY(QString serverQtBuildVersion READ serverQtBuildVersion NOTIFY serverQtVersionChanged) Q_PROPERTY(QVariantMap certificateIssuerInfo READ certificateIssuerInfo NOTIFY currentConnectionChanged) Q_PROPERTY(QVariantMap experiences READ experiences NOTIFY currentConnectionChanged) + Q_PROPERTY(UserInfo::PermissionScopes permissions READ permissions NOTIFY permissionsChanged) public: enum CloudConnectionState { @@ -94,6 +96,9 @@ public: CloudConnectionState cloudConnectionState() const; void deployCertificate(const QByteArray &rootCA, const QByteArray &certificate, const QByteArray &publicKey, const QByteArray &privateKey, const QString &endpoint); QHash cacheHashes() const; + // Note: This does not reflect the actual permission scopes of the user but is translated to effective permissions + // That, is, if the user has the admin permission, all of the other scopes will be set too even if they might not be explicitly set + UserInfo::PermissionScopes permissions() const; QString serverVersion() const; QString jsonRpcVersion() const; @@ -140,6 +145,7 @@ signals: void cloudConnectionStateChanged(); void serverQtVersionChanged(); void serverNameChanged(); + void permissionsChanged(); void responseReceived(const int &commandId, const QVariantMap &response); @@ -175,6 +181,8 @@ private: QByteArray m_receiveBuffer; QHash m_cacheHashes; QVariantMap m_experiences; + UserInfo::PermissionScopes m_permissionScopes = UserInfo::PermissionScopeNone; + QString m_username; void setNotificationsEnabled(); void getCloudConnectionStatus(); diff --git a/libnymea-app/libnymea-app-core.h b/libnymea-app/libnymea-app-core.h index 4ff13c5b..0c9afc61 100644 --- a/libnymea-app/libnymea-app-core.h +++ b/libnymea-app/libnymea-app-core.h @@ -353,6 +353,7 @@ void registerQmlTypes() { qmlRegisterUncreatableType(uri, 1, 0, "UserInfo", "Get it from UserManager"); qmlRegisterUncreatableType(uri, 1, 0, "TokenInfo", "Get it from TokenInfos"); qmlRegisterUncreatableType(uri, 1, 0, "TokenInfos", "Get it from UserManager"); + qmlRegisterUncreatableType(uri, 1, 0, "Users", "Get it from UserManager"); qmlRegisterUncreatableType(uri, 1, 0, "IOConnections", "Get it from ThingManager"); qmlRegisterUncreatableType(uri, 1, 0, "IOConnection", "Get it from IOConnections"); diff --git a/libnymea-app/types/thingclass.cpp b/libnymea-app/types/thingclass.cpp index 2c40d3e4..381230ff 100644 --- a/libnymea-app/types/thingclass.cpp +++ b/libnymea-app/types/thingclass.cpp @@ -187,9 +187,15 @@ QString ThingClass::baseInterface() const if (interface == "barcodescanner") { return "barcodescanner"; } + if (interface == "cleaningrobot") { + return "cleaningrobot"; + } if (interface == "account") { return "account"; } + if (interface == "thermostat") { + return "thermostat"; + } } return "uncategorized"; } diff --git a/libnymea-app/types/userinfo.cpp b/libnymea-app/types/userinfo.cpp index 8456d268..b4b81ca9 100644 --- a/libnymea-app/types/userinfo.cpp +++ b/libnymea-app/types/userinfo.cpp @@ -1,5 +1,8 @@ #include "userinfo.h" +#include +#include + UserInfo::UserInfo(QObject *parent): QObject(parent) { @@ -25,3 +28,40 @@ void UserInfo::setUsername(const QString &username) emit usernameChanged(); } } + +UserInfo::PermissionScopes UserInfo::scopes() const +{ + return m_scopes; +} + +void UserInfo::setScopes(PermissionScopes scopes) +{ + if (m_scopes != scopes) { + m_scopes = scopes; + emit scopesChanged(); + } +} + +QStringList UserInfo::scopesToList(PermissionScopes scopes) +{ + QStringList ret; + QMetaEnum metaEnum = QMetaEnum::fromType(); + for (int i = 0; i < metaEnum.keyCount(); i++) { + if (scopes.testFlag(static_cast(metaEnum.value(i)))) { + ret << metaEnum.key(i); + } + } + return ret; +} + +UserInfo::PermissionScopes UserInfo::listToScopes(const QStringList &scopeList) +{ + PermissionScopes ret; + QMetaEnum metaEnum = QMetaEnum::fromType(); + for (int i = 0; i < metaEnum.keyCount(); i++) { + if (scopeList.contains(metaEnum.key(i))) { + ret.setFlag(static_cast(metaEnum.value(i))); + } + } + return ret; +} diff --git a/libnymea-app/types/userinfo.h b/libnymea-app/types/userinfo.h index 1d841da8..5d88502b 100644 --- a/libnymea-app/types/userinfo.h +++ b/libnymea-app/types/userinfo.h @@ -7,18 +7,39 @@ class UserInfo : public QObject { Q_OBJECT Q_PROPERTY(QString username READ username NOTIFY usernameChanged) + Q_PROPERTY(PermissionScopes scopes READ scopes NOTIFY scopesChanged) public: + enum PermissionScope { + PermissionScopeNone = 0x0000, + PermissionScopeControlThings = 0x0001, + PermissionScopeConfigureThings = 0x0003, + PermissionScopeExecuteRules = 0x0010, + PermissionScopeConfigureRules = 0x0030, + PermissionScopeAdmin = 0xFFFF, + }; + Q_ENUM(PermissionScope) + Q_DECLARE_FLAGS(PermissionScopes, PermissionScope) + Q_FLAG(PermissionScopes) + explicit UserInfo(QObject *parent = nullptr); explicit UserInfo(const QString &username, QObject *parent = nullptr); QString username() const; void setUsername(const QString &username); + PermissionScopes scopes() const; + void setScopes(PermissionScopes scopes); + + static QStringList scopesToList(PermissionScopes scopes); + static PermissionScopes listToScopes(const QStringList &scopeList); + signals: void usernameChanged(); + void scopesChanged(); private: QString m_username; + PermissionScopes m_scopes = PermissionScopeNone; }; diff --git a/libnymea-app/usermanager.cpp b/libnymea-app/usermanager.cpp index 20062e39..ce9cf456 100644 --- a/libnymea-app/usermanager.cpp +++ b/libnymea-app/usermanager.cpp @@ -4,11 +4,23 @@ #include #include +#include "logging.h" +NYMEA_LOGGING_CATEGORY(dcUserManager, "UserManager") + UserManager::UserManager(QObject *parent): QObject(parent) { + qRegisterMetaType(); m_userInfo = new UserInfo(this); m_tokenInfos = new TokenInfos(this); + m_users = new Users(this); +} + +UserManager::~UserManager() +{ + if (m_engine) { + m_engine->jsonRpcClient()->unregisterNotificationHandler(this); + } } Engine *UserManager::engine() const @@ -19,15 +31,22 @@ Engine *UserManager::engine() const void UserManager::setEngine(Engine *engine) { if (m_engine != engine) { + if (m_engine) { + m_engine->jsonRpcClient()->unregisterNotificationHandler(this); + } + m_engine = engine; - m_engine->jsonRpcClient()->registerNotificationHandler(this, "Users", "notificationReceived"); emit engineChanged(); - m_loading = true; - emit loadingChanged(); + if (m_engine) { + m_engine->jsonRpcClient()->registerNotificationHandler(this, "Users", "notificationReceived"); - m_engine->jsonRpcClient()->sendCommand("Users.GetUserInfo", QVariantMap(), this, "getUserInfoReply"); - m_engine->jsonRpcClient()->sendCommand("Users.GetTokens", QVariantMap(), this, "getTokensReply"); + m_loading = true; + emit loadingChanged(); + m_engine->jsonRpcClient()->sendCommand("Users.GetUsers", QVariantMap(), this, "getUsersResponse"); + m_engine->jsonRpcClient()->sendCommand("Users.GetUserInfo", QVariantMap(), this, "getUserInfoResponse"); + m_engine->jsonRpcClient()->sendCommand("Users.GetTokens", QVariantMap(), this, "getTokensResponse"); + } } } @@ -46,11 +65,27 @@ TokenInfos *UserManager::tokenInfos() const return m_tokenInfos; } +Users *UserManager::users() const +{ + return m_users; +} + +int UserManager::createUser(const QString &username, const QString &password, UserInfo::PermissionScopes scopes) +{ + QVariantMap params; + params.insert("username", username); + params.insert("password", password); + if (m_engine->jsonRpcClient()->ensureServerVersion("5.6")) { + params.insert("scopes", UserInfo::scopesToList(scopes)); + } + return m_engine->jsonRpcClient()->sendCommand("Users.CreateUser", params, this, "createUserResponse"); +} + int UserManager::changePassword(const QString &newPassword) { QVariantMap params; params.insert("newPassword", newPassword); - int callId = m_engine->jsonRpcClient()->sendCommand("Users.ChangePassword", params, this, "changePasswordReply"); + int callId = m_engine->jsonRpcClient()->sendCommand("Users.ChangePassword", params, this, "changePasswordResponse"); return callId; } @@ -58,24 +93,62 @@ int UserManager::removeToken(const QUuid &id) { QVariantMap params; params.insert("tokenId", id); - int callId = m_engine->jsonRpcClient()->sendCommand("Users.RemoveToken", params, this, "deleteTokenReply"); + int callId = m_engine->jsonRpcClient()->sendCommand("Users.RemoveToken", params, this, "removeTokenResponse"); m_tokensToBeRemoved.insert(callId, id); return callId; } +int UserManager::removeUser(const QString &username) +{ + QVariantMap params; + params.insert("username", username); + return m_engine->jsonRpcClient()->sendCommand("Users.RemoveUser", params, this, "removeUserResponse"); +} + void UserManager::notificationReceived(const QVariantMap &data) { - qDebug() << "Users notification" << data; + qCDebug(dcUserManager()) << "Users notification" << data; + QString notification = data.value("notification").toString(); + if (notification == "Users.UserAdded") { + QVariantMap userMap = data.value("params").toMap().value("userInfo").toMap(); + UserInfo *info = new UserInfo(userMap.value("username").toString()); + info->setScopes(UserInfo::listToScopes(userMap.value("scopes").toStringList())); + m_users->insertUser(info); + } else if (notification == "Users.UserRemoved") { + m_users->removeUser(data.value("params").toMap().value("username").toString()); + } else if (notification == "Users.UserChanged") { + QVariantMap userMap = data.value("params").toMap().value("userInfo").toMap(); + QString username = userMap.value("username").toString(); + 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())); + } } -void UserManager::getUserInfoReply(int commandId, const QVariantMap &data) +void UserManager::getUsersResponse(int commandId, const QVariantMap &data) { - qDebug() << "User info reply" << commandId << data; + qCDebug(dcUserManager) << "Get users response:" << commandId << data; - m_userInfo->setUsername(data.value("userInfo").toMap().value("username").toString()); + foreach (const QVariant &userVariant, data.value("users").toList()) { + QVariantMap userMap = userVariant.toMap(); + UserInfo *userInfo = new UserInfo(userMap.value("username").toString()); + userInfo->setScopes(UserInfo::listToScopes(userMap.value("scopes").toStringList())); + m_users->insertUser(userInfo); + } } -void UserManager::getTokensReply(int /*commandId*/, const QVariantMap &data) +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->setScopes(UserInfo::listToScopes(userMap.value("scopes").toStringList())); +} + +void UserManager::getTokensResponse(int /*commandId*/, const QVariantMap &data) { foreach (const QVariant &tokenVariant, data.value("tokenInfoList").toList()) { @@ -91,30 +164,133 @@ void UserManager::getTokensReply(int /*commandId*/, const QVariantMap &data) } - - -void UserManager::deleteTokenReply(int commandId, const QVariantMap ¶ms) +void UserManager::removeTokenResponse(int commandId, const QVariantMap ¶ms) { - qDebug() << "Delete token reply" << commandId << params; + qCDebug(dcUserManager()) << "Delete token reply" << commandId << params; QUuid tokenId = m_tokensToBeRemoved.take(commandId); QString errorString = params.value("error").toString(); QMetaEnum metaEnum = QMetaEnum::fromType(); UserError error = static_cast(metaEnum.keyToValue(errorString.toUtf8())); - emit deleteTokenResponse(commandId, error); + emit removeTokenReply(commandId, error); if (error == UserErrorNoError) { m_tokenInfos->removeToken(tokenId); } } -void UserManager::changePasswordReply(int commandId, const QVariantMap ¶ms) +void UserManager::changePasswordResponse(int commandId, const QVariantMap ¶ms) { - qDebug() << "Change password reply" << commandId << params; + qCDebug(dcUserManager()) << "Change password reply" << commandId << params; QString errorString = params.value("error").toString(); QMetaEnum metaEnum = QMetaEnum::fromType(); UserError error = static_cast(metaEnum.keyToValue(errorString.toUtf8())); - emit changePasswordResponse(commandId, error); + emit changePasswordReply(commandId, error); +} + +void UserManager::createUserResponse(int commandId, const QVariantMap ¶ms) +{ + qCDebug(dcUserManager()) << "Create user response:" << commandId << params; +} + +void UserManager::removeUserResponse(int commandId, const QVariantMap ¶ms) +{ + qCDebug(dcUserManager()) << "Remove user response:" << commandId << params; + QMetaEnum metaEnum = QMetaEnum::fromType(); + UserError error = static_cast(metaEnum.keyToValue(params.value("error").toString().toUtf8())); + emit removeUserReply(commandId, error); +} + +void UserManager::setUserScopesResponse(int commandId, const QVariantMap ¶ms) +{ + qCDebug(dcUserManager()) << "Set user scopes response:" << commandId << params; + QMetaEnum metaEnum = QMetaEnum::fromType(); + UserError error = static_cast(metaEnum.keyToValue(params.value("error").toString().toUtf8())); + emit setUserScopesReply(commandId, error); +} + +int UserManager::setUserScopes(const QString &username, UserInfo::PermissionScopes scopes) +{ + QVariantMap params; + params.insert("username", username); + params.insert("scopes", UserInfo::scopesToList(scopes)); + qCDebug(dcUserManager()) << "Setting new permission scopes for user" << username << scopes << (int)scopes; + return m_engine->jsonRpcClient()->sendCommand("Users.SetUserScopes", params, this, "setUserScopesResponse"); +} + +Users::Users(QObject *parent): QAbstractListModel(parent) +{ + +} + +int Users::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_users.count(); +} + +QVariant Users::data(const QModelIndex &index, int role) const +{ + switch (role) { + case RoleUsername: + return m_users.at(index.row())->username(); + case RoleScopes: + return static_cast(m_users.at(index.row())->scopes()); + } + return QVariant(); +} + +QHash Users::roleNames() const +{ + QHash roles; + roles.insert(RoleUsername, "username"); + roles.insert(RoleScopes, "scopes"); + return roles; +} + +void Users::insertUser(UserInfo *userInfo) +{ + userInfo->setParent(this); + connect(userInfo, &UserInfo::scopesChanged, this, [=](){ + int idx = m_users.indexOf(userInfo); + if (idx >= 0) { + emit dataChanged(index(idx), index(idx), {RoleScopes}); + } + }); + + beginInsertRows(QModelIndex(), m_users.count(), m_users.count()); + m_users.append(userInfo); + endInsertRows(); + emit countChanged(); +} + +void Users::removeUser(const QString &username) +{ + for (int i = 0; i < m_users.count(); i++) { + if (m_users.at(i)->username() == username) { + beginRemoveRows(QModelIndex(), i, i); + m_users.takeAt(i)->deleteLater(); + endRemoveRows(); + } + } +} + +UserInfo *Users::get(int index) const +{ + if (index < 0 || index >= m_users.count()) { + return nullptr; + } + return m_users.at(index); +} + +UserInfo *Users::getUserInfo(const QString &username) const +{ + for (int i = 0; i < m_users.count(); i++) { + if (m_users.at(i)->username() == username) { + return m_users.at(i); + } + } + return nullptr; } diff --git a/libnymea-app/usermanager.h b/libnymea-app/usermanager.h index 260172ad..8923f5a1 100644 --- a/libnymea-app/usermanager.h +++ b/libnymea-app/usermanager.h @@ -9,6 +9,8 @@ #include "types/tokeninfos.h" #include "types/userinfo.h" +class Users; + class UserManager: public QObject { Q_OBJECT @@ -17,6 +19,7 @@ class UserManager: public QObject Q_PROPERTY(UserInfo* userInfo READ userInfo CONSTANT) Q_PROPERTY(TokenInfos* tokenInfos READ tokenInfos CONSTANT) + Q_PROPERTY(Users* users READ users CONSTANT) public: enum UserError { @@ -31,6 +34,7 @@ public: Q_ENUM(UserError) explicit UserManager(QObject *parent = nullptr); + ~UserManager(); Engine* engine() const; void setEngine(Engine* engine); @@ -39,24 +43,34 @@ public: UserInfo* userInfo() const; TokenInfos* tokenInfos() const; + Users *users() const; + Q_INVOKABLE int createUser(const QString &username, const QString &password, UserInfo::PermissionScopes scopes = UserInfo::PermissionScopeAdmin); Q_INVOKABLE int changePassword(const QString &newPassword); Q_INVOKABLE int removeToken(const QUuid &id); + Q_INVOKABLE int removeUser(const QString &username); + Q_INVOKABLE int setUserScopes(const QString &username, UserInfo::PermissionScopes scopes); signals: void engineChanged(); void loadingChanged(); - void deleteTokenResponse(int id, UserError error); - void changePasswordResponse(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); private slots: void notificationReceived(const QVariantMap &data); - void getUserInfoReply(int commandId, const QVariantMap &data); - void getTokensReply(int commandId, const QVariantMap &data); - void deleteTokenReply(int commandId, const QVariantMap ¶ms); - void changePasswordReply(int commandId, const QVariantMap ¶ms); + void getUsersResponse(int commandId, const QVariantMap &data); + void getUserInfoResponse(int commandId, const QVariantMap &data); + void getTokensResponse(int commandId, const QVariantMap &data); + void removeTokenResponse(int commandId, const QVariantMap ¶ms); + void changePasswordResponse(int commandId, const QVariantMap ¶ms); + void createUserResponse(int commandId, const QVariantMap ¶ms); + void removeUserResponse(int commandId, const QVariantMap ¶ms); + void setUserScopesResponse(int commandId, const QVariantMap ¶ms); private: Engine *m_engine = nullptr; @@ -65,7 +79,39 @@ private: UserInfo *m_userInfo = nullptr; TokenInfos *m_tokenInfos = nullptr; + Users *m_users = nullptr; + QHash m_tokensToBeRemoved; }; +class Users: public QAbstractListModel { + Q_OBJECT + Q_PROPERTY(int count READ rowCount NOTIFY countChanged) + +public: + enum Roles { + RoleUsername, + RoleScopes + }; + Q_ENUM(Roles) + + explicit Users(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + void insertUser(UserInfo *userInfo); + void removeUser(const QString &username); + + Q_INVOKABLE UserInfo* get(int index) const; + Q_INVOKABLE UserInfo* getUserInfo(const QString &username) const; + +signals: + void countChanged(); + +private: + QList m_users; +}; + #endif // USERMANAGER_H diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 6afa948e..f88c2ac2 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -272,5 +272,6 @@ ui/mainviews/energy/EnergySettingsPage.qml ui/mainviews/energy/ConsumersPieChart.qml ui/components/NymeaToolTip.qml + ui/components/SettingsTile.qml diff --git a/nymea-app/ui/MainMenu.qml b/nymea-app/ui/MainMenu.qml index d8d24114..a72ec6e3 100644 --- a/nymea-app/ui/MainMenu.qml +++ b/nymea-app/ui/MainMenu.qml @@ -139,7 +139,9 @@ Drawer { Layout.fillWidth: true text: qsTr("Configure things") iconName: "../images/things.svg" - visible: root.currentEngine && root.currentEngine.jsonRpcClient.currentHost && root.currentEngine.jsonRpcClient.connected + visible: root.currentEngine && root.currentEngine.jsonRpcClient.currentHost + && NymeaUtils.hasPermissionScope(root.currentEngine, UserInfo.PermissionScopeConfigureThings) + && root.currentEngine.jsonRpcClient.connected progressive: false onClicked: { root.openThingSettings() @@ -151,7 +153,9 @@ Drawer { text: qsTr("Magic") iconName: "../images/magic.svg" progressive: false - visible: root.currentEngine && root.currentEngine.jsonRpcClient.currentHost && root.currentEngine.jsonRpcClient.connected && Configuration.magicEnabled + visible: root.currentEngine && root.currentEngine.jsonRpcClient.currentHost + && NymeaUtils.hasPermissionScope(root.currentEngine, UserInfo.PermissionScopeConfigureRules) + && root.currentEngine.jsonRpcClient.connected && Configuration.magicEnabled onClicked: { root.openMagicSettings(); root.close(); diff --git a/nymea-app/ui/RootItem.qml b/nymea-app/ui/RootItem.qml index 25bda215..b59f0607 100644 --- a/nymea-app/ui/RootItem.qml +++ b/nymea-app/ui/RootItem.qml @@ -411,8 +411,8 @@ Item { } Label { text: popup.minVersion != "" - ? qsTr("The version of the %1 system you are trying to connect to is too old. This app requires at least version %2 but this %1 system only supports %3. Please update your %1 system.").arg(Configuration.systemName).arg(popup.minVersion).arg(popup.actualVersion) - : qsTr("The version of the %1 system you are trying to connect to is too new. This app supports only up to version %2 but this %1 system provides %3. Please update %4.").arg(Configuration.systemName).arg(popup.maxVersion).arg(popup.actualVersion).arg(Configuration.appName) + ? qsTr("The version of the %1 system you are trying to connect to is too old. This app requires at least API version %2 but this %1 system only supports API version %3. Please update your %1 system.").arg(Configuration.systemName).arg(popup.minVersion).arg(popup.actualVersion) + : qsTr("The version of the %1 system you are trying to connect to is too new. This app supports only up to API version %2 but this %1 system provides API version %3. Please update %4.").arg(Configuration.systemName).arg(popup.maxVersion).arg(popup.actualVersion).arg(Configuration.appName) wrapMode: Text.WordWrap Layout.fillWidth: true } diff --git a/nymea-app/ui/SettingsPage.qml b/nymea-app/ui/SettingsPage.qml index 014bf101..82d7ac35 100644 --- a/nymea-app/ui/SettingsPage.qml +++ b/nymea-app/ui/SettingsPage.qml @@ -51,246 +51,137 @@ Page { GridLayout { id: layout property bool isGrid: columns > 1 - anchors { left: parent.left; top: parent.top; right: parent.right; margins: isGrid ? app.margins : 0 } + anchors { left: parent.left; top: parent.top; right: parent.right; margins: Style.smallMargins } columns: Math.max(1, Math.floor(parent.width / 300)) - rowSpacing: isGrid ? app.margins : 0 - columnSpacing: isGrid ? app.margins : 0 + rowSpacing: 0 + columnSpacing: 0 - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - padding: 0 - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/configure.svg" - text: qsTr("General") - subText: qsTr("Change system name and time zone") - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("system/GeneralSettingsPage.qml")) - } + iconSource: "../images/configure.svg" + text: qsTr("General") + subText: qsTr("Change system name and time zone") + visible: NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) + onClicked: pageStack.push(Qt.resolvedUrl("system/GeneralSettingsPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - padding: 0 + iconSource: "../images/account.svg" + text: qsTr("User settings") + subText: qsTr("Configure who can log in") visible: engine.jsonRpcClient.ensureServerVersion("4.2") && engine.jsonRpcClient.authenticated - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/account.svg" - text: qsTr("User settings") - subText: qsTr("Configure who can log in") - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("system/UsersSettingsPage.qml")) - } + onClicked: pageStack.push(Qt.resolvedUrl("system/UsersSettingsPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - - visible: Configuration.networkSettingsEnabled - - padding: 0 - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/connections/network-wifi.svg" - text: qsTr("Networking") - subText: qsTr("Configure the system's network connection") - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("system/NetworkSettingsPage.qml")) - } + 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 + onClicked: pageStack.push(Qt.resolvedUrl("system/NetworkSettingsPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - - padding: 0 - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/connections/cloud.svg" - text: qsTr("Cloud") - subText: qsTr("Connect this %1 system to %1:cloud").arg(Configuration.systemName) - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("system/CloudSettingsPage.qml")) - } + 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) + onClicked: pageStack.push(Qt.resolvedUrl("system/CloudSettingsPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - - visible: Configuration.apiSettingsEnabled - - padding: 0 - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/connections/network-vpn.svg" - text: qsTr("API interfaces") - prominentSubText: false - wrapTexts: false - subText: qsTr("Configure how clients interact with this system") - onClicked: pageStack.push(Qt.resolvedUrl("system/ConnectionInterfacesPage.qml")) - } + 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 + onClicked: pageStack.push(Qt.resolvedUrl("system/ConnectionInterfacesPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - visible: engine.jsonRpcClient.ensureServerVersion("1.11") && Configuration.mqttSettingsEnabled - - padding: 0 - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/mqtt.svg" - text: qsTr("MQTT broker") - subText: qsTr("Configure the MQTT broker") - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("system/MqttBrokerSettingsPage.qml")) - } + 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 + onClicked: pageStack.push(Qt.resolvedUrl("system/MqttBrokerSettingsPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - - visible: Configuration.webServerSettingsEnabled - - padding: 0 - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/stock_website.svg" - text: qsTr("Web server") - subText: qsTr("Configure the web server") - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("system/WebServerSettingsPage.qml")) - } + iconSource: "../images/stock_website.svg" + text: qsTr("Web server") + subText: qsTr("Configure the web server") + visible: NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) + && Configuration.webServerSettingsEnabled + onClicked: pageStack.push(Qt.resolvedUrl("system/WebServerSettingsPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - visible: engine.jsonRpcClient.ensureServerVersion("5.3") && Configuration.zigbeeSettingsEnabled - - - padding: 0 - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/zigbee.svg" - text: qsTr("ZigBee") - subText: qsTr("Configure ZigBee networks") - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("system/ZigbeeSettingsPage.qml")) - } + 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 + onClicked: pageStack.push(Qt.resolvedUrl("system/ZigbeeSettingsPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - visible: engine.jsonRpcClient.ensureServerVersion("5.6") && Configuration.modbusSettingsEnabled - - padding: 0 - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/modbus.svg" - text: qsTr("Modbus RTU") - subText: qsTr("Configure Modbus RTU master interfaces") - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("system/ModbusRtuSettingsPage.qml")) - } + 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 + onClicked: pageStack.push(Qt.resolvedUrl("system/ModbusRtuSettingsPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - - visible: Configuration.pluginSettingsEnabled - - padding: 0 - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/plugin.svg" - text: qsTr("Plugins") - subText: qsTr("List and cofigure installed plugins") - prominentSubText: false - wrapTexts: false - onClicked:pageStack.push(Qt.resolvedUrl("system/PluginsPage.qml")) - } + iconSource: "../images/plugin.svg" + text: qsTr("Plugins") + subText: qsTr("List and cofigure installed plugins") + visible: NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) && Configuration.pluginSettingsEnabled + onClicked:pageStack.push(Qt.resolvedUrl("system/PluginsPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - - padding: 0 - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/sdk.svg" - text: qsTr("Developer tools") - subText: qsTr("Access tools for debugging and error reporting") - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("system/DeveloperTools.qml")) - } + iconSource: "../images/sdk.svg" + text: qsTr("Developer tools") + subText: qsTr("Access tools for debugging and error reporting") + visible: NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) + onClicked: pageStack.push(Qt.resolvedUrl("system/DeveloperTools.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - visible: engine.jsonRpcClient.ensureServerVersion("2.1") && engine.systemController.updateManagementAvailable - - padding: 0 - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/system-update.svg" - text: qsTr("System update") - subText: qsTr("Update your %1 system").arg(Configuration.systemName) - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("system/SystemUpdatePage.qml")) - } + iconSource: "../images/system-update.svg" + text: qsTr("System update") + subText: qsTr("Update your %1 system").arg(Configuration.systemName) + visible: engine.systemController.updateManagementAvailable && + NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) + onClicked: pageStack.push(Qt.resolvedUrl("system/SystemUpdatePage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - - padding: 0 - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/logs.svg" - text: qsTr("Log viewer") - subText: qsTr("View system log") - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("system/LogViewerPage.qml")) - } + iconSource: "../images/logs.svg" + text: qsTr("Log viewer") + subText: qsTr("View system log") + visible: NymeaUtils.hasPermissionScope(engine, UserInfo.PermissionScopeAdmin) + onClicked: pageStack.push(Qt.resolvedUrl("system/LogViewerPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - - padding: 0 - NymeaSwipeDelegate { - width: parent.width - iconName: "../images/info.svg" - text: qsTr("About %1").arg(Configuration.systemName) - subText: qsTr("Find server UUID and versions") - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("system/AboutNymeaPage.qml")) - } + iconSource: "../images/info.svg" + text: qsTr("About %1").arg(Configuration.systemName) + subText: qsTr("Find server UUID and versions") + onClicked: pageStack.push(Qt.resolvedUrl("system/AboutNymeaPage.qml")) } } } diff --git a/nymea-app/ui/StyleBase.qml b/nymea-app/ui/StyleBase.qml index 22d28822..0bc76f3b 100644 --- a/nymea-app/ui/StyleBase.qml +++ b/nymea-app/ui/StyleBase.qml @@ -3,6 +3,7 @@ import QtQuick 2.0 Item { property color backgroundColor: "#fafafa" property color foregroundColor: "#202020" + property color unobtrusiveForegroundColor: Qt.tint(foregroundColor, Qt.rgba(backgroundColor.r, backgroundColor.g, backgroundColor.b, 0.4)) property color accentColor: "#57baae" property color iconColor: "#808080" diff --git a/nymea-app/ui/appsettings/AppSettingsPage.qml b/nymea-app/ui/appsettings/AppSettingsPage.qml index e5dc6cdc..37608815 100644 --- a/nymea-app/ui/appsettings/AppSettingsPage.qml +++ b/nymea-app/ui/appsettings/AppSettingsPage.qml @@ -50,68 +50,39 @@ Page { GridLayout { id: layout - property bool isGrid: columns > 1 - anchors { left: parent.left; top: parent.top; right: parent.right; margins: isGrid ? app.margins : 0 } + anchors { left: parent.left; top: parent.top; right: parent.right; margins: Style.smallMargins } columns: Math.max(1, Math.floor(parent.width / 300)) - rowSpacing: isGrid ? app.margins : 0 - columnSpacing: isGrid ? app.margins : 0 + rowSpacing: 0 + columnSpacing: 0 - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - padding: 0 - NymeaSwipeDelegate { - width: parent.width - text: qsTr("Look & feel") - subText: qsTr("Customize the app's look and behavior") - iconName: "../images/preferences-look-and-feel.svg" - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("LookAndFeelSettingsPage.qml")) - } + text: qsTr("Look & feel") + subText: qsTr("Customize the app's look and behavior") + iconSource: "../images/preferences-look-and-feel.svg" + onClicked: pageStack.push(Qt.resolvedUrl("LookAndFeelSettingsPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - padding: 0 - NymeaSwipeDelegate { - width: parent.width - text: qsTr("Cloud login") - subText: qsTr("Log into %1:cloud and manage connected %1 systems").arg(Configuration.systemName) - iconName: "../images/connections/cloud.svg" - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("CloudLoginPage.qml")) - } + text: qsTr("Cloud login") + subText: qsTr("Log into %1:cloud and manage connected %1 systems").arg(Configuration.systemName) + iconSource: "../images/connections/cloud.svg" + onClicked: pageStack.push(Qt.resolvedUrl("CloudLoginPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - padding: 0 - NymeaSwipeDelegate { - width: parent.width - text: qsTr("Developer options") - subText: qsTr("Access tools for debugging and error reporting") - iconName: "../images/sdk.svg" - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("DeveloperOptionsPage.qml")) - } + text: qsTr("Developer options") + subText: qsTr("Access tools for debugging and error reporting") + iconSource: "../images/sdk.svg" + onClicked: pageStack.push(Qt.resolvedUrl("DeveloperOptionsPage.qml")) } - Pane { + SettingsTile { Layout.fillWidth: true - Material.elevation: layout.isGrid ? 1 : 0 - padding: 0 - NymeaSwipeDelegate { - width: parent.width - text: qsTr("About %1").arg(Configuration.appName) - subText: qsTr("Find app versions and licence information") - iconName: "../images/info.svg" - prominentSubText: false - wrapTexts: false - onClicked: pageStack.push(Qt.resolvedUrl("AboutPage.qml")) - } + text: qsTr("About %1").arg(Configuration.appName) + subText: qsTr("Find app versions and licence information") + iconSource: "../images/info.svg" + onClicked: pageStack.push(Qt.resolvedUrl("AboutPage.qml")) } } } diff --git a/nymea-app/ui/components/ColorIcon.qml b/nymea-app/ui/components/ColorIcon.qml index 85dacdc7..9262b4b8 100644 --- a/nymea-app/ui/components/ColorIcon.qml +++ b/nymea-app/ui/components/ColorIcon.qml @@ -38,7 +38,8 @@ Item { width: size height: size - property string name + property alias name: icon.source + property string source property alias color: colorizedImage.outColor property int margins: 0 property int size: Style.iconSize @@ -49,9 +50,9 @@ Item { id: image anchors.fill: parent anchors.margins: parent ? parent.margins : 0 - source: width > 0 && height > 0 && icon.name ? - icon.name.endsWith(".svg") ? icon.name - : "qrc:/ui/images/" + icon.name + ".svg" + source: width > 0 && height > 0 && icon.source ? + icon.source.endsWith(".svg") ? icon.source + : "qrc:/ui/images/" + icon.source + ".svg" : "" sourceSize { width: width diff --git a/nymea-app/ui/components/PasswordTextField.qml b/nymea-app/ui/components/PasswordTextField.qml index 86c95409..3f7fc0c9 100644 --- a/nymea-app/ui/components/PasswordTextField.qml +++ b/nymea-app/ui/components/PasswordTextField.qml @@ -39,7 +39,7 @@ ColumnLayout { property bool signup: true // Only used when signup is true - property int minPasswordLength: 12 + property int minPasswordLength: 8 property bool requireSpecialChar: true property bool requireNumber: true property bool requireUpperCaseLetter: true diff --git a/nymea-app/ui/components/SettingsTile.qml b/nymea-app/ui/components/SettingsTile.qml new file mode 100644 index 00000000..bd21bd4a --- /dev/null +++ b/nymea-app/ui/components/SettingsTile.qml @@ -0,0 +1,35 @@ +import QtQuick 2.9 +import QtQuick.Layouts 1.2 +import QtQuick.Controls 2.9 +import Nymea 1.0 + +BigTile { + id: root + + property alias iconSource: icon.name + property alias text: textLabel.text + property alias subText: subTextLabel.text + + contentItem: RowLayout { + spacing: Style.margins + ColorIcon { + id: icon + size: Style.iconSize + color: Style.accentColor + } + ColumnLayout { + Label { + id: textLabel + Layout.fillWidth: true + elide: Text.ElideRight + } + Label { + id: subTextLabel + Layout.fillWidth: true + font: Style.extraSmallFont + elide: Text.ElideRight + color: Style.unobtrusiveForegroundColor + } + } + } +} diff --git a/nymea-app/ui/connection/ConnectPage.qml b/nymea-app/ui/connection/ConnectPage.qml index 97c0b150..62e08d6a 100644 --- a/nymea-app/ui/connection/ConnectPage.qml +++ b/nymea-app/ui/connection/ConnectPage.qml @@ -33,6 +33,7 @@ import QtQuick.Controls 2.2 import QtQuick.Controls.Material 2.2 import QtQuick.Layouts 1.3 import Nymea 1.0 +import Qt.labs.settings 1.1 import "../components" Page { @@ -281,7 +282,7 @@ Page { standardButtons: Dialog.Ok - property var nymeaHost: null + property NymeaHost nymeaHost: null header: Item { implicitHeight: headerRow.height + app.margins * 2 @@ -343,6 +344,18 @@ Page { text: qsTr("Available connections") } + Button { + Layout.fillWidth: true + text: qsTr("Logout") + onClicked: tokenSettings.setValue(dialog.nymeaHost.uuid, "") + visible: tokenSettings.value(dialog.nymeaHost.uuid) !== "" + + Settings { + id: tokenSettings + category: "jsonTokens" + } + } + Flickable { Layout.columnSpan: 2 Layout.fillWidth: true diff --git a/nymea-app/ui/connection/LoginPage.qml b/nymea-app/ui/connection/LoginPage.qml index a2b9ac87..433e27ed 100644 --- a/nymea-app/ui/connection/LoginPage.qml +++ b/nymea-app/ui/connection/LoginPage.qml @@ -123,8 +123,8 @@ Page { TextField { id: usernameTextField Layout.fillWidth: true - inputMethodHints: Qt.ImhEmailCharactersOnly - placeholderText: "john.smith@cooldomain.com" + inputMethodHints: Qt.ImhEmailCharactersOnly | Qt.ImhNoAutoUppercase +// placeholderText: "john.smith@cooldomain.com" } Label { Layout.fillWidth: true diff --git a/nymea-app/ui/system/UsersSettingsPage.qml b/nymea-app/ui/system/UsersSettingsPage.qml index 74be0b27..fbb419e7 100644 --- a/nymea-app/ui/system/UsersSettingsPage.qml +++ b/nymea-app/ui/system/UsersSettingsPage.qml @@ -13,7 +13,7 @@ SettingsPageBase { id: userManager engine: _engine - onChangePasswordResponse: { + onChangePasswordReply: { if (error != UserManager.UserErrorNoError) { var component = Qt.createComponent("../components/ErrorDialog.qml") var text; @@ -64,23 +64,32 @@ SettingsPageBase { } SettingsPageSectionHeader { - text: qsTr("Devices / Apps accessing nymea:core") + text: qsTr("Device access") } - Repeater { - model: userManager.tokenInfos + Button { + Layout.fillWidth: true + Layout.leftMargin: Style.margins + Layout.rightMargin: Style.margins + text: qsTr("Manage authorized devices") + onClicked: { + pageStack.push(manageTokensComponent) + } + } - delegate: NymeaSwipeDelegate { - Layout.fillWidth: true - text: model.deviceName - subText: qsTr("Created on %1").arg(Qt.formatDateTime(model.creationTime, Qt.DefaultLocaleShortDate)) - prominentSubText: false - progressive: false - canDelete: true + SettingsPageSectionHeader { + text: qsTr("Admin") + visible: userManager.userInfo.scopes & UserInfo.PermissionScopeAdmin + } - onDeleteClicked: { - userManager.removeToken(model.id) - } + Button { + Layout.fillWidth: true + Layout.leftMargin: Style.margins + Layout.rightMargin: Style.margins + text: qsTr("Manage users") + visible: userManager.userInfo.scopes & UserInfo.PermissionScopeAdmin + onClicked: { + pageStack.push(manageUsersComponent) } } @@ -130,4 +139,203 @@ SettingsPageBase { } } } + + Component { + id: manageTokensComponent + SettingsPageBase { + id: manageTokensPage + title: qsTr("Device access") + + SettingsPageSectionHeader { + text: qsTr("Devices / Apps accessing %1").arg(Configuration.systemName) + } + + Repeater { + model: userManager.tokenInfos + + delegate: NymeaSwipeDelegate { + Layout.fillWidth: true + text: model.deviceName + subText: qsTr("Created on %1").arg(Qt.formatDateTime(model.creationTime, Qt.DefaultLocaleShortDate)) + prominentSubText: false + progressive: false + canDelete: true + + onDeleteClicked: { + userManager.removeToken(model.id) + } + } + } + } + } + + Component { + id: manageUsersComponent + SettingsPageBase { + id: manageUsersPage + + header: NymeaHeader { + text: qsTr("Users") + onBackPressed: pageStack.pop() + + HeaderButton { + imageSource: Qt.resolvedUrl("../images/add.svg") + onClicked: { + pageStack.push(addUserComponent) + } + } + } + + SettingsPageSectionHeader { + text: qsTr("Manage users for this %1 system").arg(Configuration.systemName) + } + + Repeater { + model: userManager.users + delegate: NymeaSwipeDelegate { + Layout.fillWidth: true + text: model.username + iconName: "/ui/images/account.svg" + iconColor: userManager.userInfo.scopes & UserInfo.PermissionScopeAdmin ? Style.accentColor : Style.iconColor + + canDelete: true + onClicked: { + pageStack.push(userDetailsComponent, {userInfo: userManager.users.get(index)}) + } + + onDeleteClicked: { + userManager.removeUser(model.username) + } + } + } + } + } + + Component { + id: userDetailsComponent + SettingsPageBase { + id: userDetailsPage + title: qsTr("Manage user") + + property UserInfo userInfo: null + + SettingsPageSectionHeader { + text: qsTr("User info") + } + + NymeaItemDelegate { + Layout.fillWidth: true + text: userDetailsPage.userInfo.username + progressive: false + } + + SettingsPageSectionHeader { + text: qsTr("Permissions") + } + + Repeater { + model: NymeaUtils.scopesModel + + delegate: CheckDelegate { + Layout.fillWidth: true + text: model.text + checked: (userDetailsPage.userInfo.scopes & model.scope) === model.scope + enabled: model.scope === UserInfo.ScopeAdmin || + (userDetailsPage.userInfo.scopes & UserInfo.ScopeAdmin) == 0 + onClicked: { + print("scopes:", userDetailsPage.userInfo.scopes) + var scopes = userDetailsPage.userInfo.scopes + if (checked) { + scopes |= model.scope + } else { + scopes &= ~model.scope + scopes |= model.resetOnUnset + } + userManager.setUserScopes(userDetailsPage.userInfo.username, scopes) + } + } + } + + SettingsPageSectionHeader { + text: qsTr("Remove") + } + + Button { + Layout.fillWidth: true + Layout.leftMargin: Style.margins + Layout.rightMargin: Style.margins + text: qsTr("Remove this user") + onClicked: { + userManager.removeUser(userDetailsPage.userInfo.username) + } + } + } + } + + Component { + id: addUserComponent + + SettingsPageBase { + id: createUserPage + title: qsTr("Add a user") + + property var permissionScopes: UserInfo.PermissionScopeNone + + SettingsPageSectionHeader { + text: qsTr("Login 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 + Layout.fillWidth: true + Layout.leftMargin: Style.margins + Layout.rightMargin: Style.margins + } + + SettingsPageSectionHeader { + text: qsTr("Permissions") + } + + + Repeater { + id: scopesRepeater + model: NymeaUtils.scopesModel + + delegate: CheckDelegate { + Layout.fillWidth: true + text: model.text + checked: (createUserPage.permissionScopes & model.scope) === model.scope + onClicked: { + var scopes = createUserPage.permissionScopes + if (checked) { + scopes |= model.scope + } else { + scopes &= ~model.scope + scopes |= model.resetOnUnset + } + createUserPage.permissionScopes = scopes + } + } + } + + Button { + text: qsTr("Create new user") + Layout.fillWidth: true + Layout.leftMargin: Style.margins + Layout.rightMargin: Style.margins + enabled: usernameTextField.displayText.length > 3 && passwordTextField.isValid + onClicked: { + userManager.createUser(usernameTextField.displayText, passwordTextField.password, createUserPage.permissionScopes) + } + } + } + } } diff --git a/nymea-app/ui/utils/NymeaUtils.qml b/nymea-app/ui/utils/NymeaUtils.qml index c6f69483..aab60709 100644 --- a/nymea-app/ui/utils/NymeaUtils.qml +++ b/nymea-app/ui/utils/NymeaUtils.qml @@ -1,5 +1,6 @@ pragma Singleton import QtQuick 2.9 +import Nymea 1.0 Item { id: root @@ -88,7 +89,6 @@ Item { return ((r * 299 + g * 587 + b * 114) / 1000) < 128 } - property var namedIcons: { "dashboard": "/ui/images/dashboard.svg", "group": "/ui/images/groups.svg", @@ -134,4 +134,15 @@ Item { return namedIcons[name] } + property ListModel scopesModel: ListModel { + ListElement { text: qsTr("Admin"); scope: UserInfo.PermissionScopeAdmin; resetOnUnset: UserInfo.PermissionScopeNone } + ListElement { text: qsTr("Control things"); scope: UserInfo.PermissionScopeControlThings; resetOnUnset: UserInfo.PermissionScopeNone } + ListElement { text: qsTr("Configure things"); scope: UserInfo.PermissionScopeConfigureThings; resetOnUnset: UserInfo.PermissionScopeControlThings } + ListElement { text: qsTr("Execute magic"); scope: UserInfo.PermissionScopeExecuteRules; resetOnUnset: UserInfo.PermissionScopeNone } + ListElement { text: qsTr("Configure magic"); scope: UserInfo.PermissionScopeConfigureRules; resetOnUnset: UserInfo.PermissionScopeExecuteRules } + } + + function hasPermissionScope(engine, requestedScope) { + return (engine.jsonRpcClient.permissions & requestedScope) === requestedScope; + } }