From f5ba63946b46877111dc48b760f03e98cc10548d Mon Sep 17 00:00:00 2001 From: jansol Date: Sat, 8 Jul 2017 14:41:49 +0300 Subject: [PATCH] Improve login flow (#35) * Validate both inferred and explicitly entered server addresses by attempting to call the /versions endpoint * If the domain from the mxid fails validation, try prefixing it with 'matrix' * Only show server address field if address validation ultimately fails --- CMakeLists.txt | 3 +- include/LoginPage.h | 28 ++-- include/MatrixClient.h | 2 + include/{LoginSettings.h => Versions.h} | 25 ++-- resources/icons/error.png | Bin 0 -> 621 bytes resources/res.qrc | 1 + src/LoginPage.cc | 178 +++++++++++++++++------- src/LoginSettings.cc | 61 -------- src/MatrixClient.cc | 27 +++- src/Versions.cc | 62 +++++++++ 10 files changed, 250 insertions(+), 137 deletions(-) rename include/{LoginSettings.h => Versions.h} (65%) create mode 100644 resources/icons/error.png delete mode 100644 src/LoginSettings.cc create mode 100644 src/Versions.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 26aaf1ec..ab93957a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,7 +98,6 @@ set(SRC_FILES src/InputValidator.cc src/Login.cc src/LoginPage.cc - src/LoginSettings.cc src/LogoutDialog.cc src/MainWindow.cc src/MatrixClient.cc @@ -116,6 +115,7 @@ set(SRC_FILES src/TrayIcon.cc src/TopRoomBar.cc src/UserInfoWidget.cc + src/Versions.cc src/WelcomePage.cc src/main.cc @@ -177,7 +177,6 @@ qt5_wrap_cpp(MOC_HEADERS include/TimelineView.h include/TimelineViewManager.h include/LoginPage.h - include/LoginSettings.h include/LogoutDialog.h include/MainWindow.h include/MatrixClient.h diff --git a/include/LoginPage.h b/include/LoginPage.h index 774dd8e9..8e95061a 100644 --- a/include/LoginPage.h +++ b/include/LoginPage.h @@ -23,8 +23,8 @@ #include #include +#include "CircularProgress.h" #include "FlatButton.h" -#include "LoginSettings.h" #include "MatrixClient.h" #include "OverlayModal.h" #include "RaisedButton.h" @@ -50,12 +50,20 @@ private slots: // Callback for the login button. void onLoginButtonClicked(); + // Callback for probing the server found in the mxid + void onMatrixIdEntered(); + + // Callback for probing the manually entered server + void onServerAddressEntered(); + // Displays errors produced during the login. void loginError(QString error_message); - // Manipulate settings modal. - void showSettingsModal(); - void closeSettingsModal(const QString &server); + // Callback for errors produced during server probing + void versionError(QString error_message); + + // Callback for successful server probing + void versionSuccess(); private: QVBoxLayout *top_layout_; @@ -67,8 +75,13 @@ private: QLabel *logo_; QLabel *error_label_; + QHBoxLayout *serverLayout_; + QHBoxLayout *matrixidLayout_; + CircularProgress *spinner_; + QLabel *errorIcon_; + QString inferredServerAddress_; + FlatButton *back_button_; - FlatButton *advanced_settings_button_; RaisedButton *login_button_; QWidget *form_widget_; @@ -77,10 +90,7 @@ private: TextField *matrixid_input_; TextField *password_input_; - - OverlayModal *settings_modal_; - LoginSettings *login_settings_; - QString custom_domain_; + TextField *serverInput_; // Matrix client API provider. QSharedPointer client_; diff --git a/include/MatrixClient.h b/include/MatrixClient.h index 7a4ac24b..e3613ab7 100644 --- a/include/MatrixClient.h +++ b/include/MatrixClient.h @@ -63,11 +63,13 @@ public slots: signals: void loginError(const QString &error); void registerError(const QString &error); + void versionError(const QString &error); void loggedOut(); void loginSuccess(const QString &userid, const QString &homeserver, const QString &token); void registerSuccess(const QString &userid, const QString &homeserver, const QString &token); + void versionSuccess(); void roomAvatarRetrieved(const QString &roomid, const QPixmap &img); void userAvatarRetrieved(const QString &userId, const QImage &img); diff --git a/include/LoginSettings.h b/include/Versions.h similarity index 65% rename from include/LoginSettings.h rename to include/Versions.h index 62e9b2f5..9d7f1eb4 100644 --- a/include/LoginSettings.h +++ b/include/Versions.h @@ -17,21 +17,26 @@ #pragma once -#include +#include +#include -#include "FlatButton.h" -#include "TextField.h" +#include "Deserializable.h" -class LoginSettings : public QFrame + +class VersionsResponse : public Deserializable { - Q_OBJECT public: - explicit LoginSettings(QWidget *parent = nullptr); + void deserialize(const QJsonDocument &data) override; -signals: - void closing(const QString &server); + bool isVersionSupported(unsigned int major, unsigned int minor, unsigned int patch); private: - TextField *input_; - FlatButton *submit_button_; + struct Version_ { + unsigned int major_; + unsigned int minor_; + unsigned int patch_; + }; + + QVector supported_versions_; + }; diff --git a/resources/icons/error.png b/resources/icons/error.png new file mode 100644 index 0000000000000000000000000000000000000000..295dbf06ddad543dc02ed4694d2ec69e4c80e8f8 GIT binary patch literal 621 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f+@+{-GzZ+Rj;xUkjGiz z5m^kh={g8AI%&+V01C2~c>21sKV)ZRS5lvRbagfZ1EaO4i(`n#@wHP8^O_w5T;->2 zy%Lf)_a1MqpP=`hW@d$T4;DuVI+^V7do68YvMyHH{gz0>VLs_C88=_LeNHd`H-9eY zhPk@(dp1AswY~7c_ptc@}|Ikq+W)tC=(k#Ej z@}TJ~>Gqdz-+jJo{=t3BUHt_$nE%Cc_F-lEp_(8o?o0z|IuIDYJD`+52ILN=avE_7_* zc8^-!qwB7xv@3Gq#68hBe;cp;Fn2HeDK6!+)hn95N4$@FxHZB`Z%^25g;$H7W}V-b zu=hWcyH0@X9)6eiz>rlfag8WRNi0dVN-jzTQVd20hUU5kKx7(XU~Xk>Vr6WpZD3?& zU{Jic)(b^LZhlH;S|x4`O#hC418R^2*$|wcR#Ki=l*&+EUaps!mtCBkSdglhUz9%k SosASw5re0zpUXO@geCwgb?b8g literal 0 HcmV?d00001 diff --git a/resources/res.qrc b/resources/res.qrc index 56bf7144..13a7d309 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -12,6 +12,7 @@ icons/user-shape.png icons/power-button-off.png icons/smile.png + icons/error.png icons/emoji-categories/people.png icons/emoji-categories/nature.png diff --git a/src/LoginPage.cc b/src/LoginPage.cc index 4329baad..e3f51484 100644 --- a/src/LoginPage.cc +++ b/src/LoginPage.cc @@ -21,10 +21,9 @@ #include "LoginPage.h" LoginPage::LoginPage(QSharedPointer client, QWidget *parent) - : QWidget(parent) - , settings_modal_{nullptr} - , login_settings_{nullptr} - , client_{client} + : QWidget(parent) + , inferredServerAddress_() + , client_{client} { setStyleSheet("background-color: #f9f9f9"); @@ -38,9 +37,8 @@ LoginPage::LoginPage(QSharedPointer client, QWidget *parent) back_button_->setMinimumSize(QSize(30, 30)); back_button_->setForegroundColor("#333333"); - advanced_settings_button_ = new FlatButton(this); - advanced_settings_button_->setMinimumSize(QSize(30, 30)); - advanced_settings_button_->setForegroundColor("#333333"); + top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); + top_bar_layout_->addStretch(1); QIcon icon; icon.addFile(":/icons/icons/left-angle.png", QSize(), QIcon::Normal, QIcon::Off); @@ -51,13 +49,6 @@ LoginPage::LoginPage(QSharedPointer client, QWidget *parent) QIcon advanced_settings_icon; advanced_settings_icon.addFile(":/icons/icons/cog.png", QSize(), QIcon::Normal, QIcon::Off); - advanced_settings_button_->setIcon(advanced_settings_icon); - advanced_settings_button_->setIconSize(QSize(24, 24)); - - top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter); - top_bar_layout_->addStretch(1); - top_bar_layout_->addWidget(advanced_settings_button_, 0, Qt::AlignRight | Qt::AlignVCenter); - logo_ = new QLabel(this); logo_->setPixmap(QPixmap(":/logos/nheko-128.png")); @@ -85,6 +76,19 @@ LoginPage::LoginPage(QSharedPointer client, QWidget *parent) matrixid_input_->setBackgroundColor("#f9f9f9"); matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org")); + spinner_ = new CircularProgress(this); + spinner_->setColor("#acc7dc"); + spinner_->setSize(32); + spinner_->setMaximumWidth(spinner_->width()); + spinner_->hide(); + + errorIcon_ = new QLabel(this); + errorIcon_->setPixmap(QPixmap(":/icons/icons/error.png")); + errorIcon_->hide(); + + matrixidLayout_ = new QHBoxLayout(); + matrixidLayout_->addWidget(matrixid_input_, 0, Qt::AlignVCenter); + password_input_ = new TextField(this); password_input_->setTextColor("#333333"); password_input_->setLabel(tr("Password")); @@ -92,8 +96,20 @@ LoginPage::LoginPage(QSharedPointer client, QWidget *parent) password_input_->setBackgroundColor("#f9f9f9"); password_input_->setEchoMode(QLineEdit::Password); - form_layout_->addWidget(matrixid_input_, Qt::AlignHCenter, 0); + serverInput_ = new TextField(this); + serverInput_->setTextColor("#333333"); + serverInput_->setLabel("Homeserver address"); + serverInput_->setInkColor("#555459"); + serverInput_->setBackgroundColor("#f9f9f9"); + serverInput_->setPlaceholderText("matrix.org"); + serverInput_->hide(); + + serverLayout_ = new QHBoxLayout(); + serverLayout_->addWidget(serverInput_, 0, Qt::AlignVCenter); + + form_layout_->addLayout(matrixidLayout_); form_layout_->addWidget(password_input_, Qt::AlignHCenter, 0); + form_layout_->addLayout(serverLayout_); button_layout_ = new QHBoxLayout(); button_layout_->setSpacing(0); @@ -128,8 +144,12 @@ LoginPage::LoginPage(QSharedPointer client, QWidget *parent) connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked())); connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); + connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click())); connect(client_.data(), SIGNAL(loginError(QString)), this, SLOT(loginError(QString))); - connect(advanced_settings_button_, SIGNAL(clicked()), this, SLOT(showSettingsModal())); + connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered())); + connect(client_.data(), SIGNAL(versionError(QString)), this, SLOT(versionError(QString))); + connect(client_.data(), SIGNAL(versionSuccess()), this, SLOT(versionSuccess())); + connect(serverInput_, SIGNAL(editingFinished()), this, SLOT(onServerAddressEntered())); matrixid_input_->setValidator(&InputValidator::Id); } @@ -139,63 +159,115 @@ void LoginPage::loginError(QString error) error_label_->setText(error); } -void LoginPage::onLoginButtonClicked() +void LoginPage::onMatrixIdEntered() { error_label_->setText(""); if (!matrixid_input_->hasAcceptableInput()) { loginError(tr("Invalid Matrix ID")); + return; } else if (password_input_->text().isEmpty()) { loginError(tr("Empty password")); + } + + QString homeServer = matrixid_input_->text().split(":").at(1); + if (homeServer != inferredServerAddress_) { + serverInput_->hide(); + serverLayout_->removeWidget(errorIcon_); + errorIcon_->hide(); + if (serverInput_->isVisible()) { + matrixidLayout_->removeWidget(spinner_); + serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->show(); + } else { + serverLayout_->removeWidget(spinner_); + matrixidLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->show(); + } + + inferredServerAddress_ = homeServer; + serverInput_->setText(homeServer); + client_->setServer(homeServer); + client_->versions(); + } +} + +void LoginPage::onServerAddressEntered() +{ + error_label_->setText(""); + client_->setServer(serverInput_->text()); + client_->versions(); + + serverLayout_->removeWidget(errorIcon_); + errorIcon_->hide(); + serverLayout_->addWidget(spinner_, 0, Qt::AlignVCenter | Qt::AlignRight); + spinner_->show(); +} + +void LoginPage::versionError(QString error) +{ + // Matrix homeservers are often kept on a subdomain called 'matrix' + // so let's try that next, unless the address was set explicitly or the domain part of the username already points to this subdomain + QUrl currentServer = client_->getHomeServer(); + QString mxidAddress = matrixid_input_->text().split(":").at(1); + if (currentServer.host() == inferredServerAddress_ && !currentServer.host().startsWith("matrix")) { + error_label_->setText(""); + currentServer.setHost(QString("matrix.")+currentServer.host()); + serverInput_->setText(currentServer.host()); + client_->setServer(currentServer.host()); + client_->versions(); + return; + } + + error_label_->setText(error); + serverInput_->show(); + + spinner_->hide(); + serverLayout_->removeWidget(spinner_); + serverLayout_->addWidget(errorIcon_, 0, Qt::AlignVCenter | Qt::AlignRight); + errorIcon_->show(); + matrixidLayout_->removeWidget(spinner_); +} + +void LoginPage::versionSuccess() +{ + serverLayout_->removeWidget(spinner_); + matrixidLayout_->removeWidget(spinner_); + spinner_->hide(); + + if (serverInput_->isVisible()) + serverInput_->hide(); +} + +void LoginPage::onLoginButtonClicked() +{ + error_label_->setText(""); + + if (!matrixid_input_->hasAcceptableInput()) { + loginError("Invalid Matrix ID"); + } else if (password_input_->text().isEmpty()) { + loginError("Empty password"); } else { QString user = matrixid_input_->text().split(":").at(0).split("@").at(1); QString password = password_input_->text(); - - QString home_server = custom_domain_.isEmpty() - ? matrixid_input_->text().split(":").at(1) - : custom_domain_; - - client_->setServer(home_server); + client_->setServer(serverInput_->text()); client_->login(user, password); } } -void LoginPage::showSettingsModal() -{ - if (login_settings_ == nullptr) { - login_settings_ = new LoginSettings(this); - connect(login_settings_, &LoginSettings::closing, this, &LoginPage::closeSettingsModal); - } - - if (settings_modal_ == nullptr) { - settings_modal_ = new OverlayModal(this, login_settings_); - settings_modal_->setDuration(100); - settings_modal_->setColor(QColor(55, 55, 55, 170)); - } - - settings_modal_->fadeIn(); -} - -void LoginPage::closeSettingsModal(const QString &server) -{ - custom_domain_ = server; - settings_modal_->fadeOut(); -} - void LoginPage::reset() { matrixid_input_->clear(); password_input_->clear(); + serverInput_->clear(); - if (settings_modal_ != nullptr) { - settings_modal_->deleteLater(); - settings_modal_ = nullptr; - } + spinner_->hide(); + errorIcon_->hide(); + serverLayout_->removeWidget(spinner_); + serverLayout_->removeWidget(errorIcon_); + matrixidLayout_->removeWidget(spinner_); - if (login_settings_ != nullptr) { - login_settings_->deleteLater(); - login_settings_ = nullptr; - } + inferredServerAddress_.clear(); } void LoginPage::onBackButtonClicked() diff --git a/src/LoginSettings.cc b/src/LoginSettings.cc deleted file mode 100644 index b3725caf..00000000 --- a/src/LoginSettings.cc +++ /dev/null @@ -1,61 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program 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. - * - * This program 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 this program. If not, see . - */ - -#include -#include - -#include "LoginSettings.h" - -LoginSettings::LoginSettings(QWidget *parent) - : QFrame(parent) -{ - setMaximumSize(400, 400); - setStyleSheet("background-color: #f9f9f9"); - - auto layout = new QVBoxLayout(this); - layout->setSpacing(30); - layout->setContentsMargins(20, 20, 20, 10); - - input_ = new TextField(this); - input_->setTextColor("#555459"); - input_->setLabel("Homeserver's domain"); - input_->setInkColor("#333333"); - input_->setBackgroundColor("#f9f9f9"); - input_->setPlaceholderText("e.g matrix.domain.org:3434"); - - submit_button_ = new FlatButton("OK", this); - submit_button_->setBackgroundColor("black"); - submit_button_->setForegroundColor("black"); - submit_button_->setCursor(QCursor(Qt::PointingHandCursor)); - submit_button_->setFontSize(15); - submit_button_->setFixedHeight(50); - submit_button_->setCornerRadius(3); - - auto label = new QLabel("Advanced Settings", this); - label->setStyleSheet("color: #333333"); - - layout->addWidget(label); - layout->addWidget(input_); - layout->addWidget(submit_button_); - - setLayout(layout); - - connect(input_, SIGNAL(returnPressed()), submit_button_, SIGNAL(clicked())); - connect(submit_button_, &QPushButton::clicked, [=]() { - emit closing(input_->text()); - }); -} diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc index ebecb05a..11ac61c7 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc @@ -30,6 +30,7 @@ #include "MatrixClient.h" #include "Profile.h" #include "Register.h" +#include "Versions.h" MatrixClient::MatrixClient(QString server, QObject *parent) : QNetworkAccessManager(parent) @@ -57,12 +58,34 @@ void MatrixClient::onVersionsResponse(QNetworkReply *reply) { reply->deleteLater(); - qDebug() << "Handling the versions response"; + int status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (status_code == 404) { + emit versionError("Versions endpoint was not found on the server. Possibly not a Matrix server"); + return; + } + + if (status_code >= 400) { + qWarning() << "API version error: " << reply->errorString(); + emit versionError("An unknown error occured. Please try again."); + return; + } auto data = reply->readAll(); auto json = QJsonDocument::fromJson(data); - qDebug() << json; + VersionsResponse response; + + try { + response.deserialize(json); + if (!response.isVersionSupported(0, 2, 0)) + emit versionError("Server does not support required API version."); + else + emit versionSuccess(); + } catch (DeserializationException &e) { + qWarning() << "Malformed JSON response" << e.what(); + emit versionError("Malformed response. Possibly not a Matrix server"); + } } void MatrixClient::onLoginResponse(QNetworkReply *reply) diff --git a/src/Versions.cc b/src/Versions.cc new file mode 100644 index 00000000..48895645 --- /dev/null +++ b/src/Versions.cc @@ -0,0 +1,62 @@ +/* + * nheko Copyright (C) 2017 Jan Solanti + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#include "Deserializable.h" +#include "Versions.h" + +void VersionsResponse::deserialize(const QJsonDocument &data) +{ + if (!data.isObject()) + throw DeserializationException("Versions response is not a JSON object"); + + QJsonObject object = data.object(); + + if (object.value("versions") == QJsonValue::Undefined) + throw DeserializationException("Versions: missing version list"); + + auto versions = object.value("versions").toArray(); + for (auto const &elem: versions) { + QString str = elem.toString(); + QRegExp rx("r(\\d+)\\.(\\d+)\\.(\\d+)"); + + if (rx.indexIn(str) == -1) + throw DeserializationException("Invalid version string in versions response"); + + struct Version_ v; + v.major_ = rx.cap(1).toUInt(); + v.minor_ = rx.cap(2).toUInt(); + v.patch_ = rx.cap(3).toUInt(); + + supported_versions_.push_back(v); + } +} + +bool VersionsResponse::isVersionSupported(unsigned int major, unsigned int minor, unsigned int patch) +{ + for (auto &v: supported_versions_) { + if (v.major_ == major && v.minor_ == minor && v.patch_ >= patch) + return true; + } + + return false; +}