diff --git a/libnymea-app/types/userinfo.cpp b/libnymea-app/types/userinfo.cpp index fb774ec6..3a677833 100644 --- a/libnymea-app/types/userinfo.cpp +++ b/libnymea-app/types/userinfo.cpp @@ -92,6 +92,39 @@ void UserInfo::setScopes(PermissionScopes scopes) } } +QList UserInfo::allowedThingIds() const +{ + return m_allowedThingIds; +} + +void UserInfo::setAllowedThingIds(const QList &allowedThingIds) +{ + if (m_allowedThingIds != allowedThingIds) { + m_allowedThingIds = allowedThingIds; + emit allowedThingIdsChanged(); + } +} + +bool UserInfo::thingAllowed(const QUuid &thingId) const +{ + return m_allowedThingIds.contains(thingId); +} + +void UserInfo::allowThingId(const QUuid &thingId, bool allowed) +{ + if (allowed) { + if (!m_allowedThingIds.contains(thingId)) { + m_allowedThingIds.append(thingId); + emit allowedThingIdsChanged(); + } + } else { + if (m_allowedThingIds.contains(thingId)) { + m_allowedThingIds.removeAll(thingId); + emit allowedThingIdsChanged(); + } + } +} + QStringList UserInfo::scopesToList(PermissionScopes scopes) { QStringList ret; diff --git a/libnymea-app/types/userinfo.h b/libnymea-app/types/userinfo.h index 47fd6ca9..5e180e51 100644 --- a/libnymea-app/types/userinfo.h +++ b/libnymea-app/types/userinfo.h @@ -25,6 +25,7 @@ #ifndef USERINFO_H #define USERINFO_H +#include #include class UserInfo : public QObject @@ -34,6 +35,8 @@ class UserInfo : public QObject Q_PROPERTY(QString email READ email NOTIFY emailChanged) Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged) Q_PROPERTY(PermissionScopes scopes READ scopes NOTIFY scopesChanged) + Q_PROPERTY(QList allowedThingIds READ allowedThingIds NOTIFY allowedThingIdsChanged) + public: enum PermissionScope { PermissionScopeNone = 0x0000, @@ -62,6 +65,12 @@ public: PermissionScopes scopes() const; void setScopes(PermissionScopes scopes); + QList allowedThingIds() const; + void setAllowedThingIds(const QList &allowedThingIds); + + Q_INVOKABLE bool thingAllowed(const QUuid &thingId) const; + Q_INVOKABLE void allowThingId(const QUuid &thingId, bool allowed); + static QStringList scopesToList(PermissionScopes scopes); static PermissionScopes listToScopes(const QStringList &scopeList); @@ -70,12 +79,14 @@ signals: void emailChanged(); void displayNameChanged(); void scopesChanged(); + void allowedThingIdsChanged(); private: QString m_username; QString m_email; QString m_displayName; PermissionScopes m_scopes = PermissionScopeNone; + QList m_allowedThingIds; }; diff --git a/libnymea-app/usermanager.cpp b/libnymea-app/usermanager.cpp index 9cd2a605..5cb403b5 100644 --- a/libnymea-app/usermanager.cpp +++ b/libnymea-app/usermanager.cpp @@ -67,6 +67,7 @@ void UserManager::setEngine(Engine *engine) 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"); @@ -94,8 +95,7 @@ Users *UserManager::users() const return m_users; } - -int UserManager::createUser(const QString &username, const QString &password, const QString &displayName, const QString &email, int permissionScopes) +int UserManager::createUser(const QString &username, const QString &password, const QString &displayName, const QString &email, int permissionScopes, const QList &allowedThingIds) { QVariantMap params; params.insert("username", username); @@ -105,7 +105,15 @@ int UserManager::createUser(const QString &username, const QString &password, co params.insert("email", email); params.insert("scopes", UserInfo::scopesToList((UserInfo::PermissionScopes)permissionScopes)); } - qCDebug(dcUserManager()) << "Creating user" << username << permissionScopes; + + if (m_engine->jsonRpcClient()->ensureServerVersion("8.4") && !allowedThingIds.isEmpty()) { + QVariantList thingIds; + foreach (const QUuid &thingId, allowedThingIds) + thingIds.append(thingId.toString()); + + params.insert("allowedThingIds", thingIds); + } + qCDebug(dcUserManager()) << "Creating user" << username << permissionScopes << allowedThingIds; return m_engine->jsonRpcClient()->sendCommand("Users.CreateUser", params, this, "createUserResponse"); } @@ -133,12 +141,19 @@ int UserManager::removeUser(const QString &username) return m_engine->jsonRpcClient()->sendCommand("Users.RemoveUser", params, this, "removeUserResponse"); } -int UserManager::setUserScopes(const QString &username, int scopes) +int UserManager::setUserScopes(const QString &username, int scopes, const QList &allowedThingIds) { QVariantMap params; params.insert("username", username); params.insert("scopes", UserInfo::scopesToList((UserInfo::PermissionScopes)scopes)); - qCDebug(dcUserManager()) << "Setting new permission scopes for user" << username << scopes << (int)scopes; + if (m_engine->jsonRpcClient()->ensureServerVersion("8.4") && !allowedThingIds.isEmpty()) { + QVariantList thingIds; + foreach (const QUuid &thingId, allowedThingIds) + thingIds.append(thingId); + + params.insert("allowedThingIds", thingIds); + } + qCDebug(dcUserManager()) << "Setting new permission scopes for user" << username << scopes << (int)scopes << allowedThingIds; return m_engine->jsonRpcClient()->sendCommand("Users.SetUserScopes", params, this, "setUserScopesResponse"); } @@ -162,6 +177,11 @@ void UserManager::notificationReceived(const QVariantMap &data) info->setDisplayName(userMap.value("displayName").toString()); info->setEmail(userMap.value("email").toString()); info->setScopes(UserInfo::listToScopes(userMap.value("scopes").toStringList())); + QList allowedThingIds; + foreach (const QString &thingIdString, userMap.value("allowedThingIds").toStringList()) + allowedThingIds.append(QUuid(thingIdString)); + + info->setAllowedThingIds(allowedThingIds); m_users->insertUser(info); } else if (notification == "Users.UserRemoved") { m_users->removeUser(data.value("params").toMap().value("username").toString()); @@ -171,11 +191,19 @@ void UserManager::notificationReceived(const QVariantMap &data) QString displayName = userMap.value("displayName").toString(); QString email = userMap.value("email").toString(); UserInfo::PermissionScopes scopes = UserInfo::listToScopes(userMap.value("scopes").toStringList()); + + QList allowedThingIds; + foreach (const QString &thingIdString, userMap.value("allowedThingIds").toStringList()) + allowedThingIds.append(QUuid(thingIdString)); + + // Update current user info if (m_userInfo && m_userInfo->username() == username) { m_userInfo->setDisplayName(displayName); m_userInfo->setEmail(email); m_userInfo->setScopes(scopes); + m_userInfo->setAllowedThingIds(allowedThingIds); + } // Update user info in the list of all users. UserInfo *info = m_users->getUserInfo(username); @@ -186,6 +214,7 @@ void UserManager::notificationReceived(const QVariantMap &data) info->setDisplayName(displayName); info->setEmail(email); info->setScopes(scopes); + info->setAllowedThingIds(allowedThingIds); } } @@ -195,10 +224,16 @@ void UserManager::getUsersResponse(int commandId, const QVariantMap &data) foreach (const QVariant &userVariant, data.value("users").toList()) { QVariantMap userMap = userVariant.toMap(); + + QList allowedThingIds; + foreach (const QString &thingIdString, userMap.value("allowedThingIds").toStringList()) + allowedThingIds.append(QUuid(thingIdString)); + 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())); + userInfo->setAllowedThingIds(allowedThingIds); m_users->insertUser(userInfo); } } @@ -207,15 +242,20 @@ void UserManager::getUserInfoResponse(int commandId, const QVariantMap &data) { qCDebug(dcUserManager()) << "User info reply" << commandId << data; QVariantMap userMap = data.value("userInfo").toMap(); + QList allowedThingIds; + foreach (const QString &thingIdString, userMap.value("allowedThingIds").toStringList()) + allowedThingIds.append(QUuid(thingIdString)); + 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())); + m_userInfo->setAllowedThingIds(allowedThingIds); } -void UserManager::getTokensResponse(int /*commandId*/, const QVariantMap &data) +void UserManager::getTokensResponse(int commandId, const QVariantMap &data) { - + Q_UNUSED(commandId) foreach (const QVariant &tokenVariant, data.value("tokenInfoList").toList()) { // qDebug() << "Token received" << tokenVariant.toMap(); QVariantMap token = tokenVariant.toMap(); @@ -226,7 +266,6 @@ void UserManager::getTokensResponse(int /*commandId*/, const QVariantMap &data) TokenInfo *tokenInfo = new TokenInfo(id, username, deviceName, creationTime); m_tokenInfos->addToken(tokenInfo); } - } void UserManager::removeTokenResponse(int commandId, const QVariantMap ¶ms) @@ -309,6 +348,13 @@ QVariant Users::data(const QModelIndex &index, int role) const return m_users.at(index.row())->email(); case RoleScopes: return static_cast(m_users.at(index.row())->scopes()); + case RoleAllowedThingIds: { + QVariantList thingIds; + foreach (const QUuid &thingId, m_users.at(index.row())->allowedThingIds()) + thingIds.append(thingId); + + return thingIds; + } } return QVariant(); } @@ -320,6 +366,7 @@ QHash Users::roleNames() const roles.insert(RoleDisplayName, "displayName"); roles.insert(RoleEmail, "email"); roles.insert(RoleScopes, "scopes"); + roles.insert(RoleAllowedThingIds, "allowedThingIds"); return roles; } @@ -344,6 +391,12 @@ void Users::insertUser(UserInfo *userInfo) emit dataChanged(index(idx), index(idx), {RoleScopes}); } }); + connect(userInfo, &UserInfo::allowedThingIdsChanged, this, [=](){ + int idx = m_users.indexOf(userInfo); + if (idx >= 0) { + emit dataChanged(index(idx), index(idx), {RoleAllowedThingIds}); + } + }); beginInsertRows(QModelIndex(), static_cast(m_users.count()), static_cast(m_users.count())); m_users.append(userInfo); diff --git a/libnymea-app/usermanager.h b/libnymea-app/usermanager.h index f3c07d9e..6935b5d9 100644 --- a/libnymea-app/usermanager.h +++ b/libnymea-app/usermanager.h @@ -71,12 +71,12 @@ 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, const QString &displayName, const QString &email, int permissionScopes = UserInfo::PermissionScopeAdmin); + Q_INVOKABLE int createUser(const QString &username, const QString &password, const QString &displayName, const QString &email, int permissionScopes = UserInfo::PermissionScopeAdmin, const QList &allowedThingIds = QList()); 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 setUserScopes(const QString &username, int permissionScopes, const QList &allowedThingIds = QList()); Q_INVOKABLE int setUserInfo(const QString &username, const QString &displayName, const QString &email); signals: @@ -124,7 +124,8 @@ public: RoleUsername, RoleDisplayName, RoleEmail, - RoleScopes + RoleScopes, + RoleAllowedThingIds }; Q_ENUM(Roles) @@ -137,14 +138,14 @@ public: void insertUser(UserInfo *userInfo); void removeUser(const QString &username); - Q_INVOKABLE UserInfo* get(int index) const; - Q_INVOKABLE UserInfo* getUserInfo(const QString &username) const; + Q_INVOKABLE UserInfo *get(int index) const; + Q_INVOKABLE UserInfo *getUserInfo(const QString &username) const; signals: void countChanged(); private: - QList m_users; + QList m_users; }; #endif // USERMANAGER_H diff --git a/nymea-app/ui/system/UsersSettingsPage.qml b/nymea-app/ui/system/UsersSettingsPage.qml index 7bf8ecd0..17ccdabe 100644 --- a/nymea-app/ui/system/UsersSettingsPage.qml +++ b/nymea-app/ui/system/UsersSettingsPage.qml @@ -31,6 +31,7 @@ import Nymea import NymeaApp.Utils import "../components" +import "../delegates" SettingsPageBase { id: root @@ -138,6 +139,7 @@ SettingsPageBase { } } + Component { id: editUserInfoComponent SettingsPageBase { @@ -190,6 +192,59 @@ SettingsPageBase { } } + Component { + id: configureAllowedThingsComponent + + Page { + id: configureAllowedThingsPage + + property UserInfo userInfo: null + + header: NymeaHeader { + text: root.title + backButtonVisible: true + onBackPressed: pageStack.pop() + } + + title: qsTr("Allowed things for") + " \"" + userInfo.username + "\"" + ColumnLayout { + anchors.fill: parent + + ListFilterInput { + id: filterInput + Layout.fillWidth: true + } + + GroupedListView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + model: ThingsProxy { + id: thingsProxy + engine: _engine + groupByInterface: true + nameFilter: filterInput.shown ? filterInput.text : "" + } + + delegate: ThingDelegate { + id: thingDelegate + thing: thingsProxy.getThing(model.id) + canDelete: false + progressive: false + additionalItem: CheckBox { + checked: configureAllowedThingsPage.userInfo.thingAllowed(thingDelegate.thing.id) + onCheckedChanged: { + configureAllowedThingsPage.userInfo.allowThingId(thingDelegate.thing.id, checked) + userManager.setUserScopes(configureAllowedThingsPage.userInfo.username, configureAllowedThingsPage.userInfo.scopes, configureAllowedThingsPage.userInfo.allowedThingIds) + } + } + } + } + } + } + } + Component { id: changePasswordComponent SettingsPageBase { @@ -318,12 +373,12 @@ SettingsPageBase { Component { id: userDetailsComponent + SettingsPageBase { id: userDetailsPage title: qsTr("Manage %1").arg(userInfo.username) property UserInfo userInfo: null - property bool restrictedThingAccess: userDetailsPage.userInfo.scopes & UserInfo.PermissionScopeAccessAllThings === 0 Component { id: confirmUserDeletionComponent @@ -381,8 +436,9 @@ SettingsPageBase { } Repeater { - model: NymeaUtils.scopesModel + id: permissionRepeater + model: NymeaUtils.scopesModel delegate: NymeaSwipeDelegate { Layout.fillWidth: true @@ -416,7 +472,7 @@ SettingsPageBase { // make sure the new permissions are consistant before sending them to the core scopes = NymeaUtils.getPermissionScopeAdjustments(model.scope, checked, scopes) - userManager.setUserScopes(userDetailsPage.userInfo.username, scopes) + userManager.setUserScopes(userDetailsPage.userInfo.username, scopes, userDetailsPage.userInfo.allowedThingIds) } } } @@ -425,7 +481,7 @@ SettingsPageBase { SettingsPageSectionHeader { text: qsTr("Acessable things") - visible: userDetailsPage.restrictedThingAccess + visible: (userDetailsPage.userInfo.scopes & UserInfo.PermissionScopeAccessAllThings) !== UserInfo.PermissionScopeAccessAllThings Layout.fillWidth: true } @@ -434,15 +490,14 @@ SettingsPageBase { id: allowedThingsEntry Layout.fillWidth: true text: qsTr("Allowed things for this user") - visible: userDetailsPage.restrictedThingAccess + subText: userDetailsPage.userInfo.allowedThingIds.length + " " + qsTr("things accessable") + visible: (userDetailsPage.userInfo.scopes & UserInfo.PermissionScopeAccessAllThings) !== UserInfo.PermissionScopeAccessAllThings progressive: true onClicked: { - + pageStack.push(configureAllowedThingsComponent, {userInfo: userDetailsPage.userInfo}) } - } - SettingsPageSectionHeader { text: qsTr("Remove") } diff --git a/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml b/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml index 493fa8c0..768c0606 100644 --- a/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml +++ b/nymea-app/ui/thingconfiguration/ConfigureThingPage.qml @@ -32,6 +32,7 @@ import "../delegates" SettingsPageBase { id: root + property Thing thing: null busy: d.pendingCommand != -1 @@ -47,12 +48,14 @@ SettingsPageBase { ThingInfoPane { id: infoPane + Layout.fillWidth: true thing: root.thing } Menu { id: deviceMenu + width: implicitWidth + app.margins x: parent.width - width @@ -194,6 +197,7 @@ SettingsPageBase { analogInputs: true analogOutputs: true } + Repeater { model: ioModel delegate: NymeaSwipeDelegate { @@ -269,6 +273,7 @@ SettingsPageBase { } property bool dirty: false } + Button { Layout.fillWidth: true Layout.leftMargin: app.margins @@ -295,13 +300,16 @@ SettingsPageBase { Component { id: errorDialog + ErrorDialog { } } Component { id: removeDialogComponent + NymeaDialog { id: removeDialog + title: qsTr("Remove thing?") text: qsTr("Are you sure you want to remove %1 and all associated settings?").arg(root.thing.name) standardButtons: Dialog.Yes | Dialog.No @@ -314,8 +322,10 @@ SettingsPageBase { Component { id: renameDialog + Dialog { id: dialog + width: parent.width * .8 x: (parent.width - width) / 2 y: app.margins @@ -340,6 +350,7 @@ SettingsPageBase { Component { id: ioConnectionsDialogComponent + NymeaDialog { id: ioConnectionDialog standardButtons: Dialog.NoButton @@ -356,18 +367,13 @@ SettingsPageBase { text: qsTr("Connect \"%1\" to:").arg(ioConnectionDialog.ioStateType.displayName) wrapMode: Text.WordWrap } -// Label { text: "\n" } // Fake in some spacing GridLayout { columns: (ioConnectionDialog.width / 400) * 2 -// Label { -// Layout.fillWidth: true -// text: qsTr("Thing") -// } - ComboBox { id: ioThingComboBox + model: ThingsProxy { id: connectableIODevices engine: _engine @@ -395,13 +401,9 @@ SettingsPageBase { } } -// Label { -// Layout.fillWidth: true -// text: (ioConnectionDialog.ioStateType.ioType == Types.IOTypeDigitalInput || ioConnectionDialog.ioStateType.ioType == Types.IOTypeAnalogInput) ? qsTr("Output") : qsTr("Input") -// } - ComboBox { id: ioStateComboBox + model: StateTypesProxy { id: connectableStateTypes stateTypes: connectableIODevices.get(ioThingComboBox.currentIndex).thingClass.stateTypes @@ -413,7 +415,7 @@ SettingsPageBase { textRole: "displayName" Layout.fillWidth: true onCountChanged: { -// print("loading for:", ioConnectionDialog.inputWatcher.ioConnection.outputStateTypeId) + // print("loading for:", ioConnectionDialog.inputWatcher.ioConnection.outputStateTypeId) for (var i = 0; i < connectableStateTypes.count; i++) { print("checking:", connectableStateTypes.get(i).id) if (ioConnectionDialog.ioStateType.ioType == Types.IOTypeDigitalInput || ioConnectionDialog.ioStateType.ioType == Types.IOTypeAnalogInput) { @@ -443,14 +445,16 @@ SettingsPageBase { checked: ioConnectionDialog.isInput ? ioConnectionDialog.inputWatcher.ioConnection.inverted : ioConnectionDialog.outputWatcher.ioConnection.inverted } } - } + } GridLayout { id: buttonGrid + columns: width > (cancelButton.implicitWidth + disconnectButton.implicitWidth + connectButton.implicitWidth) ? 4 : 1 layoutDirection: columns == 1 ? Qt.RightToLeft : Qt.LeftToRight + Item { Layout.fillWidth: true } @@ -461,6 +465,7 @@ SettingsPageBase { Layout.fillWidth: buttonGrid.columns === 1 onClicked: ioConnectionDialog.reject(); } + Button { id: disconnectButton text: qsTr("Disconnect") @@ -478,6 +483,7 @@ SettingsPageBase { ioConnectionDialog.reject(); } } + Button { id: connectButton text: qsTr("Connect") @@ -510,8 +516,6 @@ SettingsPageBase { } } } - - } } }