Add support for user permissions

This commit is contained in:
Michael Zanetti 2021-05-14 21:57:11 +02:00
parent 61ea4c80c7
commit 120481ed6c
21 changed files with 783 additions and 305 deletions

View File

@ -333,6 +333,11 @@ QHash<QString, QString> 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 &params)
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 &params)
}
// 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 &params)
}
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();

View File

@ -37,6 +37,7 @@
#include <QVersionNumber>
#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<QString, QString> 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<QString, QString> m_cacheHashes;
QVariantMap m_experiences;
UserInfo::PermissionScopes m_permissionScopes = UserInfo::PermissionScopeNone;
QString m_username;
void setNotificationsEnabled();
void getCloudConnectionStatus();

View File

@ -353,6 +353,7 @@ void registerQmlTypes() {
qmlRegisterUncreatableType<UserInfo>(uri, 1, 0, "UserInfo", "Get it from UserManager");
qmlRegisterUncreatableType<TokenInfo>(uri, 1, 0, "TokenInfo", "Get it from TokenInfos");
qmlRegisterUncreatableType<TokenInfos>(uri, 1, 0, "TokenInfos", "Get it from UserManager");
qmlRegisterUncreatableType<Users>(uri, 1, 0, "Users", "Get it from UserManager");
qmlRegisterUncreatableType<IOConnections>(uri, 1, 0, "IOConnections", "Get it from ThingManager");
qmlRegisterUncreatableType<IOConnection>(uri, 1, 0, "IOConnection", "Get it from IOConnections");

View File

@ -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";
}

View File

@ -1,5 +1,8 @@
#include "userinfo.h"
#include <QMetaEnum>
#include <QDebug>
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<PermissionScopes>();
for (int i = 0; i < metaEnum.keyCount(); i++) {
if (scopes.testFlag(static_cast<PermissionScope>(metaEnum.value(i)))) {
ret << metaEnum.key(i);
}
}
return ret;
}
UserInfo::PermissionScopes UserInfo::listToScopes(const QStringList &scopeList)
{
PermissionScopes ret;
QMetaEnum metaEnum = QMetaEnum::fromType<PermissionScopes>();
for (int i = 0; i < metaEnum.keyCount(); i++) {
if (scopeList.contains(metaEnum.key(i))) {
ret.setFlag(static_cast<PermissionScope>(metaEnum.value(i)));
}
}
return ret;
}

View File

@ -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;
};

View File

@ -4,11 +4,23 @@
#include <QDebug>
#include <QMetaEnum>
#include "logging.h"
NYMEA_LOGGING_CATEGORY(dcUserManager, "UserManager")
UserManager::UserManager(QObject *parent):
QObject(parent)
{
qRegisterMetaType<UserInfo::PermissionScopes>();
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 &params)
void UserManager::removeTokenResponse(int commandId, const QVariantMap &params)
{
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<UserManager::UserError>();
UserError error = static_cast<UserError>(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 &params)
void UserManager::changePasswordResponse(int commandId, const QVariantMap &params)
{
qDebug() << "Change password reply" << commandId << params;
qCDebug(dcUserManager()) << "Change password reply" << commandId << params;
QString errorString = params.value("error").toString();
QMetaEnum metaEnum = QMetaEnum::fromType<UserManager::UserError>();
UserError error = static_cast<UserError>(metaEnum.keyToValue(errorString.toUtf8()));
emit changePasswordResponse(commandId, error);
emit changePasswordReply(commandId, error);
}
void UserManager::createUserResponse(int commandId, const QVariantMap &params)
{
qCDebug(dcUserManager()) << "Create user response:" << commandId << params;
}
void UserManager::removeUserResponse(int commandId, const QVariantMap &params)
{
qCDebug(dcUserManager()) << "Remove user response:" << commandId << params;
QMetaEnum metaEnum = QMetaEnum::fromType<UserManager::UserError>();
UserError error = static_cast<UserError>(metaEnum.keyToValue(params.value("error").toString().toUtf8()));
emit removeUserReply(commandId, error);
}
void UserManager::setUserScopesResponse(int commandId, const QVariantMap &params)
{
qCDebug(dcUserManager()) << "Set user scopes response:" << commandId << params;
QMetaEnum metaEnum = QMetaEnum::fromType<UserManager::UserError>();
UserError error = static_cast<UserError>(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<int>(m_users.at(index.row())->scopes());
}
return QVariant();
}
QHash<int, QByteArray> Users::roleNames() const
{
QHash<int, QByteArray> 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;
}

View File

@ -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 &params);
void changePasswordReply(int commandId, const QVariantMap &params);
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 &params);
void changePasswordResponse(int commandId, const QVariantMap &params);
void createUserResponse(int commandId, const QVariantMap &params);
void removeUserResponse(int commandId, const QVariantMap &params);
void setUserScopesResponse(int commandId, const QVariantMap &params);
private:
Engine *m_engine = nullptr;
@ -65,7 +79,39 @@ private:
UserInfo *m_userInfo = nullptr;
TokenInfos *m_tokenInfos = nullptr;
Users *m_users = nullptr;
QHash<int, QUuid> 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<int, QByteArray> 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<UserInfo*> m_users;
};
#endif // USERMANAGER_H

View File

@ -272,5 +272,6 @@
<file>ui/mainviews/energy/EnergySettingsPage.qml</file>
<file>ui/mainviews/energy/ConsumersPieChart.qml</file>
<file>ui/components/NymeaToolTip.qml</file>
<file>ui/components/SettingsTile.qml</file>
</qresource>
</RCC>

View File

@ -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();

View File

@ -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
}

View File

@ -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"))
}
}
}

View File

@ -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"

View File

@ -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"))
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}
}
}

View File

@ -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;
}
}