diff --git a/approot/messages.xml b/approot/messages.xml index 967d65e4..000dfef9 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -6,6 +6,7 @@ Add Administration Cancel +Copy Tags Create Delete @@ -364,7 +365,14 @@ Original Release Date (Descending) Bad password Old password must be filled in -Generate +Generate +API key copied to clipboard! +Delete +Deleting the key will disconnect all clients currently using it. +Delete API key? +Regenerate +Generating a new key will invalidate all clients currently using the existing key. +Regenerate API key? ReplayGain mode No ReplayGain Auto diff --git a/approot/messages_es.xml b/approot/messages_es.xml index fb2af82a..bc9b0183 100644 --- a/approot/messages_es.xml +++ b/approot/messages_es.xml @@ -6,6 +6,7 @@ Añadir Administración Cancelar +Copiar Etiquetas Crear Eliminar @@ -364,7 +365,14 @@ Fecha de publicación original (Descendiente) Contraseña incorrecta La contraseña antigua debe ser introducida -Regenerar +Regenerar +¡Clave API copiada al portapapeles! +Eliminar +Eliminar la clave desconectará todos los clientes que la estén usando actualmente. +¿Eliminar la clave API? +Regenerar +Generar una nueva clave invalidará todos los clientes que usen la clave actual. +¿Regenerar la clave API? Modo ReplayGain Sin ReplayGain Automático diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index ebeefb3d..65d2a543 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -6,6 +6,7 @@ Ajouter Administration Annuler +Copier Tags Créer Supprimer @@ -364,7 +365,14 @@ Date de sortie originale (Décroissant) Mauvais mot de passe L'ancien mot de passe doit être renseigné -Regénérer +Regénérer +Clé API copiée dans le presse-papiers ! +Supprimer +Supprimer la clé déconnectera tous les clients qui l'utilisent actuellement. +Supprimer la clé API ? +Regénérer +Générer une nouvelle clé invalidera tous les clients utilisant actuellement la clé existante. +Regénérer la clé API ? Mode ReplayGain Pas de ReplayGain Automatique diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 49018118..39051ccb 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -6,6 +6,7 @@ Aggiungi Amministrazione Annulla +Copia Tags Crea Elimina @@ -364,7 +365,14 @@ Data di uscita originale (Decrescente) Password errata Devi inserire la password attuale -Genera +Genera +Chiave API copiata negli appunti! +Elimina +Eliminare la chiave disconnetterà tutti i client che la stanno utilizzando. +Eliminare la chiave API? +Rigenera +Generare una nuova chiave invaliderà tutti i client che attualmente utilizzano la chiave esistente. +Rigenerare la chiave API? Modalità ReplayGain No ReplayGain Automatica diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index 023dcdc0..25eba67e 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -6,6 +6,7 @@ Dodaj Zarządzanie Cofnij +Kopiuj Znaczniki Utwórz Usuń @@ -390,7 +391,14 @@ Oryginalna data wydania (Malejąco) Błędne hasło Należy podać stare hasło -Generuj +Generuj +Klucz API skopiowany do schowka! +Usuń +Usunięcie klucza rozłączy wszystkich klientów, którzy aktualnie go używają. +Usunąć klucz API? +Regeneruj +Wygenerowanie nowego klucza spowoduje unieważnienie wszystkich klientów korzystających z istniejącego klucza. +Regenerować klucz API? Tryb ReplayGain Bez ReplayGain Auto diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 9a7e6bb7..8c4146d3 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -6,6 +6,7 @@ 添加 管理 取消 +复制 标签 新建 删除 @@ -335,7 +336,14 @@ 原始发行日期(降序) 无效密码 必须填写旧密码 -生成 +生成 +API 密钥已复制到剪贴板! +删除 +删除密钥将断开所有当前使用该密钥的客户端连接。 +删除 API 密钥? +重新生成 +生成新密钥将使所有当前使用现有密钥的客户端失效。 +重新生成 API 密钥? ReplayGain 模式 没有 ReplayGain 自动模式 diff --git a/approot/misc.xml b/approot/misc.xml index 6fd85b17..b62f3215 100644 --- a/approot/misc.xml +++ b/approot/misc.xml @@ -20,6 +20,7 @@ + diff --git a/approot/settings-subsonic.xml b/approot/settings-subsonic.xml index e94c7484..ab36a43f 100644 --- a/approot/settings-subsonic.xml +++ b/approot/settings-subsonic.xml @@ -1,28 +1,9 @@ - +
${tr:Lms.Settings.subsonic-api} -
- -
- ${subsonic-token-regen-btn class="btn btn-outline-secondary"} - ${subsonic-token-del-btn class="btn btn-outline-warning"} - ${subsonic-token class="form-control"} - ${subsonic-token-visibility-btn class="btn btn-outline-secondary"} -
- ${subsonic-token-info class="help-block"} -
-
-
${subsonic-enable-transcoding-by-default class="form-check-input"} @@ -69,4 +50,68 @@ ${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"} + +
+
+ ${tr:Lms.Settings.subsonic-token} + ${} + + ${} +
+
+
+ ${subsonic-token class="form-control"} + ${subsonic-token-visibility-btn class="btn btn-outline-secondary"} +
+
+ ${subsonic-token-regen-btn class="btn btn-secondary"} + ${subsonic-token-copy-btn class="btn btn-secondary"} +
+ ${subsonic-token-del-btn class="btn btn-secondary"} +
+
+
+
+
+ + + + + + + + + diff --git a/src/lms/ui/Utils.cpp b/src/lms/ui/Utils.cpp index 7f8edad5..a5425d9e 100644 --- a/src/lms/ui/Utils.cpp +++ b/src/lms/ui/Utils.cpp @@ -26,6 +26,7 @@ #include #include +#include "core/String.hpp" #include "database/Session.hpp" #include "database/objects/Artist.hpp" #include "database/objects/Cluster.hpp" @@ -501,4 +502,9 @@ namespace lms::ui::utils return res; } + + void copyToClipboard(std::string_view text) + { + LmsApp->doJavaScript("navigator.clipboard.writeText('" + core::stringUtils::jsEscape(text) + "').catch(function(){})"); + } } // namespace lms::ui::utils diff --git a/src/lms/ui/Utils.hpp b/src/lms/ui/Utils.hpp index 13c124dc..7d285692 100644 --- a/src/lms/ui/Utils.hpp +++ b/src/lms/ui/Utils.hpp @@ -93,4 +93,6 @@ namespace lms::ui::utils std::map>> getTrackArtistsByRole(db::ObjectPtr release); std::unique_ptr createCopyright(std::string_view copyright, std::string_view copyrightURL); + + void copyToClipboard(std::string_view text); } // namespace lms::ui::utils diff --git a/src/lms/ui/settings/SubsonicSettingsView.cpp b/src/lms/ui/settings/SubsonicSettingsView.cpp index 165d797a..97ad929c 100644 --- a/src/lms/ui/settings/SubsonicSettingsView.cpp +++ b/src/lms/ui/settings/SubsonicSettingsView.cpp @@ -32,17 +32,17 @@ #include "core/UUID.hpp" #include "database/Session.hpp" -#include "database/Types.hpp" #include "database/objects/User.hpp" #include "services/auth/IAuthTokenService.hpp" #include "LmsApplication.hpp" +#include "ModalManager.hpp" #include "Tooltip.hpp" #include "common/MandatoryValidator.hpp" -#include "common/UUIDValidator.hpp" #include "common/ValueStringModel.hpp" #include "SettingsViewUtils.hpp" +#include "Utils.hpp" namespace lms::ui { @@ -51,14 +51,12 @@ namespace lms::ui class SubsonicSettingsModel : public Wt::WFormModel { public: - static inline const Field SubsonicTokenField{ "subsonic-token" }; static inline const Field SubsonicEnableTranscodingByDefault{ "subsonic-enable-transcoding-by-default" }; static inline const Field SubsonicArtistListModeField{ "subsonic-artist-list-mode" }; static inline const Field SubsonicTranscodingOutputFormatField{ "subsonic-transcoding-output-format" }; static inline const Field SubsonicTranscodingOutputBitrateField{ "subsonic-transcoding-output-bitrate" }; - explicit SubsonicSettingsModel(auth::IAuthTokenService& authTokenService) - : _authTokenService{ authTokenService } + SubsonicSettingsModel() { _transcodingOutputBitrateModel = std::make_shared>(); db::visitAllowedAudioBitrates([&](const db::Bitrate bitrate) { @@ -75,13 +73,11 @@ namespace lms::ui _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.release-artists"), db::SubsonicArtistListMode::ReleaseArtists); _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), db::SubsonicArtistListMode::TrackArtists); - addField(SubsonicTokenField); addField(SubsonicEnableTranscodingByDefault); addField(SubsonicTranscodingOutputBitrateField); addField(SubsonicTranscodingOutputFormatField); addField(SubsonicArtistListModeField); - setValidator(SubsonicTokenField, createUUIDValidator()); setValidator(SubsonicTranscodingOutputBitrateField, createMandatoryValidator()); setValidator(SubsonicTranscodingOutputFormatField, createMandatoryValidator()); @@ -97,29 +93,6 @@ namespace lms::ui auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; db::User::pointer user{ LmsApp->getUser() }; - const std::string token{ Wt::asString(value(SubsonicTokenField)).toUTF8() }; - if (token.empty()) - { - _authTokenService.clearAuthTokens("subsonic", user->getId()); - } - else - { - bool hasNonMatchingToken{ false }; - bool hasMatchingToken{ false }; - _authTokenService.visitAuthTokens("subsonic", user->getId(), [&](const auth::IAuthTokenService::AuthTokenInfo&, std::string_view storedToken) { - if (storedToken == token) - hasMatchingToken = true; - else - hasNonMatchingToken = true; - }); - - if (!hasMatchingToken || hasNonMatchingToken) - { - _authTokenService.clearAuthTokens("subsonic", user->getId()); - _authTokenService.createAuthToken("subsonic", user->getId(), token); - } - } - user.modify()->setSubsonicEnableTranscodingByDefault(Wt::asNumber(value(SubsonicEnableTranscodingByDefault)) != 0); auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromString(valueText(SubsonicTranscodingOutputBitrateField)) }; @@ -140,11 +113,6 @@ namespace lms::ui auto transaction{ LmsApp->getDbSession().createReadTransaction() }; const db::User::pointer user{ LmsApp->getUser() }; - _authTokenService.visitAuthTokens("subsonic", user->getId(), [&](const auth::IAuthTokenService::AuthTokenInfo&, std::string_view storedToken) { - if (Wt::asString(value(SubsonicTokenField)).empty()) - setValue(SubsonicTokenField, Wt::WString::fromUTF8(std::string{ storedToken })); - }); - setValue(SubsonicEnableTranscodingByDefault, user->getSubsonicEnableTranscodingByDefault()); auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodingOutputBitrate()) }; @@ -161,7 +129,6 @@ namespace lms::ui } private: - auth::IAuthTokenService& _authTokenService; std::shared_ptr> _transcodingOutputBitrateModel; std::shared_ptr> _transcodingOutputFormatModel; std::shared_ptr> _subsonicArtistListModeModel; @@ -184,34 +151,15 @@ namespace lms::ui clear(); - auto* t{ addNew(Wt::WString::tr("Lms.Settings.subsonic.template")) }; - t->setCondition("if-has-subsonic-token-usage", core::Service::get()->getBool("api-subsonic-support-user-password-auth", true)); + refreshForm(); + refreshSubsonicKey(); + } - auto model{ std::make_shared(*core::Service::get()) }; + void SubsonicSettingsView::refreshForm() + { + auto model{ std::make_shared() }; - auto subsonicToken{ std::make_unique() }; - Wt::WLineEdit* subsonicTokenPtr{ subsonicToken.get() }; - subsonicTokenPtr->setEchoMode(Wt::EchoMode::Password); - subsonicTokenPtr->setReadOnly(true); - t->setFormWidget(SubsonicSettingsModel::SubsonicTokenField, std::move(subsonicToken)); - - auto subsonicTokenRegenBtn{ std::make_unique(Wt::WString::tr("Lms.Settings.regen-token")) }; - subsonicTokenRegenBtn->clicked().connect(this, [subsonicTokenPtr] { - subsonicTokenPtr->setValueText(Wt::WString::fromUTF8(std::string{ core::UUID::generate().getAsString() })); - }); - t->bindWidget("subsonic-token-regen-btn", std::move(subsonicTokenRegenBtn)); - - auto subsonicTokenVisibilityBtn{ std::make_unique(Wt::WString::tr("Lms.template.toggle-visibility-btn"), Wt::TextFormat::XHTML) }; - subsonicTokenVisibilityBtn->clicked().connect(this, [subsonicTokenPtr] { - subsonicTokenPtr->setEchoMode(subsonicTokenPtr->echoMode() == Wt::EchoMode::Password ? Wt::EchoMode::Normal : Wt::EchoMode::Password); - }); - t->bindWidget("subsonic-token-visibility-btn", std::move(subsonicTokenVisibilityBtn)); - - auto subsonicTokenDelBtn{ std::make_unique(Wt::WString::tr("Lms.template.trash-btn"), Wt::TextFormat::XHTML) }; - subsonicTokenDelBtn->clicked().connect(this, [subsonicTokenPtr] { - subsonicTokenPtr->setValueText(""); - }); - t->bindWidget("subsonic-token-del-btn", std::move(subsonicTokenDelBtn)); + auto* t{ addNew(Wt::WString::tr("Lms.Settings.subsonic.template.form")) }; t->setFormWidget(SubsonicSettingsModel::SubsonicEnableTranscodingByDefault, std::make_unique()); @@ -231,4 +179,136 @@ namespace lms::ui t->updateView(model.get()); initTooltipsForWidgetTree(*t); } + + void SubsonicSettingsView::refreshSubsonicKey() + { + auto* t{ addNew(Wt::WString::tr("Lms.Settings.subsonic.template.key")) }; + t->addFunction("tr", &Wt::WTemplate::Functions::tr); + + t->setCondition("if-has-subsonic-token-usage", core::Service::get()->getBool("api-subsonic-support-user-password-auth", true)); + + std::string currentToken; + { + auto transaction{ LmsApp->getDbSession().createReadTransaction() }; + core::Service::get()->visitAuthTokens("subsonic", LmsApp->getUser()->getId(), + [&](const auth::IAuthTokenService::AuthTokenInfo&, std::string_view storedToken) { + currentToken = std::string{ storedToken }; + }); + } + + auto subsonicToken{ std::make_unique() }; + Wt::WLineEdit* subsonicTokenPtr{ subsonicToken.get() }; + subsonicTokenPtr->setEchoMode(Wt::EchoMode::Password); + subsonicTokenPtr->setReadOnly(true); + subsonicTokenPtr->setValueText(Wt::WString::fromUTF8(currentToken)); + t->bindWidget("subsonic-token", std::move(subsonicToken)); + + auto subsonicTokenDelBtn{ std::make_unique(Wt::WString::tr("Lms.delete")) }; + Wt::WPushButton* subsonicTokenDelBtnPtr{ subsonicTokenDelBtn.get() }; + + auto subsonicTokenCopyBtn{ std::make_unique(Wt::WString::tr("Lms.copy")) }; + Wt::WPushButton* subsonicTokenCopyBtnPtr{ subsonicTokenCopyBtn.get() }; + + auto updateKeyButtonStates{ [subsonicTokenDelBtnPtr, subsonicTokenCopyBtnPtr, subsonicTokenPtr] { + const bool hasToken{ !subsonicTokenPtr->valueText().empty() }; + subsonicTokenDelBtnPtr->setDisabled(!hasToken); + subsonicTokenCopyBtnPtr->setDisabled(!hasToken); + } }; + updateKeyButtonStates(); + + auto subsonicTokenRegenBtn{ std::make_unique(Wt::WString::tr("Lms.Settings.generate-token")) }; + subsonicTokenRegenBtn->clicked().connect(t, [t, subsonicTokenPtr, updateKeyButtonStates] { + if (LmsApp->getUserType() == db::UserType::DEMO) + { + LmsApp->notifyMsg(Notification::Type::Warning, Wt::WString::tr("Lms.Settings.demo-cannot-save")); + return; + } + + auto doGenerate{ [subsonicTokenPtr, updateKeyButtonStates] { + const std::string newToken{ core::UUID::generate().getAsString() }; + { + auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; + auto& authService{ *core::Service::get() }; + authService.clearAuthTokens("subsonic", LmsApp->getUser()->getId()); + authService.createAuthToken("subsonic", LmsApp->getUser()->getId(), newToken); + } + subsonicTokenPtr->setValueText(Wt::WString::fromUTF8(newToken)); + updateKeyButtonStates(); + } }; + + if (!subsonicTokenPtr->valueText().empty()) + { + auto modal{ std::make_unique(Wt::WString::tr("Lms.Settings.subsonic.template.regen-confirm")) }; + modal->addFunction("tr", &Wt::WTemplate::Functions::tr); + Wt::WWidget* modalPtr{ modal.get() }; + modal->bindNew("confirm-btn", Wt::WString::tr("Lms.Settings.subsonic-token-regen-confirm")) + ->clicked() + .connect(t, [doGenerate, modalPtr] { + doGenerate(); + LmsApp->getModalManager().dispose(modalPtr); + }); + modal->bindNew("cancel-btn", Wt::WString::tr("Lms.cancel")) + ->clicked() + .connect(t, [modalPtr] { + LmsApp->getModalManager().dispose(modalPtr); + }); + + LmsApp->getModalManager().show(std::move(modal)); + } + else + { + doGenerate(); + } + }); + t->bindWidget("subsonic-token-regen-btn", std::move(subsonicTokenRegenBtn)); + + subsonicTokenDelBtn->clicked().connect(t, [t, subsonicTokenPtr, updateKeyButtonStates] { + if (LmsApp->getUserType() == db::UserType::DEMO) + { + LmsApp->notifyMsg(Notification::Type::Warning, Wt::WString::tr("Lms.Settings.demo-cannot-save")); + return; + } + + auto modal{ std::make_unique(Wt::WString::tr("Lms.Settings.subsonic.template.del-confirm")) }; + modal->addFunction("tr", &Wt::WTemplate::Functions::tr); + Wt::WWidget* modalPtr{ modal.get() }; + modal->bindNew("confirm-btn", Wt::WString::tr("Lms.Settings.subsonic-token-del-confirm")) + ->clicked() + .connect(t, [subsonicTokenPtr, updateKeyButtonStates, modalPtr] { + { + auto transaction{ LmsApp->getDbSession().createWriteTransaction() }; + core::Service::get()->clearAuthTokens("subsonic", LmsApp->getUser()->getId()); + } + subsonicTokenPtr->setValueText(""); + updateKeyButtonStates(); + LmsApp->getModalManager().dispose(modalPtr); + }); + modal->bindNew("cancel-btn", Wt::WString::tr("Lms.cancel")) + ->clicked() + .connect(t, [modalPtr] { + LmsApp->getModalManager().dispose(modalPtr); + }); + + LmsApp->getModalManager().show(std::move(modal)); + }); + t->bindWidget("subsonic-token-del-btn", std::move(subsonicTokenDelBtn)); + + subsonicTokenCopyBtn->clicked().connect(t, [subsonicTokenPtr] { + const std::string val{ subsonicTokenPtr->valueText().toUTF8() }; + if (!val.empty()) + { + utils::copyToClipboard(val); + LmsApp->notifyMsg(Notification::Type::Info, Wt::WString::tr("Lms.Settings.subsonic-token-copied")); + } + }); + t->bindWidget("subsonic-token-copy-btn", std::move(subsonicTokenCopyBtn)); + + auto subsonicTokenVisibilityBtn{ std::make_unique(Wt::WString::tr("Lms.template.toggle-visibility-btn"), Wt::TextFormat::XHTML) }; + subsonicTokenVisibilityBtn->clicked().connect(t, [subsonicTokenPtr] { + subsonicTokenPtr->setEchoMode(subsonicTokenPtr->echoMode() == Wt::EchoMode::Password ? Wt::EchoMode::Normal : Wt::EchoMode::Password); + }); + t->bindWidget("subsonic-token-visibility-btn", std::move(subsonicTokenVisibilityBtn)); + + initTooltipsForWidgetTree(*t); + } } // namespace lms::ui diff --git a/src/lms/ui/settings/SubsonicSettingsView.hpp b/src/lms/ui/settings/SubsonicSettingsView.hpp index 52e77176..d04fb797 100644 --- a/src/lms/ui/settings/SubsonicSettingsView.hpp +++ b/src/lms/ui/settings/SubsonicSettingsView.hpp @@ -30,5 +30,7 @@ namespace lms::ui private: void refreshView(); + void refreshForm(); + void refreshSubsonicKey(); }; } // namespace lms::ui