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
This commit is contained in:
jansol 2017-07-08 14:41:49 +03:00 committed by mujx
parent 03437cc693
commit f5ba63946b
10 changed files with 250 additions and 137 deletions

View file

@ -98,7 +98,6 @@ set(SRC_FILES
src/InputValidator.cc src/InputValidator.cc
src/Login.cc src/Login.cc
src/LoginPage.cc src/LoginPage.cc
src/LoginSettings.cc
src/LogoutDialog.cc src/LogoutDialog.cc
src/MainWindow.cc src/MainWindow.cc
src/MatrixClient.cc src/MatrixClient.cc
@ -116,6 +115,7 @@ set(SRC_FILES
src/TrayIcon.cc src/TrayIcon.cc
src/TopRoomBar.cc src/TopRoomBar.cc
src/UserInfoWidget.cc src/UserInfoWidget.cc
src/Versions.cc
src/WelcomePage.cc src/WelcomePage.cc
src/main.cc src/main.cc
@ -177,7 +177,6 @@ qt5_wrap_cpp(MOC_HEADERS
include/TimelineView.h include/TimelineView.h
include/TimelineViewManager.h include/TimelineViewManager.h
include/LoginPage.h include/LoginPage.h
include/LoginSettings.h
include/LogoutDialog.h include/LogoutDialog.h
include/MainWindow.h include/MainWindow.h
include/MatrixClient.h include/MatrixClient.h

View file

@ -23,8 +23,8 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
#include "CircularProgress.h"
#include "FlatButton.h" #include "FlatButton.h"
#include "LoginSettings.h"
#include "MatrixClient.h" #include "MatrixClient.h"
#include "OverlayModal.h" #include "OverlayModal.h"
#include "RaisedButton.h" #include "RaisedButton.h"
@ -50,12 +50,20 @@ private slots:
// Callback for the login button. // Callback for the login button.
void onLoginButtonClicked(); 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. // Displays errors produced during the login.
void loginError(QString error_message); void loginError(QString error_message);
// Manipulate settings modal. // Callback for errors produced during server probing
void showSettingsModal(); void versionError(QString error_message);
void closeSettingsModal(const QString &server);
// Callback for successful server probing
void versionSuccess();
private: private:
QVBoxLayout *top_layout_; QVBoxLayout *top_layout_;
@ -67,8 +75,13 @@ private:
QLabel *logo_; QLabel *logo_;
QLabel *error_label_; QLabel *error_label_;
QHBoxLayout *serverLayout_;
QHBoxLayout *matrixidLayout_;
CircularProgress *spinner_;
QLabel *errorIcon_;
QString inferredServerAddress_;
FlatButton *back_button_; FlatButton *back_button_;
FlatButton *advanced_settings_button_;
RaisedButton *login_button_; RaisedButton *login_button_;
QWidget *form_widget_; QWidget *form_widget_;
@ -77,10 +90,7 @@ private:
TextField *matrixid_input_; TextField *matrixid_input_;
TextField *password_input_; TextField *password_input_;
TextField *serverInput_;
OverlayModal *settings_modal_;
LoginSettings *login_settings_;
QString custom_domain_;
// Matrix client API provider. // Matrix client API provider.
QSharedPointer<MatrixClient> client_; QSharedPointer<MatrixClient> client_;

View file

@ -63,11 +63,13 @@ public slots:
signals: signals:
void loginError(const QString &error); void loginError(const QString &error);
void registerError(const QString &error); void registerError(const QString &error);
void versionError(const QString &error);
void loggedOut(); void loggedOut();
void loginSuccess(const QString &userid, const QString &homeserver, const QString &token); void loginSuccess(const QString &userid, const QString &homeserver, const QString &token);
void registerSuccess(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 roomAvatarRetrieved(const QString &roomid, const QPixmap &img);
void userAvatarRetrieved(const QString &userId, const QImage &img); void userAvatarRetrieved(const QString &userId, const QImage &img);

View file

@ -17,21 +17,26 @@
#pragma once #pragma once
#include <QFrame> #include <QJsonDocument>
#include <QVector>
#include "FlatButton.h" #include "Deserializable.h"
#include "TextField.h"
class LoginSettings : public QFrame
class VersionsResponse : public Deserializable
{ {
Q_OBJECT
public: public:
explicit LoginSettings(QWidget *parent = nullptr); void deserialize(const QJsonDocument &data) override;
signals: bool isVersionSupported(unsigned int major, unsigned int minor, unsigned int patch);
void closing(const QString &server);
private: private:
TextField *input_; struct Version_ {
FlatButton *submit_button_; unsigned int major_;
unsigned int minor_;
unsigned int patch_;
};
QVector<Version_> supported_versions_;
}; };

BIN
resources/icons/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

View file

@ -12,6 +12,7 @@
<file>icons/user-shape.png</file> <file>icons/user-shape.png</file>
<file>icons/power-button-off.png</file> <file>icons/power-button-off.png</file>
<file>icons/smile.png</file> <file>icons/smile.png</file>
<file>icons/error.png</file>
<file>icons/emoji-categories/people.png</file> <file>icons/emoji-categories/people.png</file>
<file>icons/emoji-categories/nature.png</file> <file>icons/emoji-categories/nature.png</file>

View file

@ -22,8 +22,7 @@
LoginPage::LoginPage(QSharedPointer<MatrixClient> client, QWidget *parent) LoginPage::LoginPage(QSharedPointer<MatrixClient> client, QWidget *parent)
: QWidget(parent) : QWidget(parent)
, settings_modal_{nullptr} , inferredServerAddress_()
, login_settings_{nullptr}
, client_{client} , client_{client}
{ {
setStyleSheet("background-color: #f9f9f9"); setStyleSheet("background-color: #f9f9f9");
@ -38,9 +37,8 @@ LoginPage::LoginPage(QSharedPointer<MatrixClient> client, QWidget *parent)
back_button_->setMinimumSize(QSize(30, 30)); back_button_->setMinimumSize(QSize(30, 30));
back_button_->setForegroundColor("#333333"); back_button_->setForegroundColor("#333333");
advanced_settings_button_ = new FlatButton(this); top_bar_layout_->addWidget(back_button_, 0, Qt::AlignLeft | Qt::AlignVCenter);
advanced_settings_button_->setMinimumSize(QSize(30, 30)); top_bar_layout_->addStretch(1);
advanced_settings_button_->setForegroundColor("#333333");
QIcon icon; QIcon icon;
icon.addFile(":/icons/icons/left-angle.png", QSize(), QIcon::Normal, QIcon::Off); icon.addFile(":/icons/icons/left-angle.png", QSize(), QIcon::Normal, QIcon::Off);
@ -51,13 +49,6 @@ LoginPage::LoginPage(QSharedPointer<MatrixClient> client, QWidget *parent)
QIcon advanced_settings_icon; QIcon advanced_settings_icon;
advanced_settings_icon.addFile(":/icons/icons/cog.png", QSize(), QIcon::Normal, QIcon::Off); 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_ = new QLabel(this);
logo_->setPixmap(QPixmap(":/logos/nheko-128.png")); logo_->setPixmap(QPixmap(":/logos/nheko-128.png"));
@ -85,6 +76,19 @@ LoginPage::LoginPage(QSharedPointer<MatrixClient> client, QWidget *parent)
matrixid_input_->setBackgroundColor("#f9f9f9"); matrixid_input_->setBackgroundColor("#f9f9f9");
matrixid_input_->setPlaceholderText(tr("e.g @joe:matrix.org")); 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_ = new TextField(this);
password_input_->setTextColor("#333333"); password_input_->setTextColor("#333333");
password_input_->setLabel(tr("Password")); password_input_->setLabel(tr("Password"));
@ -92,8 +96,20 @@ LoginPage::LoginPage(QSharedPointer<MatrixClient> client, QWidget *parent)
password_input_->setBackgroundColor("#f9f9f9"); password_input_->setBackgroundColor("#f9f9f9");
password_input_->setEchoMode(QLineEdit::Password); 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_->addWidget(password_input_, Qt::AlignHCenter, 0);
form_layout_->addLayout(serverLayout_);
button_layout_ = new QHBoxLayout(); button_layout_ = new QHBoxLayout();
button_layout_->setSpacing(0); button_layout_->setSpacing(0);
@ -128,8 +144,12 @@ LoginPage::LoginPage(QSharedPointer<MatrixClient> client, QWidget *parent)
connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked())); connect(login_button_, SIGNAL(clicked()), this, SLOT(onLoginButtonClicked()));
connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); connect(matrixid_input_, SIGNAL(returnPressed()), login_button_, SLOT(click()));
connect(password_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(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); matrixid_input_->setValidator(&InputValidator::Id);
} }
@ -139,63 +159,115 @@ void LoginPage::loginError(QString error)
error_label_->setText(error); error_label_->setText(error);
} }
void LoginPage::onLoginButtonClicked() void LoginPage::onMatrixIdEntered()
{ {
error_label_->setText(""); error_label_->setText("");
if (!matrixid_input_->hasAcceptableInput()) { if (!matrixid_input_->hasAcceptableInput()) {
loginError(tr("Invalid Matrix ID")); loginError(tr("Invalid Matrix ID"));
return;
} else if (password_input_->text().isEmpty()) { } else if (password_input_->text().isEmpty()) {
loginError(tr("Empty password")); 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 { } else {
QString user = matrixid_input_->text().split(":").at(0).split("@").at(1); QString user = matrixid_input_->text().split(":").at(0).split("@").at(1);
QString password = password_input_->text(); QString password = password_input_->text();
client_->setServer(serverInput_->text());
QString home_server = custom_domain_.isEmpty()
? matrixid_input_->text().split(":").at(1)
: custom_domain_;
client_->setServer(home_server);
client_->login(user, password); 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() void LoginPage::reset()
{ {
matrixid_input_->clear(); matrixid_input_->clear();
password_input_->clear(); password_input_->clear();
serverInput_->clear();
if (settings_modal_ != nullptr) { spinner_->hide();
settings_modal_->deleteLater(); errorIcon_->hide();
settings_modal_ = nullptr; serverLayout_->removeWidget(spinner_);
} serverLayout_->removeWidget(errorIcon_);
matrixidLayout_->removeWidget(spinner_);
if (login_settings_ != nullptr) { inferredServerAddress_.clear();
login_settings_->deleteLater();
login_settings_ = nullptr;
}
} }
void LoginPage::onBackButtonClicked() void LoginPage::onBackButtonClicked()

View file

@ -1,61 +0,0 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <QLabel>
#include <QVBoxLayout>
#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());
});
}

View file

@ -30,6 +30,7 @@
#include "MatrixClient.h" #include "MatrixClient.h"
#include "Profile.h" #include "Profile.h"
#include "Register.h" #include "Register.h"
#include "Versions.h"
MatrixClient::MatrixClient(QString server, QObject *parent) MatrixClient::MatrixClient(QString server, QObject *parent)
: QNetworkAccessManager(parent) : QNetworkAccessManager(parent)
@ -57,12 +58,34 @@ void MatrixClient::onVersionsResponse(QNetworkReply *reply)
{ {
reply->deleteLater(); 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 data = reply->readAll();
auto json = QJsonDocument::fromJson(data); 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) void MatrixClient::onLoginResponse(QNetworkReply *reply)

62
src/Versions.cc Normal file
View file

@ -0,0 +1,62 @@
/*
* nheko Copyright (C) 2017 Jan Solanti <jhs@psonet.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QRegExp>
#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;
}