Support deleting API key

PR #23388.
This commit is contained in:
Tom Piccirello 2025-10-22 01:05:51 -07:00 committed by GitHub
parent 9ce5463d9d
commit a8e9e800b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 158 additions and 37 deletions

View File

@ -3,6 +3,8 @@
## 2.14.1
* [#23212](https://github.com/qbittorrent/qBittorrent/pull/23212)
* Add `app/rotateAPIKey` endpoint for generating, and rotating, the WebAPI API key
* [#23388](https://github.com/qbittorrent/qBittorrent/pull/23388)
* Add `app/deleteAPIKey` endpoint for deleting the existing WebAPI API key
## 2.14.0
* [#23202](https://github.com/qbittorrent/qBittorrent/pull/23202)

View File

@ -1356,21 +1356,10 @@ void OptionsDialog::loadWebUITabOptions()
// API Key
if (const QString apiKey = pref->getWebUIApiKey(); Utils::APIKey::isValid(apiKey))
{
m_currentAPIKey = apiKey;
m_ui->textWebUIAPIKey->setText(maskAPIKey(m_currentAPIKey));
m_ui->textWebUIAPIKey->setEnabled(true);
m_ui->btnWebUIAPIKeyCopy->setEnabled(true);
m_ui->btnWebUIAPIKeyRotate->setToolTip(tr("Rotate API key"));
}
else
{
m_currentAPIKey.clear();
m_ui->textWebUIAPIKey->clear();
m_ui->textWebUIAPIKey->setEnabled(false);
m_ui->btnWebUIAPIKeyCopy->setEnabled(false);
m_ui->btnWebUIAPIKeyRotate->setToolTip(tr("Generate API key"));
}
setupWebUIAPIKey();
m_ui->checkBypassLocalAuth->setChecked(!pref->isWebUILocalAuthEnabled());
m_ui->checkBypassAuthSubnetWhitelist->setChecked(pref->isWebUIAuthSubnetWhitelistEnabled());
@ -1412,8 +1401,9 @@ void OptionsDialog::loadWebUITabOptions()
connect(m_ui->textWebUIUsername, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->textWebUIPassword, &QLineEdit::textChanged, this, &ThisType::enableApplyButton);
connect(m_ui->btnWebUIAPIKeyCopy, &QPushButton::clicked, this, &ThisType::onBtnWebUIAPIKeyCopy);
connect(m_ui->btnWebUIAPIKeyRotate, &QPushButton::clicked, this, &ThisType::onBtnWebUIAPIKeyRotate);
connect(m_ui->btnWebUIAPIKeyCopy, &QPushButton::clicked, this, &ThisType::onBtnWebUIAPIKeyCopyClicked);
connect(m_ui->btnWebUIAPIKeyRotate, &QPushButton::clicked, this, &ThisType::onBtnWebUIAPIKeyRotateClicked);
connect(m_ui->btnWebUIAPIKeyDelete, &QPushButton::clicked, this, &ThisType::onBtnWebUIAPIKeyDeleteClicked);
connect(m_ui->checkBypassLocalAuth, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkBypassAuthSubnetWhitelist, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
@ -1490,13 +1480,13 @@ void OptionsDialog::saveWebUITabOptions() const
pref->setDynDNSPassword(m_ui->DNSPasswordTxt->text());
}
void OptionsDialog::onBtnWebUIAPIKeyCopy()
void OptionsDialog::onBtnWebUIAPIKeyCopyClicked()
{
if (!m_currentAPIKey.isEmpty())
QApplication::clipboard()->setText(m_currentAPIKey);
}
void OptionsDialog::onBtnWebUIAPIKeyRotate()
void OptionsDialog::onBtnWebUIAPIKeyRotateClicked()
{
const QString title = m_currentAPIKey.isEmpty()
? tr("Generate API key")
@ -1511,16 +1501,51 @@ void OptionsDialog::onBtnWebUIAPIKeyRotate()
if (button == QMessageBox::Yes)
{
m_currentAPIKey = Utils::APIKey::generate();
m_ui->textWebUIAPIKey->setText(maskAPIKey(m_currentAPIKey));
m_ui->textWebUIAPIKey->setEnabled(true);
m_ui->btnWebUIAPIKeyCopy->setEnabled(true);
m_ui->btnWebUIAPIKeyRotate->setToolTip(tr("Rotate API key"));
setupWebUIAPIKey();
auto *preferences = Preferences::instance();
preferences->setWebUIApiKey(m_currentAPIKey);
preferences->apply();
}
}
void OptionsDialog::onBtnWebUIAPIKeyDeleteClicked()
{
const QString title = tr("Delete API key");
const QString message = tr("Delete this API key? The current key will immediately stop working.");
const QMessageBox::StandardButton button = QMessageBox::question(
this, title, message, (QMessageBox::Yes | QMessageBox::No), QMessageBox::No);
if (button == QMessageBox::Yes)
{
m_currentAPIKey.clear();
setupWebUIAPIKey();
auto *preferences = Preferences::instance();
preferences->setWebUIApiKey(m_currentAPIKey);
preferences->apply();
}
}
void OptionsDialog::setupWebUIAPIKey()
{
if (Utils::APIKey::isValid(m_currentAPIKey))
{
m_ui->textWebUIAPIKey->setText(maskAPIKey(m_currentAPIKey));
m_ui->textWebUIAPIKey->setEnabled(true);
m_ui->btnWebUIAPIKeyCopy->setEnabled(true);
m_ui->btnWebUIAPIKeyRotate->setToolTip(tr("Rotate API key"));
m_ui->btnWebUIAPIKeyDelete->setEnabled(true);
}
else
{
m_ui->textWebUIAPIKey->clear();
m_ui->textWebUIAPIKey->setEnabled(false);
m_ui->btnWebUIAPIKeyCopy->setEnabled(false);
m_ui->btnWebUIAPIKeyRotate->setToolTip(tr("Generate API key"));
m_ui->btnWebUIAPIKeyDelete->setEnabled(false);
}
}
#endif // DISABLE_WEBUI
void OptionsDialog::initializeLanguageCombo()

View File

@ -110,8 +110,10 @@ private slots:
void webUIHttpsCertChanged(const Path &path);
void webUIHttpsKeyChanged(const Path &path);
void on_registerDNSBtn_clicked();
void onBtnWebUIAPIKeyCopy();
void onBtnWebUIAPIKeyRotate();
void onBtnWebUIAPIKeyCopyClicked();
void onBtnWebUIAPIKeyRotateClicked();
void onBtnWebUIAPIKeyDeleteClicked();
void setupWebUIAPIKey();
#endif
private:

View File

@ -3861,6 +3861,35 @@ Specify an IPv4 or IPv6 address. You can specify "0.0.0.0" for any IPv
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnWebUIAPIKeyDelete">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="toolTip">
<string>Delete API key</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<normaloff>:/icons/list-remove.svg</normaloff>:/icons/list-remove.svg</iconset>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>

View File

@ -1326,6 +1326,13 @@ void AppController::rotateAPIKeyAction()
setResult(QJsonObject {{u"apiKey"_s, key}});
}
void AppController::deleteAPIKeyAction()
{
auto *preferences = Preferences::instance();
preferences->setWebUIApiKey({});
preferences->apply();
}
void AppController::networkInterfaceListAction()
{
QJsonArray ifaceList;

View File

@ -53,6 +53,7 @@ private slots:
void cookiesAction();
void setCookiesAction();
void rotateAPIKeyAction();
void deleteAPIKeyAction();
void networkInterfaceListAction();
void networkInterfaceAddressListAction();

View File

@ -53,7 +53,7 @@
#include "base/utils/version.h"
#include "api/isessionmanager.h"
inline const Utils::Version<3, 2> API_VERSION {2, 14, 0};
inline const Utils::Version<3, 2> API_VERSION {2, 14, 1};
class APIController;
class AuthController;
@ -150,6 +150,7 @@ private:
const QHash<std::pair<QString, QString>, QString> m_allowedMethod =
{
// <<controller name, action name>, HTTP method>
{{u"app"_s, u"deleteAPIKey"_s}, Http::METHOD_POST},
{{u"app"_s, u"rotateAPIKey"_s}, Http::METHOD_POST},
{{u"app"_s, u"sendTestEmail"_s}, Http::METHOD_POST},
{{u"app"_s, u"setCookies"_s}, Http::METHOD_POST},

View File

@ -1,24 +1,24 @@
<div id="confirmRotateAPIKeyDialog">
<div id="confirmAPIKeyDialog">
<div class="genericConfirmGrid">
<span class="confirmGridItem confirmWarning"></span>
<span class="confirmGridItem dialogMessage"></span>
</div>
</div>
<div>
<input type="button" value="QBT_TR(Yes)QBT_TR[CONTEXT=MainWindow]" id="confirmRotateButton">
<input type="button" value="QBT_TR(No)QBT_TR[CONTEXT=MainWindow]" id="cancelRotateButton">
<input type="button" value="QBT_TR(Yes)QBT_TR[CONTEXT=MainWindow]" id="confirmAPIKeyButton">
<input type="button" value="QBT_TR(No)QBT_TR[CONTEXT=MainWindow]" id="cancelAPIKeyButton">
</div>
<script>
"use strict";
(() => {
const { windowEl, options } = window.MUI.Windows.instances["confirmRotateAPIKeyDialog"];
const { message } = options.data;
const { windowEl, options } = window.MUI.Windows.instances["confirmAPIKeyDialog"];
const { message, action } = options.data;
const confirmButton = document.getElementById("confirmRotateButton");
const cancelButton = document.getElementById("cancelRotateButton");
const dialog = document.getElementById("confirmRotateAPIKeyDialog");
const confirmButton = document.getElementById("confirmAPIKeyButton");
const cancelButton = document.getElementById("cancelAPIKeyButton");
const dialog = document.getElementById("confirmAPIKeyDialog");
dialog.querySelector("span.dialogMessage").textContent = message;
@ -26,7 +26,14 @@
window.qBittorrent.Client.closeWindow(dialog);
});
confirmButton.addEventListener("click", (e) => {
window.qBittorrent.Preferences.rotateAPIKey();
switch (action) {
case "rotate":
window.qBittorrent.Preferences.rotateAPIKey();
break;
case "delete":
window.qBittorrent.Preferences.deleteAPIKey();
break;
}
window.qBittorrent.Client.closeWindow(dialog);
});

View File

@ -1012,6 +1012,11 @@
<img src="images/force-recheck.svg" alt="QBT_TR(Generate API key)QBT_TR[CONTEXT=OptionsDialog]" title="QBT_TR(Generate API key)QBT_TR[CONTEXT=OptionsDialog]" width="16" height="16" style="margin: 4px; top: 2px; position: relative;">
</button>
</td>
<td>
<button type="button" disabled id="webUIAPIKeyDeleteButton" style="padding: 0;" data-has-key="false" aria-label="QBT_TR(Delete API key)QBT_TR[CONTEXT=OptionsDialog]">
<img src="images/list-remove.svg" alt="QBT_TR(Delete API key)QBT_TR[CONTEXT=OptionsDialog]" title="QBT_TR(Delete API key)QBT_TR[CONTEXT=OptionsDialog]" width="16" height="16" style="margin: 4px; top: 2px; position: relative;">
</button>
</td>
</tr>
</tbody>
</table>
@ -1823,6 +1828,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
updateWebuiLocaleSelect: updateWebuiLocaleSelect,
registerDynDns: registerDynDns,
rotateAPIKey: rotateAPIKey,
deleteAPIKey: deleteAPIKey,
applyPreferences: applyPreferences
};
};
@ -2586,6 +2592,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
document.getElementById("webUIAPIKeyCopyButton").disabled = false;
document.getElementById("webUIAPIKeyRotateButton").dataset.hasKey = "true";
document.querySelector("#webUIAPIKeyRotateButton img").title = "QBT_TR(Rotate API key)QBT_TR[CONTEXT=OptionsDialog]";
document.getElementById("webUIAPIKeyDeleteButton").disabled = false;
}
// Use alternative WebUI
@ -3287,12 +3294,37 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
document.getElementById("webUIAPIKeyCopyButton").disabled = false;
document.getElementById("webUIAPIKeyRotateButton").dataset.hasKey = "true";
document.querySelector("#webUIAPIKeyRotateButton img").title = "QBT_TR(Rotate API key)QBT_TR[CONTEXT=OptionsDialog]";
document.getElementById("webUIAPIKeyDeleteButton").disabled = false;
})
.catch((error) => {
alert(`QBT_TR(Unable to rotate API key.)QBT_TR[CONTEXT=HttpServer] ${error.toString()}`);
});
};
const deleteAPIKey = () => {
fetch("api/v2/app/deleteAPIKey", {
method: "POST",
})
.then(async (response) => {
if (!response.ok) {
alert(await response.text());
return;
}
const apiKeyTextElem = document.getElementById("WebUIAPIKeyText");
apiKeyTextElem.value = "";
apiKeyTextElem.dataset.apiKey = "";
document.getElementById("webUIAPIKeyCopyButton").disabled = true;
document.getElementById("webUIAPIKeyRotateButton").dataset.hasKey = "false";
document.querySelector("#webUIAPIKeyRotateButton img").title = "QBT_TR(Generate API key)QBT_TR[CONTEXT=OptionsDialog]";
document.getElementById("webUIAPIKeyDeleteButton").disabled = true;
})
.catch((error) => {
alert(`QBT_TR(Unable to delete API key.)QBT_TR[CONTEXT=HttpServer] ${error.toString()}`);
});
};
document.getElementById("webUIAPIKeyCopyButton").addEventListener("click", async (e) => {
const apiKey = document.getElementById("WebUIAPIKeyText").dataset.apiKey;
await clipboardCopy(apiKey);
@ -3310,7 +3342,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
});
document.getElementById("webUIAPIKeyRotateButton").addEventListener("click", (e) => {
const hasKey = e.target.parentElement.dataset.hasKey;
const hasKey = e.target.parentElement.dataset.hasKey === "true";
const title = hasKey
? "QBT_TR(Rotate API key)QBT_TR[CONTEXT=OptionsDialog]"
: "QBT_TR(Generate API key)QBT_TR[CONTEXT=OptionsDialog]";
@ -3320,11 +3352,26 @@ Use ';' to split multiple entries. Can use wildcard '*'.)QBT_TR[CONTEXT=OptionsD
new MochaUI.Modal({
...window.qBittorrent.Dialog.baseModalOptions,
id: "confirmRotateAPIKeyDialog",
title: title,
contentURL: "views/confirmRotateAPIKey.html?v=${CACHEID}",
id: "confirmAPIKeyDialog",
contentURL: "views/confirmAPIKey.html?v=${CACHEID}",
data: {
hasKey: hasKey,
action: "rotate",
message: message,
},
});
});
document.getElementById("webUIAPIKeyDeleteButton").addEventListener("click", (e) => {
const title = "QBT_TR(Delete API key)QBT_TR[CONTEXT=OptionsDialog]";
const message = "QBT_TR(Delete this API key? The current key will immediately stop working.)QBT_TR[CONTEXT=confirmRotateAPIKeyDialog]";
new MochaUI.Modal({
...window.qBittorrent.Dialog.baseModalOptions,
title: title,
id: "confirmAPIKeyDialog",
contentURL: "views/confirmAPIKey.html?v=${CACHEID}",
data: {
action: "delete",
message: message,
},
});

View File

@ -424,7 +424,7 @@
<file>private/views/confirmAutoTMM.html</file>
<file>private/views/confirmdeletion.html</file>
<file>private/views/confirmRecheck.html</file>
<file>private/views/confirmRotateAPIKey.html</file>
<file>private/views/confirmAPIKey.html</file>
<file>private/views/cookies.html</file>
<file>private/views/createtorrent.html</file>
<file>private/views/filters.html</file>