Add support for user permissions
This commit is contained in:
parent
61ea4c80c7
commit
120481ed6c
@ -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 ¶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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@ -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 ¶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<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 ¶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<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 ¶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<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 ¶ms)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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<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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
35
nymea-app/ui/components/SettingsTile.qml
Normal file
35
nymea-app/ui/components/SettingsTile.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user