diff --git a/Dockerfile b/Dockerfile index dca5f0650..2b01e0b08 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,7 +42,7 @@ RUN ./emsdk activate 4.0.7 && \ qt-cmake -S /app -B /build \ -DQT_HOST_PATH=$QTSDK_DIR/6.10.3/gcc_64 \ -DCMAKE_BUILD_TYPE=Release \ - -DWITH_OPENSSL=OFF -DWITH_TESTS=OFF -DWITH_WEBSOCKET=ON -DWITH_UPDATER=OFF -DPACKAGE_TYPE=Wasm && \ + -DWITH_OPENSSL=OFF -DWITH_TESTS=OFF -DWITH_WEBSOCKET=ON -DWITH_UPDATER=OFF -DWITH_QTKEYCHAIN=ON -DPACKAGE_TYPE=Wasm && \ ACTUAL_JOBS=${JOBS:-$(nproc)} && \ cmake --build /build --parallel ${ACTUAL_JOBS} && \ cmake --install /build --prefix /dist && \ diff --git a/external/qtkeychain/CMakeLists.txt b/external/qtkeychain/CMakeLists.txt index 07f2e088e..1787cfe61 100644 --- a/external/qtkeychain/CMakeLists.txt +++ b/external/qtkeychain/CMakeLists.txt @@ -1,7 +1,7 @@ if(QTKEYCHAIN_FOUND) message(STATUS "QtKeychain found: ${QTKEYCHAIN_LIBRARIES} ${QTKEYCHAIN_INCLUDE_DIRS}") else() - if(WIN32 OR APPLE) + if(WIN32 OR APPLE OR EMSCRIPTEN) message(STATUS "Could not find system QtKeychain; building from local source tree") else() message(FATAL_ERROR "QtKeychain NOT found: use `-DWITH_QTKEYCHAIN=OFF` to build without account autologin") @@ -33,8 +33,8 @@ else() else() include(ExternalProject) ExternalProject_Add(qtkeychain_repo - GIT_REPOSITORY "https://github.com/frankosterfeld/qtkeychain.git" - GIT_TAG "7668a63a3669400223c5a563928ae042f499dbf4" + GIT_REPOSITORY "https://github.com/nschimme/qtkeychain.git" + GIT_TAG "wasm" SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/qtkeychain-src" BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/qtkeychain-build" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 91168fc0c..6226c6907 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -525,6 +525,8 @@ set(mmapper_SRCS preferences/grouppage.h preferences/mumeprotocolpage.cpp preferences/mumeprotocolpage.h + preferences/ManagePasswordDialog.cpp + preferences/ManagePasswordDialog.h preferences/parserpage.cpp preferences/parserpage.h preferences/pathmachinepage.cpp @@ -665,6 +667,7 @@ set(mmapper_UIS preferences/graphicspage.ui preferences/grouppage.ui preferences/mumeprotocolpage.ui + preferences/ManagePasswordDialog.ui preferences/parserpage.ui preferences/pathmachinepage.ui ) diff --git a/src/configuration/PasswordConfig.cpp b/src/configuration/PasswordConfig.cpp index 383eaf8e4..7f7361ff7 100644 --- a/src/configuration/PasswordConfig.cpp +++ b/src/configuration/PasswordConfig.cpp @@ -3,11 +3,13 @@ #include "PasswordConfig.h" +#include "../global/ConfigConsts-Computed.h" +#include "../global/ConfigEnums.h" #include "../global/macros.h" #ifndef MMAPPER_NO_QTKEYCHAIN -static const QLatin1String PASSWORD_KEY("password"); static const QLatin1String APP_NAME("org.mume.mmapper"); +static const QLatin1String PASSWORD_KEY("password"); #endif PasswordConfig::PasswordConfig(QObject *const parent) @@ -15,21 +17,43 @@ PasswordConfig::PasswordConfig(QObject *const parent) #ifndef MMAPPER_NO_QTKEYCHAIN , m_readJob(APP_NAME) , m_writeJob(APP_NAME) + , m_deleteJob(APP_NAME) { m_readJob.setAutoDelete(false); m_writeJob.setAutoDelete(false); + m_deleteJob.setAutoDelete(false); + + auto handleError = [this](const QKeychain::Job &job) { + if constexpr (CURRENT_PLATFORM == PlatformEnum::Wasm) { + if (job.error() == QKeychain::AccessDeniedByUser) { + return true; + } + } + emit sig_error(job.errorString()); + return false; + }; - connect(&m_readJob, &QKeychain::ReadPasswordJob::finished, [this]() { + connect(&m_readJob, &QKeychain::ReadPasswordJob::finished, [this, handleError]() { if (m_readJob.error()) { - emit sig_error(m_readJob.errorString()); + handleError(m_readJob); } else { emit sig_incomingPassword(m_readJob.textData()); } }); - connect(&m_writeJob, &QKeychain::WritePasswordJob::finished, [this]() { + connect(&m_writeJob, &QKeychain::WritePasswordJob::finished, [this, handleError]() { if (m_writeJob.error()) { - emit sig_error(m_writeJob.errorString()); + handleError(m_writeJob); + } else { + emit sig_passwordSaved(); + } + }); + + connect(&m_deleteJob, &QKeychain::DeletePasswordJob::finished, [this, handleError]() { + if (m_deleteJob.error()) { + handleError(m_deleteJob); + } else { + emit sig_passwordDeleted(); } }); } @@ -38,24 +62,49 @@ PasswordConfig::PasswordConfig(QObject *const parent) } #endif -void PasswordConfig::setPassword(const QString &password) +void PasswordConfig::setPassword(const QString &accountName, const QString &password) { #ifndef MMAPPER_NO_QTKEYCHAIN - m_writeJob.setKey(PASSWORD_KEY); + if constexpr (CURRENT_PLATFORM == PlatformEnum::Wasm) { + m_writeJob.setKey(accountName); + } else { + m_writeJob.setKey(PASSWORD_KEY); + } m_writeJob.setTextData(password); m_writeJob.start(); #else + std::ignore = accountName; std::ignore = password; emit sig_error("Password setting is not available."); #endif } -void PasswordConfig::getPassword() +void PasswordConfig::getPassword(const QString &accountName) { #ifndef MMAPPER_NO_QTKEYCHAIN - m_readJob.setKey(PASSWORD_KEY); + if constexpr (CURRENT_PLATFORM == PlatformEnum::Wasm) { + m_readJob.setKey(accountName); + } else { + m_readJob.setKey(PASSWORD_KEY); + } m_readJob.start(); #else + std::ignore = accountName; emit sig_error("Password retrieval is not available."); #endif } + +void PasswordConfig::deletePassword(const QString &accountName) +{ +#ifndef MMAPPER_NO_QTKEYCHAIN + if constexpr (CURRENT_PLATFORM == PlatformEnum::Wasm) { + m_deleteJob.setKey(accountName); + } else { + m_deleteJob.setKey(PASSWORD_KEY); + } + m_deleteJob.start(); +#else + std::ignore = accountName; + emit sig_error("Password deletion is not available."); +#endif +} diff --git a/src/configuration/PasswordConfig.h b/src/configuration/PasswordConfig.h index fc8186a5a..533d84694 100644 --- a/src/configuration/PasswordConfig.h +++ b/src/configuration/PasswordConfig.h @@ -3,6 +3,7 @@ // Copyright (C) 2019 The MMapper Authors #include "../global/RuleOf5.h" +#include "../global/macros.h" #include #include @@ -11,7 +12,7 @@ #include #endif -class PasswordConfig final : public QObject +class NODISCARD_QOBJECT PasswordConfig final : public QObject { Q_OBJECT @@ -19,6 +20,7 @@ class PasswordConfig final : public QObject #ifndef MMAPPER_NO_QTKEYCHAIN QKeychain::ReadPasswordJob m_readJob; QKeychain::WritePasswordJob m_writeJob; + QKeychain::DeletePasswordJob m_deleteJob; #endif public: @@ -26,10 +28,13 @@ class PasswordConfig final : public QObject ~PasswordConfig() final = default; DELETE_CTORS_AND_ASSIGN_OPS(PasswordConfig); - void setPassword(const QString &password); - void getPassword(); + void setPassword(const QString &accountName, const QString &password); + void getPassword(const QString &accountName); + void deletePassword(const QString &accountName); signals: void sig_error(const QString &msg); void sig_incomingPassword(const QString &password); + void sig_passwordSaved(); + void sig_passwordDeleted(); }; diff --git a/src/preferences/ManagePasswordDialog.cpp b/src/preferences/ManagePasswordDialog.cpp new file mode 100644 index 000000000..fec4fa9e5 --- /dev/null +++ b/src/preferences/ManagePasswordDialog.cpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2019 The MMapper Authors + +#include "ManagePasswordDialog.h" + +#include "../global/ConfigConsts-Computed.h" +#include "../global/ConfigEnums.h" +#include "ui_ManagePasswordDialog.h" + +#include +#include + +ManagePasswordDialog::ManagePasswordDialog(QWidget *parent) + : QDialog(parent) + , ui(new Ui::ManagePasswordDialog) +{ + ui->setupUi(this); + + if constexpr (CURRENT_PLATFORM == PlatformEnum::Wasm) { + setWindowTitle("Manage Account"); + ui->label_2->hide(); + ui->accountPassword->hide(); + ui->showPassword->hide(); + ui->deleteButton->hide(); + } else { + connect(ui->showPassword, &QToolButton::toggled, this, [this](bool checked) { + ui->accountPassword->setEchoMode(checked ? QLineEdit::Normal : QLineEdit::Password); + }); + + connect(ui->deleteButton, &QPushButton::clicked, this, [this]() { + emit sig_deleteRequested(); + ui->accountPassword->clear(); + ui->deleteButton->setEnabled(false); + }); + } +} + +ManagePasswordDialog::~ManagePasswordDialog() +{ + delete ui; +} + +void ManagePasswordDialog::setAccountName(const QString &name) +{ + ui->accountName->setText(name); +} + +QString ManagePasswordDialog::accountName() const +{ + return ui->accountName->text(); +} + +void ManagePasswordDialog::setPassword(const QString &password) +{ + ui->accountPassword->setText(password); + if constexpr (CURRENT_PLATFORM != PlatformEnum::Wasm) { + ui->deleteButton->setEnabled(!password.isEmpty()); + } +} + +QString ManagePasswordDialog::password() const +{ + return ui->accountPassword->text(); +} diff --git a/src/preferences/ManagePasswordDialog.h b/src/preferences/ManagePasswordDialog.h new file mode 100644 index 000000000..639d40316 --- /dev/null +++ b/src/preferences/ManagePasswordDialog.h @@ -0,0 +1,32 @@ +#pragma once +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2019 The MMapper Authors + +#include "../global/macros.h" + +#include + +namespace Ui { +class ManagePasswordDialog; +} + +class NODISCARD_QOBJECT ManagePasswordDialog final : public QDialog +{ + Q_OBJECT + +private: + Ui::ManagePasswordDialog *const ui; + +public: + explicit ManagePasswordDialog(QWidget *parent = nullptr); + ~ManagePasswordDialog() final; + + void setAccountName(const QString &name); + NODISCARD QString accountName() const; + + void setPassword(const QString &password); + NODISCARD QString password() const; + +signals: + void sig_deleteRequested(); +}; diff --git a/src/preferences/ManagePasswordDialog.ui b/src/preferences/ManagePasswordDialog.ui new file mode 100644 index 000000000..438399eab --- /dev/null +++ b/src/preferences/ManagePasswordDialog.ui @@ -0,0 +1,93 @@ + + + ManagePasswordDialog + + + + 0 + 0 + 350 + 180 + + + + Manage Password + + + + + + + + Account: + + + accountName + + + + + + + Password: + + + accountPassword + + + + + + + + + + QLineEdit::Password + + + + + + + View + + + true + + + + + + + + + + + Delete + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + ManagePasswordDialog + accept() + + + diff --git a/src/preferences/configdialog.ui b/src/preferences/configdialog.ui index a0828c8e7..9f7898302 100644 --- a/src/preferences/configdialog.ui +++ b/src/preferences/configdialog.ui @@ -17,13 +17,13 @@ - 110 + 100 0 - 110 + 100 16777215 @@ -43,11 +43,8 @@ - - - 520 - 300 - + + Qt::ScrollBarAlwaysOff true diff --git a/src/preferences/generalpage.cpp b/src/preferences/generalpage.cpp index ed75b49f3..06e95807f 100644 --- a/src/preferences/generalpage.cpp +++ b/src/preferences/generalpage.cpp @@ -101,15 +101,19 @@ GeneralPage::GeneralPage(QWidget *parent) }); connect(ui->configurationResetButton, &QAbstractButton::clicked, this, [this]() { - QMessageBox::StandardButton reply - = QMessageBox::question(this, + auto *box = new QMessageBox(QMessageBox::Question, "MMapper Factory Reset", "Are you sure you want to perform a factory reset?", - QMessageBox::Yes | QMessageBox::No); - if (reply == QMessageBox::Yes) { - setConfig().reset(); - emit sig_reloadConfig(); - } + QMessageBox::Yes | QMessageBox::No, + this); + box->setAttribute(Qt::WA_DeleteOnClose); + connect(box, &QMessageBox::finished, this, [this](int result) { + if (result == QMessageBox::Yes) { + setConfig().reset(); + emit sig_reloadConfig(); + } + }); + box->open(); }); connect(ui->configurationExportButton, &QAbstractButton::clicked, this, []() { @@ -170,41 +174,79 @@ GeneralPage::GeneralPage(QWidget *parent) QFileDialog::getOpenFileContent(nameFilter, importFile); }); - connect(ui->autoLogin, &QCheckBox::stateChanged, this, [this]() { + connect(ui->autoLogin, &QCheckBox::clicked, this, [this](bool /*checked*/) { setConfig().account.rememberLogin = ui->autoLogin->isChecked(); }); - connect(ui->accountName, &QLineEdit::textChanged, this, [](const QString &account) { - setConfig().account.accountName = account; - }); - connect(&passCfg, &PasswordConfig::sig_error, this, [this](const QString &msg) { qWarning() << msg; - QMessageBox::warning(this, "Password Error", msg); + auto *box = new QMessageBox(QMessageBox::Warning, + "Password Error", + msg, + QMessageBox::Ok, + this); + box->setAttribute(Qt::WA_DeleteOnClose); + box->open(); }); connect(&passCfg, &PasswordConfig::sig_incomingPassword, this, [this](const QString &password) { - ui->showPassword->setText("Hide Password"); - ui->accountPassword->setText(password); - ui->accountPassword->setEchoMode(QLineEdit::Normal); - }); + if (ui->setPassword->property("requesting").toBool()) { + ui->setPassword->setProperty("requesting", false); + const QString accountName = getConfig().account.accountName; + auto *dlg = new ManagePasswordDialog(this); + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->setAccountName(accountName); + dlg->setPassword(password); + connect(dlg, &ManagePasswordDialog::sig_deleteRequested, this, [this, accountName]() { + passCfg.deletePassword(accountName); + }); - connect(ui->accountPassword, &QLineEdit::textEdited, this, [this](const QString &password) { - setConfig().account.accountPassword = !password.isEmpty(); - passCfg.setPassword(password); + connect(dlg, &QDialog::accepted, this, [this, dlg]() { + const QString newAccountName = dlg->accountName(); + const QString newPassword = dlg->password(); + if (!newPassword.isEmpty()) { + passCfg.setPassword(newAccountName, newPassword); + setConfig().account.accountName = newAccountName; + } + }); + dlg->open(); + } }); - connect(ui->showPassword, &QAbstractButton::clicked, this, [this]() { - if (ui->showPassword->text() == "Hide Password") { - ui->showPassword->setText("Show Password"); - ui->accountPassword->clear(); - ui->accountPassword->setEchoMode(QLineEdit::Password); - } else if (getConfig().account.accountPassword && ui->accountPassword->text().isEmpty()) { - ui->showPassword->setText("Request Password"); - passCfg.getPassword(); + connect(&passCfg, &PasswordConfig::sig_passwordSaved, this, [this]() { + setConfig().account.accountPassword = true; + updateAutoLoginEnabled(); + + if (!ui->autoLogin->isChecked()) { + auto *box = new QMessageBox(QMessageBox::Question, + "Login", + "A password was saved. Would you like to also enable " + "automatic login for this account?", + QMessageBox::Yes | QMessageBox::No, + this); + box->setAttribute(Qt::WA_DeleteOnClose); + connect(box, &QMessageBox::finished, this, [this](int result) { + if (result == QMessageBox::Yes) { + ui->autoLogin->setChecked(true); + setConfig().account.rememberLogin = true; + } + }); + box->open(); } }); + connect(&passCfg, &PasswordConfig::sig_passwordDeleted, this, [this]() { + auto &account = setConfig().account; + account.accountPassword = false; + account.rememberLogin = false; + ui->autoLogin->setChecked(false); + updateAutoLoginEnabled(); + }); + + connect(ui->setPassword, &QPushButton::clicked, this, &GeneralPage::slot_setPasswordClicked); + + connect(ui->remoteName, &QLineEdit::textChanged, this, [this]() { updateAutoLoginEnabled(); }); + connect(ui->resourceLineEdit, &QLineEdit::textChanged, this, [](const QString &text) { setConfig().canvas.resourcesDirectory = text; }); @@ -288,15 +330,10 @@ void GeneralPage::slot_loadConfig() if constexpr (NO_QTKEYCHAIN) { ui->autoLogin->setEnabled(false); - ui->accountName->setEnabled(false); - ui->accountPassword->setEnabled(false); - ui->showPassword->setEnabled(false); + ui->setPassword->setEnabled(false); } else { ui->autoLogin->setChecked(account.rememberLogin); - ui->accountName->setText(account.accountName); - if (!account.accountPassword) { - ui->accountPassword->setPlaceholderText(""); - } + updateAutoLoginEnabled(); } } @@ -375,3 +412,58 @@ void GeneralPage::slot_themeComboBoxChanged(int index) { setConfig().general.setTheme(static_cast(index)); } + +void GeneralPage::updateAutoLoginEnabled() +{ + if constexpr (NO_QTKEYCHAIN) { + ui->autoLogin->setEnabled(false); + return; + } + + const auto &account = getConfig().account; + const bool hasAccountName = !account.accountName.isEmpty(); + const bool hasPassword = account.accountPassword; + + ui->autoLogin->setEnabled(hasAccountName && hasPassword); +} + +void GeneralPage::slot_setPasswordClicked() +{ + const QString accountName = getConfig().account.accountName; + + if constexpr (CURRENT_PLATFORM == PlatformEnum::Wasm) { + auto *dlg = new ManagePasswordDialog(this); + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->setAccountName(accountName); + connect(dlg, &QDialog::accepted, this, [this, dlg]() { + const QString newAccountName = dlg->accountName(); + if (!newAccountName.isEmpty()) { + passCfg.setPassword(newAccountName, ""); + setConfig().account.accountName = newAccountName; + } + }); + dlg->open(); + } else { + if (getConfig().account.accountPassword) { + ui->setPassword->setProperty("requesting", true); + passCfg.getPassword(accountName); + } else { + auto *dlg = new ManagePasswordDialog(this); + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->setAccountName(accountName); + connect(dlg, &ManagePasswordDialog::sig_deleteRequested, this, [this, accountName]() { + passCfg.deletePassword(accountName); + }); + + connect(dlg, &QDialog::accepted, this, [this, dlg]() { + const QString newAccountName = dlg->accountName(); + const QString password = dlg->password(); + if (!password.isEmpty()) { + passCfg.setPassword(newAccountName, password); + setConfig().account.accountName = newAccountName; + } + }); + dlg->open(); + } + } +} diff --git a/src/preferences/generalpage.h b/src/preferences/generalpage.h index 2a1e9fe50..7e0014b0d 100644 --- a/src/preferences/generalpage.h +++ b/src/preferences/generalpage.h @@ -7,6 +7,7 @@ #include "../configuration/PasswordConfig.h" #include "../global/macros.h" +#include "ManagePasswordDialog.h" #include #include @@ -48,4 +49,8 @@ public slots: void slot_displayMumeClockStateChanged(int); void slot_displayXPStatusStateChanged(int); void slot_themeComboBoxChanged(int); + void slot_setPasswordClicked(); + +private: + void updateAutoLoginEnabled(); }; diff --git a/src/preferences/generalpage.ui b/src/preferences/generalpage.ui index 532cc0e98..f24ff3cbd 100644 --- a/src/preferences/generalpage.ui +++ b/src/preferences/generalpage.ui @@ -2,26 +2,12 @@ GeneralPage - - - 0 - 0 - 459 - 792 - - Form1 - - - 435 - 179 - - Connection @@ -111,7 +97,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -158,53 +144,39 @@ Account - - - - - Account: - - - accountName - - - - - + + + - Password: - - - accountPassword + Automatic login - - - - - - - QLineEdit::Password - - - *********** + + + + + 0 + 0 + - - - - - Show Password + Manage Account - - - - Remember my login + + + + Qt::Orientation::Horizontal - + + + 40 + 20 + + + @@ -215,13 +187,13 @@ Startup - + - QFrame::NoFrame + QFrame::Shape::NoFrame - QFrame::Plain + QFrame::Shadow::Plain 0 @@ -278,7 +250,7 @@ - + Check for update @@ -290,12 +262,6 @@ - - - 435 - 73 - - Apperance @@ -303,22 +269,14 @@ 9 - - - - Show Mume clock - - - - - - - Show Session XP - - - + + + 0 + 0 + + System @@ -338,6 +296,12 @@ + + + 0 + 0 + + Theme: @@ -346,6 +310,20 @@ + + + + Show Mume clock + + + + + + + Show Session XP + + + @@ -374,34 +352,28 @@ - - - 435 - 105 - - Emulated Exits - + Show emulated exits - - + + - Show notes + Show hidden exit flags - - + + - Show hidden exit flags + Show notes @@ -410,20 +382,14 @@ - - - 435 - 107 - - Advanced - - + + - Import Settings... + Reset to Default Settings @@ -437,10 +403,10 @@ - - + + - Export Settings... + Import Settings... @@ -454,17 +420,30 @@ - - + + - Reset to Default Settings + Export Settings... - + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + - Qt::Horizontal + Qt::Orientation::Horizontal @@ -480,7 +459,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -498,17 +477,12 @@ encryptionCheckBox localPort charsetComboBox - autoLogin - accountName - accountPassword - showPassword checkForUpdateCheckBox autoLoadCheck autoLoadFileName selectWorldFileButton themeComboBox displayMumeClockCheckBox - displayXPStatusCheckBox resourceLineEdit resourcePushButton emulatedExitsCheckBox diff --git a/src/proxy/proxy.cpp b/src/proxy/proxy.cpp index 78a62f3bd..1842dbe47 100644 --- a/src/proxy/proxy.cpp +++ b/src/proxy/proxy.cpp @@ -505,7 +505,7 @@ void Proxy::allocMudTelnet() const auto &account = getConfig().account; if (account.rememberLogin && !account.accountName.isEmpty() && account.accountPassword) { // fetch asynchronously from keychain - getProxy().getPasswordConfig().getPassword(); + getProxy().getPasswordConfig().getPassword(account.accountName); } }