Add user management

This commit is contained in:
Simon Stürz 2025-11-13 10:22:57 +01:00
parent 7b408be5fa
commit 367562ea96
7 changed files with 273 additions and 16 deletions

View File

@ -52,6 +52,7 @@ public:
EvDashErrorNoError = 0,
EvDashErrorBackendError,
EvDashErrorDuplicateUser,
EvDashErrorUserNotFound,
EvDashErrorBadPassword
};
Q_ENUM(EvDashError)

View File

@ -30,18 +30,22 @@
#include "evdashjsonhandler.h"
#include "evdashengine.h"
#include "evdashwebserverresource.h"
#include <QLoggingCategory>
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<EvDashEngine::EvDashError>();
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<EvDashEngine::EvDashError>());
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<EvDashEngine::EvDashError>());
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<EvDashEngine::EvDashError>());
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 &params)
return createReply(returns);
}
JsonReply *EvDashJsonHandler::GetUsers(const QVariantMap &params)
{
Q_UNUSED(params)
QVariantMap returns;
returns.insert("usernames", m_resource->usernames());
return createReply(returns);
}
JsonReply *EvDashJsonHandler::AddUser(const QVariantMap &params)
{
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 &params)
{
QString username = params.value("username").toString();
EvDashEngine::EvDashError error = m_resource->removeUser(username);
QVariantMap returns;
returns.insert("evDashError", enumValueName(error));
return createReply(returns);
}

View File

@ -36,23 +36,32 @@
#include <jsonrpc/jsonhandler.h>
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 &params);
Q_INVOKABLE JsonReply *SetEnabled(const QVariantMap &params);
Q_INVOKABLE JsonReply *GetUsers(const QVariantMap &params);
Q_INVOKABLE JsonReply *AddUser(const QVariantMap &params);
Q_INVOKABLE JsonReply *RemoveUser(const QVariantMap &params);
signals:
void EnabledChanged(const QVariantMap &params);
void UserAdded(const QVariantMap &params);
void UserRemoved(const QVariantMap &params);
private:
EvDashEngine *m_engine = nullptr;
EvDashWebServerResource *m_resource = nullptr;
};

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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 <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QUuid>
#include <QCryptographicHash>
#include <QLoggingCategory>
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;
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*
* 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 <webserver/webserverresource.h>
#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<QString, UserInfo> m_users;
QHash<QString, TokenInfo> 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<QString, TokenInfo> m_activeTokens;
};
#endif // EVDASHWEBSERVERRESOURCE_H

View File

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

View File

@ -55,7 +55,7 @@ public:
private:
EvDashEngine *m_engine = nullptr;
EvDashWebServerResource *m_webServerResource = nullptr;
EvDashWebServerResource *m_resource = nullptr;
};