diff --git a/KomplexHubContent/pages/FeaturedImagesPage.qml b/KomplexHubContent/pages/FeaturedImagesPage.qml index 20fe056..edfa52c 100644 --- a/KomplexHubContent/pages/FeaturedImagesPage.qml +++ b/KomplexHubContent/pages/FeaturedImagesPage.qml @@ -107,10 +107,10 @@ Rectangle function showVideoPopup(index) { imagesModel.setItem(index) - imageDetailsPopup.author = searchModel.data(searchModel.index(index,0), VideoSearchModel.AuthorRole) - imageDetailsPopup.authorUrl = searchModel.data(searchModel.index(index,0), VideoSearchModel.AuthorUrlRole) - imageDetailsPopup.uuid = searchModel.data(searchModel.index(index,0), VideoSearchModel.UuidRole) - imageDetailsPopup.thumbnail = searchModel.data(searchModel.index(index,0), VideoSearchModel.ThumbnailRole) + imageDetailsPopup.author = searchModel.data(searchModel.index(index,0), ImageSearchModel.AuthorRole) + imageDetailsPopup.authorUrl = searchModel.data(searchModel.index(index,0), ImageSearchModel.AuthorUrlRole) + imageDetailsPopup.uuid = searchModel.data(searchModel.index(index,0), ImageSearchModel.UuidRole) + imageDetailsPopup.thumbnail = searchModel.data(searchModel.index(index,0), ImageSearchModel.ThumbnailRole) imageDetailsPopup.modelIndex = index imageDetailsPopup.model = searchModel.itemModel imageDetailsPopup.opacity = 1 diff --git a/KomplexHubPlugin/CMakeLists.txt b/KomplexHubPlugin/CMakeLists.txt index 77ffa2a..f25573a 100644 --- a/KomplexHubPlugin/CMakeLists.txt +++ b/KomplexHubPlugin/CMakeLists.txt @@ -56,6 +56,9 @@ qt_add_qml_module( common/shaderpack.h packcompiler.h packcompiler.cpp + SOURCES common/userservices.h + SOURCES common/userservices.cpp + SOURCES common/qwallet.h common/qwallet.cpp ) target_compile_definitions( diff --git a/KomplexHubPlugin/common/komplex_global.h b/KomplexHubPlugin/common/komplex_global.h index 4c2d23f..00ec803 100644 --- a/KomplexHubPlugin/common/komplex_global.h +++ b/KomplexHubPlugin/common/komplex_global.h @@ -37,44 +37,44 @@ #ifdef KOMPLEX_LOCAL_DEV #define KOMPLEX_API_HOST "https://komplexapi.hlab" #else -#define KOMPLEX_API_HOST "https://api.komplex.dev" +inline const QString KOMPLEX_API_HOST = QStringLiteral("https://api.komplex.dev"); #endif -#define KOMPLEX_API_VERSION "v2" +inline const QString KOMPLEX_API_VERSION = QStringLiteral("v2"); -inline QString KOMPLEX_ENDPOINT_IMAGES_SEARCH = QStringLiteral("images/search"); -inline QString KOMPLEX_ENDPOINT_IMAGES_FEATURED = QStringLiteral("featured/images"); -inline QString KOMPLEX_ENDPOINT_IMAGES_ITEM = QStringLiteral("images/item"); +inline const QString KOMPLEX_ENDPOINT_IMAGES_SEARCH = QStringLiteral("images/search"); +inline const QString KOMPLEX_ENDPOINT_IMAGES_FEATURED = QStringLiteral("featured/images"); +inline const QString KOMPLEX_ENDPOINT_IMAGES_ITEM = QStringLiteral("images/item"); -inline QString KOMPLEX_ENDPOINT_VIDEOS_SEARCH = QStringLiteral("videos/search"); -inline QString KOMPLEX_ENDPOINT_VIDEOS_FEATURED = QStringLiteral("featured/videos"); -inline QString KOMPLEX_ENDPOINT_VIDEOS_ITEM = QStringLiteral("videos/item"); +inline const QString KOMPLEX_ENDPOINT_VIDEOS_SEARCH = QStringLiteral("videos/search"); +inline const QString KOMPLEX_ENDPOINT_VIDEOS_FEATURED = QStringLiteral("featured/videos"); +inline const QString KOMPLEX_ENDPOINT_VIDEOS_ITEM = QStringLiteral("videos/item"); -inline QString KOMPLEX_ENDPOINT_SHADERS_SEARCH = QStringLiteral("shaders/search"); -inline QString KOMPLEX_ENDPOINT_SHADERS_FEATURED = QStringLiteral("shaders/featured"); -inline QString KOMPLEX_ENDPOINT_SHADERS_ITEM = QStringLiteral("shaders/item"); -inline QString KOMPLEX_ENDPOINT_SHADERS_NEWEST = QStringLiteral("shaders/newest"); +inline const QString KOMPLEX_ENDPOINT_SHADERS_SEARCH = QStringLiteral("shaders/search"); +inline const QString KOMPLEX_ENDPOINT_SHADERS_FEATURED = QStringLiteral("shaders/featured"); +inline const QString KOMPLEX_ENDPOINT_SHADERS_ITEM = QStringLiteral("shaders/item"); +inline const QString KOMPLEX_ENDPOINT_SHADERS_NEWEST = QStringLiteral("shaders/newest"); -inline QString KOMPLEX_ENDPOINT_CUBEMAPS_SEARCH = QStringLiteral("cubemaps/search"); -inline QString KOMPLEX_ENDPOINT_CUBEMAPS_FEATURED = QStringLiteral("cubemaps/featured"); -inline QString KOMPLEX_ENDPOINT_CUBEMAPS_ITEM = QStringLiteral("cubemaps/item"); -inline QString KOMPLEX_ENDPOINT_CUBEMAPS_NEWEST = QStringLiteral("cubemaps/newest"); +inline const QString KOMPLEX_ENDPOINT_CUBEMAPS_SEARCH = QStringLiteral("cubemaps/search"); +inline const QString KOMPLEX_ENDPOINT_CUBEMAPS_FEATURED = QStringLiteral("cubemaps/featured"); +inline const QString KOMPLEX_ENDPOINT_CUBEMAPS_ITEM = QStringLiteral("cubemaps/item"); +inline const QString KOMPLEX_ENDPOINT_CUBEMAPS_NEWEST = QStringLiteral("cubemaps/newest"); -inline QString KOMPLEX_ENDPOINT_PACKS_SEARCH = QStringLiteral("packs/search"); -inline QString KOMPLEX_ENDPOINT_PACKS_FEATURED = QStringLiteral("packs/featured"); -inline QString KOMPLEX_ENDPOINT_PACKS_ITEM = QStringLiteral("packs/item"); -inline QString KOMPLEX_ENDPOINT_PACKS_NEWEST = QStringLiteral("packs/newest"); +inline const QString KOMPLEX_ENDPOINT_PACKS_SEARCH = QStringLiteral("packs/search"); +inline const QString KOMPLEX_ENDPOINT_PACKS_FEATURED = QStringLiteral("packs/featured"); +inline const QString KOMPLEX_ENDPOINT_PACKS_ITEM = QStringLiteral("packs/item"); +inline const QString KOMPLEX_ENDPOINT_PACKS_NEWEST = QStringLiteral("packs/newest"); -inline QString KOMPLEX_ENDPOINT_USER_AUTH = QStringLiteral("user/auth"); +inline const QString KOMPLEX_ENDPOINT_USER_AUTH = QStringLiteral("user/auth"); -inline qsizetype nullsize = std::numeric_limits::min(); -inline std::chrono::milliseconds KOMPLEX_RATE_LIMIT(500); -inline QString KOMPLEX_KEYCHAIN_PASSWORD_KEY = QStringLiteral +inline const qsizetype nullsize = std::numeric_limits::min(); +inline const std::chrono::milliseconds KOMPLEX_RATE_LIMIT(500); +inline const QString KOMPLEX_KEYCHAIN_PASSWORD_KEY = QStringLiteral ( "com.digitalartifex.komplex.keychain.password" ); -inline QString KOMPLEX_KEYCHAIN_TOKEN_KEY = QStringLiteral +inline const QString KOMPLEX_KEYCHAIN_TOKEN_KEY = QStringLiteral ( "com.digitalartifex.komplex.keychain.token" ); diff --git a/KomplexHubPlugin/common/qwallet.cpp b/KomplexHubPlugin/common/qwallet.cpp new file mode 100644 index 0000000..fd8f08e --- /dev/null +++ b/KomplexHubPlugin/common/qwallet.cpp @@ -0,0 +1,135 @@ +#include "qwallet.h" +#include "../3rdparty/qtkeychain/qtkeychain/keychain.h" + +auto QWallet::read(const QString &service, const QString &key) const -> QFuture +{ + return QtConcurrent::run + ( + [this, service(service), key(key)]() -> QByteArray + { + QEventLoop loop; + QByteArray value; + + auto readCredentialJob = new QKeychain::ReadPasswordJob(service); + readCredentialJob->setAutoDelete(true); + readCredentialJob->setKey(key); + + QObject::connect + ( + readCredentialJob, + &QKeychain::ReadPasswordJob::finished, + this, + [this, key, &loop, &value](QKeychain::Job *job) + { + auto j = static_cast(job); + if (j->error() == QKeychain::NoError) + { + value = j->binaryData(); + } + else + { + m_errorString = qPrintable(j->errorString()); + throw std::exception(); + } + + loop.quit(); + } + ); + + readCredentialJob->start(); + loop.exec(); + + return value; + } + ); +} + +auto QWallet::write(const QString &service, const QString &key, const QByteArray &value) -> QFuture +{ + return QtConcurrent::run + ( + [this, service(service), key(key), value(value)]() -> void + { + QEventLoop loop; + bool success; + + auto writeCredentialJob = new QKeychain::WritePasswordJob(service); + writeCredentialJob->setAutoDelete(true); + writeCredentialJob->setKey(key); + writeCredentialJob->setBinaryData(value); + + QObject::connect + ( + writeCredentialJob, + &QKeychain::WritePasswordJob::finished, + this, + [this, key, &loop, &success](QKeychain::Job *job) + { + auto j = static_cast(job); + if (j->error() != QKeychain::NoError) + { + setErrorString(qPrintable(j->errorString())); + throw std::exception(); + } + + loop.quit(); + } + ); + + writeCredentialJob->start(); + loop.exec(); + } + ); +} + +auto QWallet::remove(const QString &service, const QString &key) -> QFuture +{ + return QtConcurrent::run + ( + [this, service(service), key(key)]() -> void + { + QEventLoop loop; + bool value; + + auto deleteCredentialJob = new QKeychain::DeletePasswordJob(service); + deleteCredentialJob->setAutoDelete(true); + deleteCredentialJob->setKey(key); + + QObject::connect + ( + deleteCredentialJob, + &QKeychain::DeletePasswordJob::finished, + this, + [this, key, &loop, &value](QKeychain::Job *job) + { + auto j = static_cast(job); + if (j->error() != QKeychain::NoError) + { + m_errorString = qPrintable(j->errorString()); + throw std::exception(); + } + + loop.quit(); + } + ); + + deleteCredentialJob->start(); + loop.exec(); + } + ); +} + +QWallet::QWallet(QObject *parent) : QObject(parent) +{ + +} + +QWallet::~QWallet() +{ + +} + +auto QWallet::errorString() const -> const QString & +{ + return m_errorString; +} diff --git a/KomplexHubPlugin/common/qwallet.h b/KomplexHubPlugin/common/qwallet.h new file mode 100644 index 0000000..55f3203 --- /dev/null +++ b/KomplexHubPlugin/common/qwallet.h @@ -0,0 +1,100 @@ +/* + * Komplex Wallpaper Engine + * Copyright (C) 2026 @DigitalArtifex + * https://digitalartifex.dev - https://github.com/DigitalArtifex + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 program. If not, see + */ +#ifndef QWALLET_H +#define QWALLET_H + +#include +#include +#include +#include + +#include "komplex_global.h" + +/** + * @brief The QWallet class + * QWallet is a QFuture wrapper for QtKeychain to make interaction with the keychain easier + */ +class KOMPLEX_EXPORT QWallet : QObject +{ + Q_OBJECT + +public: + explicit QWallet(QObject *parent = nullptr); + ~QWallet(); + + /** + * @brief read + * Reads the provided key from the system keychain/wallet + * @param service + * Service name of the application making the request + * @param key + * Key to read + * @return + * QFuture for the binary value + */ + [[nodiscard]] + auto read(const QString &service, const QString &key) const -> QFuture; + + /** + * @brief write + * Writes the provided value to the system kaychain/wallet + * @param service + * Service name of the application making the request + * @param key + * Key to read + * @param value + * Binary data to store + * @return + * QFuture + */ + [[nodiscard]] + auto write(const QString &service, const QString &key, const QByteArray &value) -> QFuture; + + /** + * @brief write + * Removes the provided value from the system kaychain/wallet + * @param service + * Service name of the application making the request + * @param key + * Key to read + * @return + */ + [[nodiscard]] + auto remove(const QString &service, const QString &key) -> QFuture; + + /** + * @brief errorString + * String describing the last encountered error. + * @return + */ + [[nodiscard]] + auto errorString() const -> const QString &; + +private: + /** + * @brief setErrorString + * Sets the error string + * @param errorString + */ + auto setErrorString(const QString &errorString) const -> void; + + mutable QString m_errorString; +}; + +#endif // QWALLET_H diff --git a/KomplexHubPlugin/common/userservices.cpp b/KomplexHubPlugin/common/userservices.cpp new file mode 100644 index 0000000..4d85e4e --- /dev/null +++ b/KomplexHubPlugin/common/userservices.cpp @@ -0,0 +1,148 @@ +#include "userservices.h" +#include "coreservices.h" + +UserServices::UserServices(QObject *parent) : QObject(parent) +{ + m_wallet = new QWallet(this); +} + +UserServices::~UserServices() +{ +} + +auto UserServices::setUserCredentials(const UserCredentials &credentials) -> void +{ + s_userCredentials = credentials; + Q_EMIT userCredentialsChanged(); +} + +auto UserServices::errorString() const -> const QString & +{ + return m_errorString; +} + +auto UserServices::setErrorString(const QString &errorString) -> void +{ + if (m_errorString == errorString) + return; + m_errorString = errorString; + emit errorStringChanged(); +} + +auto UserServices::instance() const -> QWeakPointer +{ + if(!s_pointer) + { + s_instance = new UserServices(QCoreApplication::instance()); + s_pointer = QSharedPointer + ( + s_instance, + &UserServices::deleteLater + ); + } + + return s_pointer.toWeakRef(); +} + +auto UserServices::userCredentials() -> const UserCredentials & +{ + return s_userCredentials; +} + +auto UserServices::login(const QString &username, const QByteArray &password) -> QFuture +{ + return QtConcurrent::run + ( + [this, username = std::move(username), password = std::move(password)] () -> UserCredentials + { + QSharedPointer manager = CoreServices::networkAccessManager().toStrongRef(); + + if(!manager) + { + Q_EMIT loginFailed(); + return {}; + } + + QNetworkRequest request + ( + QUrl + ( + QStringLiteral("%1/%2/%3").arg + ( + KOMPLEX_API_HOST, + KOMPLEX_API_VERSION, + KOMPLEX_ENDPOINT_USER_AUTH + ) + ) + ); + + QJsonObject loginRootObject + { + { + QString("username"), + QJsonValue::fromVariant(username.toUtf8()) + }, + { + QString("password"), + QJsonValue::fromVariant(password.toBase64()) + } + }; + + QJsonDocument loginDocument(loginRootObject); + + QNetworkReply *reply = manager->post(request, loginDocument.toJson(QJsonDocument::Compact)); + + while(!reply->isFinished()) + { + std::chrono::milliseconds(10); + } + + if(reply->error() != QNetworkReply::NoError) + { + setErrorString(reply->errorString()); + throw std::exception(); + } + + QByteArray data = reply->readAll(); + + QJsonParseError parseError; + QJsonDocument document = QJsonDocument::fromJson(data, &parseError); + + if(parseError.error != QJsonParseError::NoError) + { + setErrorString(parseError.errorString()); + } + + QJsonObject rootObject; + + if(!rootObject.value("success").toBool()) + { + QJsonObject dataObject = rootObject.value(QStringLiteral("data")).toObject(); + + setErrorString + ( + QStringLiteral("Login failure (%1): %2").arg + ( + dataObject.value(QStringLiteral("errorCode")).toString(), + dataObject.value(QStringLiteral("message")).toString() + ) + ); + } + + UserCredentials credentials + { + .username = username.toUtf8(), + .sessionToken = rootObject.value + ( + QStringLiteral("jwt") + ).toString().toUtf8(), + .expiry = QDateTime::currentDateTime() + }; + + setUserCredentials(credentials); + + Q_EMIT loginComplete(); + return credentials; + } + ); +} \ No newline at end of file diff --git a/KomplexHubPlugin/common/userservices.h b/KomplexHubPlugin/common/userservices.h new file mode 100644 index 0000000..63755b9 --- /dev/null +++ b/KomplexHubPlugin/common/userservices.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "komplex_global.h" +#include "qwallet.h" + +struct UserCredentials +{ + QByteArray username; + QByteArray sessionToken; + QDateTime expiry; + + UserCredentials &operator = (const UserCredentials &other) + { + username = other.username; + sessionToken = other.sessionToken; + expiry = other.expiry; + + return *this; + } + + bool isValid() + { + return !username.isEmpty() && username.isValidUtf8() && + !sessionToken.isEmpty() && sessionToken.isValidUtf8(); + } +}; + +class KOMPLEX_EXPORT UserServices : public QObject +{ + Q_OBJECT + +public: + auto instance() const -> QWeakPointer; + static auto userCredentials() -> const UserCredentials &; + auto login(const QString &username, const QByteArray &password) -> QFuture; + + auto errorString() const -> const QString &; + +signals: + auto loginComplete() -> void; + auto loginFailed() -> void; + auto userCredentialsChanged() -> void; + + auto errorStringChanged() -> void; + +private: + inline static UserServices *s_instance = nullptr; + inline static QSharedPointer s_pointer; + + explicit UserServices(QObject *parent); + ~UserServices(); + + auto setUserCredentials(const UserCredentials &credentials) -> void; + auto setErrorString(const QString &errorString) -> void; + + QWallet *m_wallet = nullptr; + QString m_errorString; + inline static UserCredentials s_userCredentials; + Q_PROPERTY(UserCredentials userCredentials READ userCredentials NOTIFY userCredentialsChanged FINAL) + Q_PROPERTY(QString errorString READ errorString WRITE setErrorString NOTIFY errorStringChanged FINAL) +}; \ No newline at end of file