Compare commits

3 Commits

Author SHA1 Message Date
Digital Artifex 4182e2d5a7 Added additional UserServices functionality 2026-08-15 02:35:45 -04:00
Digital Artifex a56532a047 Corrected QObject visibility 2026-08-15 02:34:33 -04:00
Digital Artifex ab0ab4b4fb Added UserModel 2026-08-15 02:33:48 -04:00
7 changed files with 604 additions and 17 deletions
+6 -3
View File
@@ -56,9 +56,12 @@ qt_add_qml_module(
common/shaderpack.h common/shaderpack.h
packcompiler.h packcompiler.h
packcompiler.cpp packcompiler.cpp
SOURCES common/userservices.h common/userservices.h
SOURCES common/userservices.cpp common/userservices.cpp
SOURCES common/qwallet.h common/qwallet.cpp common/qwallet.h
common/qwallet.cpp
usermodel.h
usermodel.cpp
) )
target_compile_definitions( target_compile_definitions(
+2 -7
View File
@@ -69,14 +69,9 @@ inline const QString KOMPLEX_ENDPOINT_USER_AUTH = QStringLiteral("
inline const qsizetype nullsize = std::numeric_limits<qsizetype>::min(); inline const qsizetype nullsize = std::numeric_limits<qsizetype>::min();
inline const std::chrono::milliseconds KOMPLEX_RATE_LIMIT(500); inline const std::chrono::milliseconds KOMPLEX_RATE_LIMIT(500);
inline const QString KOMPLEX_KEYCHAIN_PASSWORD_KEY = QStringLiteral inline const QString KOMPLEX_KEYCHAIN_SERVICE_NAME = QStringLiteral
( (
"com.digitalartifex.komplex.keychain.password" "com.digitalartifex.komplex"
);
inline const QString KOMPLEX_KEYCHAIN_TOKEN_KEY = QStringLiteral
(
"com.digitalartifex.komplex.keychain.token"
); );
#endif // KOMPLEX_GLOBAL_H #endif // KOMPLEX_GLOBAL_H
+1 -1
View File
@@ -30,7 +30,7 @@
* @brief The QWallet class * @brief The QWallet class
* QWallet is a QFuture wrapper for QtKeychain to make interaction with the keychain easier * QWallet is a QFuture wrapper for QtKeychain to make interaction with the keychain easier
*/ */
class KOMPLEX_EXPORT QWallet : QObject class KOMPLEX_EXPORT QWallet : public QObject
{ {
Q_OBJECT Q_OBJECT
+206 -3
View File
@@ -1,5 +1,11 @@
#include "userservices.h" #include "userservices.h"
#include "common/komplex_global.h"
#include "coreservices.h" #include "coreservices.h"
#include <QtConcurrent/qtconcurrentrun.h>
#include <exception>
#include <qcontainerfwd.h>
#include <qstandardpaths.h>
#include <qstringview.h>
UserServices::UserServices(QObject *parent) : QObject(parent) UserServices::UserServices(QObject *parent) : QObject(parent)
{ {
@@ -8,6 +14,10 @@ UserServices::UserServices(QObject *parent) : QObject(parent)
UserServices::~UserServices() UserServices::~UserServices()
{ {
if(m_wallet)
{
m_wallet->deleteLater();
}
} }
auto UserServices::setUserCredentials(const UserCredentials &credentials) -> void auto UserServices::setUserCredentials(const UserCredentials &credentials) -> void
@@ -24,12 +34,135 @@ auto UserServices::errorString() const -> const QString &
auto UserServices::setErrorString(const QString &errorString) -> void auto UserServices::setErrorString(const QString &errorString) -> void
{ {
if (m_errorString == errorString) if (m_errorString == errorString)
{
return; return;
}
m_errorString = errorString; m_errorString = errorString;
emit errorStringChanged(); emit errorStringChanged();
} }
auto UserServices::instance() const -> QWeakPointer<UserServices> auto UserServices::savePassword(const QString &username, const QByteArray &password) -> void
{
QFuture saveAttempt = m_wallet->write
(
KOMPLEX_KEYCHAIN_SERVICE_NAME,
username,
password
);
}
auto UserServices::readPassword(const QString &username) -> QFuture<QByteArray>
{
return m_wallet->read(KOMPLEX_KEYCHAIN_SERVICE_NAME, username);
}
auto UserServices::readToken(const QString &username) -> QFuture<UserCredentials>
{
return QtConcurrent::run
(
[this, username] () -> UserCredentials
{
UserCredentials credentials;
QFuture readAttempt = m_wallet->read
(
KOMPLEX_KEYCHAIN_SERVICE_NAME,
username + QStringLiteral("_token")
);
QString _username = username;
readAttempt
.then
(
[this, &credentials, _username] (const QByteArray &value) -> void
{
auto parts = value.split(PART_SEPARATOR);
QDateTime expiry;
if(parts.count() == 1)
{
setErrorString(QStringLiteral("Token malformed"));
throw std::exception();
}
if(parts[0].length() != sizeof(qint64))
{
setErrorString(QStringLiteral("Token expiry malformed"));
throw std::exception();
}
QByteArray numericData = parts[0];
qint64 numericConversion = 0;
for(int i = 0; i < sizeof(qint64); ++i)
{
numericConversion |=
(
static_cast<qint64>
(
(
static_cast<qint8>(numericData[i]) &
0xFF
)
<< (i * sizeof(qint8))
)
);
}
expiry = QDateTime::fromMSecsSinceEpoch(numericConversion);
if(parts[1].isEmpty() && parts.count() == 3)
{
parts[1] = parts[2];
}
credentials = UserCredentials
{
.username = _username.toUtf8(),
.sessionToken = parts[1],
.expiry = expiry
};
}
)
.onFailed
(
[this] () -> void
{
setErrorString(m_wallet->errorString());
throw std::exception();
}
);
readAttempt.waitForFinished();
return credentials;
}
);
}
auto UserServices::saveToken(const QString &username, const QByteArray &token, const QDateTime &expiry) -> void
{
QByteArray data;
qint64 numericData = expiry.toMSecsSinceEpoch();
for(int i = 0; i < sizeof(qint64); ++i)
{
data[i] = 0xFF & static_cast<qint8>(numericData >> (i * sizeof(qint8)));
}
data += PART_SEPARATOR + token;
QFuture saveAttempt = m_wallet->write
(
KOMPLEX_KEYCHAIN_SERVICE_NAME,
username + QString("_token"),
data
);
}
auto UserServices::instance() -> QWeakPointer<UserServices>
{ {
if(!s_pointer) if(!s_pointer)
{ {
@@ -53,16 +186,19 @@ auto UserServices::login(const QString &username, const QByteArray &password) ->
{ {
return QtConcurrent::run return QtConcurrent::run
( (
[this, username = std::move(username), password = std::move(password)] () -> UserCredentials [this, username, password] () -> UserCredentials
{ {
QSharedPointer<QNetworkAccessManager> manager = CoreServices::networkAccessManager().toStrongRef(); QSharedPointer<QNetworkAccessManager> manager = CoreServices::networkAccessManager().toStrongRef();
if(!manager) if(!manager)
{ {
Q_EMIT loginFailed(); Q_EMIT loginFailed();
return {}; setErrorString(QStringLiteral("Network manager reference deleted"));
throw std::exception();
} }
saveDefaultUsername(username);
QNetworkRequest request QNetworkRequest request
( (
QUrl QUrl
@@ -139,10 +275,77 @@ auto UserServices::login(const QString &username, const QByteArray &password) ->
.expiry = QDateTime::currentDateTime() .expiry = QDateTime::currentDateTime()
}; };
savePassword(username, password);
saveToken(username, credentials.sessionToken, credentials.expiry);
setUserCredentials(credentials); setUserCredentials(credentials);
Q_EMIT loginComplete(); Q_EMIT loginComplete();
return credentials; return credentials;
} }
); );
}
auto UserServices::saveDefaultUsername(const QString &username) -> void
{
QDir configDirectory(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
if(!configDirectory.exists())
{
if(!configDirectory.mkpath(configDirectory.absolutePath()))
{
setErrorString(QStringLiteral("Could not make config directory"));
throw std::exception();
}
}
QFile usernameFile(configDirectory.absoluteFilePath(QStringLiteral("uname")));
if(!usernameFile.open(QFile::ReadWrite | QFile::Truncate))
{
setErrorString(QStringLiteral("Could not open uname file"));
throw std::exception();
}
QByteArray data = username.toUtf8().toBase64();
if(usernameFile.write(data) != data.length())
{
setErrorString(QStringLiteral("Could not write uname file"));
}
usernameFile.close();
}
auto UserServices::defaultUsername() -> QFuture<QString>
{
return QtConcurrent::run
(
[this] () -> QString
{
QDir configDirectory(QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation));
if(!configDirectory.exists())
{
if(!configDirectory.mkpath(configDirectory.absolutePath()))
{
setErrorString(QStringLiteral("Could not make config directory"));
throw std::exception();
}
}
QFile usernameFile(configDirectory.absoluteFilePath(QStringLiteral("uname")));
if(!usernameFile.open(QFile::ReadOnly))
{
setErrorString(QStringLiteral("Could not open uname file"));
throw std::exception();
}
QByteArray data = QByteArray::fromBase64(usernameFile.readAll());
usernameFile.close();
return QString(data);
}
);
} }
+25 -3
View File
@@ -8,6 +8,10 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QDir>
#include <QFile>
#include <QStandardPaths>
#include "komplex_global.h" #include "komplex_global.h"
#include "qwallet.h" #include "qwallet.h"
@@ -32,23 +36,34 @@ struct UserCredentials
!sessionToken.isEmpty() && sessionToken.isValidUtf8(); !sessionToken.isEmpty() && sessionToken.isValidUtf8();
} }
}; };
Q_DECLARE_METATYPE(UserCredentials)
class KOMPLEX_EXPORT UserServices : public QObject class KOMPLEX_EXPORT UserServices : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
auto instance() const -> QWeakPointer<UserServices>; static auto instance() -> QWeakPointer<UserServices>;
static auto userCredentials() -> const UserCredentials &; static auto userCredentials() -> const UserCredentials &;
[[nodiscard]]
auto login(const QString &username, const QByteArray &password) -> QFuture<UserCredentials>; auto login(const QString &username, const QByteArray &password) -> QFuture<UserCredentials>;
auto errorString() const -> const QString &; auto errorString() const -> const QString &;
[[nodiscard]]
auto readPassword(const QString &username) -> QFuture<QByteArray>;
[[nodiscard]]
auto readToken(const QString &username) -> QFuture<UserCredentials>;
[[nodiscard]]
auto defaultUsername() -> QFuture<QString>;
signals: signals:
auto loginComplete() -> void; auto loginComplete() -> void;
auto loginFailed() -> void; auto loginFailed() -> void;
auto userCredentialsChanged() -> void; auto userCredentialsChanged() -> void;
auto errorStringChanged() -> void; auto errorStringChanged() -> void;
private: private:
@@ -60,10 +75,17 @@ private:
auto setUserCredentials(const UserCredentials &credentials) -> void; auto setUserCredentials(const UserCredentials &credentials) -> void;
auto setErrorString(const QString &errorString) -> void; auto setErrorString(const QString &errorString) -> void;
auto savePassword(const QString &username, const QByteArray &password) -> void;
auto saveToken(const QString &username, const QByteArray &token, const QDateTime &expiry) -> void;
auto saveDefaultUsername(const QString &username) -> void;
QWallet *m_wallet = nullptr; QWallet *m_wallet = nullptr;
QString m_errorString; QString m_errorString;
inline static UserCredentials s_userCredentials; inline static UserCredentials s_userCredentials;
inline const static char PART_SEPARATOR = static_cast<char>('\0x1f');
Q_PROPERTY(UserCredentials userCredentials READ userCredentials NOTIFY userCredentialsChanged FINAL) Q_PROPERTY(UserCredentials userCredentials READ userCredentials NOTIFY userCredentialsChanged FINAL)
Q_PROPERTY(QString errorString READ errorString WRITE setErrorString NOTIFY errorStringChanged FINAL) Q_PROPERTY(QString errorString READ errorString WRITE setErrorString NOTIFY errorStringChanged FINAL)
}; };
Q_DECLARE_METATYPE(UserServices)
+288
View File
@@ -0,0 +1,288 @@
#include "usermodel.h"
#include "common/userservices.h"
#include <QtConcurrent/qtconcurrentrun.h>
#include <qdatetime.h>
#include <qfuture.h>
#include <qobjectdefs.h>
#include <qstringview.h>
UserModel::UserModel(QObject *parent)
: QObject{parent}
{
m_stateResetTimer = new QTimer(this);
m_stateResetTimer->setInterval(1000);
m_stateResetTimer->setSingleShot(true);
QObject::connect
(
m_stateResetTimer,
&QTimer::timeout,
this,
&UserModel::resetState
);
setState(Busy);
QSharedPointer<UserServices> service = UserServices::instance();
if(service == nullptr)
{
setErrorString(QStringLiteral("UserServices object deleted"));
setState(LoginFailure);
return;
}
QFuture<QString> usernameFuture = service->defaultUsername();
usernameFuture
.then
(
[this] (const QString username)
{
login(username);
}
)
.onCanceled
(
[this] () -> void
{
reset();
}
)
.onFailed
(
[this] () -> void
{
reset();
}
);
service.clear();
}
UserModel::~UserModel()
{
if(m_stateResetTimer != nullptr)
{
m_stateResetTimer->deleteLater();
}
}
auto UserModel::login() -> void
{
QFuture aether = QtConcurrent::run
(
[this] () -> void
{
QMutexLocker locker(&m_loginMutex);
setState(LoggingIn);
QSharedPointer<UserServices> service = UserServices::instance();
if(service == nullptr)
{
setErrorString(QStringLiteral("UserServices object deleted"));
setState(LoginFailure);
return;
}
m_loginAttempt = service->login(username(), password().toUtf8());
m_loginAttempt
.then
(
[this] (UserCredentials credentials) -> void
{
setState(LoggedIn);
}
)
.onCanceled
(
[this] () -> void
{
setState(LoginFailure);
}
)
.onFailed
(
[this] () -> void
{
setState(LoginCancelled);
}
);
}
);
}
auto UserModel::login(const QString username) -> void
{
if(username == m_username)
{
return;
}
m_username = username;
Q_EMIT usernameChanged();
QSharedPointer<UserServices> service = UserServices::instance();
if(service == nullptr)
{
setErrorString(QStringLiteral("UserServices object deleted"));
setState(LoggedOut);
return;
}
QFuture<UserCredentials> tokenRead = service->readToken(username);
tokenRead
.then
(
[this] (const UserCredentials credentials) -> void
{
if(QDateTime::currentDateTime() <= credentials.expiry)
{
setState(LoggedIn);
return;
}
QSharedPointer<UserServices> service = UserServices::instance();
if(service == nullptr)
{
setErrorString(QStringLiteral("UserServices object deleted"));
setState(LoggedOut);
return;
}
QFuture<QByteArray> passwordRead = service->readPassword(credentials.username);
passwordRead
.then
(
[this] (const QByteArray password)
{
if(password.isEmpty())
{
setState(LoggedOut);
return;
}
setPassword(password);
}
);
service.clear();
}
);
service.clear();
}
auto UserModel::cancelLogin() -> void
{
QFuture aether = QtConcurrent::run
(
[this] () -> void
{
QMutexLocker locker(&m_loginMutex);
if(m_loginAttempt.isRunning())
{
m_loginAttempt.cancel();
}
}
);
}
auto UserModel::reset() -> void
{
resetState();
setUsername({});
setPassword({});
}
auto UserModel::setUsername(const QString &username) -> void
{
if(username == m_username)
{
return;
}
m_username = username;
Q_EMIT usernameChanged();
if(!password().isEmpty())
{
return;
}
setState(Busy);
QSharedPointer<UserServices> service = UserServices::instance();
if(service == nullptr)
{
return;
}
QFuture<QByteArray> passwordRead = service->readPassword(username);
passwordRead
.then
(
[this] (const QByteArray password) -> void
{
setPassword(password);
}
);
}
auto UserModel::setPassword(const QString &password) -> void
{
if(password == m_password)
{
return;
}
m_password = password;
Q_EMIT passwordChanged();
}
auto UserModel::resetState() -> void
{
setState(Idle);
}
auto UserModel::setState(State state) -> void
{
if(state == m_state)
{
return;
}
switch(state)
{
case LoginFailure:
case LoggedOut:
case LoginCancelled:
m_stateResetTimer->start();
break;
default:
break;
}
m_state = state;
Q_EMIT stateChanged();
}
auto UserModel::setErrorString(const QString &string) -> void
{
if(string == m_errorString)
{
return;
}
m_errorString = string;
Q_EMIT errorStringChanged();
}
+76
View File
@@ -0,0 +1,76 @@
#ifndef USERMODEL_H
#define USERMODEL_H
#include <QObject>
#include <QTimer>
#include <QMutex>
#include <QMutexLocker>
#include <qqmlintegration.h>
#include "common/userservices.h"
class UserModel : public QObject
{
Q_OBJECT
QML_ELEMENT
public:
enum State
{
Idle,
LoggingIn,
LoggedIn,
LoginFailure,
LoginCancelled,
LoggedOut,
Busy
};
Q_ENUM(State)
explicit UserModel(QObject *parent = nullptr);
~UserModel();
Q_INVOKABLE auto login() -> void;
Q_INVOKABLE auto login(const QString username) -> void;
Q_INVOKABLE auto cancelLogin() -> void;
Q_INVOKABLE auto reset() -> void;
auto username() const -> const QString & { return m_username; }
auto setUsername(const QString &username) -> void;
auto password() const -> const QString & { return m_password; }
auto setPassword(const QString &password) -> void;
auto state() const -> State { return m_state; }
auto errorString() const -> QString { return m_errorString; }
signals:
auto loginSuccessful() -> void;
auto loginFailed() -> void;
auto stateChanged() -> void;
auto usernameChanged() -> void;
auto passwordChanged() -> void;
auto errorStringChanged() -> void;
protected slots:
auto resetState() -> void;
private:
auto setState(State state) -> void;
auto setErrorString(const QString &string) -> void;
State m_state = Idle;
QString m_errorString;
QString m_username;
QString m_password;
QTimer *m_stateResetTimer = nullptr;
QMutex m_loginMutex;
QFuture<UserCredentials> m_loginAttempt;
Q_PROPERTY(State state READ state NOTIFY stateChanged FINAL)
Q_PROPERTY(QString username READ username WRITE setUsername NOTIFY usernameChanged FINAL)
Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged FINAL)
Q_PROPERTY(QString errorString READ errorString WRITE setErrorString NOTIFY errorStringChanged FINAL)
};
#endif // USERMODEL_H