aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-05-01 01:00:10 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-05-01 01:00:10 -0500
commit4bb4fe5ca0e7e75533d908a91f0d7aa086db9e61 (patch)
tree968c71f3b40987379b3c92371b3abc9d3b5abd62
parentaba2dc0df5c50c244904f2b1bb458334a851d426 (diff)
Port upstream PR #2374: Nexus OAuth authentication
Replaces legacy API-key flow with OAuth 2.0 PKCE (public client, client_id "modorganizer2", overridable via MO2_NEXUS_CLIENT_ID env). Backwards-compatible: stored API key is used as fallback if no OAuth token is present. Fixes the symptom users hit after Nexus deprecated personal API keys for download endpoints — Settings showed "Connected." (key still validates) but downloads/CDN list returned empty. Cherry-picked from upstream/dev/oauth-graphql against merge-base 925bade3, with these adjustments for our Linux-only fork: - Dockerfile: aqtinstall +qtnetworkauth module - CMakeLists.txt: find_package + link Qt6::NetworkAuth - Dropped Windows-only hunks: dlls.manifest.qt6{,debug}, pch.h QWebSocket include - Skipped src/CMakeLists.txt and .gitignore upstream hunks (their layout differs from ours) - Skipped organizer_en.ts (regenerable via lupdate) - Skipped tutorials/tutorial_firststeps_settings.js (defer to the tutorials-disable PR port) - Kept our defensive default member initialisers in nxmaccessmanager.h (m_Reply{nullptr}, m_Result{None}, etc.) and `override` on createRequest() - Resolved trivial conflicts in modlistviewactions.cpp by taking upstream's NexusOAuthTokens API while retaining our [=, this] capture style - createinstancedialog.h, instancemanager.cpp: only meaningful PR delta is a comment text change / unrelated drift; ours retained OAuth callback uses local loopback http://127.0.0.1:28635/callback served by QOAuthHttpServerReplyHandler (no firewall punch needed). Token storage piggybacks the existing Linux QSettings INI credential backend (~/.config/ModOrganizer/credentials.ini). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
-rw-r--r--docker/Dockerfile2
-rw-r--r--src/src/CMakeLists.txt3
-rw-r--r--src/src/apiuseraccount.cpp197
-rw-r--r--src/src/apiuseraccount.h288
-rw-r--r--src/src/createinstancedialog.h2
-rw-r--r--src/src/createinstancedialogpages.cpp3
-rw-r--r--src/src/moapplication.cpp7
-rw-r--r--src/src/modlistviewactions.cpp14
-rw-r--r--src/src/nexusinterface.cpp67
-rw-r--r--src/src/nexusoauthconfig.cpp62
-rw-r--r--src/src/nexusoauthconfig.h34
-rw-r--r--src/src/nexusoauthlogin.cpp78
-rw-r--r--src/src/nexusoauthlogin.h59
-rw-r--r--src/src/nexusoauthtokens.h98
-rw-r--r--src/src/nxmaccessmanager.cpp799
-rw-r--r--src/src/nxmaccessmanager.h567
-rw-r--r--src/src/organizercore.cpp16
-rw-r--r--src/src/plugincontainer.cpp18
-rw-r--r--src/src/settings.cpp63
-rw-r--r--src/src/settings.h19
-rw-r--r--src/src/settingsdialog.ui2
-rw-r--r--src/src/settingsdialognexus.cpp983
-rw-r--r--src/src/settingsdialognexus.h148
-rw-r--r--src/src/spawn.cpp2
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;