From 44515159af50756ff26a4996ca1526f7e51e9e98 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Sat, 2 May 2026 00:18:53 -0500 Subject: nxmaccessmanager: derive legacy API key from OAuth session Some plugins (Plugin Browser for MO2 and a number of older Python plugins) read only the legacy `apiKey` field. The OAuth login flow populates `accessToken` but never the legacy key, so those plugins fail with "User API Key is missing" even after a successful sign-in. Nexus' v1 `/users/validate.json` endpoint accepts the OAuth bearer token and returns the user's personal API key in the `key` field. We hit it once after `notifyTokens()` (fresh login) and once via the constructor queued call (already-logged-in users on first launch with this build), then persist the result through `GlobalSettings::setNexusApiKey()`. The fetch is a no-op when an apiKey is already present (manual entry or prior session) so it never overwrites a user-set value, and silently fails on non-200 / missing field so we don't break the OAuth-only path when Nexus eventually deprecates personal API keys. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/src/nxmaccessmanager.cpp | 58 ++++++++++++++++++++++++++++++++++++++++++++ src/src/nxmaccessmanager.h | 13 ++++++++++ 2 files changed, 71 insertions(+) (limited to 'src') diff --git a/src/src/nxmaccessmanager.cpp b/src/src/nxmaccessmanager.cpp index 18d83e1..a04ea3d 100644 --- a/src/src/nxmaccessmanager.cpp +++ b/src/src/nxmaccessmanager.cpp @@ -677,6 +677,14 @@ NXMAccessManager::NXMAccessManager(QObject* parent, Settings* s, setCookieJar(new PersistentCookieJar(QDir::fromNativeSeparators( m_Settings->paths().cache() + "/nexus_cookies.dat"))); } + + // Migrate users who only have an OAuth token but no legacy API key — some + // plugins still read only the legacy field. Defer to the event loop so + // startup isn't blocked on the network. + if (!m_Tokens.accessToken.isEmpty() && m_Tokens.apiKey.isEmpty()) { + QMetaObject::invokeMethod( + this, [this]() { fetchAndStoreLegacyApiKey(); }, Qt::QueuedConnection); + } } void NXMAccessManager::setTopLevelWidget(QWidget* w) @@ -782,6 +790,56 @@ void NXMAccessManager::notifyTokens() startValidationCheck(tokens); emit authorizationEnded(); + + // OAuth alone doesn't populate the legacy `apiKey`. Some plugins still + // only read that field; fetch the user's personal key from v1/validate + // and persist it so they don't error with "User API Key is missing". + fetchAndStoreLegacyApiKey(); +} + +void NXMAccessManager::fetchAndStoreLegacyApiKey() +{ + if (!m_Tokens.apiKey.isEmpty()) { + // already have one (manually entered or carried over) + return; + } + if (m_Tokens.accessToken.isEmpty()) { + return; + } + + QNetworkRequest request(NexusV1BaseUrl + "users/validate.json"); + request.setRawHeader("Authorization", + ("Bearer " + m_Tokens.accessToken).toUtf8()); + addAPIHeaders(request); + QNetworkReply* reply = get(request); + if (!reply) { + log::debug("nexus: cannot fetch legacy API key, request creation failed"); + return; + } + + QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { + reply->deleteLater(); + + const auto code = + reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (code != 200) { + log::debug("nexus: legacy API key fetch returned HTTP {}", code); + return; + } + + const auto doc = QJsonDocument::fromJson(reply->readAll()); + const QJsonObject obj = doc.object(); + const QString key = obj.value("key").toString(); + if (key.isEmpty()) { + log::debug("nexus: legacy API key not present in validate response"); + return; + } + + if (GlobalSettings::setNexusApiKey(key)) { + m_Tokens.apiKey = key; + log::info("nexus: stored legacy API key derived from OAuth session"); + } + }); } void NXMAccessManager::saveRefreshedTokens(const NexusOAuthTokens tokens) diff --git a/src/src/nxmaccessmanager.h b/src/src/nxmaccessmanager.h index c651bee..1668e90 100644 --- a/src/src/nxmaccessmanager.h +++ b/src/src/nxmaccessmanager.h @@ -215,6 +215,19 @@ public: void refuseValidation(); + /** + * @brief fetch the user's legacy personal API key via the OAuth bearer + * token and persist it as the legacy API key. + * + * Several plugins (Plugin Browser, older Python plugins) only consume the + * legacy `apiKey` field. The OAuth flow alone never populates it, so they + * fail with "User API Key is missing" even after a successful login. + * Calling Nexus' v1 `/users/validate.json` with the bearer token returns + * the user's personal API key in the `key` field; we store it so legacy + * code paths see a key. + **/ + void fetchAndStoreLegacyApiKey(); + signals: /** -- cgit v1.3.1