diff --git a/plugin/evdashengine.h b/plugin/evdashengine.h index 1b9fa44..df6fc47 100644 --- a/plugin/evdashengine.h +++ b/plugin/evdashengine.h @@ -52,6 +52,7 @@ public: EvDashErrorNoError = 0, EvDashErrorBackendError, EvDashErrorDuplicateUser, + EvDashErrorUserNotFound, EvDashErrorBadPassword }; Q_ENUM(EvDashError) diff --git a/plugin/evdashjsonhandler.cpp b/plugin/evdashjsonhandler.cpp index f72cec6..60338f1 100644 --- a/plugin/evdashjsonhandler.cpp +++ b/plugin/evdashjsonhandler.cpp @@ -30,18 +30,22 @@ #include "evdashjsonhandler.h" #include "evdashengine.h" +#include "evdashwebserverresource.h" + #include Q_DECLARE_LOGGING_CATEGORY(dcEvDashExperience) -EvDashJsonHandler::EvDashJsonHandler(EvDashEngine *engine, QObject *parent): +EvDashJsonHandler::EvDashJsonHandler(EvDashEngine *engine, EvDashWebServerResource *resource, QObject *parent): JsonHandler{parent}, - m_engine{engine} + m_engine{engine}, + m_resource{resource} { registerEnum(); - QVariantMap params, returns; QString description; + QVariantMap params, returns; + params.clear(); returns.clear(); description = "Get the enabled status of EV Dash service."; @@ -54,16 +58,52 @@ EvDashJsonHandler::EvDashJsonHandler(EvDashEngine *engine, QObject *parent): returns.insert("evDashError", enumRef()); registerMethod("SetEnabled", description, params, returns); + + params.clear(); returns.clear(); + description = "Get the list of available users names."; + returns.insert("usernames", enumValueName(StringList)); + registerMethod("GetUsers", description, params, returns); + + params.clear(); returns.clear(); + description = "Add a new user with the given username and password."; + params.insert("username", enumValueName(String)); + params.insert("password", enumValueName(String)); + returns.insert("evDashError", enumRef()); + registerMethod("AddUser", description, params, returns); + + params.clear(); returns.clear(); + description = "Remove the user with the given username."; + params.insert("username", enumValueName(String)); + returns.insert("evDashError", enumRef()); + registerMethod("RemoveUser", description, params, returns); + // Notifications params.clear(); description = "Emitted whenever the EV Dash service has been enabled or disabled."; params.insert("enabled", enumValueName(Bool)); registerNotification("EnabledChanged", description, params); - connect(m_engine, &EvDashEngine::enabledChanged, this, [=](bool enabled){ + connect(m_engine, &EvDashEngine::enabledChanged, this, [this](bool enabled){ emit EnabledChanged({{"enabled", enabled}}); }); + params.clear(); + description = "Emitted whenever a new username has been added for the dashboard."; + params.insert("username", enumValueName(String)); + registerNotification("UserAdded", description, params); + + connect(m_resource, &EvDashWebServerResource::userAdded, this, [this](const QString &username){ + emit UserAdded({{"username", username}}); + }); + + params.clear(); + description = "Emitted whenever a username has been removed from the dashboard."; + params.insert("username", enumValueName(String)); + registerNotification("UserRemoved", description, params); + + connect(m_resource, &EvDashWebServerResource::userRemoved, this, [this](const QString &username){ + emit UserRemoved({{"username", username}}); + }); } QString EvDashJsonHandler::name() const @@ -93,3 +133,36 @@ JsonReply *EvDashJsonHandler::SetEnabled(const QVariantMap ¶ms) return createReply(returns); } +JsonReply *EvDashJsonHandler::GetUsers(const QVariantMap ¶ms) +{ + Q_UNUSED(params) + + QVariantMap returns; + returns.insert("usernames", m_resource->usernames()); + return createReply(returns); +} + +JsonReply *EvDashJsonHandler::AddUser(const QVariantMap ¶ms) +{ + QString username = params.value("username").toString(); + QString password = params.value("password").toString(); + + EvDashEngine::EvDashError error = m_resource->addUser(username, password); + + QVariantMap returns; + returns.insert("evDashError", enumValueName(error)); + return createReply(returns); +} + +JsonReply *EvDashJsonHandler::RemoveUser(const QVariantMap ¶ms) +{ + QString username = params.value("username").toString(); + + EvDashEngine::EvDashError error = m_resource->removeUser(username); + + QVariantMap returns; + returns.insert("evDashError", enumValueName(error)); + return createReply(returns); + +} + diff --git a/plugin/evdashjsonhandler.h b/plugin/evdashjsonhandler.h index 4cbdbcc..9a8a565 100644 --- a/plugin/evdashjsonhandler.h +++ b/plugin/evdashjsonhandler.h @@ -36,23 +36,32 @@ #include class EvDashEngine; +class EvDashWebServerResource; class EvDashJsonHandler : public JsonHandler { Q_OBJECT public: - explicit EvDashJsonHandler(EvDashEngine *engine, QObject *parent = nullptr); + explicit EvDashJsonHandler(EvDashEngine *engine, EvDashWebServerResource *resource, QObject *parent = nullptr); QString name() const override; Q_INVOKABLE JsonReply *GetEnabled(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *SetEnabled(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *GetUsers(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *AddUser(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *RemoveUser(const QVariantMap ¶ms); + signals: void EnabledChanged(const QVariantMap ¶ms); + void UserAdded(const QVariantMap ¶ms); + void UserRemoved(const QVariantMap ¶ms); + private: EvDashEngine *m_engine = nullptr; + EvDashWebServerResource *m_resource = nullptr; }; diff --git a/plugin/evdashwebserverresource.cpp b/plugin/evdashwebserverresource.cpp index 3542ed7..1f65bf9 100644 --- a/plugin/evdashwebserverresource.cpp +++ b/plugin/evdashwebserverresource.cpp @@ -1,10 +1,42 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2025, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #include "evdashwebserverresource.h" +#include "evdashsettings.h" #include #include #include #include #include +#include #include Q_DECLARE_LOGGING_CATEGORY(dcEvDashExperience) @@ -12,7 +44,25 @@ Q_DECLARE_LOGGING_CATEGORY(dcEvDashExperience) EvDashWebServerResource::EvDashWebServerResource(QObject *parent) : WebServerResource{"/evdash", parent} { + // Load users + EvDashSettings settings; + settings.beginGroup("Users"); + foreach (const QString &username, settings.childGroups()) { + UserInfo info; + + settings.beginGroup(username); + info.username = username; + info.passwordHash = settings.value("hash").toString().toUtf8(); + info.passwordSalt = settings.value("salt").toString().toUtf8(); + settings.endGroup(); // username + + m_users.insert(username, info); + } + + settings.endGroup(); // Users + + qCInfo(dcEvDashExperience()) << "Loaded" << m_users.count() << "users for the dashboard."; } HttpReply *EvDashWebServerResource::processRequest(const HttpRequest &request) @@ -52,6 +102,72 @@ HttpReply *EvDashWebServerResource::processRequest(const HttpRequest &request) return redirectToIndex(); } +QStringList EvDashWebServerResource::usernames() const +{ + return m_users.keys(); +} + +EvDashEngine::EvDashError EvDashWebServerResource::addUser(const QString &username, const QString &password) +{ + if (m_users.keys().contains(username)) { + qCWarning(dcEvDashExperience()) << "Cannot add new user. There is already a user with the username" << username; + return EvDashEngine::EvDashErrorDuplicateUser; + } + + if (password.size() < s_minimalPasswordLength) { + qCWarning(dcEvDashExperience()) << "Cannot add new user. The given password is to short. The minimum size is" << s_minimalPasswordLength; + return EvDashEngine::EvDashErrorBadPassword; + } + + UserInfo info; + info.username = username; + info.passwordSalt = QUuid::createUuid().toString().remove(QRegularExpression("[{}]")).toUtf8(); + info.passwordHash = QCryptographicHash::hash(QString(password + info.passwordSalt).toUtf8(), QCryptographicHash::Sha3_512).toBase64(); + + EvDashSettings settings; + settings.beginGroup("Users"); + settings.beginGroup(username); + settings.setValue("hash", QString::fromUtf8(info.passwordHash)); + settings.setValue("salt", QString::fromUtf8(info.passwordSalt)); + settings.endGroup(); // username + settings.endGroup(); // Users + + qCDebug(dcEvDashExperience()) << "Added successfully new user with username" << username; + + m_users.insert(username, info); + emit userAdded(username); + + return EvDashEngine::EvDashErrorNoError; +} + +EvDashEngine::EvDashError EvDashWebServerResource::removeUser(const QString &username) +{ + if (!m_users.contains(username)) { + qCWarning(dcEvDashExperience()) << "Cannot remove user with username" << username << "because there is no such user."; + return EvDashEngine::EvDashErrorUserNotFound; + } + + m_users.remove(username); + + foreach (const QString &token, m_activeTokens.keys()) { + if (m_activeTokens.value(token).username == username) { + qCDebug(dcEvDashExperience()) << "Revoke active token" << token << "for user" << username; + m_activeTokens.remove(token); + } + } + + EvDashSettings settings; + settings.beginGroup("Users"); + settings.remove(username); + settings.endGroup(); // Users + + qCDebug(dcEvDashExperience()) << "User with username" << username << "removed successfully"; + + emit userRemoved(username); + + return EvDashEngine::EvDashErrorNoError; +} + HttpReply *EvDashWebServerResource::redirectToIndex() { HttpReply *reply = HttpReply::createErrorReply(HttpReply::PermanentRedirect); @@ -156,8 +272,12 @@ HttpReply *EvDashWebServerResource::handleRefreshRequest(const HttpRequest &requ bool EvDashWebServerResource::verifyCredentials(const QString &username, const QString &password) const { - Q_UNUSED(username) - Q_UNUSED(password) + const UserInfo info = m_users.value(username); + if (info.passwordHash != QCryptographicHash::hash(QString(password + info.passwordSalt).toUtf8(), QCryptographicHash::Sha3_512).toBase64()) { + qCWarning(dcEvDashExperience()) << "Authentication error for user:" << username; + return false; + } + return true; } diff --git a/plugin/evdashwebserverresource.h b/plugin/evdashwebserverresource.h index 4c41243..21b1839 100644 --- a/plugin/evdashwebserverresource.h +++ b/plugin/evdashwebserverresource.h @@ -1,3 +1,33 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2025, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU General Public License as published by the Free Software +* Foundation, GNU version 3. This project is distributed in the hope that it +* will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty +* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along with +* this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + #ifndef EVDASHWEBSERVERRESOURCE_H #define EVDASHWEBSERVERRESOURCE_H @@ -8,6 +38,8 @@ #include +#include "evdashengine.h" + class QJsonObject; class EvDashWebServerResource : public WebServerResource @@ -18,26 +50,48 @@ public: HttpReply *processRequest(const HttpRequest &request) override; + // User management + QStringList usernames() const; + + EvDashEngine::EvDashError addUser(const QString &username, const QString &password); + EvDashEngine::EvDashError removeUser(const QString &username); + bool validateToken(const QString &token); +signals: + void userAdded(const QString &username); + void userRemoved(const QString &username); + private: struct TokenInfo { QString username; QDateTime expiresAt; }; + struct UserInfo { + QString username; + QByteArray passwordHash; + QByteArray passwordSalt; + }; + + static constexpr int s_tokenLifetimeSeconds = 3600; + static constexpr int s_minimalPasswordLength = 4; + + QHash m_users; + QHash m_activeTokens; + HttpReply *handleLoginRequest(const HttpRequest &request); HttpReply *handleRefreshRequest(const HttpRequest &request); HttpReply *redirectToIndex(); - bool verifyStaticFile(const QString &fileName); - bool verifyCredentials(const QString &username, const QString &password) const; QString issueToken(const QString &username); void purgeExpiredTokens(); - static constexpr int s_tokenLifetimeSeconds = 3600; + bool verifyStaticFile(const QString &fileName); + + bool verifyCredentials(const QString &username, const QString &password) const; + - QHash m_activeTokens; }; #endif // EVDASHWEBSERVERRESOURCE_H diff --git a/plugin/experiencepluginevdash.cpp b/plugin/experiencepluginevdash.cpp index 234e61e..9c51289 100644 --- a/plugin/experiencepluginevdash.cpp +++ b/plugin/experiencepluginevdash.cpp @@ -49,13 +49,13 @@ void ExperiencePluginEvDash::init() { qCDebug(dcEvDashExperience()) << "Initializing experience..."; - m_webServerResource = new EvDashWebServerResource(this); - m_engine = new EvDashEngine(thingManager(), m_webServerResource, this); + m_resource = new EvDashWebServerResource(this); + m_engine = new EvDashEngine(thingManager(), m_resource, this); - jsonRpcServer()->registerExperienceHandler(new EvDashJsonHandler(m_engine, this), 1, 0); + jsonRpcServer()->registerExperienceHandler(new EvDashJsonHandler(m_engine, m_resource, this), 1, 0); } WebServerResource *ExperiencePluginEvDash::webServerResource() const { - return m_webServerResource; + return m_resource; } diff --git a/plugin/experiencepluginevdash.h b/plugin/experiencepluginevdash.h index 8f48c2b..3d5644b 100644 --- a/plugin/experiencepluginevdash.h +++ b/plugin/experiencepluginevdash.h @@ -55,7 +55,7 @@ public: private: EvDashEngine *m_engine = nullptr; - EvDashWebServerResource *m_webServerResource = nullptr; + EvDashWebServerResource *m_resource = nullptr; };