diff --git a/.gitignore b/.gitignore
index 935c42609..d1ac808a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,9 +8,12 @@ src/*.bak
CMakeLists.txt.user
edit
/CMakeFiles
-.idea
+.idea/*
+!.idea/filetypes/
+!.idea/filetypes/qt-translations.xml
/msbuild.log
/*std*.log
/*build
/src/version.aps
+.idea/
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 8b425272b..698ad03d5 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -12,7 +12,7 @@ find_package(mo2-esptk CONFIG REQUIRED)
find_package(mo2-dds-header CONFIG REQUIRED)
find_package(mo2-libbsarch CONFIG REQUIRED)
-find_package(Qt6 REQUIRED COMPONENTS WebEngineWidgets WebSockets)
+find_package(Qt6 REQUIRED COMPONENTS WebEngineWidgets WebSockets NetworkAuth)
find_package(Boost CONFIG REQUIRED COMPONENTS program_options thread interprocess signals2 uuid accumulators)
find_package(7zip CONFIG REQUIRED)
find_package(lz4 CONFIG REQUIRED)
@@ -41,7 +41,7 @@ target_link_libraries(organizer PRIVATE
usvfs::usvfs mo2::uibase mo2::archive mo2::libbsarch
mo2::bsatk mo2::esptk mo2::lootcli-header
Boost::program_options Boost::signals2 Boost::uuid Boost::accumulators
- Qt6::WebEngineWidgets Qt6::WebSockets Version Dbghelp)
+ Qt6::WebEngineWidgets Qt6::WebSockets Qt6::NetworkAuth Version Dbghelp)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/dlls.manifest.qt6"
DESTINATION ${_bin}/dlls
@@ -130,6 +130,9 @@ mo2_add_filter(NAME src/core GROUPS
githubpp
installationmanager
nexusinterface
+ nexusoauthlogin
+ nexusoauthtokens
+ nexusoauthconfig
nxmaccessmanager
organizercore
game_features
diff --git a/src/apiuseraccount.cpp b/src/apiuseraccount.cpp
index 4b908df0c..e9785b404 100644
--- a/src/apiuseraccount.cpp
+++ b/src/apiuseraccount.cpp
@@ -19,12 +19,17 @@ APIUserAccount::APIUserAccount() : m_type(APIUserAccountTypes::None) {}
bool APIUserAccount::isValid() const
{
- return !m_key.isEmpty();
+ return !m_accessToken.isEmpty() || !m_apiKey.isEmpty();
+}
+
+const QString& APIUserAccount::accessToken() const
+{
+ return m_accessToken;
}
const QString& APIUserAccount::apiKey() const
{
- return m_key;
+ return m_apiKey;
}
const QString& APIUserAccount::id() const
@@ -47,9 +52,15 @@ const APILimits& APIUserAccount::limits() const
return m_limits;
}
-APIUserAccount& APIUserAccount::apiKey(const QString& key)
+APIUserAccount& APIUserAccount::accessToken(const QString& token)
+{
+ m_accessToken = token;
+ return *this;
+}
+
+APIUserAccount& APIUserAccount::apiKey(const QString& apiKey)
{
- m_key = key;
+ m_apiKey = apiKey;
return *this;
}
diff --git a/src/apiuseraccount.h b/src/apiuseraccount.h
index 829654a54..628337f85 100644
--- a/src/apiuseraccount.h
+++ b/src/apiuseraccount.h
@@ -65,7 +65,12 @@ class APIUserAccount
bool isValid() const;
/**
- * api key
+ * OAuth access token
+ */
+ const QString& accessToken() const;
+
+ /**
+ * OAuth access token
*/
const QString& apiKey() const;
@@ -90,9 +95,14 @@ class APIUserAccount
const APILimits& limits() const;
/**
- * sets the api key
+ * sets the OAuth access token
+ */
+ APIUserAccount& accessToken(const QString& token);
+
+ /**
+ * sets the OAuth access token
*/
- APIUserAccount& apiKey(const QString& key);
+ APIUserAccount& apiKey(const QString& apiKey);
/**
* sets the user id
@@ -132,7 +142,7 @@ class APIUserAccount
bool exhausted() const;
private:
- QString m_key, m_id, m_name;
+ QString m_accessToken, m_apiKey, m_id, m_name;
APIUserAccountTypes m_type;
APILimits m_limits;
};
diff --git a/src/createinstancedialog.h b/src/createinstancedialog.h
index 4495cc78e..270f347f0 100644
--- a/src/createinstancedialog.h
+++ b/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/createinstancedialogpages.cpp b/src/createinstancedialogpages.cpp
index 66f4c31cc..ac4103ea5 100644
--- a/src/createinstancedialogpages.cpp
+++ b/src/createinstancedialogpages.cpp
@@ -103,6 +103,7 @@ void Page::next()
bool Page::action(CreateInstanceDialog::Actions a)
{
+ Q_UNUSED(a);
// no-op
return false;
}
@@ -1203,7 +1204,7 @@ NexusPage::NexusPage(CreateInstanceDialog& dlg) : Page(dlg), m_skip(false)
// 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/dlls.manifest.debug.qt6 b/src/dlls.manifest.debug.qt6
index e216a6fc0..eb0cf2845 100644
--- a/src/dlls.manifest.debug.qt6
+++ b/src/dlls.manifest.debug.qt6
@@ -12,6 +12,7 @@
+
diff --git a/src/dlls.manifest.qt6 b/src/dlls.manifest.qt6
index 8f3ba1359..4702d1117 100644
--- a/src/dlls.manifest.qt6
+++ b/src/dlls.manifest.qt6
@@ -12,6 +12,7 @@
+
diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp
index fcb7bf676..54da7f9ac 100644
--- a/src/instancemanager.cpp
+++ b/src/instancemanager.cpp
@@ -53,8 +53,10 @@ QString Instance::displayName() const
{
if (isPortable())
return QObject::tr("Portable");
- else
- return QDir(m_dir).dirName();
+ else {
+ QDir instanceDir(m_dir.toUtf8());
+ return instanceDir.dirName();
+ }
}
QString Instance::gameName() const
diff --git a/src/moapplication.cpp b/src/moapplication.cpp
index e676436af..a8900acb8 100644
--- a/src/moapplication.cpp
+++ b/src/moapplication.cpp
@@ -319,9 +319,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/modlistviewactions.cpp b/src/modlistviewactions.cpp
index 6b26a1e33..df2432968 100644
--- a/src/modlistviewactions.cpp
+++ b/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([=]() {
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([=]() {
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/nexusinterface.cpp b/src/nexusinterface.cpp
index 1486a59c1..0f9318edd 100644
--- a/src/nexusinterface.cpp
+++ b/src/nexusinterface.cpp
@@ -985,33 +985,54 @@ void NexusInterface::nextRequest()
} else {
url = info.m_URL;
}
+
+ 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);
- 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);
+ 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/nexusoauthconfig.cpp b/src/nexusoauthconfig.cpp
new file mode 100644
index 000000000..370e0d6bf
--- /dev/null
+++ b/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 .
+*/
+
+#include "nexusoauthconfig.h"
+#include
+
+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/nexusoauthconfig.h b/src/nexusoauthconfig.h
new file mode 100644
index 000000000..e0ccd6312
--- /dev/null
+++ b/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 .
+*/
+
+#ifndef NEXUSOAUTHCONFIG_H
+#define NEXUSOAUTHCONFIG_H
+
+#include
+
+namespace NexusOAuth
+{
+QString clientId();
+QString redirectUri();
+quint16 redirectPort();
+QString authorizeUrl();
+QString tokenUrl();
+} // namespace NexusOAuth
+
+#endif // NEXUSOAUTHCONFIG_H
diff --git a/src/nexusoauthlogin.cpp b/src/nexusoauthlogin.cpp
new file mode 100644
index 000000000..7d13642d6
--- /dev/null
+++ b/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 .
+*/
+
+#include "nexusoauthlogin.h"
+#include "nexusinterface.h"
+#include "nexusoauthconfig.h"
+#include "nxmaccessmanager.h"
+#include "utility.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+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/nexusoauthlogin.h b/src/nexusoauthlogin.h
new file mode 100644
index 000000000..66b6b392e
--- /dev/null
+++ b/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 .
+*/
+
+#ifndef NEXUSOAUTHLOGIN_H
+#define NEXUSOAUTHLOGIN_H
+
+#include "nexusoauthtokens.h"
+#include "nxmaccessmanager.h"
+#include
+#include
+#include
+#include
+
+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 tokensReceived;
+ std::function stateChanged;
+
+private slots:
+ void authorizationEnded();
+
+private:
+ std::unique_ptr m_flow;
+ std::unique_ptr m_replyHandler;
+ bool m_active;
+
+ QByteArray m_codeVerifier;
+};
+
+#endif // NEXUSOAUTHLOGIN_H
diff --git a/src/nexusoauthtokens.h b/src/nexusoauthtokens.h
new file mode 100644
index 000000000..78f3f22c2
--- /dev/null
+++ b/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 .
+*/
+
+#ifndef NEXUSOAUTHTOKENS_H
+#define NEXUSOAUTHTOKENS_H
+
+#include
+#include
+#include
+#include
+
+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 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/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp
index 3efdcda01..18d83e145 100644
--- a/src/nxmaccessmanager.cpp
+++ b/src/nxmaccessmanager.cpp
@@ -20,6 +20,8 @@ along with Mod Organizer. If not, see .
#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 .
#include "utility.h"
#include
#include
+#include
#include
#include
#include
@@ -42,13 +45,11 @@ along with Mod Organizer. If not, see .
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_updateTimer(nullptr), m_first(true)
+ : 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,260 +156,62 @@ void ValidationProgressDialog::updateProgress()
}
}
-NexusSSOLogin::NexusSSOLogin() : m_keyReceived(false), m_active(false)
+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();
- });
-
- QObject::connect(&m_socket,
- qOverload(&QWebSocket::error),
- [&](auto&& e) {
- onError(e);
- });
-
- QObject::connect(&m_socket, &QWebSocket::sslErrors, [&](auto&& errors) {
- onSslErrors(errors);
- });
-
- QObject::connect(&m_socket, &QWebSocket::textMessageReceived, [&](auto&& s) {
- onMessage(s);
- });
+ m_Timeout.setSingleShot(true);
+ m_Timeout.setInterval(timeout);
- 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)
-{
- if (stateChanged) {
- stateChanged(s, error);
- }
-}
-
-void NexusSSOLogin::onConnected()
-{
- setState(WaitingForToken);
-
- m_keyReceived = false;
-
- boost::uuids::random_generator generator;
- boost::uuids::uuid 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
-
- // 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);
- }
-}
+ m_Elapsed.start();
+ m_Timeout.start();
-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& 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_reply(nullptr), m_result(None)
-{
- 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);
});
@@ -415,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();
@@ -431,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(
- 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"));
@@ -471,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()) {
@@ -515,30 +320,60 @@ 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& errors)
@@ -566,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();
@@ -579,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);
@@ -589,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()
@@ -609,21 +444,21 @@ NexusKeyValidator::~NexusKeyValidator()
std::vector 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();
@@ -645,10 +480,10 @@ void NexusKeyValidator::start(const QString& key, Behaviour b)
void NexusKeyValidator::createAttempts(
const std::vector& timeouts)
{
- m_attempts.clear();
+ m_Attempts.clear();
for (auto&& t : timeouts) {
- m_attempts.push_back(std::make_unique(t));
+ m_Attempts.push_back(std::make_unique(t));
}
}
@@ -656,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;
}
@@ -676,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 {
@@ -689,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();
}
@@ -700,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);
@@ -709,7 +549,7 @@ bool NexusKeyValidator::nextTry()
onAttemptFailure(*a);
};
- a->start(m_manager, m_key);
+ a->start(m_Manager, *m_Tokens);
return true;
}
}
@@ -759,6 +599,7 @@ void NexusKeyValidator::onAttemptFailure(const ValidationAttempt& a)
void NexusKeyValidator::setFinished(ValidationAttempt::Result r, const QString& message,
std::optional user)
{
+ m_Attempts.clear();
if (finished) {
finished(r, message, user);
}
@@ -767,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_validationState(NotChecked)
-{
- m_validator.finished = [&](auto&& r, auto&& m, auto&& u) {
+ m_Validator(s, *this), m_ValidationState(NotChecked)
+{
+ 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);
};
@@ -791,7 +687,7 @@ void NXMAccessManager::setTopLevelWidget(QWidget* w)
}
} else {
m_ProgressDialog.reset();
- m_validator.cancel();
+ m_Validator.cancel();
}
}
@@ -818,7 +714,7 @@ NXMAccessManager::createRequest(QNetworkAccessManager::Operation operation,
void NXMAccessManager::showCookies() const
{
- QUrl url(NexusBaseUrl + "/");
+ QUrl url(NexusV1BaseUrl + "/");
for (const QNetworkCookie& cookie : cookieJar()->cookiesForUrl(url)) {
log::debug("{} - {} (expires: {})", cookie.name().constData(),
cookie.value().constData(), cookie.expirationDate().toString());
@@ -835,10 +731,115 @@ void NXMAccessManager::clearCookies()
}
}
-void NXMAccessManager::startValidationCheck(const QString& key)
+void NXMAccessManager::setTokens(const NexusOAuthTokens& tokens)
+{
+ m_Tokens = tokens;
+}
+
+std::optional 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)
{
- m_validationState = NotChecked;
- m_validator.start(key, NexusKeyValidator::Retry);
+ 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(tokens, NexusKeyValidator::Retry);
if (m_ProgressDialog) {
// don't show the progress dialog on startup for the first attempt; the
@@ -854,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);
}
}
@@ -890,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(this)->startProgress();
}
@@ -903,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::apiCheck(const QString& apiKey, bool force)
+void NXMAccessManager::connectOrRefresh(const NexusOAuthTokens tokens)
{
- if (m_validator.isActive()) {
+ 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 scope = {"openid", "profile", "email"};
+ m_NexusOAuth->setRequestedScopeTokens(scope);
+ m_NexusOAuthReplyHandler->close();
+ m_NexusOAuthReplyHandler->setCallbackPath(QUrl(NexusOAuth::redirectUri()).path());
+ m_NexusOAuthReplyHandler->setCallbackText(
+ QObject::tr("
Mod Organizer
Authorization complete. You "
+ "may close this "
+ "window.
"));
+ 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());
+}
+
+QNetworkReply* NXMAccessManager::makeOAuthGetRequest(const QUrl url)
+{
+ 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
@@ -964,16 +1083,30 @@ QString NXMAccessManager::userAgent(const QString& subModule) const
.arg(m_MOVersion, comments.join("; "), qVersion());
}
-void NXMAccessManager::clearApiKey()
-{
- m_validator.cancel();
+void NXMAccessManager::clearCredentials()
+{
+ 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/nxmaccessmanager.h b/src/nxmaccessmanager.h
index 79371a457..ca96deb8a 100644
--- a/src/nxmaccessmanager.h
+++ b/src/nxmaccessmanager.h
@@ -21,6 +21,7 @@ along with Mod Organizer. If not, see .
#define NXMACCESSMANAGER_H
#include "apiuseraccount.h"
+#include "nexusoauthtokens.h"
#include "ui_validationprogressdialog.h"
#include
#include
@@ -28,7 +29,9 @@ along with Mod Organizer. If not, see .
#include
#include
#include
-#include
+#include
+#include
+#include
#include
namespace MOBase
@@ -38,53 +41,6 @@ class IPluginGame;
class NXMAccessManager;
class Settings;
-class NexusSSOLogin
-{
-public:
- enum States
- {
- ConnectingToSSO,
- WaitingForToken,
- WaitingForBrowser,
- Finished,
- Timeout,
- ClosedByRemote,
- Cancelled,
- Error
- };
-
- std::function keyChanged;
- std::function 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;
- bool m_active;
- QTimer m_timeout;
-
- void setState(States s, const QString& error = {});
-
- void close();
- void abort();
-
- void onConnected();
- void onMessage(const QString& s);
- void onDisconnected();
- void onError(QAbstractSocket::SocketError e);
- void onSslErrors(const QList& errors);
- void onTimeout();
-};
-
class ValidationAttempt
{
public:
@@ -104,7 +60,7 @@ class ValidationAttempt
ValidationAttempt(const ValidationAttempt&) = delete;
ValidationAttempt& operator=(const ValidationAttempt&) = delete;
- void start(NXMAccessManager& m, const QString& key);
+ void start(NXMAccessManager& m, const NexusOAuthTokens& tokens);
void cancel();
bool done() const;
@@ -114,13 +70,14 @@ class ValidationAttempt
QElapsedTimer elapsed() const;
private:
- QNetworkReply* m_reply;
- Result m_result;
- QString m_message;
- QTimer m_timeout;
- QElapsedTimer m_elapsed;
+ QNetworkReply* m_Reply;
+ Result m_Result;
+ QString m_Message;
+ QTimer m_Timeout;
+ QElapsedTimer m_Elapsed;
+ NexusOAuthTokens m_Tokens;
- bool sendRequest(NXMAccessManager& m, const QString& key);
+ bool sendRequest(NXMAccessManager& m, const NexusOAuthTokens& tokens);
void onFinished();
void onSslErrors(const QList& errors);
@@ -150,7 +107,7 @@ class NexusKeyValidator
NexusKeyValidator(Settings* s, NXMAccessManager& am);
~NexusKeyValidator();
- void start(const QString& key, Behaviour b);
+ void start(const NexusOAuthTokens& tokens, Behaviour b);
void cancel();
bool isActive() const;
@@ -158,10 +115,10 @@ class NexusKeyValidator
const ValidationAttempt* currentAttempt() const;
private:
- Settings* m_settings;
- NXMAccessManager& m_manager;
- QString m_key;
- std::vector> m_attempts;
+ Settings* m_Settings;
+ NXMAccessManager& m_Manager;
+ std::optional m_Tokens;
+ std::vector> m_Attempts;
void createAttempts(const std::vector& timeouts);
std::vector getTimeouts() const;
@@ -192,10 +149,10 @@ class ValidationProgressDialog : public QDialog
private:
std::unique_ptr ui;
- Settings* m_settings;
- NexusKeyValidator& m_validator;
- QTimer* m_updateTimer;
- bool m_first;
+ Settings* m_Settings;
+ NexusKeyValidator& m_Validator;
+ QTimer* m_UpdateTimer;
+ bool m_First;
void onHide();
void onCancel();
@@ -210,6 +167,17 @@ 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);
@@ -219,7 +187,22 @@ class NXMAccessManager : public QNetworkAccessManager
bool validateAttempted() const;
bool validateWaiting() const;
- void apiCheck(const QString& apiKey, bool force = false);
+ 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 tokens() const;
void showCookies() const;
@@ -228,7 +211,7 @@ class NXMAccessManager : public QNetworkAccessManager
QString userAgent(const QString& subModule = QString()) const;
const QString& MOVersion() const;
- void clearApiKey();
+ void clearCredentials();
void refuseValidation();
@@ -250,6 +233,9 @@ class NXMAccessManager : public QNetworkAccessManager
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:
virtual QNetworkReply* createRequest(QNetworkAccessManager::Operation operation,
@@ -268,10 +254,18 @@ class NXMAccessManager : public QNetworkAccessManager
Settings* m_Settings;
mutable std::unique_ptr m_ProgressDialog;
QString m_MOVersion;
- NexusKeyValidator m_validator;
- States m_validationState;
+ NexusKeyValidator m_Validator;
+ States m_ValidationState;
+ std::optional m_Tokens;
+ std::unique_ptr m_NexusOAuth;
+ std::unique_ptr m_NexusOAuthReplyHandler;
+
+ void handleOAuthError(const QString& message);
- void startValidationCheck(const QString& key);
+ void setOAuthState(OAuthState state, const QString& message = {});
+ void notifyTokens();
+
+ void startValidationCheck(const NexusOAuthTokens& tokens);
void onValidatorFinished(ValidationAttempt::Result r, const QString& message,
std::optional);
@@ -280,6 +274,9 @@ class NXMAccessManager : public QNetworkAccessManager
void startProgress();
void stopProgress();
+
+private slots:
+ void saveRefreshedTokens(const NexusOAuthTokens tokens);
};
#endif // NXMACCESSMANAGER_H
diff --git a/src/organizer_en.ts b/src/organizer_en.ts
index b4d852907..abcf2fd2f 100644
--- a/src/organizer_en.ts
+++ b/src/organizer_en.ts
@@ -710,42 +710,37 @@ p, li { white-space: pre-wrap; }
-
- Enter API Key Manually
-
-
-
-
+ <h3>Confirmation</h3>
-
+ The instance is about to be created. Review the information below and press 'Finish'.
-
+ Instance creation log
-
+ Launch the new instance
-
+ < Back
-
+ Next >
-
+ Cancel
@@ -1008,145 +1003,150 @@ p, li { white-space: pre-wrap; }
+ Visit the uploader's profile
+
+
+
+ Open File
-
+ Open Meta File
-
-
-
+
+
+ Reveal in Explorer
-
-
+
+ Delete...
-
+ Un-Hide
-
+ Hide
-
-
+
+ Cancel
-
+ Pause
-
+ Resume
-
+ Delete Installed Downloads...
-
+ Delete Uninstalled Downloads...
-
+ Delete All Downloads...
-
+ Hide Installed...
-
+ Hide Uninstalled...
-
+ Hide All...
-
+ Un-Hide All...
-
+ Delete download
-
+ Move to the Recycle Bin
-
-
-
+
+
+ Delete Files?
-
+ This will remove all finished downloads from this list and from disk.
Are you absolutely sure you want to proceed?
-
+ This will remove all installed downloads from this list and from disk.
Are you absolutely sure you want to proceed?
-
+ This will remove all uninstalled downloads from this list and from disk.
Are you absolutely sure you want to proceed?
-
-
-
+
+
+ Hide Files?
-
+ This will remove all finished downloads from this list (but NOT from disk).
-
+ This will remove all installed downloads from this list (but NOT from disk).
-
+ This will remove all uninstalled downloads from this list (but NOT from disk).
@@ -1154,22 +1154,22 @@ Are you absolutely sure you want to proceed?
DownloadManager
-
+ failed to rename "%1" to "%2"
-
+ Memory allocation error (in refreshing directory).
-
+ Query Metadata
-
+ There are %1 downloads with incomplete metadata.
Do you want to fetch all incomplete metadata?
@@ -1177,32 +1177,32 @@ API requests will be consumed, and Mod Organizer may stutter.
-
+ failed to download %1: could not open output file: %2
-
+ Download again?
-
+ A file with the same name "%1" has already been downloaded. Do you want to download it again? The new file will receive a different name.
-
+ Wrong Game
-
+ The download link is for a mod for "%1" but this instance of MO has been set up for "%2".
-
+ There is already a download queued for this file.
Mod %1
@@ -1210,12 +1210,12 @@ File %2
-
+ Already Queued
-
+ There is already a download started for this file.
Mod %1: %2
@@ -1223,287 +1223,297 @@ File %3: %4
-
+ Already Started
-
-
+
+ remove: invalid download index %1
-
+ failed to delete %1
-
+ failed to delete meta file for %1
-
+ restore: invalid download index: %1
-
+ cancel: invalid download index %1
-
+ pause: invalid download index %1
-
+ resume: invalid download index %1
-
+ resume (int): invalid download index %1
-
+ No known download urls. Sorry, this download can't be resumed.
-
-
+
+ query: invalid download index %1
-
+ Please enter the Nexus mod ID
-
+ Mod ID:
-
+ Please select the source game code for %1
-
+ Hashing download file '%1'
-
+ Cancel
-
+ VisitNexus: invalid download index %1
-
+ Nexus ID for this Mod is unknown
-
+
+ VisitUploaderProfile: invalid download index %1
+
+
+
+
+ Uploader for this Mod is unknown
+
+
+
+ OpenFile: invalid download index %1
-
+ OpenFileInDownloadsFolder: invalid download index %1
-
+ get pending: invalid download index %1
-
+ get path: invalid download index %1
-
+ Main
-
+ Update
-
+ Optional
-
+ Old
-
+ Miscellaneous
-
+ Deleted
-
+ Archived
-
+ Unknown
-
+ display name: invalid download index %1
-
+ file name: invalid download index %1
-
+ file time: invalid download index %1
-
+ file size: invalid download index %1
-
+ progress: invalid download index %1
-
+ state: invalid download index %1
-
+ infocomplete: invalid download index %1
-
-
-
+
+
+ mod id: invalid download index %1
-
+ ishidden: invalid download index %1
-
+ file info: invalid download index %1
-
+ mark installed: invalid download index %1
-
+ mark uninstalled: invalid download index %1
-
+ %1% - %2 - ~%3
-
+ Memory allocation error (in processing progress event).
-
+ Memory allocation error (in processing downloaded data).
-
+ Information updated
-
-
+
+ No matching file found on Nexus! Maybe this file is no longer available or it was renamed?
-
+ No file on Nexus matches the selected file by name. Please manually choose the correct one.
-
+ No download server available. Please try again later.
-
+ Failed to request file info from nexus: %1
-
+ Warning: Content type is: %1
-
+ Download header content length: %1 downloaded file size: %2
-
+ Download failed: %1 (%2)
-
+ We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers.
-
+ failed to re-open %1
-
+ Unable to write download to drive (return %1).
Check the drive's available storage.
@@ -1514,12 +1524,12 @@ Canceling download "%2"...DownloadsTab
-
+ Query Metadata
-
+ Cannot query metadata while offline mode is enabled. Do you want to disable offline mode?
@@ -3896,120 +3906,120 @@ You will have to visit the mod page on the %1 Nexus site to change your mind.
-
+ Thank you!
-
+ Thank you for your endorsement!
-
+ Mod ID %1 no longer seems to be available on Nexus.
-
+ Error %1: Request to Nexus failed: %2
-
-
+
+ failed to read %1: %2
-
+ Error
-
+ failed to extract %1 (errorcode %2)
-
+ Extract BSA
-
+ This archive contains invalid hashes. Some files may be broken.
-
+ Extract...
-
+ Remove '%1' from the toolbar
-
+ Backup of load order created
-
+ Choose backup to restore
-
+ This file might be left over following a crash or power loss event. Check its contents before restoring.
-
+ No Backups
-
+ There are no backups to restore
-
-
+
+ Restore failed
-
-
+
+ Failed to restore the backup. Errorcode: %1
-
+ Backup of mod list created
-
+ A file with the same name has already been downloaded. What would you like to do?
-
+ Overwrite
-
+ Rename new file
-
+ Ignore file
@@ -4478,12 +4488,12 @@ p, li { white-space: pre-wrap; }
ModInfoRegular
-
+ %1 contains no esp/esm/esl and no asset (textures, meshes, interface, ...) directory
-
+ Categories: <br>
@@ -4584,178 +4594,198 @@ p, li { white-space: pre-wrap; }
-
+ invalid
-
+ installed version: "%1", newest version: "%2"
-
+ The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn (i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. Either way you may want to "upgrade".
-
+ This file has been marked as "Old". There is most likely an updated version of this file available.
-
+ This file has been marked as "Deleted"! You may want to check for an update or remove the nexus ID from this mod!
-
+ %1 minute(s) and %2 second(s)
-
+ This mod will be available to check in %2.
-
+ Categories: <br>
-
+ Invalid name
-
+ Name is already in use by another mod
-
+ Confirm
-
+ Are you sure you want to remove "%1"?
-
+ Conflicts
-
+ Flags
-
+ Content
-
+ Mod Name
-
+ Version
-
+ Priority
-
+ Category
-
+
+ Author
+
+
+
+
+ Uploader
+
+
+
+ Source Game
-
+ Nexus ID
-
+ Installation
-
+ Notes
-
-
+
+ unknown
-
+ Name of your mods
-
+ Version of the mod (if available)
-
+ Installation priority of your mod. The higher, the more "important" it is and thus overwrites files from mods with lower priority.
-
+ Primary category of the mod.
-
+
+ Author(s) of the mod.
+
+
+
+
+ Uploader of the mod. This is not necessarily the same as the author.
+
+
+
+ The source game which was the origin of this mod.
-
+ Id of the mod as used on Nexus.
-
+ Indicators of file conflicts between mods.
-
+ Emblems to highlight things that might require attention.
-
+ Depicts the content of the mod:
-
+ Time this mod was installed
-
+ User notes about the mod
@@ -4852,8 +4882,8 @@ p, li { white-space: pre-wrap; }
-
-
+
+ Open in Explorer
@@ -4869,13 +4899,13 @@ p, li { white-space: pre-wrap; }
-
+ Select Color...
-
+ Reset Color
@@ -4891,121 +4921,127 @@ p, li { white-space: pre-wrap; }
-
+ Ignore missing data
-
+ Mark as converted/working
-
+ Visit on Nexus
-
-
+
+
+ Visit the uploader's profile
+
+
+
+
+ Visit on %1
-
+ Change versioning scheme
-
+ Force-check updates
-
+ Un-ignore update
-
+ Ignore update
-
+ Enable selected
-
+ Disable selected
-
+ Rename Mod...
-
+ Reinstall Mod
-
+ Remove Mod...
-
+ Create Backup
-
+ Restore hidden files
-
+ Un-Endorse
-
-
+
+ Endorse
-
+ Won't endorse
-
+ Endorsement state unknown
-
+ Remap Category (From Nexus)
-
+ Start tracking
-
+ Stop tracking
-
+ Tracked state unknown
@@ -5124,7 +5160,7 @@ p, li { white-space: pre-wrap; }
ModListSortProxy
-
+ Drag&Drop is only supported when sorting by priority
@@ -5153,17 +5189,17 @@ Please enter the name:
-
+ Exception:
-
+ Unknown exception
-
+ <Multiple>
@@ -5182,7 +5218,7 @@ Please enter the name:
-
+ Create Mod...
@@ -5194,7 +5230,7 @@ Please enter a name:
-
+ A mod with this name already exists
@@ -5226,8 +5262,8 @@ Please enter a name:
-
-
+
+ Confirm
@@ -5239,8 +5275,8 @@ Please enter a name:
-
-
+
+ Are you sure?
@@ -5317,178 +5353,197 @@ You can also use online editors and converters instead.
- Nexus_ID
+ Mod_Author
- Mod_Nexus_URL
+ Mod_Uploader
- Mod_Version
+ Nexus_ID
- Install_Date
+ Mod_Nexus_URL
+ Mod_Uploader_URL
+
+
+
+
+ Mod_Version
+
+
+
+
+ Install_Date
+
+
+
+ Download_File_Name
-
+ export failed: %1
-
+ Failed to display overwrite dialog: %1
-
+ Set Priority
-
+ Set the priority of the selected mods
-
+ failed to rename mod: %1
-
+ Remove the following mods?<br><ul>%1</ul>
-
+ failed to remove mod: %1
-
+ Continue?
-
+ The versioning scheme decides which version is considered newer than another.
This function will guess the versioning scheme under the assumption that the installed version is outdated.
-
+ Sorry
-
+ I don't know a versioning scheme where %1 is newer than %2.
-
- Opening Nexus Links
+
+ Nexus Links
-
- You are trying to open %1 links to Nexus Mods. Are you sure you want to do this?
+
+
+ Web Pages
-
-
- Opening Web Pages
+
+ Uploader Profiles
-
-
- You are trying to open %1 Web Pages. Are you sure you want to do this?
+
+ Opening %1
-
-
-
+
+ You are trying to open %1 %2. Are you sure you want to do this?
+
+
+
+
+
+ Failed
-
+ Installation file no longer exists
-
+ Mods installed with old versions of MO can't be reinstalled in this way.
-
+ Failed to create backup.
-
+ Restore all hidden files in the following mods?<br><ul>%1</ul>
-
+ About to restore all hidden files in:
-
+ Endorsing multiple mods will take a while. Please wait...
-
+ Overwrite?
-
+ This will replace the existing mod "%1". Continue?
-
+ failed to remove mod "%1"
-
+ failed to rename "%1" to "%2"
-
+ Move successful.
-
+ This will move all files from overwrite into a new, regular mod.
Please enter a name:
-
+ About to recursively delete:
@@ -5510,122 +5565,80 @@ Please enter a name:
NexusConnectionUI
-
+ Connected.
-
+ Not connected.
-
+ Disconnected.
-
- Checking API key...
+
+ Authorizing with Nexus...
-
- Received API key.
+
+ Received authorization from Nexus.
-
+ Received user account information
-
+
+ Linked with Nexus successfully.
-
- Failed to set API key
+
+ Failed to store OAuth tokens.NexusInterface
-
+ Please pick the mod ID for "%1"
-
+ You must authorize MO2 in Settings -> Nexus to use the Nexus API.
-
+ You've exceeded the Nexus API rate limit and requests are now being throttled. Your next batch of requests will be available in approximately %1 minutes and %2 seconds.
-
+ Aborting download: Either you clicked on a premium-only link and your account is not premium, or the download link was generated by a different account than the one stored in Mod Organizer.
-
+ empty response
-
+ invalid response
-
- NexusManualKeyDialog
-
-
- Manual Nexus API Key
-
-
-
-
- 1. Get your personal API key
-
-
-
-
- Open Browser
-
-
-
-
- 2. Enter your personal API key
-
-
-
-
- Paste
-
-
-
-
- Clear
-
-
-
-
- Enter API key here
-
-
-
-
- <b>Sharing this key with anyone could get your Nexus account banned. You can always revoke this key from your account on the Nexus website.</b>
-
-
-NexusTab
@@ -5996,48 +6009,93 @@ Continue?
PluginContainer
-
+
+ Plugin
+
+
+
+
+ Diagnose
+
+
+
+
+ Game
+
+
+
+
+ Installer
+
+
+
+
+ Mod Page
+
+
+
+
+ Preview
+
+
+
+
+ Tool
+
+
+
+
+ Proxy
+
+
+
+
+ File Mapper
+
+
+
+ Plugin error
-
+ Mod Organizer failed to load the plugin '%1' last time it was started.
-
+ The plugin can be skipped for this session, blacklisted, or loaded normally, in which case it might fail again. Blacklisted plugins can be re-enabled later in the settings.
-
+ Skip this plugin
-
+ Blacklist this plugin
-
+ Load this plugin
-
+ Some plugins could not be loaded
-
-
+
+ Description missing
-
+ The following plugins could not be loaded. The reason may be missing dependencies (i.e. python) or an outdated version:
@@ -6397,49 +6455,6 @@ Continue?
-
- PluginTypeName
-
-
- Diagnose
-
-
-
-
- Game
-
-
-
-
- Installer
-
-
-
-
- Mod Page
-
-
-
-
- Preview
-
-
-
-
- Tool
-
-
-
-
- Proxy
-
-
-
-
- File Mapper
-
-
-PreviewDialog
@@ -6998,7 +7013,7 @@ p, li { white-space: pre-wrap; }
-
+ Instance type: %1
@@ -7008,193 +7023,192 @@ p, li { white-space: pre-wrap; }
-
+ Find game installation for %1
-
+ Find game installation
-
-
+
+ Unrecognized game
-
+ The folder %1 does not seem to contain a game Mod Organizer can manage.
-
+ See details for the list of supported games.
-
+ No installation found
-
+ Browse...
-
+ The folder must contain a valid game installation
-
+ Microsoft Store game
-
+ The folder %1 seems to be a Microsoft Store game install. Games installed through the Microsoft Store are not supported by Mod Organizer and will not work properly.
-
-
-
+
+
+ Use this folder for %1
-
+ Use this folder
-
-
-
+
+
+ I know what I'm doing
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+ Cancel
-
+ The folder %1 does not seem to contain an installation for <span style="white-space: nowrap; font-weight: bold;">%2</span> or for any other game Mod Organizer can manage.
-
+ Incorrect game
-
+ The folder %1 seems to contain an installation for <span style="white-space: nowrap; font-weight: bold;">%2</span>, not <span style="white-space: nowrap; font-weight: bold;">%3</span>.
-
+ Manage %1 instead
-
+ Instance location: %1
-
+ Instance name: %1
-
+ Profile settings:
-
+ Local INIs: %1
-
-
-
+
+
+ yes
-
-
-
+
+
+ no
-
+ Local Saves: %1
-
+ Automatic Archive Invalidation: %1
-
-
+
+ Base directory: %1
-
+ Downloads
-
+ Mods
-
+ Profiles
-
+ Overwrite
-
+ Game: %1
-
+ Game location: %1
@@ -7438,12 +7452,12 @@ Destination:
-
+ Mod Organizer
-
+ An instance of Mod Organizer is already running
@@ -7515,94 +7529,114 @@ Destination:
-
+ Connecting to Nexus...
-
+ Waiting for Nexus...
-
+ Opened Nexus in browser.
-
+ Switch to your browser and accept the request.
-
+ Finished.
-
- No answer from Nexus.
+
+ An unknown error has occurred.
+
+
+
+
+ No OAuth client id configured.
+
+
+
+
+ <html><body><h2>Mod Organizer</h2><p>Authorization complete. You may close this window.</p></body></html>
-
-
- A firewall might be blocking Mod Organizer.
+
+ Failed to bind to localhost on port %1.
-
- Nexus closed the connection.
+
+ Authorization failed (%1)
-
+
+ Internal error: OAuth flow is missing.
+
+
+
+
+ Invalid OAuth token payload.
+
+
+
+ Cancelled.
-
+ Failed to request %1
-
-
+
+ Cancelled
-
+ Internal error
-
+ HTTP code %1
-
+ Invalid JSON
-
+ Bad response
-
- API key is empty
+
+
+ Access token is empty
-
+ SSL error
-
+ Timed out
@@ -7675,19 +7709,19 @@ This program is known to cause issues with Mod Organizer, such as freezing or bl
-
-
-
+
+
+ attempt to store setting for unknown plugin "%1"
-
+ Failed
-
+ Failed to start the helper application: %1
@@ -7734,40 +7768,33 @@ This program is known to cause issues with Mod Organizer, such as freezing or bl
-
-
-
- Enter API Key Manually
-
-
-
-
-
-
+
+
+ Connect to Nexus
-
-
-
-
-
+
+
+
+
+ N/A
-
+ Executables (*.exe)
-
+ All Files (*.*)
-
+ Select the browser executable
@@ -7924,196 +7951,196 @@ Example:
-
+ This error typically happens because an antivirus has deleted critical files from Mod Organizer's installation folder or has made them generally inaccessible. Add an exclusion for Mod Organizer's installation folder in your antivirus, reinstall Mod Organizer and try again.
-
+ This error typically happens because an antivirus is preventing Mod Organizer from starting programs. Add an exclusion for Mod Organizer's installation folder in your antivirus and try again.
-
+ The file '%1' does not exist.
-
+ The working directory '%1' does not exist.
-
-
-
-
+
+
+
+ Cannot start Steam
-
+ The path to the Steam executable cannot be found. You might try reinstalling Steam.
-
-
-
+
+
+ Continue without starting Steam
-
-
+
+ The program may fail to launch.
-
+ Cannot launch program
-
-
-
+
+
+ Cannot start %1
-
+ Cannot launch helper
-
-
+
+ Elevation required
-
+ This program is requesting to run as administrator but Mod Organizer itself is not running as administrator. Running programs as administrator is typically unnecessary as long as the game and Mod Organizer have been installed outside "Program Files".
You can restart Mod Organizer as administrator and try launching the program again.
-
-
+
+ Restart Mod Organizer as administrator
-
-
+
+ You must allow "helper.exe" to make changes to the system.
-
+ Launch Steam
-
+ This program requires Steam
-
+ Mod Organizer has detected that this program likely requires Steam to be running to function properly.
-
+ Start Steam
-
-
+
+ The program might fail to run.
-
+ Steam is running as administrator
-
+ Running Steam as administrator is typically unnecessary and can cause problems when Mod Organizer itself is not running as administrator.
You can restart Mod Organizer as administrator and try launching the program again.
-
-
-
+
+
+ Continue
-
+ Event Log not running
-
+ The Event Log service is not running
-
+ The Windows Event Log service is not running. This can prevent USVFS from running properly and your mods may not be recognized by the program being launched.
-
-
+
+ Your mods might not work.
-
+ Blacklisted program
-
+ The program %1 is blacklisted
-
+ The program you are attempting to launch is blacklisted in the virtual filesystem. This will likely prevent it from seeing any mods, INI files or any other virtualized files.
-
+ Change the blacklist
-
+ Waiting
-
+ Please press OK once you're logged into steam.
-
+ Select binary
-
+ Binary
@@ -8730,7 +8757,7 @@ If you disable this feature, MO will only display official DLCs this way. Please
-
+ ...
@@ -8847,205 +8874,195 @@ If you disable this feature, MO will only display official DLCs this way. Please
- Manually enter the API key and try to login
+ Clear the stored Nexus authorization and force reauthorization.
- Enter API Key Manually
-
-
-
-
- Clear the stored Nexus API key and force reauthorization.
-
-
-
- Disconnect from Nexus
-
-
+
+ Options
-
+ Endorsement Integration
-
+ Tracked Integration
-
+ Use Nexus category mappings
-
-
+
+ <html><head/><body><p>By default, a counter is displayed in the bottom right corner. This informs the user of their remaining API requests. The Nexus API becomes unusable once these API requests run out. Checking this option will hide that counter.</p></body></html>
-
+ Hide API Request Counter
-
+ Associate with "Download with manager" links
-
+ Remove cache and cookies.
-
+ Clear Cache
-
+ Servers
-
+ Known Servers (updated on download)
-
+ Preferred Servers (Drag & Drop)
-
+ Plugins
-
+ Author:
-
+ Version:
-
+ Description:
-
+ Enabled
-
+ Key
-
+ Value
-
+ No plugin found.
-
+ Blacklisted Plugins (use <del> to remove):
-
+ Workarounds
-
+ If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on)
-
+ If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on)
Uncheck this if you want to use Mod Organizer with total conversions (like Nehrim) but be aware that the game will crash if required files are not enabled.
-
+ Force-enable game files
-
+ Enable parsing of Archives. This is an Experimental Feature. Has negative effects on performance and known incorrectness.
-
+ <html><head/><body><p>By default, MO will parse archive files (BSA, BA2) to calculate conflicts between the contents of the archive files and other loose files. This process has a noticeable cost in performance.</p><p>This feature should not be confused with the archive management feature offered by MO1. MO2 will only show conflicts with archives and will NOT load them into the game or program.</p><p>If you disable this feature, MO will only display conflicts between loose files.</p></body></html>
-
+ Enable archives parsing (experimental)
-
-
+
+ Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior.
-
+ Lock GUI when running executable
-
+ Steam
-
+ Password
-
+ Username
-
+ Steam App ID
-
+ The Steam AppID for your game
-
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li { white-space: pre-wrap; }
@@ -9061,69 +9078,69 @@ p, li { white-space: pre-wrap; }
-
+ Network
-
+ Disable automatic internet features
-
+ Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser)
-
+ Offline Mode
-
+ Use a proxy for network connections.
-
+ Use a proxy for network connections. This uses the system-wide settings which can be configured in Internet Explorer. Please note that MO will start up a few seconds slower on some systems when using a proxy.
-
+ Use System HTTP Proxy
-
-
+
+
+
-
-
-
+
+ Use "%1" as a placeholder for the URL.
-
+ Custom browser
-
-
+
+ Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations.
-
+ Reset Window Geometries
-
-
+
+
For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles.
For the other games this is not a sufficient replacement for AI!
@@ -9131,12 +9148,12 @@ p, li { white-space: pre-wrap; }
-
+ Back-date BSAs
-
+ Add executables to the blacklist to prevent them from
accessing the virtual file system. This is useful to prevent
unintended programs from being hooked. Hooking unintended
@@ -9145,64 +9162,64 @@ programs you are intentionally running.
-
+ Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended programs may affect the execution of these programs or the programs you are intentionally running.
-
+ Executables Blacklist
-
-
+
+ Files to skip or ignore from the virtual file system.
-
+ Skip File Suffixes
-
-
+
+ Directories to skip or ignore from the virtual file system.
-
+ Skip Directories
-
+ These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here.
-
+ Diagnostics
-
+ Logs and Crashes
-
+ Log Level
-
+ Decides the amount of data printed to "ModOrganizer.log"
-
+
Decides the amount of data printed to "ModOrganizer.log".
"Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regular use. On the "Error" level the log file usually remains empty.
@@ -9210,17 +9227,17 @@ programs you are intentionally running.
-
+ Crash Dumps
-
+ Decides which type of crash dumps are collected when injected processes crash.
-
+
Decides which type of crash dumps are collected when injected processes crash.
"None" Disables the generation of crash dumps by MO.
@@ -9231,17 +9248,17 @@ programs you are intentionally running.
-
+ Max Dumps To Keep
-
+ Maximum number of crash dumps to keep on disk. Use 0 for unlimited.
-
+
Maximum number of crash dumps to keep on disk. Use 0 for unlimited.
Set "Crash Dumps" above to None to disable crash dump collection.
@@ -9249,22 +9266,22 @@ programs you are intentionally running.
-
+ Integrated LOOT
-
+ LOOT Log Level
-
+ Click a link to open the location
-
+
Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a>
and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders.
@@ -9325,14 +9342,6 @@ programs you are intentionally running.
-
- T
-
-
- Plugin
-
-
-TransferSavesDialog
@@ -9479,7 +9488,7 @@ On Windows XP:
-
+ Connecting to Nexus...
@@ -9494,7 +9503,7 @@ On Windows XP:
-
+ Trying again...
@@ -9840,7 +9849,8 @@ Please open the "Nexus" tab.
- Use this interface to obtain an API key from NexusMods. This is used for all API connections - downloads, updates etc. MO2 uses the Windows Credential Manager to store this data securely. If the SSO page on Nexus is failing, use the manual entry and copy the API key from your profile.
+ Use this interface to authorize Mod Organizer with Nexus Mods. This login is used for all API connections - downloads, updates etc. MO2 uses the Windows Credential Manager to store these credentials securely.
+ Use this interface to authorize Mod Organizer with Nexus Mods. This login is used for all Nexus API connections such as downloads and update checks. MO2 uses the Windows Credential Manager to store the resulting OAuth tokens securely.
diff --git a/src/organizercore.cpp b/src/organizercore.cpp
index a8fc67c27..fe7736e7b 100644
--- a/src/organizercore.cpp
+++ b/src/organizercore.cpp
@@ -299,11 +299,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
@@ -1472,12 +1474,12 @@ void OrganizerCore::loggedInAction(QWidget* parent, std::function 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/pch.h b/src/pch.h
index a5bb4fb7d..5868ad199 100644
--- a/src/pch.h
+++ b/src/pch.h
@@ -250,7 +250,6 @@
#include
#include
#include
-#include
#include
#include
#include
diff --git a/src/plugincontainer.cpp b/src/plugincontainer.cpp
index 97c6bb09d..d70bdc728 100644
--- a/src/plugincontainer.cpp
+++ b/src/plugincontainer.cpp
@@ -86,47 +86,47 @@ struct PluginTypeName;
template <>
struct PluginTypeName
{
- static QString value() { return QT_TR_NOOP("Plugin"); }
+ static QString value() { return PluginContainer::tr("Plugin"); }
};
template <>
struct PluginTypeName
{
- static QString value() { return QT_TR_NOOP("Diagnose"); }
+ static QString value() { return PluginContainer::tr("Diagnose"); }
};
template <>
struct PluginTypeName
{
- static QString value() { return QT_TR_NOOP("Game"); }
+ static QString value() { return PluginContainer::tr("Game"); }
};
template <>
struct PluginTypeName
{
- static QString value() { return QT_TR_NOOP("Installer"); }
+ static QString value() { return PluginContainer::tr("Installer"); }
};
template <>
struct PluginTypeName
{
- static QString value() { return QT_TR_NOOP("Mod Page"); }
+ static QString value() { return PluginContainer::tr("Mod Page"); }
};
template <>
struct PluginTypeName
{
- static QString value() { return QT_TR_NOOP("Preview"); }
+ static QString value() { return PluginContainer::tr("Preview"); }
};
template <>
struct PluginTypeName
{
- static QString value() { return QT_TR_NOOP("Tool"); }
+ static QString value() { return PluginContainer::tr("Tool"); }
};
template <>
struct PluginTypeName
{
- static QString value() { return QT_TR_NOOP("Proxy"); }
+ static QString value() { return PluginContainer::tr("Proxy"); }
};
template <>
struct PluginTypeName
{
- static QString value() { return QT_TR_NOOP("File Mapper"); }
+ static QString value() { return PluginContainer::tr("File Mapper"); }
};
QStringList PluginContainer::pluginInterfaces()
diff --git a/src/settings.cpp b/src/settings.cpp
index ef1785b16..144ad0928 100644
--- a/src/settings.cpp
+++ b/src/settings.cpp
@@ -26,8 +26,10 @@ along with Mod Organizer. If not, see .
#include "serverinfo.h"
#include "settingsutilities.h"
#include "shared/appconfig.h"
+#include
#include
#include
+#include
#include
using namespace MOBase;
@@ -2498,9 +2500,29 @@ void GlobalSettings::setHideAssignCategoriesQuestion(bool b)
settings().setValue("HideAssignCategoriesQuestion", b);
}
+namespace
+{
+constexpr auto NexusLegacyCredentialKey = "APIKEY";
+constexpr auto NexusOAuthCredentialKey = "NEXUS_OAUTH_TOKENS";
+
+std::optional 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;
@@ -2510,7 +2532,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;
@@ -2526,7 +2548,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/settings.h b/src/settings.h
index edc1d6445..112c6599c 100644
--- a/src/settings.h
+++ b/src/settings.h
@@ -21,6 +21,7 @@ along with Mod Organizer. If not, see .
#define SETTINGS_H
#include "envdump.h"
+#include "nexusoauthtokens.h"
#include
#include
#include
@@ -961,6 +962,24 @@ class GlobalSettings
//
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/settingsdialog.ui b/src/settingsdialog.ui
index 6011b1588..49bef6dc2 100644
--- a/src/settingsdialog.ui
+++ b/src/settingsdialog.ui
@@ -1214,7 +1214,7 @@ If you disable this feature, MO will only display official DLCs this way. Please
- Clear the stored Nexus API key and force reauthorization.
+ Clear the stored Nexus authorization and force reauthorization.Disconnect from Nexus
diff --git a/src/settingsdialognexus.cpp b/src/settingsdialognexus.cpp
index abd487f14..673ab844b 100644
--- a/src/settingsdialognexus.cpp
+++ b/src/settingsdialognexus.cpp
@@ -1,6 +1,7 @@
#include "settingsdialognexus.h"
#include "log.h"
#include "nexusinterface.h"
+#include "nexusoauthlogin.h"
#include "serverinfo.h"
#include "ui_nexusmanualkey.h"
#include "ui_settingsdialog.h"
@@ -100,7 +101,19 @@ NexusConnectionUI::NexusConnectionUI(QWidget* parent, Settings* s,
});
}
- if (GlobalSettings::hasNexusApiKey()) {
+ 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."));
@@ -117,18 +130,11 @@ void NexusConnectionUI::connect()
}
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_nexusLogin.reset(new NexusOAuthLogin(m_parent));
}
m_log->clear();
+ m_pendingTokens.reset();
m_nexusLogin->start();
updateState();
}
@@ -147,22 +153,29 @@ void NexusConnectionUI::manual()
const auto key = d.key();
if (key.isEmpty()) {
- clearKey();
+ clearCredentials();
return;
}
m_log->clear();
- validateKey(key);
+ 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()
{
- clearKey();
+ clearCredentials();
m_log->clear();
addLog(tr("Disconnected."));
}
-void NexusConnectionUI::validateKey(const QString& key)
+void NexusConnectionUI::validateCredentials(const NexusOAuthTokens& tokens)
{
if (!m_nexusValidator) {
m_nexusValidator.reset(new NexusKeyValidator(
@@ -173,25 +186,40 @@ void NexusConnectionUI::validateKey(const QString& key)
};
}
- addLog(tr("Checking API key..."));
- m_nexusValidator->start(key, NexusKeyValidator::OneShot);
+ addLog(tr("Authorizing with Nexus..."));
+ m_nexusValidator->start(tokens, NexusKeyValidator::OneShot);
}
-void NexusConnectionUI::onSSOKeyChanged(const QString& key)
+void NexusConnectionUI::onTokensReceived(const NexusOAuthTokens& tokens)
{
- if (key.isEmpty()) {
- clearKey();
+ 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 {
- addLog(tr("Received API key."));
- validateKey(key);
+ m_pendingTokens = tokens;
}
+ addLog(tr("Received authorization from Nexus."));
+ validateCredentials(tokens);
}
-void NexusConnectionUI::onSSOStateChanged(NexusSSOLogin::States s, const QString& e)
+void NexusConnectionUI::onOAuthStateChanged(NXMAccessManager::OAuthState s,
+ const QString& e)
{
- if (s != NexusSSOLogin::Finished) {
- // finished state is handled in onSSOKeyChanged()
- const auto log = NexusSSOLogin::stateToString(s, e);
+ if (s != NXMAccessManager::OAuthState::Finished) {
+ const auto log = NXMAccessManager::stateToString(s, e);
for (auto&& line : log.split("\n")) {
addLog(line);
@@ -205,14 +233,19 @@ void NexusConnectionUI::onValidatorFinished(ValidationAttempt::Result r,
const QString& message,
std::optional user)
{
+ Q_UNUSED(r);
if (user) {
NexusInterface::instance().setUserAccount(*user);
addLog(tr("Received user account information"));
- if (setKey(user->apiKey())) {
- addLog(tr("Linked with Nexus successfully."));
+ if (m_pendingTokens) {
+ if (persistTokens(*m_pendingTokens)) {
+ addLog(tr("Linked with Nexus successfully."));
+ } else {
+ addLog(tr("Failed to store OAuth tokens."));
+ }
} else {
- addLog(tr("Failed to set API key"));
+ addLog(tr("Linked with Nexus successfully."));
}
} else {
if (message.isEmpty()) {
@@ -223,6 +256,7 @@ void NexusConnectionUI::onValidatorFinished(ValidationAttempt::Result r,
}
}
+ m_pendingTokens.reset();
updateState();
}
@@ -232,26 +266,32 @@ void NexusConnectionUI::addLog(const QString& s)
m_log->scrollToBottom();
}
-bool NexusConnectionUI::setKey(const QString& key)
+bool NexusConnectionUI::persistTokens(const NexusOAuthTokens& tokens)
{
- const bool ret = GlobalSettings::setNexusApiKey(key);
+ 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;
+ return ret && ret2;
}
-bool NexusConnectionUI::clearKey()
+bool NexusConnectionUI::clearCredentials()
{
- const auto ret = GlobalSettings::clearNexusApiKey();
+ auto ret = GlobalSettings::clearNexusOAuthTokens();
+ auto ret2 = GlobalSettings::clearNexusApiKey();
- NexusInterface::instance().getAccessManager()->clearApiKey();
+ NexusInterface::instance().getAccessManager()->clearCredentials();
updateState();
emit keyChanged();
- return ret;
+ return ret && ret2;
}
void NexusConnectionUI::updateState()
@@ -266,22 +306,29 @@ void NexusConnectionUI::updateState()
};
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"));
+ } 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);
- setButton(m_manual, false, QObject::tr("Enter API Key Manually"));
+ 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 {
- // 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"));
diff --git a/src/settingsdialognexus.h b/src/settingsdialognexus.h
index 41ab207a4..f3033315d 100644
--- a/src/settingsdialognexus.h
+++ b/src/settingsdialognexus.h
@@ -1,10 +1,16 @@
#ifndef SETTINGSDIALOGNEXUS_H
#define SETTINGSDIALOGNEXUS_H
+#include "nexusoauthlogin.h"
+#include "nexusoauthtokens.h"
#include "nxmaccessmanager.h"
#include "settings.h"
#include "settingsdialog.h"
+#include
+#include
+class QAbstractButton;
+class QListWidget;
// used by the settings dialog and the create instance dialog
//
class NexusConnectionUI : public QObject
@@ -32,22 +38,23 @@ class NexusConnectionUI : public QObject
QAbstractButton* m_manual;
QListWidget* m_log;
- std::unique_ptr m_nexusLogin;
+ std::unique_ptr m_nexusLogin;
std::unique_ptr m_nexusValidator;
+ std::optional m_pendingTokens;
void addLog(const QString& s);
void updateState();
- void validateKey(const QString& key);
- bool setKey(const QString& key);
- bool clearKey();
+ void validateCredentials(const NexusOAuthTokens& tokens);
+ bool persistTokens(const NexusOAuthTokens& tokens);
+ bool clearCredentials();
- void onSSOKeyChanged(const QString& key);
- void onSSOStateChanged(NexusSSOLogin::States s, const QString& e);
+ void onTokensReceived(const NexusOAuthTokens& tokens);
+ void onOAuthStateChanged(NXMAccessManager::OAuthState s, const QString& message);
void onValidatorFinished(ValidationAttempt::Result r, const QString& message,
- std::optional useR);
+ std::optional user);
};
class NexusSettingsTab : public SettingsTab
diff --git a/src/spawn.cpp b/src/spawn.cpp
index e9ff1276e..b82cbba48 100644
--- a/src/spawn.cpp
+++ b/src/spawn.cpp
@@ -37,6 +37,8 @@ along with Mod Organizer. If not, see .
#include
#include
+class QMessageBox;
+
using namespace MOBase;
using namespace MOShared;
diff --git a/src/tutorials/tutorial_firststeps_settings.js b/src/tutorials/tutorial_firststeps_settings.js
index 94ca26a3a..19926eb87 100644
--- a/src/tutorials/tutorial_firststeps_settings.js
+++ b/src/tutorials/tutorial_firststeps_settings.js
@@ -18,11 +18,10 @@ function getTutorialSteps()
function() {
highlightItem("nexusBox", false)
- tutorial.text = qsTr("Use this interface to obtain an API key from NexusMods. "
- +"This is used for all API connections - downloads, updates "
- +"etc. MO2 uses the Windows Credential Manager to store "
- +"this data securely. If the SSO page on Nexus is failing, "
- +"use the manual entry and copy the API key from your profile.")
+ tutorial.text = qsTr("Use this interface to authorize Mod Organizer with Nexus Mods. "
+ +"This login is used for all API connections - downloads, updates "
+ +"etc. MO2 uses the Windows Credential Manager to store "
+ +"these credentials securely.")
waitForClick()
}
]