diff options
| -rw-r--r-- | docker/Dockerfile | 2 | ||||
| -rw-r--r-- | src/src/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | src/src/apiuseraccount.cpp | 197 | ||||
| -rw-r--r-- | src/src/apiuseraccount.h | 288 | ||||
| -rw-r--r-- | src/src/createinstancedialog.h | 2 | ||||
| -rw-r--r-- | src/src/createinstancedialogpages.cpp | 3 | ||||
| -rw-r--r-- | src/src/moapplication.cpp | 7 | ||||
| -rw-r--r-- | src/src/modlistviewactions.cpp | 14 | ||||
| -rw-r--r-- | src/src/nexusinterface.cpp | 67 | ||||
| -rw-r--r-- | src/src/nexusoauthconfig.cpp | 62 | ||||
| -rw-r--r-- | src/src/nexusoauthconfig.h | 34 | ||||
| -rw-r--r-- | src/src/nexusoauthlogin.cpp | 78 | ||||
| -rw-r--r-- | src/src/nexusoauthlogin.h | 59 | ||||
| -rw-r--r-- | src/src/nexusoauthtokens.h | 98 | ||||
| -rw-r--r-- | src/src/nxmaccessmanager.cpp | 799 | ||||
| -rw-r--r-- | src/src/nxmaccessmanager.h | 567 | ||||
| -rw-r--r-- | src/src/organizercore.cpp | 16 | ||||
| -rw-r--r-- | src/src/plugincontainer.cpp | 18 | ||||
| -rw-r--r-- | src/src/settings.cpp | 63 | ||||
| -rw-r--r-- | src/src/settings.h | 19 | ||||
| -rw-r--r-- | src/src/settingsdialog.ui | 2 | ||||
| -rw-r--r-- | src/src/settingsdialognexus.cpp | 983 | ||||
| -rw-r--r-- | src/src/settingsdialognexus.h | 148 | ||||
| -rw-r--r-- | src/src/spawn.cpp | 2 |
24 files changed, 2086 insertions, 1445 deletions
diff --git a/docker/Dockerfile b/docker/Dockerfile index a96fb71..827d45b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -43,7 +43,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ # ── Qt 6.10 via aqtinstall ── RUN pip3 install --break-system-packages aqtinstall && \ aqt install-qt linux desktop 6.10.2 linux_gcc_64 \ - -m qtwebsockets qtwaylandcompositor \ + -m qtwebsockets qtwaylandcompositor qtnetworkauth \ --outputdir /opt/qt6 && \ ls /opt/qt6/6.10.2/gcc_64/bin/qmake && \ (pip3 uninstall -y aqtinstall --break-system-packages 2>/dev/null || true) diff --git a/src/src/CMakeLists.txt b/src/src/CMakeLists.txt index b003677..70da406 100644 --- a/src/src/CMakeLists.txt +++ b/src/src/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) find_package(Qt6 REQUIRED COMPONENTS - Widgets WebSockets Network Concurrent) + Widgets WebSockets Network NetworkAuth Concurrent) option(MO2_ENABLE_WEBENGINE "Build optional Qt WebEngine browser support" OFF) if(MO2_ENABLE_WEBENGINE) find_package(Qt6 QUIET COMPONENTS WebEngineWidgets) @@ -108,6 +108,7 @@ target_link_libraries(organizer PRIVATE $<$<TARGET_EXISTS:Qt6::WebEngineWidgets>:Qt6::WebEngineWidgets> Qt6::WebSockets Qt6::Network + Qt6::NetworkAuth # Boost Boost::program_options # spdlog diff --git a/src/src/apiuseraccount.cpp b/src/src/apiuseraccount.cpp index a4907e8..478b47b 100644 --- a/src/src/apiuseraccount.cpp +++ b/src/src/apiuseraccount.cpp @@ -1,93 +1,104 @@ -#include "apiuseraccount.h"
-
-QString localizedUserAccountType(APIUserAccountTypes t)
-{
- switch (t) {
- case APIUserAccountTypes::Regular:
- return QObject::tr("Regular");
-
- case APIUserAccountTypes::Premium:
- return QObject::tr("Premium");
-
- case APIUserAccountTypes::None: // fall-through
- default:
- return QObject::tr("None");
- }
-}
-
-APIUserAccount::APIUserAccount() = default;
-
-bool APIUserAccount::isValid() const
-{
- return !m_key.isEmpty();
-}
-
-const QString& APIUserAccount::apiKey() const
-{
- return m_key;
-}
-
-const QString& APIUserAccount::id() const
-{
- return m_id;
-}
-
-const QString& APIUserAccount::name() const
-{
- return m_name;
-}
-
-APIUserAccountTypes APIUserAccount::type() const
-{
- return m_type;
-}
-
-const APILimits& APIUserAccount::limits() const
-{
- return m_limits;
-}
-
-APIUserAccount& APIUserAccount::apiKey(const QString& key)
-{
- m_key = key;
- return *this;
-}
-
-APIUserAccount& APIUserAccount::id(const QString& id)
-{
- m_id = id;
- return *this;
-}
-
-APIUserAccount& APIUserAccount::name(const QString& name)
-{
- m_name = name;
- return *this;
-}
-
-APIUserAccount& APIUserAccount::type(APIUserAccountTypes type)
-{
- m_type = type;
- return *this;
-}
-
-APIUserAccount& APIUserAccount::limits(const APILimits& limits)
-{
- m_limits = limits;
- return *this;
-}
-
-int APIUserAccount::remainingRequests() const
-{
- return std::max(m_limits.remainingDailyRequests, m_limits.remainingHourlyRequests);
-}
-
-bool APIUserAccount::shouldThrottle() const
-{
- return (remainingRequests() < ThrottleThreshold);
-}
-
-bool APIUserAccount::exhausted() const
-{
- return isValid() && (remainingRequests() <= 0);
-}
+#include "apiuseraccount.h" + +QString localizedUserAccountType(APIUserAccountTypes t) +{ + switch (t) { + case APIUserAccountTypes::Regular: + return QObject::tr("Regular"); + + case APIUserAccountTypes::Premium: + return QObject::tr("Premium"); + + case APIUserAccountTypes::None: // fall-through + default: + return QObject::tr("None"); + } +} + +APIUserAccount::APIUserAccount() = default; + +bool APIUserAccount::isValid() const +{ + return !m_accessToken.isEmpty() || !m_apiKey.isEmpty(); +} + +const QString& APIUserAccount::accessToken() const +{ + return m_accessToken; +} + +const QString& APIUserAccount::apiKey() const +{ + return m_apiKey; +} + +const QString& APIUserAccount::id() const +{ + return m_id; +} + +const QString& APIUserAccount::name() const +{ + return m_name; +} + +APIUserAccountTypes APIUserAccount::type() const +{ + return m_type; +} + +const APILimits& APIUserAccount::limits() const +{ + return m_limits; +} + +APIUserAccount& APIUserAccount::accessToken(const QString& token) +{ + m_accessToken = token; + return *this; +} + +APIUserAccount& APIUserAccount::apiKey(const QString& key) +{ + m_apiKey = key; + return *this; +} + +APIUserAccount& APIUserAccount::id(const QString& id) +{ + m_id = id; + return *this; +} + +APIUserAccount& APIUserAccount::name(const QString& name) +{ + m_name = name; + return *this; +} + +APIUserAccount& APIUserAccount::type(APIUserAccountTypes type) +{ + m_type = type; + return *this; +} + +APIUserAccount& APIUserAccount::limits(const APILimits& limits) +{ + m_limits = limits; + return *this; +} + +int APIUserAccount::remainingRequests() const +{ + return std::max(m_limits.remainingDailyRequests, m_limits.remainingHourlyRequests); +} + +bool APIUserAccount::shouldThrottle() const +{ + return (remainingRequests() < ThrottleThreshold); +} + +bool APIUserAccount::exhausted() const +{ + return isValid() && (remainingRequests() <= 0); +} diff --git a/src/src/apiuseraccount.h b/src/src/apiuseraccount.h index 3a7ef77..326ad68 100644 --- a/src/src/apiuseraccount.h +++ b/src/src/apiuseraccount.h @@ -1,140 +1,150 @@ -#ifndef APIUSERACCOUNT_H
-#define APIUSERACCOUNT_H
-
-#include <QString>
-
-/**
- * represents user account types on a mod provider website such as nexus
- */
-enum class APIUserAccountTypes
-{
- // not logged in
- None = 0,
-
- // regular account
- Regular,
-
- // premium account
- Premium
-};
-
-QString localizedUserAccountType(APIUserAccountTypes t);
-
-/**
- * current limits imposed on the user account
- **/
-struct APILimits
-{
- // maximum number of requests per day
- int maxDailyRequests = 0;
-
- // remaining number of requests today
- int remainingDailyRequests = 0;
-
- // maximum number of requests per hour
- int maxHourlyRequests = 0;
-
- // remaining number of requests this hour
- int remainingHourlyRequests = 0;
-};
-
-/**
- * API statistics
- */
-struct APIStats
-{
- // number of API requests currently queued
- int requestsQueued = 0;
-};
-
-/**
- * represents a user account on the mod provider website
- */
-class APIUserAccount
-{
-public:
- // when the number of remaining requests is under this number, further
- // requests will be throttled by avoiding non-critical ones
+#ifndef APIUSERACCOUNT_H +#define APIUSERACCOUNT_H + +#include <QString> + +/** + * represents user account types on a mod provider website such as nexus + */ +enum class APIUserAccountTypes +{ + // not logged in + None = 0, + + // regular account + Regular, + + // premium account + Premium +}; + +QString localizedUserAccountType(APIUserAccountTypes t); + +/** + * current limits imposed on the user account + **/ +struct APILimits +{ + // maximum number of requests per day + int maxDailyRequests = 0; + + // remaining number of requests today + int remainingDailyRequests = 0; + + // maximum number of requests per hour + int maxHourlyRequests = 0; + + // remaining number of requests this hour + int remainingHourlyRequests = 0; +}; + +/** + * API statistics + */ +struct APIStats +{ + // number of API requests currently queued + int requestsQueued = 0; +}; + +/** + * represents a user account on the mod provider website + */ +class APIUserAccount +{ +public: + // when the number of remaining requests is under this number, further + // requests will be throttled by avoiding non-critical ones static constexpr int ThrottleThreshold = 200; -
- APIUserAccount();
-
- /**
- * whether the user is logged in
- */
- bool isValid() const;
-
- /**
- * api key
- */
- const QString& apiKey() const;
-
- /**
- * user id
- */
- const QString& id() const;
-
- /**
- * user name
- */
- const QString& name() const;
-
- /**
- * account type
- */
- APIUserAccountTypes type() const;
-
- /**
- * current API limits
- */
- const APILimits& limits() const;
-
- /**
- * sets the api key
- */
- APIUserAccount& apiKey(const QString& key);
-
- /**
- * sets the user id
- */
- APIUserAccount& id(const QString& id);
-
- /**
- * sets the user name
- **/
- APIUserAccount& name(const QString& name);
-
- /**
- * sets the account type
- */
- APIUserAccount& type(APIUserAccountTypes type);
-
- /**
- * sets the current limits
- */
- APIUserAccount& limits(const APILimits& limits);
-
- /**
- * returns the number of remaining requests
- */
- int remainingRequests() const;
-
- /**
- * whether the number of remaining requests is low enough that further
- * requests should be throttled
- */
- bool shouldThrottle() const;
-
- /**
- * true if all the remaining requests have been used and the API will refuse
- * further requests
- */
- bool exhausted() const;
-
-private:
- QString m_key, m_id, m_name;
- APIUserAccountTypes m_type{APIUserAccountTypes::None};
- APILimits m_limits;
-};
-
-#endif // APIUSERACCOUNT_H
+ + APIUserAccount(); + + /** + * whether the user is logged in + */ + bool isValid() const; + + /** + * OAuth access token + */ + const QString& accessToken() const; + + /** + * legacy api key + */ + const QString& apiKey() const; + + /** + * user id + */ + const QString& id() const; + + /** + * user name + */ + const QString& name() const; + + /** + * account type + */ + APIUserAccountTypes type() const; + + /** + * current API limits + */ + const APILimits& limits() const; + + /** + * sets the OAuth access token + */ + APIUserAccount& accessToken(const QString& token); + + /** + * sets the legacy api key + */ + APIUserAccount& apiKey(const QString& key); + + /** + * sets the user id + */ + APIUserAccount& id(const QString& id); + + /** + * sets the user name + **/ + APIUserAccount& name(const QString& name); + + /** + * sets the account type + */ + APIUserAccount& type(APIUserAccountTypes type); + + /** + * sets the current limits + */ + APIUserAccount& limits(const APILimits& limits); + + /** + * returns the number of remaining requests + */ + int remainingRequests() const; + + /** + * whether the number of remaining requests is low enough that further + * requests should be throttled + */ + bool shouldThrottle() const; + + /** + * true if all the remaining requests have been used and the API will refuse + * further requests + */ + bool exhausted() const; + +private: + QString m_accessToken, m_apiKey, m_id, m_name; + APIUserAccountTypes m_type{APIUserAccountTypes::None}; + APILimits m_limits; +}; + +#endif // APIUSERACCOUNT_H diff --git a/src/src/createinstancedialog.h b/src/src/createinstancedialog.h index 24fa5df..327b3a1 100644 --- a/src/src/createinstancedialog.h +++ b/src/src/createinstancedialog.h @@ -27,7 +27,7 @@ class Settings; //
// pages can be disabled if they return true in skip(), which happens globally
// for some (IntroPage has a setting in the registry), depending on context
-// (NexusPage is skipped if the API key already exists) or explicitly (when
+// (NexusPage is skipped if the Nexus authorization already exists) or explicitly (when
// only some info about the instance is missing on startup, such as a game
// variant)
//
diff --git a/src/src/createinstancedialogpages.cpp b/src/src/createinstancedialogpages.cpp index 0bd4a6e..5fd3117 100644 --- a/src/src/createinstancedialogpages.cpp +++ b/src/src/createinstancedialogpages.cpp @@ -123,6 +123,7 @@ void Page::next() bool Page::action(CreateInstanceDialog::Actions a) { + Q_UNUSED(a); // no-op return false; } @@ -1252,7 +1253,7 @@ NexusPage::NexusPage(CreateInstanceDialog& dlg) : Page(dlg) // just check it once, or connecting and then going back and forth would skip // the page, which would be unexpected - m_skip = GlobalSettings::hasNexusApiKey(); + m_skip = GlobalSettings::hasNexusOAuthTokens() || GlobalSettings::hasNexusApiKey(); } NexusPage::~NexusPage() = default; diff --git a/src/src/moapplication.cpp b/src/src/moapplication.cpp index 2d5be5f..40ec09a 100644 --- a/src/src/moapplication.cpp +++ b/src/src/moapplication.cpp @@ -446,9 +446,10 @@ int MOApplication::run(MOMultiProcess& multiProcess) tt.start("MOApplication::doOneRun() finishing"); // start an api check - QString apiKey; - if (GlobalSettings::nexusApiKey(apiKey)) { - m_nexus->getAccessManager()->apiCheck(apiKey); + NexusOAuthTokens tokens; + if (GlobalSettings::nexusOAuthTokens(tokens) || + GlobalSettings::nexusApiKey(tokens.apiKey)) { + m_nexus->getAccessManager()->apiCheck(tokens); } // tutorials diff --git a/src/src/modlistviewactions.cpp b/src/src/modlistviewactions.cpp index cf83b76..b28fe12 100644 --- a/src/src/modlistviewactions.cpp +++ b/src/src/modlistviewactions.cpp @@ -230,12 +230,13 @@ void ModListViewActions::checkModsForUpdates() const QString()); NexusInterface::instance().requestTrackingInfo(m_receiver, QVariant(), QString()); } else { - QString apiKey; - if (GlobalSettings::nexusApiKey(apiKey)) { + NexusOAuthTokens tokens; + if (GlobalSettings::nexusOAuthTokens(tokens) || + GlobalSettings::nexusApiKey(tokens.apiKey)) { m_core.doAfterLogin([=, this]() { checkModsForUpdates(); }); - NexusInterface::instance().getAccessManager()->apiCheck(apiKey); + NexusInterface::instance().getAccessManager()->apiCheck(tokens); } else { log::warn("{}", tr("You are not currently authenticated with Nexus. Please do so " "under Settings -> Nexus.")); @@ -310,12 +311,13 @@ void ModListViewActions::checkModsForUpdates( if (NexusInterface::instance().getAccessManager()->validated()) { ModInfo::manualUpdateCheck(m_receiver, IDs); } else { - QString apiKey; - if (GlobalSettings::nexusApiKey(apiKey)) { + NexusOAuthTokens tokens; + if (GlobalSettings::nexusOAuthTokens(tokens) || + GlobalSettings::nexusApiKey(tokens.apiKey)) { m_core.doAfterLogin([=, this]() { checkModsForUpdates(IDs); }); - NexusInterface::instance().getAccessManager()->apiCheck(apiKey); + NexusInterface::instance().getAccessManager()->apiCheck(tokens); } else log::warn("{}", tr("You are not currently authenticated with Nexus. Please do so " "under Settings -> Nexus.")); diff --git a/src/src/nexusinterface.cpp b/src/src/nexusinterface.cpp index 29e7a2d..7ca75a6 100644 --- a/src/src/nexusinterface.cpp +++ b/src/src/nexusinterface.cpp @@ -991,33 +991,54 @@ void NexusInterface::nextRequest() } else { url = info.m_URL; } - QNetworkRequest request(url); - request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false); - request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, - QNetworkRequest::AlwaysNetwork); - request.setRawHeader("APIKEY", m_User.apiKey().toUtf8()); - request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, - m_AccessManager->userAgent(info.m_SubModule)); - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, - "application/json"); - request.setRawHeader("Protocol-Version", "1.0.0"); - request.setRawHeader("Application-Name", "MO2"); - request.setRawHeader("Application-Version", - QApplication::applicationVersion().toUtf8()); - if (postData.object().isEmpty()) { - if (!requestIsDelete) { - info.m_Reply = m_AccessManager->get(request); + const auto currentTokens = m_AccessManager->tokens(); + if (!currentTokens || + (currentTokens->accessToken.isEmpty() && currentTokens->apiKey.isEmpty())) { + log::error("nexus: no OAuth token available, request aborted"); + info.m_Reply = nullptr; + return; + } + + QNetworkRequest request(url); + if (!currentTokens->accessToken.isEmpty()) { + if (currentTokens->isExpired()) { + m_AccessManager->connectOrRefresh(*currentTokens); + return; + } + if (postData.object().isEmpty()) { + if (!requestIsDelete) { + info.m_Reply = m_AccessManager->makeOAuthGetRequest(url); + } else { + m_AccessManager->addAPIHeaders(request); + info.m_Reply = m_AccessManager->makeOAuthDeleteRequest(request); + } + } else if (!requestIsDelete) { + info.m_Reply = m_AccessManager->makeOAuthPostRequest(url, postData.toJson()); } else { - info.m_Reply = m_AccessManager->deleteResource(request); + // Qt doesn't support DELETE with a payload as that's technically against the HTTP + // standard... + info.m_Reply = + m_AccessManager->makeOAuthCustomRequest(request, "DELETE", postData.toJson()); } - } else if (!requestIsDelete) { - info.m_Reply = m_AccessManager->post(request, postData.toJson()); } else { - // Qt doesn't support DELETE with a payload as that's technically against the HTTP - // standard... - info.m_Reply = - m_AccessManager->sendCustomRequest(request, "DELETE", postData.toJson()); + request.setRawHeader("APIKEY", currentTokens->apiKey.toUtf8()); + m_AccessManager->addAPIHeaders(request); + + if (postData.object().isEmpty()) { + if (!requestIsDelete) { + info.m_Reply = m_AccessManager->get(request); + } else { + info.m_Reply = m_AccessManager->deleteResource(request); + } + } else if (!requestIsDelete) { + info.m_Reply = m_AccessManager->post(request, postData.toJson()); + } else { + // Qt doesn't support DELETE with a payload as that's technically against the HTTP + // standard... + info.m_Reply = + m_AccessManager->sendCustomRequest(request, "DELETE", postData.toJson()); + } } connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished())); diff --git a/src/src/nexusoauthconfig.cpp b/src/src/nexusoauthconfig.cpp new file mode 100644 index 0000000..370e0d6 --- /dev/null +++ b/src/src/nexusoauthconfig.cpp @@ -0,0 +1,62 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer 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. + +Mod Organizer 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 Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "nexusoauthconfig.h" +#include <QProcessEnvironment> + +namespace +{ +QString envOrDefault(const char* name, const QString& fallback) +{ + const auto value = qEnvironmentVariable(name); + if (!value.isEmpty()) { + return value; + } + + return fallback; +} +} // namespace + +namespace NexusOAuth +{ +QString clientId() +{ + return envOrDefault("MO2_NEXUS_CLIENT_ID", QStringLiteral("modorganizer2")); +} + +quint16 redirectPort() +{ + return 28635; +} + +QString redirectUri() +{ + return QStringLiteral("http://127.0.0.1:%1/callback").arg(redirectPort()); +} + +QString authorizeUrl() +{ + return QStringLiteral("https://users.nexusmods.com/oauth/authorize"); +} + +QString tokenUrl() +{ + return QStringLiteral("https://users.nexusmods.com/oauth/token"); +} +} // namespace NexusOAuth diff --git a/src/src/nexusoauthconfig.h b/src/src/nexusoauthconfig.h new file mode 100644 index 0000000..e0ccd63 --- /dev/null +++ b/src/src/nexusoauthconfig.h @@ -0,0 +1,34 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer 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. + +Mod Organizer 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 Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef NEXUSOAUTHCONFIG_H +#define NEXUSOAUTHCONFIG_H + +#include <QString> + +namespace NexusOAuth +{ +QString clientId(); +QString redirectUri(); +quint16 redirectPort(); +QString authorizeUrl(); +QString tokenUrl(); +} // namespace NexusOAuth + +#endif // NEXUSOAUTHCONFIG_H diff --git a/src/src/nexusoauthlogin.cpp b/src/src/nexusoauthlogin.cpp new file mode 100644 index 0000000..7d13642 --- /dev/null +++ b/src/src/nexusoauthlogin.cpp @@ -0,0 +1,78 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer 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. + +Mod Organizer 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 Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "nexusoauthlogin.h" +#include "nexusinterface.h" +#include "nexusoauthconfig.h" +#include "nxmaccessmanager.h" +#include "utility.h" +#include <QAbstractOAuth> +#include <QCryptographicHash> +#include <QHostAddress> +#include <QJsonObject> +#include <QJsonValue> +#include <QMultiMap> +#include <QRandomGenerator> +#include <QUrl> +#include <QVariant> +#include <QtNetworkAuth/QOAuth2AuthorizationCodeFlow> +#include <QtNetworkAuth/QOAuthHttpServerReplyHandler> + +using namespace MOBase; + +namespace +{ +QString callbackPath() +{ + return QUrl(NexusOAuth::redirectUri()).path(); +} +} // namespace + +NexusOAuthLogin::NexusOAuthLogin(QObject* parent) : QObject(parent), m_active(false) {} + +NexusOAuthLogin::~NexusOAuthLogin() = default; + +void NexusOAuthLogin::start() +{ + if (m_active) { + cancel(); + } + auto accessManager = NexusInterface::instance().getAccessManager(); + connect(accessManager, &NXMAccessManager::authorizationEnded, this, + &NexusOAuthLogin::authorizationEnded); + + NexusInterface::instance().getAccessManager()->connectOrRefresh(NexusOAuthTokens()); + m_active = true; +} + +void NexusOAuthLogin::authorizationEnded() +{ + m_active = false; +} + +void NexusOAuthLogin::cancel() +{ + NexusInterface::instance().getAccessManager()->cancelAuth(); + m_active = false; +} + +bool NexusOAuthLogin::isActive() const +{ + return m_active; +} diff --git a/src/src/nexusoauthlogin.h b/src/src/nexusoauthlogin.h new file mode 100644 index 0000000..66b6b39 --- /dev/null +++ b/src/src/nexusoauthlogin.h @@ -0,0 +1,59 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer 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. + +Mod Organizer 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 Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef NEXUSOAUTHLOGIN_H +#define NEXUSOAUTHLOGIN_H + +#include "nexusoauthtokens.h" +#include "nxmaccessmanager.h" +#include <QAbstractOAuth> +#include <QObject> +#include <functional> +#include <memory> + +class QOAuth2AuthorizationCodeFlow; +class QOAuthHttpServerReplyHandler; + +class NexusOAuthLogin : public QObject +{ + Q_OBJECT + +public: + explicit NexusOAuthLogin(QObject* parent = nullptr); + ~NexusOAuthLogin(); + + void start(); + void cancel(); + bool isActive() const; + + std::function<void(const NexusOAuthTokens&)> tokensReceived; + std::function<void(NXMAccessManager::OAuthState, QString)> stateChanged; + +private slots: + void authorizationEnded(); + +private: + std::unique_ptr<QOAuth2AuthorizationCodeFlow> m_flow; + std::unique_ptr<QOAuthHttpServerReplyHandler> m_replyHandler; + bool m_active; + + QByteArray m_codeVerifier; +}; + +#endif // NEXUSOAUTHLOGIN_H diff --git a/src/src/nexusoauthtokens.h b/src/src/nexusoauthtokens.h new file mode 100644 index 0000000..78f3f22 --- /dev/null +++ b/src/src/nexusoauthtokens.h @@ -0,0 +1,98 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer 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. + +Mod Organizer 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 Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef NEXUSOAUTHTOKENS_H +#define NEXUSOAUTHTOKENS_H + +#include <QDateTime> +#include <QJsonObject> +#include <chrono> +#include <optional> + +struct NexusOAuthTokens +{ + QString accessToken; + QString refreshToken; + QString scope; + QString tokenType; + QDateTime expiresAt; + QString apiKey; + + bool isValid() const { return !accessToken.isEmpty() && expiresAt.isValid(); } + + bool isExpired(std::chrono::seconds skew = std::chrono::seconds(300)) const + { + if (!expiresAt.isValid()) { + return true; + } + + const auto now = QDateTime::currentDateTimeUtc(); + return now.addSecs(skew.count()) >= expiresAt; + } + + QJsonObject toJson() const + { + QJsonObject json; + json.insert(QStringLiteral("access_token"), accessToken); + json.insert(QStringLiteral("refresh_token"), refreshToken); + json.insert(QStringLiteral("scope"), scope); + json.insert(QStringLiteral("token_type"), tokenType); + json.insert(QStringLiteral("expires_at"), expiresAt.toString(Qt::ISODateWithMs)); + return json; + } + + static std::optional<NexusOAuthTokens> fromJson(const QJsonObject& json) + { + NexusOAuthTokens tokens; + tokens.accessToken = json.value(QStringLiteral("access_token")).toString(); + tokens.refreshToken = json.value(QStringLiteral("refresh_token")).toString(); + tokens.scope = json.value(QStringLiteral("scope")).toString(); + tokens.tokenType = json.value(QStringLiteral("token_type")).toString(); + tokens.expiresAt = QDateTime::fromString( + json.value(QStringLiteral("expires_at")).toString(), Qt::ISODateWithMs); + if (!tokens.expiresAt.isValid()) { + tokens.expiresAt = QDateTime::fromString( + json.value(QStringLiteral("expires_at")).toString(), Qt::ISODate); + } + + if (!tokens.isValid()) { + return std::nullopt; + } + + if (tokens.expiresAt.isValid() && tokens.expiresAt.timeSpec() != Qt::UTC) { + tokens.expiresAt = tokens.expiresAt.toUTC(); + } + + return tokens; + } +}; + +inline NexusOAuthTokens makeTokensFromResponse(const QVariantMap& data) +{ + NexusOAuthTokens tokens; + tokens.accessToken = data["access_token"].toString(); + tokens.refreshToken = data["refresh_token"].toString(); + tokens.scope = data["scope"].toString(); + tokens.tokenType = data["token_type"].toString(); + tokens.expiresAt = data["expiration_at"].toDateTime(); + + return tokens; +} + +#endif // NEXUSOAUTHTOKENS_H diff --git a/src/src/nxmaccessmanager.cpp b/src/src/nxmaccessmanager.cpp index bd15b9a..18d83e1 100644 --- a/src/src/nxmaccessmanager.cpp +++ b/src/src/nxmaccessmanager.cpp @@ -20,6 +20,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "nxmaccessmanager.h" #include "iplugingame.h" #include "nexusinterface.h" +#include "nexusoauthconfig.h" +#include "nexusoauthlogin.h" #include "nxmurl.h" #include "persistentcookiejar.h" #include "report.h" @@ -28,6 +30,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "utility.h" #include <QCoreApplication> #include <QDir> +#include <QEventLoop> #include <QJsonArray> #include <QJsonDocument> #include <QMessageBox> @@ -42,13 +45,11 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. using namespace MOBase; using namespace std::chrono_literals; -const QString NexusBaseUrl("https://api.nexusmods.com/v1"); -const QString NexusSSO("wss://sso.nexusmods.com"); -const QString - NexusSSOPage("https://www.nexusmods.com/sso?id=%1&application=modorganizer2"); +const QString NexusUserUrl("https://users.nexusmods.com/oauth/"); +const QString NexusV1BaseUrl("https://api.nexusmods.com/v1/"); ValidationProgressDialog::ValidationProgressDialog(Settings* s, NexusKeyValidator& v) - : m_settings(s), m_validator(v) + : m_Settings(s), m_Validator(v), m_UpdateTimer(nullptr), m_First(true) { ui.reset(new Ui::ValidationProgressDialog); ui->setupUi(this); @@ -77,24 +78,24 @@ void ValidationProgressDialog::setParentWidget(QWidget* w) void ValidationProgressDialog::start() { - if (!m_updateTimer) { - m_updateTimer = new QTimer(this); - connect(m_updateTimer, &QTimer::timeout, [&] { + if (!m_UpdateTimer) { + m_UpdateTimer = new QTimer(this); + connect(m_UpdateTimer, &QTimer::timeout, [&] { onTimer(); }); - m_updateTimer->setInterval(100ms); + m_UpdateTimer->setInterval(100ms); } updateProgress(); - m_updateTimer->start(); + m_UpdateTimer->start(); show(); } void ValidationProgressDialog::stop() { - if (m_updateTimer) { - m_updateTimer->stop(); + if (m_UpdateTimer) { + m_UpdateTimer->stop(); } hide(); @@ -102,13 +103,15 @@ void ValidationProgressDialog::stop() void ValidationProgressDialog::showEvent(QShowEvent* e) { - if (m_first) { - if (m_settings) { - m_settings->geometry().centerOnMainWindowMonitor(this); + if (m_First) { + if (m_Settings) { + m_Settings->geometry().centerOnMainWindowMonitor(this); } - m_first = false; + m_First = false; } + + QDialog::showEvent(e); } void ValidationProgressDialog::closeEvent(QCloseEvent* e) @@ -124,7 +127,7 @@ void ValidationProgressDialog::onHide() void ValidationProgressDialog::onCancel() { - m_validator.cancel(); + m_Validator.cancel(); } void ValidationProgressDialog::onTimer() @@ -134,7 +137,7 @@ void ValidationProgressDialog::onTimer() void ValidationProgressDialog::updateProgress() { - const auto* current = m_validator.currentAttempt(); + const auto* current = m_Validator.currentAttempt(); if (current) { ui->progress->setRange(0, current->timeout().count()); @@ -144,7 +147,7 @@ void ValidationProgressDialog::updateProgress() ui->progress->setRange(0, 0); } - if (const auto* a = m_validator.lastAttempt()) { + if (const auto* a = m_Validator.lastAttempt()) { ui->label->setText(a->message() + ". " + tr("Trying again...")); } else if (current) { ui->label->setText(tr("Connecting to Nexus...")); @@ -153,258 +156,62 @@ void ValidationProgressDialog::updateProgress() } } -NexusSSOLogin::NexusSSOLogin() +ValidationAttempt::ValidationAttempt(std::chrono::seconds timeout) + : m_Reply(nullptr), m_Result(None) { - m_timeout.setInterval(10s); - m_timeout.setSingleShot(true); - - QObject::connect(&m_socket, &QWebSocket::connected, [&] { - onConnected(); - }); + m_Timeout.setSingleShot(true); + m_Timeout.setInterval(timeout); - QObject::connect(&m_socket, &QWebSocket::errorOccurred, [&](auto&& e) { - onError(e); - }); - - QObject::connect(&m_socket, &QWebSocket::sslErrors, [&](auto&& errors) { - onSslErrors(errors); - }); - - QObject::connect(&m_socket, &QWebSocket::textMessageReceived, [&](auto&& s) { - onMessage(s); - }); - - QObject::connect(&m_socket, &QWebSocket::disconnected, [&] { - onDisconnected(); - }); - - QObject::connect(&m_timeout, &QTimer::timeout, [&] { + QObject::connect(&m_Timeout, &QTimer::timeout, [&] { onTimeout(); }); } -QString NexusSSOLogin::stateToString(States s, const QString& e) -{ - switch (s) { - case ConnectingToSSO: - return QObject::tr("Connecting to Nexus..."); - - case WaitingForToken: - return QObject::tr("Waiting for Nexus..."); - - case WaitingForBrowser: - return QObject::tr("Opened Nexus in browser.") + "\n" + - QObject::tr("Switch to your browser and accept the request."); - - case Finished: - return QObject::tr("Finished."); - - case Timeout: - return QObject::tr("No answer from Nexus.") + "\n" + - QObject::tr("A firewall might be blocking Mod Organizer."); - - case ClosedByRemote: - return QObject::tr("Nexus closed the connection.") + "\n" + - QObject::tr("A firewall might be blocking Mod Organizer."); - - case Cancelled: - return QObject::tr("Cancelled."); - - case Error: // fall-through - default: { - if (e.isEmpty()) { - return QString("%1").arg(s); - } else { - return e; - } - } - } -} - -void NexusSSOLogin::start() -{ - m_active = true; - setState(ConnectingToSSO); - m_timeout.start(); - m_socket.open(NexusSSO); -} - -void NexusSSOLogin::cancel() -{ - if (m_active) { - abort(); - setState(Cancelled); - } -} - -void NexusSSOLogin::close() -{ - if (m_active) { - m_active = false; - m_timeout.stop(); - m_socket.close(); - } -} - -void NexusSSOLogin::abort() -{ - m_active = false; - m_timeout.stop(); - m_socket.abort(); -} - -bool NexusSSOLogin::isActive() const -{ - return m_active; -} - -void NexusSSOLogin::setState(States s, const QString& error) const -{ - if (stateChanged) { - stateChanged(s, error); - } -} - -void NexusSSOLogin::onConnected() -{ - setState(WaitingForToken); - - m_keyReceived = false; - - boost::uuids::random_generator generator; - boost::uuids::uuid const sessionId = generator(); - m_guid = boost::uuids::to_string(sessionId).c_str(); - - QJsonObject data; - data.insert(QString("id"), QJsonValue(m_guid)); - data.insert(QString("protocol"), 2); - - const QString message = QJsonDocument(data).toJson(); - m_socket.sendTextMessage(message); -} - -void NexusSSOLogin::onMessage(const QString& s) +void ValidationAttempt::start(NXMAccessManager& m, const NexusOAuthTokens& tokens) { - const QJsonDocument doc = QJsonDocument::fromJson(s.toUtf8()); - const QVariantMap root = doc.object().toVariantMap(); - - if (!root["success"].toBool()) { - close(); - - setState(Error, QString("There was a problem with SSO initialization: %1") - .arg(root["error"].toString())); + m_Tokens = tokens; + if (!sendRequest(m, tokens)) { return; } - const QVariantMap data = root["data"].toMap(); - - if (data.contains("connection_token")) { - // first answer + m_Elapsed.start(); + m_Timeout.start(); - // open browser - const QUrl url = NexusSSOPage.arg(m_guid); - shell::Open(url); - - m_timeout.stop(); - setState(WaitingForBrowser); - } else { - // second answer - const auto key = data["api_key"].toString(); - close(); - - if (keyChanged) { - keyChanged(key); - } - - setState(Finished); - } -} - -void NexusSSOLogin::onDisconnected() -{ - if (m_active) { - if (!m_keyReceived) { - close(); - setState(ClosedByRemote); - } else { - m_active = false; - } - } + log::debug("nexus: attempt started with timeout of {} seconds", timeout().count()); } -void NexusSSOLogin::onError(QAbstractSocket::SocketError e) +bool ValidationAttempt::sendRequest(NXMAccessManager& m, const NexusOAuthTokens& tokens) { - if (m_active) { - close(); - setState(Error, m_socket.errorString()); - } -} -void NexusSSOLogin::onSslErrors(const QList<QSslError>& errors) -{ - if (m_active) { - for (const auto& e : errors) { - setState(Error, e.errorString()); - } + if (tokens.accessToken.isEmpty() && tokens.apiKey.isEmpty()) { + setFailure(HardError, QObject::tr("No access token or API key")); + return false; } -} - -void NexusSSOLogin::onTimeout() -{ - abort(); - setState(Timeout); -} - -ValidationAttempt::ValidationAttempt(std::chrono::seconds timeout) - -{ - m_timeout.setSingleShot(true); - m_timeout.setInterval(timeout); - - QObject::connect(&m_timeout, &QTimer::timeout, [&] { - onTimeout(); - }); -} -void ValidationAttempt::start(NXMAccessManager& m, const QString& key) -{ - if (!sendRequest(m, key)) { - return; + QNetworkRequest request; + QString requestUrl; + if (!tokens.accessToken.isEmpty()) { + requestUrl = NexusUserUrl + "userinfo"; + m_Reply = + NexusInterface::instance().getAccessManager()->makeOAuthGetRequest(requestUrl); + } else { + requestUrl = NexusV1BaseUrl + "users/validate"; + request.setUrl(requestUrl); + request.setRawHeader("APIKEY", tokens.apiKey.toUtf8()); + m_Reply = m.get(request); } - m_elapsed.start(); - m_timeout.start(); - - log::debug("nexus: attempt started with timeout of {} seconds", timeout().count()); -} - -bool ValidationAttempt::sendRequest(NXMAccessManager& m, const QString& key) -{ - const QString requestUrl(NexusBaseUrl + "/users/validate"); - QNetworkRequest request(requestUrl); - - request.setRawHeader("APIKEY", key.toUtf8()); - request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, - m.userAgent().toUtf8()); - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, - "application/json"); - request.setRawHeader("Protocol-Version", "1.0.0"); - request.setRawHeader("Application-Name", "MO2"); - request.setRawHeader("Application-Version", m.MOVersion().toUtf8()); - - m_reply = m.get(request); - - if (!m_reply) { + if (!m_Reply) { setFailure(SoftError, QObject::tr("Failed to request %1").arg(requestUrl)); return false; } - QObject::connect(m_reply, &QNetworkReply::finished, [&] { + QObject::connect(m_Reply, &QNetworkReply::finished, [&] { onFinished(); }); - QObject::connect(m_reply, &QNetworkReply::sslErrors, [&](auto&& errors) { + QObject::connect(m_Reply, &QNetworkReply::sslErrors, [&](auto&& errors) { onSslErrors(errors); }); @@ -413,15 +220,15 @@ bool ValidationAttempt::sendRequest(NXMAccessManager& m, const QString& key) void ValidationAttempt::cancel() { - if (!m_reply || m_result != None) { + if (!m_Reply || m_Result != None) { // not running return; } setFailure(Cancelled, QObject::tr("Cancelled")); - if (m_reply) { - m_reply->abort(); + if (m_Reply) { + m_Reply->abort(); } cleanup(); @@ -429,39 +236,39 @@ void ValidationAttempt::cancel() bool ValidationAttempt::done() const { - return (m_result != None); + return (m_Result != None); } ValidationAttempt::Result ValidationAttempt::result() const { - return m_result; + return m_Result; } const QString& ValidationAttempt::message() const { - return m_message; + return m_Message; } std::chrono::seconds ValidationAttempt::timeout() const { return std::chrono::duration_cast<std::chrono::seconds>( - m_timeout.intervalAsDuration()); + m_Timeout.intervalAsDuration()); } QElapsedTimer ValidationAttempt::elapsed() const { - return m_elapsed; + return m_Elapsed; } void ValidationAttempt::onFinished() { - if (m_result == Cancelled) { + if (m_Result == Cancelled) { return; } log::debug("nexus: request has finished"); - if (!m_reply) { + if (!m_Reply) { // shouldn't happen log::error("nexus: reply is null"); setFailure(HardError, QObject::tr("Internal error")); @@ -469,25 +276,25 @@ void ValidationAttempt::onFinished() } const auto code = - m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + m_Reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (code == 0) { // request wasn't even sent log::error("nexus: code is 0"); - setFailure(SoftError, m_reply->errorString()); + setFailure(SoftError, m_Reply->errorString()); return; } - const auto doc = QJsonDocument::fromJson(m_reply->readAll()); - const auto headers = m_reply->rawHeaderPairs(); - const auto httpError = m_reply->errorString(); + const auto doc = QJsonDocument::fromJson(m_Reply->readAll()); + const auto headers = m_Reply->rawHeaderPairs(); + const auto httpError = m_Reply->errorString(); const QJsonObject data = doc.object(); if (code != 200) { // http request failed - QString s = m_reply->errorString(); + QString s = m_Reply->errorString(); const auto nexusMessage = data.value("message").toString(); if (!nexusMessage.isEmpty()) { @@ -513,37 +320,67 @@ void ValidationAttempt::onFinished() return; } - if (!data.contains("user_id")) { - setFailure(HardError, QObject::tr("Bad response")); - return; - } + if (!m_Tokens.accessToken.isEmpty()) { + if (!data.contains("sub")) { + setFailure(HardError, QObject::tr("Bad response")); + return; + } - const int id = data.value("user_id").toInt(); - const QString key = data.value("key").toString(); - const QString name = data.value("name").toString(); - const bool premium = data.value("is_premium").toBool(); + const QString id = data.value("sub").toString(); + const QString name = data.value("name").toString(); + const auto roles = data.value("membership_roles").toArray(); + QStringList validRoles = {"premium", "lifetimepremium", "supporter"}; + bool premium = false; + for (auto role : roles) { + QString roleVal = role.toString(); + if (validRoles.contains(roleVal)) { + premium = true; + break; + } + } - if (key.isEmpty()) { - setFailure(HardError, QObject::tr("API key is empty")); - return; - } + if (m_Tokens.accessToken.isEmpty()) { + setFailure(HardError, QObject::tr("Access token is empty")); + return; + } + + const auto user = + APIUserAccount() + .accessToken(m_Tokens.accessToken) + .id(QString("%1").arg(id)) + .name(name) + .type(premium ? APIUserAccountTypes::Premium : APIUserAccountTypes::Regular) + .limits(NexusInterface::defaultAPILimits()); + + setSuccess(user); + } else if (!m_Tokens.apiKey.isEmpty()) { + if (!data.contains("user_id")) { + setFailure(HardError, QObject::tr("Bad response")); + return; + } + + const int id = data.value("user_id").toInt(); + const QString key = data.value("key").toString(); + const QString name = data.value("name").toString(); + const bool premium = data.value("is_premium").toBool(); - const auto user = - APIUserAccount() - .apiKey(key) - .id(QString("%1").arg(id)) - .name(name) - .type(premium ? APIUserAccountTypes::Premium : APIUserAccountTypes::Regular) - .limits(NexusInterface::parseLimits(headers)); + const auto user = + APIUserAccount() + .apiKey(m_Tokens.apiKey) + .id(QString("%1").arg(id)) + .name(name) + .type(premium ? APIUserAccountTypes::Premium : APIUserAccountTypes::Regular) + .limits(NexusInterface::parseLimits(headers)); - setSuccess(user); + setSuccess(user); + } } void ValidationAttempt::onSslErrors(const QList<QSslError>& errors) { log::error("nexus: ssl errors"); - for (const auto& e : errors) { + for (auto& e : errors) { log::error(" . {}", e.errorString()); } @@ -564,8 +401,8 @@ void ValidationAttempt::setFailure(Result r, const QString& error) cleanup(); - m_result = r; - m_message = error; + m_Result = r; + m_Message = error; if (failure) { failure(); @@ -577,8 +414,8 @@ void ValidationAttempt::setSuccess(const APIUserAccount& user) log::debug("nexus connection successful"); cleanup(); - m_result = Success; - m_message = ""; + m_Result = Success; + m_Message = ""; if (success) { success(user); @@ -587,17 +424,17 @@ void ValidationAttempt::setSuccess(const APIUserAccount& user) void ValidationAttempt::cleanup() { - m_timeout.stop(); + m_Timeout.stop(); - if (m_reply) { - m_reply->disconnect(); - m_reply->deleteLater(); - m_reply = nullptr; + if (m_Reply) { + m_Reply->disconnect(); + m_Reply->deleteLater(); + m_Reply = nullptr; } } NexusKeyValidator::NexusKeyValidator(Settings* s, NXMAccessManager& am) - : m_settings(s), m_manager(am) + : m_Settings(s), m_Manager(am) {} NexusKeyValidator::~NexusKeyValidator() @@ -607,21 +444,21 @@ NexusKeyValidator::~NexusKeyValidator() std::vector<std::chrono::seconds> NexusKeyValidator::getTimeouts() const { - if (m_settings) { - return m_settings->nexus().validationTimeouts(); + if (m_Settings) { + return m_Settings->nexus().validationTimeouts(); } else { return {10s, 15s, 20s}; } } -void NexusKeyValidator::start(const QString& key, Behaviour b) +void NexusKeyValidator::start(const NexusOAuthTokens& tokens, Behaviour b) { if (isActive()) { log::debug("nexus: trying to start while ongoing; ignoring"); return; } - m_key = key; + m_Tokens = tokens; const auto timeouts = getTimeouts(); @@ -643,10 +480,10 @@ void NexusKeyValidator::start(const QString& key, Behaviour b) void NexusKeyValidator::createAttempts( const std::vector<std::chrono::seconds>& timeouts) { - m_attempts.clear(); + m_Attempts.clear(); for (auto&& t : timeouts) { - m_attempts.push_back(std::make_unique<ValidationAttempt>(t)); + m_Attempts.push_back(std::make_unique<ValidationAttempt>(t)); } } @@ -654,14 +491,14 @@ void NexusKeyValidator::cancel() { log::debug("nexus: connection cancelled"); - for (auto&& a : m_attempts) { + for (auto&& a : m_Attempts) { a->cancel(); } } bool NexusKeyValidator::isActive() const { - for (auto&& a : m_attempts) { + for (auto&& a : m_Attempts) { if (!a->done()) { return true; } @@ -674,7 +511,7 @@ const ValidationAttempt* NexusKeyValidator::lastAttempt() const { const ValidationAttempt* last = nullptr; - for (auto&& a : m_attempts) { + for (auto&& a : m_Attempts) { if (a->done()) { last = a.get(); } else { @@ -687,7 +524,7 @@ const ValidationAttempt* NexusKeyValidator::lastAttempt() const const ValidationAttempt* NexusKeyValidator::currentAttempt() const { - for (auto&& a : m_attempts) { + for (auto&& a : m_Attempts) { if (!a->done()) { return a.get(); } @@ -698,7 +535,12 @@ const ValidationAttempt* NexusKeyValidator::currentAttempt() const bool NexusKeyValidator::nextTry() { - for (auto&& a : m_attempts) { + if (!m_Tokens) { + log::error("nexus: validator invoked without tokens"); + return false; + } + + for (auto&& a : m_Attempts) { if (!a->done()) { a->success = [&](auto&& user) { onAttemptSuccess(*a, user); @@ -707,7 +549,7 @@ bool NexusKeyValidator::nextTry() onAttemptFailure(*a); }; - a->start(m_manager, m_key); + a->start(m_Manager, *m_Tokens); return true; } } @@ -733,10 +575,6 @@ void NexusKeyValidator::onAttemptFailure(const ValidationAttempt& a) } switch (a.result()) { - case ValidationAttempt::None: - case ValidationAttempt::Success: - break; - case ValidationAttempt::SoftError: { if (!nextTry()) { setFinished(a.result(), a.message(), {}); @@ -759,8 +597,9 @@ void NexusKeyValidator::onAttemptFailure(const ValidationAttempt& a) } void NexusKeyValidator::setFinished(ValidationAttempt::Result r, const QString& message, - std::optional<APIUserAccount> user) const + std::optional<APIUserAccount> user) { + m_Attempts.clear(); if (finished) { finished(r, message, user); } @@ -769,13 +608,68 @@ void NexusKeyValidator::setFinished(ValidationAttempt::Result r, const QString& NXMAccessManager::NXMAccessManager(QObject* parent, Settings* s, const QString& moVersion) : QNetworkAccessManager(parent), m_Settings(s), m_MOVersion(moVersion), - m_validator(s, *this) + m_Validator(s, *this), m_ValidationState(NotChecked) { - m_validator.finished = [&](auto&& r, auto&& m, auto&& u) { + NexusOAuthTokens tokens; + GlobalSettings::nexusOAuthTokens(tokens); + GlobalSettings::nexusApiKey(tokens.apiKey); + m_Tokens = tokens; + m_NexusOAuth.reset(new QOAuth2AuthorizationCodeFlow); + m_NexusOAuthReplyHandler.reset(new QOAuthHttpServerReplyHandler( + QHostAddress::LocalHost, NexusOAuth::redirectPort(), this)); + m_NexusOAuth->setReplyHandler(m_NexusOAuthReplyHandler.get()); + + connect(m_NexusOAuth.get(), &QOAuth2AuthorizationCodeFlow::requestFailed, this, + [&](QAbstractOAuth::Error error) { + handleOAuthError(QObject::tr("Authorization failed (%1)").arg(int(error))); + }); + + connect(m_NexusOAuth.get(), &QOAuth2AuthorizationCodeFlow::granted, this, [&]() { + notifyTokens(); + }); + + connect(m_NexusOAuth.get(), &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, + [&](const QUrl& url) { + shell::Open(url); + setOAuthState(OAuthState::WaitingForBrowser); + }); + + connect(m_NexusOAuth.get(), &QOAuth2AuthorizationCodeFlow::accessTokenAboutToExpire, + this, [&] { + if (!m_NexusOAuthReplyHandler->isListening() && + !m_NexusOAuthReplyHandler->listen(QHostAddress::LocalHost, + NexusOAuth::redirectPort())) { + handleOAuthError(QObject::tr("Failed to bind to localhost on port %1.") + .arg(NexusOAuth::redirectPort())); + return; + } + }); + + connect(m_NexusOAuth.get(), &QOAuth2AuthorizationCodeFlow::statusChanged, this, + [&](QAbstractOAuth::Status status) { + switch (status) { + case QAbstractOAuth::Status::RefreshingToken: + setOAuthState(OAuthState::Refreshing); + break; + case QAbstractOAuth::Status::TemporaryCredentialsReceived: + setOAuthState(OAuthState::Authorizing); + break; + case QAbstractOAuth::Status::Granted: + setOAuthState(OAuthState::Finished); + break; + default: + break; + } + }); + + connect(this, &NXMAccessManager::tokensReceived, this, + &NXMAccessManager::saveRefreshedTokens); + + m_Validator.finished = [&](auto&& r, auto&& m, auto&& u) { onValidatorFinished(r, m, u); }; - m_validator.attemptFinished = [&](auto&& a) { + m_Validator.attemptFinished = [&](auto&& a) { onValidatorAttemptFinished(a); }; @@ -793,7 +687,7 @@ void NXMAccessManager::setTopLevelWidget(QWidget* w) } } else { m_ProgressDialog.reset(); - m_validator.cancel(); + m_Validator.cancel(); } } @@ -820,7 +714,7 @@ NXMAccessManager::createRequest(QNetworkAccessManager::Operation operation, void NXMAccessManager::showCookies() const { - QUrl const url(NexusBaseUrl + "/"); + QUrl url(NexusV1BaseUrl + "/"); for (const QNetworkCookie& cookie : cookieJar()->cookiesForUrl(url)) { log::debug("{} - {} (expires: {})", cookie.name().constData(), cookie.value().constData(), cookie.expirationDate().toString()); @@ -837,10 +731,115 @@ void NXMAccessManager::clearCookies() } } -void NXMAccessManager::startValidationCheck(const QString& key) +void NXMAccessManager::setTokens(const NexusOAuthTokens& tokens) +{ + m_Tokens = tokens; +} + +std::optional<NexusOAuthTokens> NXMAccessManager::tokens() const +{ + return m_Tokens; +} + +void NXMAccessManager::handleOAuthError(const QString& message) +{ + m_NexusOAuthReplyHandler->close(); + emit updateOAuthState(OAuthState::Error, message); + emit authorizationEnded(); +} + +void NXMAccessManager::notifyTokens() +{ + if (!m_NexusOAuth) { + handleOAuthError(QObject::tr("Internal error: OAuth flow is missing.")); + return; + } + + QVariantMap payload; + + auto scopeTokens = m_NexusOAuth->grantedScopeTokens(); + QStringList scopes; + for (auto token : scopeTokens) { + scopes.append(QString::fromUtf8(token.constData())); + } + payload["access_token"] = m_NexusOAuth->token(); + payload["refresh_token"] = m_NexusOAuth->refreshToken(); + payload["scope"] = scopes.join(" "); + payload["expiration_at"] = m_NexusOAuth->expirationAt(); + + const auto extras = m_NexusOAuth->extraTokens(); + payload.insert(extras); + + auto tokens = makeTokensFromResponse(payload); + if (!tokens.isValid()) { + handleOAuthError(QObject::tr("Invalid OAuth token payload.")); + return; + } + + tokens.scope = scopes.join(" "); + + emit tokensReceived(tokens); + + startValidationCheck(tokens); + emit authorizationEnded(); +} + +void NXMAccessManager::saveRefreshedTokens(const NexusOAuthTokens tokens) +{ + NexusOAuthTokens finalTokens; + if (GlobalSettings::hasNexusOAuthTokens() || GlobalSettings::hasNexusApiKey()) { + NexusOAuthTokens oldTokens; + GlobalSettings::nexusOAuthTokens(oldTokens); + GlobalSettings::nexusApiKey(oldTokens.apiKey); + NexusOAuthTokens newTokens(tokens); + if (tokens.apiKey.isEmpty()) { + newTokens.apiKey = oldTokens.apiKey; + } + finalTokens = newTokens; + } else { + finalTokens = tokens; + } + const bool ret = GlobalSettings::setNexusOAuthTokens(finalTokens); + const bool ret2 = GlobalSettings::setNexusApiKey(finalTokens.apiKey); + if (ret && ret2) { + setTokens(finalTokens); + } +} + +void NXMAccessManager::setOAuthState(OAuthState state, const QString& message) +{ + emit updateOAuthState(state, message); +} + +QString NXMAccessManager::stateToString(OAuthState state, const QString& details) +{ + switch (state) { + case OAuthState::Initializing: + return QObject::tr("Connecting to Nexus..."); + + case OAuthState::WaitingForBrowser: + return QObject::tr("Opened Nexus in browser.") + "\n" + + QObject::tr("Switch to your browser and accept the request."); + + case OAuthState::Authorizing: + return QObject::tr("Waiting for Nexus..."); + + case OAuthState::Finished: + return QObject::tr("Finished."); + + case OAuthState::Cancelled: + return QObject::tr("Cancelled."); + + case OAuthState::Error: + default: + return details.isEmpty() ? QObject::tr("An unknown error has occurred.") : details; + } +} + +void NXMAccessManager::startValidationCheck(const NexusOAuthTokens& tokens) { - m_validationState = NotChecked; - m_validator.start(key, NexusKeyValidator::Retry); + m_ValidationState = NotChecked; + m_Validator.start(tokens, NexusKeyValidator::Retry); if (m_ProgressDialog) { // don't show the progress dialog on startup for the first attempt; the @@ -856,14 +855,14 @@ void NXMAccessManager::onValidatorFinished(ValidationAttempt::Result r, stopProgress(); if (user) { - m_validationState = Valid; + m_ValidationState = Valid; emit credentialsReceived(*user); emit validateSuccessful(true); } else { if (r == ValidationAttempt::Cancelled) { - m_validationState = NotChecked; + m_ValidationState = NotChecked; } else { - m_validationState = Invalid; + m_ValidationState = Invalid; emit validateFailed(message); } } @@ -892,11 +891,11 @@ void NXMAccessManager::onValidatorAttemptFinished(const ValidationAttempt& a) bool NXMAccessManager::validated() const { - if (m_validationState == Valid) { + if (m_ValidationState == Valid) { return true; } - if (m_validator.isActive()) { + if (m_Validator.isActive()) { const_cast<NXMAccessManager*>(this)->startProgress(); } @@ -905,40 +904,158 @@ bool NXMAccessManager::validated() const void NXMAccessManager::refuseValidation() { - m_validationState = Invalid; + m_ValidationState = Invalid; } bool NXMAccessManager::validateAttempted() const { - return (m_validationState != NotChecked); + return (m_ValidationState != NotChecked); } bool NXMAccessManager::validateWaiting() const { - return m_validator.isActive(); + return m_Validator.isActive(); +} + +void NXMAccessManager::connectOrRefresh(const NexusOAuthTokens tokens) +{ + const auto clientId = NexusOAuth::clientId(); + if (clientId.isEmpty()) { + handleOAuthError(QObject::tr("No OAuth client id configured.")); + return; + } + m_NexusOAuth->setAuthorizationUrl(QUrl(NexusOAuth::authorizeUrl())); + m_NexusOAuth->setTokenUrl(QUrl(NexusOAuth::tokenUrl())); + m_NexusOAuth->setClientIdentifier(clientId); + m_NexusOAuth->setPkceMethod(QOAuth2AuthorizationCodeFlow::PkceMethod::S256); + QSet<QByteArray> scope = {"openid", "profile", "email"}; + m_NexusOAuth->setRequestedScopeTokens(scope); + m_NexusOAuthReplyHandler->close(); + m_NexusOAuthReplyHandler->setCallbackPath(QUrl(NexusOAuth::redirectUri()).path()); + m_NexusOAuthReplyHandler->setCallbackText( + QObject::tr("<html><body><h2>Mod Organizer</h2><p>Authorization complete. You " + "may close this " + "window.</p></body></html>")); + if (!m_NexusOAuthReplyHandler->listen(QHostAddress::LocalHost, + NexusOAuth::redirectPort())) { + handleOAuthError(QObject::tr("Failed to bind to localhost on port %1.") + .arg(NexusOAuth::redirectPort())); + return; + } + if (!tokens.accessToken.isEmpty()) { + m_NexusOAuth->setToken(tokens.accessToken); + m_NexusOAuth->setRefreshToken(tokens.refreshToken); + scope.clear(); + for (const QString scopeItem : tokens.scope.split(" ")) { + scope.insert(scopeItem.toUtf8()); + } + m_NexusOAuth->setRequestedScopeTokens(scope); + + setOAuthState(OAuthState::Refreshing); + m_NexusOAuth->refreshTokens(); + } else { + setOAuthState(OAuthState::Initializing); + m_NexusOAuth->grant(); + } +} + +void NXMAccessManager::cancelAuth() +{ + if (m_NexusOAuthReplyHandler) { + m_NexusOAuthReplyHandler->close(); + } + + m_NexusOAuth.reset(); + m_NexusOAuthReplyHandler.reset(); + setOAuthState(OAuthState::Cancelled); +} + +void NXMAccessManager::addAPIHeaders(QNetworkRequest& request) +{ + request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, + QNetworkRequest::AlwaysNetwork); + request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, + userAgent().toUtf8()); + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, + "application/json"); + request.setRawHeader("Protocol-Version", "1.0.0"); + request.setRawHeader("Application-Name", "MO2"); + request.setRawHeader("Application-Version", MOVersion().toUtf8()); } -void NXMAccessManager::apiCheck(const QString& apiKey, bool force) +QNetworkReply* NXMAccessManager::makeOAuthGetRequest(const QUrl url) { - if (m_validator.isActive()) { + if (!m_NexusOAuth->token().isEmpty()) { + QNetworkRequest request(url); + m_NexusOAuth->prepareRequest(&request, "GET"); + addAPIHeaders(request); + return m_NexusOAuth->networkAccessManager()->get(request); + } + return nullptr; +} + +QNetworkReply* NXMAccessManager::makeOAuthPostRequest(const QUrl url, + const QByteArray payload = {}) +{ + if (!m_NexusOAuth->token().isEmpty()) { + QNetworkRequest request(url); + m_NexusOAuth->prepareRequest(&request, "POST", payload); + addAPIHeaders(request); + return m_NexusOAuth->networkAccessManager()->post(request, payload); + } + return nullptr; +} + +QNetworkReply* NXMAccessManager::makeOAuthDeleteRequest(QNetworkRequest request) +{ + if (!m_NexusOAuth->token().isEmpty()) { + m_NexusOAuth->prepareRequest(&request, "DELETE"); + addAPIHeaders(request); + return m_NexusOAuth->networkAccessManager()->deleteResource(request); + } + return nullptr; +} + +QNetworkReply* NXMAccessManager::makeOAuthCustomRequest(QNetworkRequest request, + const QByteArray& verb, + const QByteArray& data) +{ + if (!m_NexusOAuth->token().isEmpty()) { + m_NexusOAuth->prepareRequest(&request, verb, data); + addAPIHeaders(request); + return m_NexusOAuth->networkAccessManager()->sendCustomRequest(request, verb, data); + } + return nullptr; +} + +void NXMAccessManager::apiCheck(const NexusOAuthTokens& tokens, bool force) +{ + if (m_Validator.isActive()) { return; } + setTokens(tokens); + if (m_Settings && m_Settings->network().offlineMode()) { - m_validationState = NotChecked; + m_ValidationState = NotChecked; return; } if (force) { - m_validationState = NotChecked; + m_ValidationState = NotChecked; } - if (m_validationState == Valid) { + if (m_ValidationState == Valid) { emit validateSuccessful(false); return; } - startValidationCheck(apiKey); + if (m_NexusOAuth->token().isEmpty() && !tokens.accessToken.isEmpty()) { + connectOrRefresh(tokens); + } else if (!tokens.apiKey.isEmpty()) { + startValidationCheck(tokens); + } } const QString& NXMAccessManager::MOVersion() const @@ -949,7 +1066,7 @@ const QString& NXMAccessManager::MOVersion() const QString NXMAccessManager::userAgent(const QString& subModule) const { QStringList comments; - QString const os; + QString os; if (QSysInfo::productType() == "windows") comments << ((QSysInfo::kernelType() == "winnt") ? "Windows_NT " : "Windows ") + QSysInfo::kernelVersion(); @@ -966,16 +1083,30 @@ QString NXMAccessManager::userAgent(const QString& subModule) const .arg(m_MOVersion, comments.join("; "), qVersion()); } -void NXMAccessManager::clearApiKey() +void NXMAccessManager::clearCredentials() { - m_validator.cancel(); + m_Validator.cancel(); + // TODO: Verify revocation process + // if (m_Tokens && !m_Tokens->accessToken.isEmpty()) { + // QNetworkRequest request(NexusOAuth::tokenUrl()); + // QUrlQuery params; + // params.addQueryItem("token", m_Tokens->refreshToken); + // params.addQueryItem("token_type_hint", "refresh_token"); + // m_NexusOAuth->prepareRequest(&request, "POST", + // params.toString(QUrl::FullyEncoded).toUtf8()); + // m_NexusOAuth->networkAccessManager()->post( + // request, params.toString(QUrl::FullyEncoded).toUtf8()); + //} + m_Tokens.reset(); + m_NexusOAuth->setToken(""); + m_NexusOAuthReplyHandler->close(); emit credentialsReceived(APIUserAccount()); } void NXMAccessManager::startProgress() { if (!m_ProgressDialog) { - m_ProgressDialog.reset(new ValidationProgressDialog(m_Settings, m_validator)); + m_ProgressDialog.reset(new ValidationProgressDialog(m_Settings, m_Validator)); } m_ProgressDialog->start(); diff --git a/src/src/nxmaccessmanager.h b/src/src/nxmaccessmanager.h index 6027fa6..c651bee 100644 --- a/src/src/nxmaccessmanager.h +++ b/src/src/nxmaccessmanager.h @@ -1,285 +1,282 @@ -/*
-Copyright (C) 2012 Sebastian Herbord. All rights reserved.
-
-This file is part of Mod Organizer.
-
-Mod Organizer 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.
-
-Mod Organizer 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 Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-#ifndef NXMACCESSMANAGER_H
-#define NXMACCESSMANAGER_H
-
-#include "apiuseraccount.h"
-#include "ui_validationprogressdialog.h"
-#include <QDialogButtonBox>
-#include <QElapsedTimer>
-#include <QNetworkAccessManager>
-#include <QNetworkReply>
-#include <QProgressDialog>
-#include <QTimer>
-#include <QWebSocket>
-#include <set>
-
-namespace MOBase
-{
-class IPluginGame;
-}
-class NXMAccessManager;
-class Settings;
-
-class NexusSSOLogin
-{
-public:
- enum States
- {
- ConnectingToSSO,
- WaitingForToken,
- WaitingForBrowser,
- Finished,
- Timeout,
- ClosedByRemote,
- Cancelled,
- Error
- };
-
- std::function<void(QString)> keyChanged;
- std::function<void(States, QString)> stateChanged;
-
- static QString stateToString(States s, const QString& e);
-
- NexusSSOLogin();
-
- void start();
- void cancel();
-
- bool isActive() const;
-
-private:
- QWebSocket m_socket;
- QString m_guid;
- bool m_keyReceived{false};
- bool m_active{false};
- QTimer m_timeout;
-
- void setState(States s, const QString& error = {}) const;
-
- void close();
- void abort();
-
- void onConnected();
- void onMessage(const QString& s);
- void onDisconnected();
- void onError(QAbstractSocket::SocketError e);
- void onSslErrors(const QList<QSslError>& errors);
- void onTimeout();
-};
-
-class ValidationAttempt
-{
-public:
- enum Result
- {
- None,
- Success,
- SoftError,
- HardError,
- Cancelled
- };
-
- std::function<void(APIUserAccount)> success;
- std::function<void()> failure;
-
- ValidationAttempt(std::chrono::seconds timeout);
- ValidationAttempt(const ValidationAttempt&) = delete;
- ValidationAttempt& operator=(const ValidationAttempt&) = delete;
-
- void start(NXMAccessManager& m, const QString& key);
- void cancel();
-
- bool done() const;
- Result result() const;
- const QString& message() const;
- std::chrono::seconds timeout() const;
- QElapsedTimer elapsed() const;
-
-private:
- QNetworkReply* m_reply{nullptr};
- Result m_result{None};
- QString m_message;
- QTimer m_timeout;
- QElapsedTimer m_elapsed;
-
- bool sendRequest(NXMAccessManager& m, const QString& key);
-
- void onFinished();
- void onSslErrors(const QList<QSslError>& errors);
- void onTimeout();
-
- void setFailure(Result r, const QString& error);
- void setSuccess(const APIUserAccount& user);
-
- void cleanup();
-};
-
-class NexusKeyValidator
-{
-public:
- enum Behaviour
- {
- OneShot = 0,
- Retry
- };
-
- using FinishedCallback = void(ValidationAttempt::Result, const QString&,
- std::optional<APIUserAccount>);
-
- std::function<FinishedCallback> finished;
- std::function<void(const ValidationAttempt&)> attemptFinished;
-
- NexusKeyValidator(Settings* s, NXMAccessManager& am);
- ~NexusKeyValidator();
-
- void start(const QString& key, Behaviour b);
- void cancel();
-
- bool isActive() const;
- const ValidationAttempt* lastAttempt() const;
- const ValidationAttempt* currentAttempt() const;
-
-private:
- Settings* m_settings;
- NXMAccessManager& m_manager;
- QString m_key;
- std::vector<std::unique_ptr<ValidationAttempt>> m_attempts;
-
- void createAttempts(const std::vector<std::chrono::seconds>& timeouts);
- std::vector<std::chrono::seconds> getTimeouts() const;
-
- bool nextTry();
- void onAttemptSuccess(const ValidationAttempt& a, const APIUserAccount& u);
- void onAttemptFailure(const ValidationAttempt& a);
-
- void setFinished(ValidationAttempt::Result r, const QString& message,
- std::optional<APIUserAccount> user) const;
-};
-
-class ValidationProgressDialog : public QDialog
-{
- Q_OBJECT;
-
-public:
- ValidationProgressDialog(Settings* s, NexusKeyValidator& v);
-
- void setParentWidget(QWidget* w);
-
- void start();
- void stop();
-
-protected:
- void showEvent(QShowEvent* e) override;
- void closeEvent(QCloseEvent* e) override;
-
-private:
- std::unique_ptr<Ui::ValidationProgressDialog> ui;
- Settings* m_settings;
- NexusKeyValidator& m_validator;
- QTimer* m_updateTimer{nullptr};
- bool m_first{true};
-
- void onHide();
- void onCancel();
- void onTimer();
- void updateProgress();
-};
-
-/**
- * @brief access manager extended to handle nxm links
- **/
-class NXMAccessManager : public QNetworkAccessManager
-{
- Q_OBJECT
-public:
- NXMAccessManager(QObject* parent, Settings* s, const QString& moVersion);
-
- void setTopLevelWidget(QWidget* w);
-
- bool validated() const;
-
- bool validateAttempted() const;
- bool validateWaiting() const;
-
- void apiCheck(const QString& apiKey, bool force = false);
-
- void showCookies() const;
-
- void clearCookies();
-
- QString userAgent(const QString& subModule = QString()) const;
- const QString& MOVersion() const;
-
- void clearApiKey();
-
- void refuseValidation();
-
-signals:
-
- /**
- * @brief emitted when a nxm:// link is opened
- *
- * @param url the nxm-link
- **/
- void requestNXMDownload(const QString& url);
-
- /**
- * @brief emitted after a successful login or if login was not necessary
- *
- * @param necessary true if a login was necessary and succeeded, false if the user is
- *still logged in
- **/
- void validateSuccessful(bool necessary);
- void validateFailed(const QString& message);
- void credentialsReceived(const APIUserAccount& user);
-
-protected:
- QNetworkReply* createRequest(QNetworkAccessManager::Operation operation,
- const QNetworkRequest& request,
- QIODevice* device) override;
-
-private:
- enum States
- {
- NotChecked,
- Valid,
- Invalid
- };
-
- QWidget* m_TopLevel;
- Settings* m_Settings;
- mutable std::unique_ptr<ValidationProgressDialog> m_ProgressDialog;
- QString m_MOVersion;
- NexusKeyValidator m_validator;
- States m_validationState{NotChecked};
-
- void startValidationCheck(const QString& key);
-
- void onValidatorFinished(ValidationAttempt::Result r, const QString& message,
- std::optional<APIUserAccount>);
-
- void onValidatorAttemptFinished(const ValidationAttempt& a);
-
- void startProgress();
- void stopProgress();
-};
-
-#endif // NXMACCESSMANAGER_H
+/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer 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. + +Mod Organizer 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 Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef NXMACCESSMANAGER_H +#define NXMACCESSMANAGER_H + +#include "apiuseraccount.h" +#include "nexusoauthtokens.h" +#include "ui_validationprogressdialog.h" +#include <QDialogButtonBox> +#include <QElapsedTimer> +#include <QNetworkAccessManager> +#include <QNetworkReply> +#include <QProgressDialog> +#include <QTimer> +#include <QtNetworkAuth/QOAuth2AuthorizationCodeFlow> +#include <QtNetworkAuth/QOAuthHttpServerReplyHandler> +#include <optional> +#include <set> + +namespace MOBase +{ +class IPluginGame; +} +class NXMAccessManager; +class Settings; + +class ValidationAttempt +{ +public: + enum Result + { + None, + Success, + SoftError, + HardError, + Cancelled + }; + + std::function<void(APIUserAccount)> success; + std::function<void()> failure; + + ValidationAttempt(std::chrono::seconds timeout); + ValidationAttempt(const ValidationAttempt&) = delete; + ValidationAttempt& operator=(const ValidationAttempt&) = delete; + + void start(NXMAccessManager& m, const NexusOAuthTokens& tokens); + void cancel(); + + bool done() const; + Result result() const; + const QString& message() const; + std::chrono::seconds timeout() const; + QElapsedTimer elapsed() const; + +private: + QNetworkReply* m_Reply{nullptr}; + Result m_Result{None}; + QString m_Message; + QTimer m_Timeout; + QElapsedTimer m_Elapsed; + NexusOAuthTokens m_Tokens; + + bool sendRequest(NXMAccessManager& m, const NexusOAuthTokens& tokens); + + void onFinished(); + void onSslErrors(const QList<QSslError>& errors); + void onTimeout(); + + void setFailure(Result r, const QString& error); + void setSuccess(const APIUserAccount& user); + + void cleanup(); +}; + +class NexusKeyValidator +{ +public: + enum Behaviour + { + OneShot = 0, + Retry + }; + + using FinishedCallback = void(ValidationAttempt::Result, const QString&, + std::optional<APIUserAccount>); + + std::function<FinishedCallback> finished; + std::function<void(const ValidationAttempt&)> attemptFinished; + + NexusKeyValidator(Settings* s, NXMAccessManager& am); + ~NexusKeyValidator(); + + void start(const NexusOAuthTokens& tokens, Behaviour b); + void cancel(); + + bool isActive() const; + const ValidationAttempt* lastAttempt() const; + const ValidationAttempt* currentAttempt() const; + +private: + Settings* m_Settings; + NXMAccessManager& m_Manager; + std::optional<NexusOAuthTokens> m_Tokens; + std::vector<std::unique_ptr<ValidationAttempt>> m_Attempts; + + void createAttempts(const std::vector<std::chrono::seconds>& timeouts); + std::vector<std::chrono::seconds> getTimeouts() const; + + bool nextTry(); + void onAttemptSuccess(const ValidationAttempt& a, const APIUserAccount& u); + void onAttemptFailure(const ValidationAttempt& a); + + void setFinished(ValidationAttempt::Result r, const QString& message, + std::optional<APIUserAccount> user); +}; + +class ValidationProgressDialog : public QDialog +{ + Q_OBJECT; + +public: + ValidationProgressDialog(Settings* s, NexusKeyValidator& v); + + void setParentWidget(QWidget* w); + + void start(); + void stop(); + +protected: + void showEvent(QShowEvent* e) override; + void closeEvent(QCloseEvent* e) override; + +private: + std::unique_ptr<Ui::ValidationProgressDialog> ui; + Settings* m_Settings; + NexusKeyValidator& m_Validator; + QTimer* m_UpdateTimer{nullptr}; + bool m_First{true}; + + void onHide(); + void onCancel(); + void onTimer(); + void updateProgress(); +}; + +/** + * @brief access manager extended to handle nxm links + **/ +class NXMAccessManager : public QNetworkAccessManager +{ + Q_OBJECT +public: + enum class OAuthState + { + Initializing, + WaitingForBrowser, + Authorizing, + Refreshing, + Finished, + Cancelled, + Error + }; + + NXMAccessManager(QObject* parent, Settings* s, const QString& moVersion); + + void setTopLevelWidget(QWidget* w); + + bool validated() const; + + bool validateAttempted() const; + bool validateWaiting() const; + + void connectOrRefresh(const NexusOAuthTokens tokens); + void cancelAuth(); + + QNetworkReply* makeOAuthGetRequest(const QUrl url); + QNetworkReply* makeOAuthPostRequest(const QUrl url, const QByteArray payload); + QNetworkReply* makeOAuthDeleteRequest(QNetworkRequest request); + QNetworkReply* makeOAuthCustomRequest(QNetworkRequest request, const QByteArray& verb, + const QByteArray& data); + + static QString stateToString(OAuthState state, const QString& details = {}); + + void addAPIHeaders(QNetworkRequest& request); + + void apiCheck(const NexusOAuthTokens& tokens, bool force = false); + void setTokens(const NexusOAuthTokens& tokens); + std::optional<NexusOAuthTokens> tokens() const; + + void showCookies() const; + + void clearCookies(); + + QString userAgent(const QString& subModule = QString()) const; + const QString& MOVersion() const; + + void clearCredentials(); + + void refuseValidation(); + +signals: + + /** + * @brief emitted when a nxm:// link is opened + * + * @param url the nxm-link + **/ + void requestNXMDownload(const QString& url); + + /** + * @brief emitted after a successful login or if login was not necessary + * + * @param necessary true if a login was necessary and succeeded, false if the user is + *still logged in + **/ + void validateSuccessful(bool necessary); + void validateFailed(const QString& message); + void credentialsReceived(const APIUserAccount& user); + void authorizationEnded(); + void tokensReceived(const NexusOAuthTokens tokens); + void updateOAuthState(OAuthState state, QString message); + +protected: + QNetworkReply* createRequest(QNetworkAccessManager::Operation operation, + const QNetworkRequest& request, + QIODevice* device) override; + +private: + enum States + { + NotChecked, + Valid, + Invalid + }; + + QWidget* m_TopLevel; + Settings* m_Settings; + mutable std::unique_ptr<ValidationProgressDialog> m_ProgressDialog; + QString m_MOVersion; + NexusKeyValidator m_Validator; + States m_ValidationState{NotChecked}; + std::optional<NexusOAuthTokens> m_Tokens; + std::unique_ptr<QOAuth2AuthorizationCodeFlow> m_NexusOAuth; + std::unique_ptr<QOAuthHttpServerReplyHandler> m_NexusOAuthReplyHandler; + + void handleOAuthError(const QString& message); + + void setOAuthState(OAuthState state, const QString& message = {}); + void notifyTokens(); + + void startValidationCheck(const NexusOAuthTokens& tokens); + + void onValidatorFinished(ValidationAttempt::Result r, const QString& message, + std::optional<APIUserAccount>); + + void onValidatorAttemptFinished(const ValidationAttempt& a); + + void startProgress(); + void stopProgress(); + +private slots: + void saveRefreshedTokens(const NexusOAuthTokens tokens); +}; + +#endif // NXMACCESSMANAGER_H diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index 1897ea3..ff4d6af 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -481,11 +481,13 @@ bool OrganizerCore::nexusApi(bool retry) // previous attempt, maybe even successful return false; } else { - QString apiKey; - if (GlobalSettings::nexusApiKey(apiKey)) { + NexusOAuthTokens tokens; + GlobalSettings::nexusOAuthTokens(tokens); + GlobalSettings::nexusApiKey(tokens.apiKey); + if (tokens.isValid() || !tokens.apiKey.isEmpty()) { // credentials stored or user entered them manually - log::debug("attempt to verify nexus api key"); - accessManager->apiCheck(apiKey); + log::debug("attempt to verify nexus credentials"); + accessManager->apiCheck(tokens); return true; } else { // no credentials stored and user didn't enter them @@ -1778,12 +1780,12 @@ void OrganizerCore::loggedInAction(QWidget* parent, std::function<void()> f) if (NexusInterface::instance().getAccessManager()->validated()) { f(); } else if (!m_Settings.network().offlineMode()) { - QString apiKey; - if (GlobalSettings::nexusApiKey(apiKey)) { + NexusOAuthTokens tokens; + if (GlobalSettings::nexusOAuthTokens(tokens)) { doAfterLogin([f] { f(); }); - NexusInterface::instance().getAccessManager()->apiCheck(apiKey); + NexusInterface::instance().getAccessManager()->apiCheck(tokens); } else { MessageDialog::showMessage(tr("You need to be logged in with Nexus"), parent); } diff --git a/src/src/plugincontainer.cpp b/src/src/plugincontainer.cpp index a309001..99e8ddb 100644 --- a/src/src/plugincontainer.cpp +++ b/src/src/plugincontainer.cpp @@ -92,47 +92,47 @@ struct PluginTypeName; template <> struct PluginTypeName<MOBase::IPlugin> { - static QString value() { return QT_TR_NOOP("Plugin"); } + static QString value() { return PluginContainer::tr("Plugin"); } }; template <> struct PluginTypeName<MOBase::IPluginDiagnose> { - static QString value() { return QT_TR_NOOP("Diagnose"); } + static QString value() { return PluginContainer::tr("Diagnose"); } }; template <> struct PluginTypeName<MOBase::IPluginGame> { - static QString value() { return QT_TR_NOOP("Game"); } + static QString value() { return PluginContainer::tr("Game"); } }; template <> struct PluginTypeName<MOBase::IPluginInstaller> { - static QString value() { return QT_TR_NOOP("Installer"); } + static QString value() { return PluginContainer::tr("Installer"); } }; template <> struct PluginTypeName<MOBase::IPluginModPage> { - static QString value() { return QT_TR_NOOP("Mod Page"); } + static QString value() { return PluginContainer::tr("Mod Page"); } }; template <> struct PluginTypeName<MOBase::IPluginPreview> { - static QString value() { return QT_TR_NOOP("Preview"); } + static QString value() { return PluginContainer::tr("Preview"); } }; template <> struct PluginTypeName<MOBase::IPluginTool> { - static QString value() { return QT_TR_NOOP("Tool"); } + static QString value() { return PluginContainer::tr("Tool"); } }; template <> struct PluginTypeName<MOBase::IPluginProxy> { - static QString value() { return QT_TR_NOOP("Proxy"); } + static QString value() { return PluginContainer::tr("Proxy"); } }; template <> struct PluginTypeName<MOBase::IPluginFileMapper> { - static QString value() { return QT_TR_NOOP("File Mapper"); } + static QString value() { return PluginContainer::tr("File Mapper"); } }; QStringList PluginContainer::pluginInterfaces() diff --git a/src/src/settings.cpp b/src/src/settings.cpp index 4a67666..2b408c0 100644 --- a/src/src/settings.cpp +++ b/src/src/settings.cpp @@ -28,8 +28,10 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "serverinfo.h" #include "settingsutilities.h" #include "shared/appconfig.h" +#include <QJsonDocument> #include <expanderwidget.h> #include <iplugingame.h> +#include <optional> #include <utility.h> using namespace MOBase; @@ -2530,9 +2532,29 @@ void GlobalSettings::setHideAssignCategoriesQuestion(bool b) settings().setValue("HideAssignCategoriesQuestion", b); } +namespace +{ +constexpr auto NexusLegacyCredentialKey = "APIKEY"; +constexpr auto NexusOAuthCredentialKey = "NEXUS_OAUTH_TOKENS"; + +std::optional<NexusOAuthTokens> parseStoredTokens(const QString& raw) +{ + if (raw.isEmpty()) { + return std::nullopt; + } + + const auto doc = QJsonDocument::fromJson(raw.toUtf8()); + if (doc.isNull() || !doc.isObject()) { + return std::nullopt; + } + + return NexusOAuthTokens::fromJson(doc.object()); +} +} // namespace + bool GlobalSettings::nexusApiKey(QString& apiKey) { - QString tempKey = getWindowsCredential("APIKEY"); + QString tempKey = getWindowsCredential(NexusLegacyCredentialKey); if (tempKey.isEmpty()) return false; @@ -2542,7 +2564,7 @@ bool GlobalSettings::nexusApiKey(QString& apiKey) bool GlobalSettings::setNexusApiKey(const QString& apiKey) { - if (!setWindowsCredential("APIKEY", apiKey)) { + if (!setWindowsCredential(NexusLegacyCredentialKey, apiKey)) { const auto e = GetLastError(); log::error("Storing API key failed: {}", formatSystemMessage(e)); return false; @@ -2558,7 +2580,42 @@ bool GlobalSettings::clearNexusApiKey() bool GlobalSettings::hasNexusApiKey() { - return !getWindowsCredential("APIKEY").isEmpty(); + return !getWindowsCredential(NexusLegacyCredentialKey).isEmpty(); +} + +bool GlobalSettings::nexusOAuthTokens(NexusOAuthTokens& tokens) +{ + const auto raw = getWindowsCredential(NexusOAuthCredentialKey); + const auto parsed = parseStoredTokens(raw); + if (!parsed) { + return false; + } else { + tokens = *parsed; + } + return true; +} + +bool GlobalSettings::setNexusOAuthTokens(const NexusOAuthTokens& tokens) +{ + const auto payload = QJsonDocument(tokens.toJson()).toJson(QJsonDocument::Compact); + + if (!setWindowsCredential(NexusOAuthCredentialKey, payload)) { + const auto e = GetLastError(); + log::error("Storing OAuth tokens failed: {}", formatSystemMessage(e)); + return false; + } + + return true; +} + +bool GlobalSettings::clearNexusOAuthTokens() +{ + return setWindowsCredential(NexusOAuthCredentialKey, ""); +} + +bool GlobalSettings::hasNexusOAuthTokens() +{ + return !getWindowsCredential(NexusOAuthCredentialKey).isEmpty(); } void GlobalSettings::resetDialogs() diff --git a/src/src/settings.h b/src/src/settings.h index 38ad505..97b671d 100644 --- a/src/src/settings.h +++ b/src/src/settings.h @@ -21,6 +21,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #define SETTINGS_H #include "envdump.h" +#include "nexusoauthtokens.h" #include <questionboxmemory.h> #include <uibase/filterwidget.h> #include <uibase/log.h> @@ -961,6 +962,24 @@ public: // static bool hasNexusApiKey(); + // Retrieves the stored OAuth tokens. Returns false if the credential doesn't exist + // or can't be parsed. + // + static bool nexusOAuthTokens(NexusOAuthTokens& tokens); + + // Persists the OAuth tokens inside the credentials store, replacing the previous + // entry. Returns false on errors. + // + static bool setNexusOAuthTokens(const NexusOAuthTokens& tokens); + + // Removes the stored OAuth tokens; returns false on errors. + // + static bool clearNexusOAuthTokens(); + + // Returns whether OAuth tokens are currently stored. + // + static bool hasNexusOAuthTokens(); + // resets anything that the user can disable static void resetDialogs(); diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui index 4e10085..73ca890 100644 --- a/src/src/settingsdialog.ui +++ b/src/src/settingsdialog.ui @@ -1214,7 +1214,7 @@ If you disable this feature, MO will only display official DLCs this way. Please <item> <widget class="QPushButton" name="nexusDisconnect"> <property name="toolTip"> - <string>Clear the stored Nexus API key and force reauthorization.</string> + <string>Clear the stored Nexus authorization and force reauthorization.</string> </property> <property name="text"> <string>Disconnect from Nexus</string> diff --git a/src/src/settingsdialognexus.cpp b/src/src/settingsdialognexus.cpp index 59c5dad..fac2a41 100644 --- a/src/src/settingsdialognexus.cpp +++ b/src/src/settingsdialognexus.cpp @@ -1,470 +1,517 @@ -#include "settingsdialognexus.h"
-#include "log.h"
-#include "nexusinterface.h"
-#include "serverinfo.h"
-#include "ui_nexusmanualkey.h"
-#include "ui_settingsdialog.h"
-#include <utility.h>
-
-using namespace MOBase;
-
-template <typename T>
-class ServerItem : public QListWidgetItem
-{
-public:
- ServerItem(const QString& text, int sortRole = Qt::DisplayRole,
- QListWidget* parent = 0, int type = Type)
- : QListWidgetItem(text, parent, type), m_SortRole(sortRole)
- {}
-
- virtual bool operator<(const QListWidgetItem& other) const
- {
+#include "settingsdialognexus.h" +#include "log.h" +#include "nexusinterface.h" +#include "nexusoauthlogin.h" +#include "serverinfo.h" +#include "ui_nexusmanualkey.h" +#include "ui_settingsdialog.h" +#include <utility.h> + +using namespace MOBase; + +template <typename T> +class ServerItem : public QListWidgetItem +{ +public: + ServerItem(const QString& text, int sortRole = Qt::DisplayRole, + QListWidget* parent = 0, int type = Type) + : QListWidgetItem(text, parent, type), m_SortRole(sortRole) + {} + + virtual bool operator<(const QListWidgetItem& other) const + { return this->data(m_SortRole).template value<T>() < other.data(m_SortRole).template value<T>(); - }
-
-private:
- int m_SortRole;
-};
-
-class NexusManualKeyDialog : public QDialog
-{
-public:
- NexusManualKeyDialog(QWidget* parent)
- : QDialog(parent), ui(new Ui::NexusManualKeyDialog)
- {
- ui->setupUi(this);
- ui->key->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
-
- connect(ui->openBrowser, &QPushButton::clicked, [&] {
- openBrowser();
- });
- connect(ui->paste, &QPushButton::clicked, [&] {
- paste();
- });
- connect(ui->clear, &QPushButton::clicked, [&] {
- clear();
- });
- }
-
- void accept() override
- {
- m_key = ui->key->toPlainText();
- QDialog::accept();
- }
-
- const QString& key() const { return m_key; }
-
- void openBrowser()
- {
- shell::Open(QUrl("https://www.nexusmods.com/users/myaccount?tab=api"));
- }
-
- void paste()
- {
- const auto text = QApplication::clipboard()->text();
- if (!text.isEmpty()) {
- ui->key->setPlainText(text);
- }
- }
-
- void clear() { ui->key->clear(); }
-
-private:
- std::unique_ptr<Ui::NexusManualKeyDialog> ui;
- QString m_key;
-};
-
-NexusConnectionUI::NexusConnectionUI(QWidget* parent, Settings* s,
- QAbstractButton* connectButton,
- QAbstractButton* disconnectButton,
- QAbstractButton* manualButton,
- QListWidget* logList)
- : m_parent(parent), m_settings(s), m_connect(connectButton),
- m_disconnect(disconnectButton), m_manual(manualButton), m_log(logList)
-{
- if (m_connect) {
- QObject::connect(m_connect, &QPushButton::clicked, [&] {
- connect();
- });
- }
-
- if (m_disconnect) {
- QObject::connect(m_disconnect, &QPushButton::clicked, [&] {
- disconnect();
- });
- }
-
- if (m_manual) {
- QObject::connect(manualButton, &QPushButton::clicked, [&] {
- manual();
- });
- }
-
- if (GlobalSettings::hasNexusApiKey()) {
- addLog(tr("Connected."));
- } else {
- addLog(tr("Not connected."));
- }
-
- updateState();
-}
-
-void NexusConnectionUI::connect()
-{
- if (m_nexusLogin && m_nexusLogin->isActive()) {
- m_nexusLogin->cancel();
- return;
- }
-
- if (!m_nexusLogin) {
- m_nexusLogin.reset(new NexusSSOLogin);
-
- m_nexusLogin->keyChanged = [&](auto&& s) {
- onSSOKeyChanged(s);
- };
-
- m_nexusLogin->stateChanged = [&](auto&& s, auto&& e) {
- onSSOStateChanged(s, e);
- };
- }
-
- m_log->clear();
- m_nexusLogin->start();
- updateState();
-}
-
-void NexusConnectionUI::manual()
-{
- if (m_nexusValidator && m_nexusValidator->isActive()) {
- m_nexusValidator->cancel();
- return;
- }
-
- NexusManualKeyDialog d(m_parent);
- if (d.exec() != QDialog::Accepted) {
- return;
- }
-
- const auto key = d.key();
- if (key.isEmpty()) {
- clearKey();
- return;
- }
-
- m_log->clear();
- validateKey(key);
-}
-
-void NexusConnectionUI::disconnect()
-{
- clearKey();
- m_log->clear();
- addLog(tr("Disconnected."));
-}
-
-void NexusConnectionUI::validateKey(const QString& key)
-{
- if (!m_nexusValidator) {
- m_nexusValidator.reset(new NexusKeyValidator(
- m_settings, *NexusInterface::instance().getAccessManager()));
-
- m_nexusValidator->finished = [&](auto&& r, auto&& m, auto&& u) {
- onValidatorFinished(r, m, u);
- };
- }
-
- addLog(tr("Checking API key..."));
- m_nexusValidator->start(key, NexusKeyValidator::OneShot);
-}
-
-void NexusConnectionUI::onSSOKeyChanged(const QString& key)
-{
- if (key.isEmpty()) {
- clearKey();
- } else {
- addLog(tr("Received API key."));
- validateKey(key);
- }
-}
-
-void NexusConnectionUI::onSSOStateChanged(NexusSSOLogin::States s, const QString& e)
-{
- if (s != NexusSSOLogin::Finished) {
- // finished state is handled in onSSOKeyChanged()
- const auto log = NexusSSOLogin::stateToString(s, e);
-
- for (auto&& line : log.split("\n")) {
- addLog(line);
- }
- }
-
- updateState();
-}
-
-void NexusConnectionUI::onValidatorFinished(ValidationAttempt::Result r,
- const QString& message,
- std::optional<APIUserAccount> user)
-{
- if (user) {
- NexusInterface::instance().setUserAccount(*user);
- addLog(tr("Received user account information"));
-
- if (setKey(user->apiKey())) {
- addLog(tr("Linked with Nexus successfully."));
- } else {
- addLog(tr("Failed to set API key"));
- }
- } else {
- if (message.isEmpty()) {
- // shouldn't happen
- addLog("Unknown error");
- } else {
- addLog(message);
- }
- }
-
- updateState();
-}
-
-void NexusConnectionUI::addLog(const QString& s)
-{
- m_log->addItem(s);
- m_log->scrollToBottom();
-}
-
-bool NexusConnectionUI::setKey(const QString& key)
-{
- const bool ret = GlobalSettings::setNexusApiKey(key);
- updateState();
-
- emit keyChanged();
-
- return ret;
-}
-
-bool NexusConnectionUI::clearKey()
-{
- const auto ret = GlobalSettings::clearNexusApiKey();
-
- NexusInterface::instance().getAccessManager()->clearApiKey();
- updateState();
-
- emit keyChanged();
-
- return ret;
-}
-
-void NexusConnectionUI::updateState()
-{
- auto setButton = [&](QAbstractButton* b, bool enabled, QString caption = {}) {
- if (b) {
- b->setEnabled(enabled);
- if (!caption.isEmpty()) {
- b->setText(caption);
- }
- }
- };
-
- if (m_nexusLogin && m_nexusLogin->isActive()) {
- // api key is in the process of being retrieved
- setButton(m_connect, true, QObject::tr("Cancel"));
- setButton(m_disconnect, false);
- setButton(m_manual, false, QObject::tr("Enter API Key Manually"));
- } else if (m_nexusValidator && m_nexusValidator->isActive()) {
- // api key is in the process of being tested
- setButton(m_connect, false, QObject::tr("Connect to Nexus"));
- setButton(m_disconnect, false);
- setButton(m_manual, true, QObject::tr("Cancel"));
- } else if (GlobalSettings::hasNexusApiKey()) {
- // api key is present
- setButton(m_connect, false, QObject::tr("Connect to Nexus"));
- setButton(m_disconnect, true);
- setButton(m_manual, false, QObject::tr("Enter API Key Manually"));
- } else {
- // api key not present
- setButton(m_connect, true, QObject::tr("Connect to Nexus"));
- setButton(m_disconnect, false);
- setButton(m_manual, true, QObject::tr("Enter API Key Manually"));
- }
-
- emit stateChanged();
-}
-
-NexusSettingsTab::NexusSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab(s, d)
-{
- ui->endorsementBox->setChecked(settings().nexus().endorsementIntegration());
- ui->trackedBox->setChecked(settings().nexus().trackedIntegration());
- ui->categoryMappingsBox->setChecked(settings().nexus().categoryMappings());
- ui->hideAPICounterBox->setChecked(settings().interface().hideAPICounter());
-
- // display server preferences
- for (const auto& server : s.network().servers()) {
- QString descriptor = server.name();
-
- if (!descriptor.compare("CDN", Qt::CaseInsensitive)) {
- descriptor += QStringLiteral(" (automatic)");
- }
-
- const auto averageSpeed = server.averageSpeed();
- if (averageSpeed > 0) {
- descriptor += QString(" (%1)").arg(MOBase::localizedByteSpeed(averageSpeed));
- }
-
- QListWidgetItem* newItem = new ServerItem<int>(descriptor, Qt::UserRole + 1);
-
- newItem->setData(Qt::UserRole, server.name());
- newItem->setData(Qt::UserRole + 1, server.preferred());
-
- if (server.preferred() > 0) {
- ui->preferredServersList->addItem(newItem);
- } else {
- ui->knownServersList->addItem(newItem);
- }
-
- ui->preferredServersList->sortItems(Qt::DescendingOrder);
- }
-
- m_connectionUI.reset(new NexusConnectionUI(&dialog(), &settings(), ui->nexusConnect,
- ui->nexusDisconnect, ui->nexusManualKey,
- ui->nexusLog));
-
- QObject::connect(
- m_connectionUI.get(), &NexusConnectionUI::stateChanged, &d,
- [&] {
- updateNexusData();
- },
- Qt::QueuedConnection);
-
- QObject::connect(m_connectionUI.get(), &NexusConnectionUI::keyChanged, &d, [&] {
- dialog().setExitNeeded(Exit::Restart);
- });
-
- QObject::connect(ui->clearCacheButton, &QPushButton::clicked, [&] {
- clearCache();
- });
- QObject::connect(ui->associateButton, &QPushButton::clicked, [&] {
- associate();
- });
- QObject::connect(ui->useCustomBrowser, &QCheckBox::clicked, [&] {
- updateCustomBrowser();
- });
- QObject::connect(ui->browseCustomBrowser, &QPushButton::clicked, [&] {
- browseCustomBrowser();
- });
-
- updateNexusData();
- updateCustomBrowser();
-}
-
-void NexusSettingsTab::update()
-{
- settings().nexus().setEndorsementIntegration(ui->endorsementBox->isChecked());
- settings().nexus().setTrackedIntegration(ui->trackedBox->isChecked());
- settings().nexus().setCategoryMappings(ui->categoryMappingsBox->isChecked());
- settings().interface().setHideAPICounter(ui->hideAPICounterBox->isChecked());
-
- auto servers = settings().network().servers();
-
- // store server preference
- for (int i = 0; i < ui->knownServersList->count(); ++i) {
- const QString key = ui->knownServersList->item(i)->data(Qt::UserRole).toString();
-
- bool found = false;
-
- for (auto& server : servers) {
- if (server.name() == key) {
- server.setPreferred(0);
- found = true;
- break;
- }
- }
-
- if (!found) {
- log::error("while setting preferred to 0, server '{}' not found", key);
- }
- }
-
- const int count = ui->preferredServersList->count();
-
- for (int i = 0; i < count; ++i) {
- const QString key =
- ui->preferredServersList->item(i)->data(Qt::UserRole).toString();
- const int newPreferred = count - i;
-
- bool found = false;
-
- for (auto& server : servers) {
-
- if (server.name() == key) {
- server.setPreferred(newPreferred);
- found = true;
- break;
- }
- }
-
- if (!found) {
- log::error("while setting preference to {}, server '{}' not found", newPreferred,
- key);
- }
- }
-
- settings().network().updateServers(servers);
-}
-
-void NexusSettingsTab::clearCache()
-{
- QDir(Settings::instance().paths().cache()).removeRecursively();
- NexusInterface::instance().clearCache();
-}
-
-void NexusSettingsTab::associate()
-{
- Settings::instance().nexus().registerAsNXMHandler(true);
-}
-
-void NexusSettingsTab::updateNexusData()
-{
- const auto user = NexusInterface::instance().getAPIUserAccount();
-
- if (user.isValid()) {
- ui->nexusUserID->setText(user.id());
- ui->nexusName->setText(user.name());
- ui->nexusAccount->setText(localizedUserAccountType(user.type()));
-
- ui->nexusDailyRequests->setText(QString("%1/%2")
- .arg(user.limits().remainingDailyRequests)
- .arg(user.limits().maxDailyRequests));
-
- ui->nexusHourlyRequests->setText(QString("%1/%2")
- .arg(user.limits().remainingHourlyRequests)
- .arg(user.limits().maxHourlyRequests));
- } else {
- ui->nexusUserID->setText(QObject::tr("N/A"));
- ui->nexusName->setText(QObject::tr("N/A"));
- ui->nexusAccount->setText(QObject::tr("N/A"));
- ui->nexusDailyRequests->setText(QObject::tr("N/A"));
- ui->nexusHourlyRequests->setText(QObject::tr("N/A"));
- }
-}
-
-void NexusSettingsTab::updateCustomBrowser()
-{
- ui->browserCommand->setEnabled(ui->useCustomBrowser->isChecked());
-}
-
-void NexusSettingsTab::browseCustomBrowser()
-{
- const QString Filters =
- QObject::tr("Executables (*.exe)") + ";;" + QObject::tr("All Files (*.*)");
-
- QString file = QFileDialog::getOpenFileName(
- parentWidget(), QObject::tr("Select the browser executable"),
- ui->browserCommand->text(), Filters);
-
- if (file.isNull() || file == "") {
- return;
- }
-
- ui->browserCommand->setText(file + " \"%1\"");
-}
+ } + +private: + int m_SortRole; +}; + +class NexusManualKeyDialog : public QDialog +{ +public: + NexusManualKeyDialog(QWidget* parent) + : QDialog(parent), ui(new Ui::NexusManualKeyDialog) + { + ui->setupUi(this); + ui->key->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + + connect(ui->openBrowser, &QPushButton::clicked, [&] { + openBrowser(); + }); + connect(ui->paste, &QPushButton::clicked, [&] { + paste(); + }); + connect(ui->clear, &QPushButton::clicked, [&] { + clear(); + }); + } + + void accept() override + { + m_key = ui->key->toPlainText(); + QDialog::accept(); + } + + const QString& key() const { return m_key; } + + void openBrowser() + { + shell::Open(QUrl("https://www.nexusmods.com/users/myaccount?tab=api")); + } + + void paste() + { + const auto text = QApplication::clipboard()->text(); + if (!text.isEmpty()) { + ui->key->setPlainText(text); + } + } + + void clear() { ui->key->clear(); } + +private: + std::unique_ptr<Ui::NexusManualKeyDialog> ui; + QString m_key; +}; + +NexusConnectionUI::NexusConnectionUI(QWidget* parent, Settings* s, + QAbstractButton* connectButton, + QAbstractButton* disconnectButton, + QAbstractButton* manualButton, + QListWidget* logList) + : m_parent(parent), m_settings(s), m_connect(connectButton), + m_disconnect(disconnectButton), m_manual(manualButton), m_log(logList) +{ + if (m_connect) { + QObject::connect(m_connect, &QPushButton::clicked, [&] { + connect(); + }); + } + + if (m_disconnect) { + QObject::connect(m_disconnect, &QPushButton::clicked, [&] { + disconnect(); + }); + } + + if (m_manual) { + QObject::connect(manualButton, &QPushButton::clicked, [&] { + manual(); + }); + } + + QObject::connect(NexusInterface::instance().getAccessManager(), + &NXMAccessManager::updateOAuthState, this, + [&](NXMAccessManager::OAuthState state, QString message) { + onOAuthStateChanged(state, message); + }); + + QObject::connect(NexusInterface::instance().getAccessManager(), + &NXMAccessManager::tokensReceived, this, + [&](const NexusOAuthTokens tokens) { + onTokensReceived(tokens); + }); + + if (GlobalSettings::hasNexusOAuthTokens() || GlobalSettings::hasNexusApiKey()) { + addLog(tr("Connected.")); + } else { + addLog(tr("Not connected.")); + } + + updateState(); +} + +void NexusConnectionUI::connect() +{ + if (m_nexusLogin && m_nexusLogin->isActive()) { + m_nexusLogin->cancel(); + return; + } + + if (!m_nexusLogin) { + m_nexusLogin.reset(new NexusOAuthLogin(m_parent)); + } + + m_log->clear(); + m_pendingTokens.reset(); + m_nexusLogin->start(); + updateState(); +} + +void NexusConnectionUI::manual() +{ + if (m_nexusValidator && m_nexusValidator->isActive()) { + m_nexusValidator->cancel(); + return; + } + + NexusManualKeyDialog d(m_parent); + if (d.exec() != QDialog::Accepted) { + return; + } + + const auto key = d.key(); + if (key.isEmpty()) { + clearCredentials(); + return; + } + + m_log->clear(); + auto tokens = NexusInterface::instance().getAccessManager()->tokens(); + if (!tokens) { + tokens = NexusOAuthTokens(); + } + tokens->apiKey = key; + NexusInterface::instance().getAccessManager()->setTokens(*tokens); + m_pendingTokens = tokens; + validateCredentials(*tokens); +} + +void NexusConnectionUI::disconnect() +{ + clearCredentials(); + m_log->clear(); + addLog(tr("Disconnected.")); +} + +void NexusConnectionUI::validateCredentials(const NexusOAuthTokens& tokens) +{ + if (!m_nexusValidator) { + m_nexusValidator.reset(new NexusKeyValidator( + m_settings, *NexusInterface::instance().getAccessManager())); + + m_nexusValidator->finished = [&](auto&& r, auto&& m, auto&& u) { + onValidatorFinished(r, m, u); + }; + } + + addLog(tr("Authorizing with Nexus...")); + m_nexusValidator->start(tokens, NexusKeyValidator::OneShot); +} + +void NexusConnectionUI::onTokensReceived(const NexusOAuthTokens& tokens) +{ + if (GlobalSettings::hasNexusOAuthTokens() || GlobalSettings::hasNexusApiKey()) { + NexusOAuthTokens oldTokens; + GlobalSettings::nexusOAuthTokens(oldTokens); + GlobalSettings::nexusApiKey(oldTokens.apiKey); + NexusOAuthTokens newTokens(tokens); + if (tokens.apiKey.isEmpty()) { + newTokens.apiKey = oldTokens.apiKey; + } + if (tokens.accessToken.isEmpty()) { + newTokens.accessToken = oldTokens.accessToken; + newTokens.refreshToken = oldTokens.refreshToken; + newTokens.tokenType = oldTokens.tokenType; + newTokens.expiresAt = oldTokens.expiresAt; + newTokens.scope = oldTokens.scope; + } + m_pendingTokens = newTokens; + } else { + m_pendingTokens = tokens; + } + addLog(tr("Received authorization from Nexus.")); + validateCredentials(tokens); +} + +void NexusConnectionUI::onOAuthStateChanged(NXMAccessManager::OAuthState s, + const QString& e) +{ + if (s != NXMAccessManager::OAuthState::Finished) { + const auto log = NXMAccessManager::stateToString(s, e); + + for (auto&& line : log.split("\n")) { + addLog(line); + } + } + + updateState(); +} + +void NexusConnectionUI::onValidatorFinished(ValidationAttempt::Result r, + const QString& message, + std::optional<APIUserAccount> user) +{ + Q_UNUSED(r); + if (user) { + NexusInterface::instance().setUserAccount(*user); + addLog(tr("Received user account information")); + + if (m_pendingTokens) { + if (persistTokens(*m_pendingTokens)) { + addLog(tr("Linked with Nexus successfully.")); + } else { + addLog(tr("Failed to store OAuth tokens.")); + } + } else { + addLog(tr("Linked with Nexus successfully.")); + } + } else { + if (message.isEmpty()) { + // shouldn't happen + addLog("Unknown error"); + } else { + addLog(message); + } + } + + m_pendingTokens.reset(); + updateState(); +} + +void NexusConnectionUI::addLog(const QString& s) +{ + m_log->addItem(s); + m_log->scrollToBottom(); +} + +bool NexusConnectionUI::persistTokens(const NexusOAuthTokens& tokens) +{ + const bool ret = GlobalSettings::setNexusOAuthTokens(tokens); + const bool ret2 = GlobalSettings::setNexusApiKey(tokens.apiKey); + if (ret && ret2) { + NexusInterface::instance().getAccessManager()->setTokens(tokens); + } + + updateState(); + + emit keyChanged(); + + return ret && ret2; +} + +bool NexusConnectionUI::clearCredentials() +{ + auto ret = GlobalSettings::clearNexusOAuthTokens(); + auto ret2 = GlobalSettings::clearNexusApiKey(); + + NexusInterface::instance().getAccessManager()->clearCredentials(); + updateState(); + + emit keyChanged(); + + return ret && ret2; +} + +void NexusConnectionUI::updateState() +{ + auto setButton = [&](QAbstractButton* b, bool enabled, QString caption = {}) { + if (b) { + b->setEnabled(enabled); + if (!caption.isEmpty()) { + b->setText(caption); + } + } + }; + + if (m_nexusLogin && m_nexusLogin->isActive()) { + setButton(m_connect, true, QObject::tr("Cancel")); + setButton(m_disconnect, false); + setButton(m_manual, false, QObject::tr("Enter API Key Manually")); + } else if (m_nexusValidator && m_nexusValidator->isActive()) { + setButton(m_connect, false, QObject::tr("Connect to Nexus")); + setButton(m_disconnect, false); + } else if (GlobalSettings::hasNexusOAuthTokens() || + GlobalSettings::hasNexusApiKey()) { + NexusOAuthTokens tokens; + GlobalSettings::nexusOAuthTokens(tokens); + GlobalSettings::nexusApiKey(tokens.apiKey); + if (tokens.accessToken.isEmpty()) { + setButton(m_connect, true, QObject::tr("Connect to Nexus")); + } else { + setButton(m_connect, false, QObject::tr("Connect to Nexus")); + } + setButton(m_disconnect, true); + if (tokens.apiKey.isEmpty()) { + setButton(m_manual, true, QObject::tr("Enter API Key Manually")); + } else { + setButton(m_manual, false, QObject::tr("Enter API Key Manually")); + } + } else { + setButton(m_connect, true, QObject::tr("Connect to Nexus")); + setButton(m_disconnect, false); + setButton(m_manual, true, QObject::tr("Enter API Key Manually")); + } + + emit stateChanged(); +} + +NexusSettingsTab::NexusSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab(s, d) +{ + ui->endorsementBox->setChecked(settings().nexus().endorsementIntegration()); + ui->trackedBox->setChecked(settings().nexus().trackedIntegration()); + ui->categoryMappingsBox->setChecked(settings().nexus().categoryMappings()); + ui->hideAPICounterBox->setChecked(settings().interface().hideAPICounter()); + + // display server preferences + for (const auto& server : s.network().servers()) { + QString descriptor = server.name(); + + if (!descriptor.compare("CDN", Qt::CaseInsensitive)) { + descriptor += QStringLiteral(" (automatic)"); + } + + const auto averageSpeed = server.averageSpeed(); + if (averageSpeed > 0) { + descriptor += QString(" (%1)").arg(MOBase::localizedByteSpeed(averageSpeed)); + } + + QListWidgetItem* newItem = new ServerItem<int>(descriptor, Qt::UserRole + 1); + + newItem->setData(Qt::UserRole, server.name()); + newItem->setData(Qt::UserRole + 1, server.preferred()); + + if (server.preferred() > 0) { + ui->preferredServersList->addItem(newItem); + } else { + ui->knownServersList->addItem(newItem); + } + + ui->preferredServersList->sortItems(Qt::DescendingOrder); + } + + m_connectionUI.reset(new NexusConnectionUI(&dialog(), &settings(), ui->nexusConnect, + ui->nexusDisconnect, ui->nexusManualKey, + ui->nexusLog)); + + QObject::connect( + m_connectionUI.get(), &NexusConnectionUI::stateChanged, &d, + [&] { + updateNexusData(); + }, + Qt::QueuedConnection); + + QObject::connect(m_connectionUI.get(), &NexusConnectionUI::keyChanged, &d, [&] { + dialog().setExitNeeded(Exit::Restart); + }); + + QObject::connect(ui->clearCacheButton, &QPushButton::clicked, [&] { + clearCache(); + }); + QObject::connect(ui->associateButton, &QPushButton::clicked, [&] { + associate(); + }); + QObject::connect(ui->useCustomBrowser, &QCheckBox::clicked, [&] { + updateCustomBrowser(); + }); + QObject::connect(ui->browseCustomBrowser, &QPushButton::clicked, [&] { + browseCustomBrowser(); + }); + + updateNexusData(); + updateCustomBrowser(); +} + +void NexusSettingsTab::update() +{ + settings().nexus().setEndorsementIntegration(ui->endorsementBox->isChecked()); + settings().nexus().setTrackedIntegration(ui->trackedBox->isChecked()); + settings().nexus().setCategoryMappings(ui->categoryMappingsBox->isChecked()); + settings().interface().setHideAPICounter(ui->hideAPICounterBox->isChecked()); + + auto servers = settings().network().servers(); + + // store server preference + for (int i = 0; i < ui->knownServersList->count(); ++i) { + const QString key = ui->knownServersList->item(i)->data(Qt::UserRole).toString(); + + bool found = false; + + for (auto& server : servers) { + if (server.name() == key) { + server.setPreferred(0); + found = true; + break; + } + } + + if (!found) { + log::error("while setting preferred to 0, server '{}' not found", key); + } + } + + const int count = ui->preferredServersList->count(); + + for (int i = 0; i < count; ++i) { + const QString key = + ui->preferredServersList->item(i)->data(Qt::UserRole).toString(); + const int newPreferred = count - i; + + bool found = false; + + for (auto& server : servers) { + + if (server.name() == key) { + server.setPreferred(newPreferred); + found = true; + break; + } + } + + if (!found) { + log::error("while setting preference to {}, server '{}' not found", newPreferred, + key); + } + } + + settings().network().updateServers(servers); +} + +void NexusSettingsTab::clearCache() +{ + QDir(Settings::instance().paths().cache()).removeRecursively(); + NexusInterface::instance().clearCache(); +} + +void NexusSettingsTab::associate() +{ + Settings::instance().nexus().registerAsNXMHandler(true); +} + +void NexusSettingsTab::updateNexusData() +{ + const auto user = NexusInterface::instance().getAPIUserAccount(); + + if (user.isValid()) { + ui->nexusUserID->setText(user.id()); + ui->nexusName->setText(user.name()); + ui->nexusAccount->setText(localizedUserAccountType(user.type())); + + ui->nexusDailyRequests->setText(QString("%1/%2") + .arg(user.limits().remainingDailyRequests) + .arg(user.limits().maxDailyRequests)); + + ui->nexusHourlyRequests->setText(QString("%1/%2") + .arg(user.limits().remainingHourlyRequests) + .arg(user.limits().maxHourlyRequests)); + } else { + ui->nexusUserID->setText(QObject::tr("N/A")); + ui->nexusName->setText(QObject::tr("N/A")); + ui->nexusAccount->setText(QObject::tr("N/A")); + ui->nexusDailyRequests->setText(QObject::tr("N/A")); + ui->nexusHourlyRequests->setText(QObject::tr("N/A")); + } +} + +void NexusSettingsTab::updateCustomBrowser() +{ + ui->browserCommand->setEnabled(ui->useCustomBrowser->isChecked()); +} + +void NexusSettingsTab::browseCustomBrowser() +{ + const QString Filters = + QObject::tr("Executables (*.exe)") + ";;" + QObject::tr("All Files (*.*)"); + + QString file = QFileDialog::getOpenFileName( + parentWidget(), QObject::tr("Select the browser executable"), + ui->browserCommand->text(), Filters); + + if (file.isNull() || file == "") { + return; + } + + ui->browserCommand->setText(file + " \"%1\""); +} diff --git a/src/src/settingsdialognexus.h b/src/src/settingsdialognexus.h index ffeb60c..8b7b56c 100644 --- a/src/src/settingsdialognexus.h +++ b/src/src/settingsdialognexus.h @@ -1,70 +1,78 @@ -#ifndef SETTINGSDIALOGNEXUS_H
-#define SETTINGSDIALOGNEXUS_H
-
-#include "nxmaccessmanager.h"
-#include "settings.h"
-#include "settingsdialog.h"
-
-// used by the settings dialog and the create instance dialog
-//
-class NexusConnectionUI : public QObject
-{
- Q_OBJECT;
-
-public:
- NexusConnectionUI(QWidget* parent, Settings* s, QAbstractButton* connectButton,
- QAbstractButton* disconnectButton, QAbstractButton* manualButton,
- QListWidget* logList);
-
- void connect();
- void manual();
- void disconnect();
-
-signals:
- void stateChanged();
- void keyChanged();
-
-private:
- QWidget* m_parent;
- Settings* m_settings;
- QAbstractButton* m_connect;
- QAbstractButton* m_disconnect;
- QAbstractButton* m_manual;
- QListWidget* m_log;
-
- std::unique_ptr<NexusSSOLogin> m_nexusLogin;
- std::unique_ptr<NexusKeyValidator> m_nexusValidator;
-
- void addLog(const QString& s);
-
- void updateState();
-
- void validateKey(const QString& key);
- bool setKey(const QString& key);
- bool clearKey();
-
- void onSSOKeyChanged(const QString& key);
- void onSSOStateChanged(NexusSSOLogin::States s, const QString& e);
-
- void onValidatorFinished(ValidationAttempt::Result r, const QString& message,
- std::optional<APIUserAccount> useR);
-};
-
-class NexusSettingsTab : public SettingsTab
-{
-public:
- NexusSettingsTab(Settings& settings, SettingsDialog& dialog);
- void update() override;
-
-private:
- std::unique_ptr<NexusConnectionUI> m_connectionUI;
-
- void clearCache();
- void associate();
-
- void updateNexusData();
- void updateCustomBrowser();
- void browseCustomBrowser();
-};
-
-#endif // SETTINGSDIALOGNEXUS_H
+#ifndef SETTINGSDIALOGNEXUS_H +#define SETTINGSDIALOGNEXUS_H + +#include "nexusoauthlogin.h" +#include "nexusoauthtokens.h" +#include "nxmaccessmanager.h" +#include "settings.h" +#include "settingsdialog.h" +#include <memory> +#include <optional> + +class QAbstractButton; +class QListWidget; + +// used by the settings dialog and the create instance dialog +// +class NexusConnectionUI : public QObject +{ + Q_OBJECT; + +public: + NexusConnectionUI(QWidget* parent, Settings* s, QAbstractButton* connectButton, + QAbstractButton* disconnectButton, QAbstractButton* manualButton, + QListWidget* logList); + + void connect(); + void manual(); + void disconnect(); + +signals: + void stateChanged(); + void keyChanged(); + +private: + QWidget* m_parent; + Settings* m_settings; + QAbstractButton* m_connect; + QAbstractButton* m_disconnect; + QAbstractButton* m_manual; + QListWidget* m_log; + + std::unique_ptr<NexusOAuthLogin> m_nexusLogin; + std::unique_ptr<NexusKeyValidator> m_nexusValidator; + std::optional<NexusOAuthTokens> m_pendingTokens; + + void addLog(const QString& s); + + void updateState(); + + void validateCredentials(const NexusOAuthTokens& tokens); + bool persistTokens(const NexusOAuthTokens& tokens); + bool clearCredentials(); + + void onTokensReceived(const NexusOAuthTokens& tokens); + void onOAuthStateChanged(NXMAccessManager::OAuthState s, const QString& message); + + void onValidatorFinished(ValidationAttempt::Result r, const QString& message, + std::optional<APIUserAccount> user); +}; + +class NexusSettingsTab : public SettingsTab +{ +public: + NexusSettingsTab(Settings& settings, SettingsDialog& dialog); + void update() override; + +private: + std::unique_ptr<NexusConnectionUI> m_connectionUI; + + void clearCache(); + void associate(); + + void updateNexusData(); + void updateCustomBrowser(); + void browseCustomBrowser(); +}; + +#endif // SETTINGSDIALOGNEXUS_H diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index ef7d04b..76d4e0e 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -50,6 +50,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <csignal> #include <sys/types.h> +class QMessageBox; + using namespace MOBase; using namespace MOShared; |
