diff --git a/imports/NeoChat/Component/Login/Homeserver.qml b/imports/NeoChat/Component/Login/Homeserver.qml new file mode 100644 index 000000000..6cf7edfb9 --- /dev/null +++ b/imports/NeoChat/Component/Login/Homeserver.qml @@ -0,0 +1,64 @@ +/** + * SPDX-FileCopyrightText: 2020 Tobias Fella + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.14 +import QtQuick.Controls 2.14 as QQC2 +import QtQuick.Layouts 1.14 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.neochat 1.0 + +LoginStep { + id: root + + readonly property var homeserver: customHomeserver.visible ? customHomeserver.text : serverCombo.currentText + property bool loading: false + + title: i18n("@title", "Select a Homeserver") + + action: Kirigami.Action { + enabled: LoginHelper.homeserverReachable && !customHomeserver.visible || customHomeserver.acceptableInput + onTriggered: { + // TODO + console.log("register todo") + } + } + + onHomeserverChanged: { + LoginHelper.testHomeserver("@user:" + homeserver) + } + + Kirigami.FormLayout { + Component.onCompleted: Controller.testHomeserver(homeserver) + + QQC2.ComboBox { + id: serverCombo + + Kirigami.FormData.label: i18n("Homeserver:") + model: ["matrix.org", "kde.org", "tchncs.de", i18n("Other...")] + } + + QQC2.TextField { + id: customHomeserver + + Kirigami.FormData.label: i18n("Url:") + visible: serverCombo.currentIndex === 3 + onTextChanged: { + Controller.testHomeserver(text) + } + validator: RegularExpressionValidator { + regularExpression: /([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9]+(:[0-9]+)?/ + } + } + + QQC2.Button { + id: continueButton + text: i18nc("@action:button", "Continue") + action: root.action + } + } +} diff --git a/imports/NeoChat/Component/Login/Loading.qml b/imports/NeoChat/Component/Login/Loading.qml new file mode 100644 index 000000000..432fee63a --- /dev/null +++ b/imports/NeoChat/Component/Login/Loading.qml @@ -0,0 +1,23 @@ +/** + * SPDX-FileCopyrightText: 2020 Tobias Fella + * + * SPDX-License-Identifier: GPL-3.0-only + */ +import QtQuick 2.15 +import QtQuick.Controls 2.12 as QQC2 +import QtQuick.Layouts 1.12 + +import org.kde.neochat 1.0 + +import NeoChat.Component 1.0 + +import org.kde.kirigami 2.12 as Kirigami + +QQC2.BusyIndicator { + + property var showContinueButton: false + property var showBackButton: false + property string title: i18n("Loading") + + anchors.centerIn: parent +} diff --git a/imports/NeoChat/Component/Login/Login.qml b/imports/NeoChat/Component/Login/Login.qml new file mode 100644 index 000000000..9a9e2fd2c --- /dev/null +++ b/imports/NeoChat/Component/Login/Login.qml @@ -0,0 +1,68 @@ +/** + * SPDX-FileCopyrightText: 2020 Carl Schwan + * SPDX-FileCopyrightText: 2020 Tobias Fella + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.12 as QQC2 +import QtQuick.Layouts 1.12 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.neochat 1.0 +import NeoChat.Component 1.0 + +LoginStep { + id: login + + showContinueButton: true + showBackButton: false + + title: i18nc("@title", "Login") + message: i18n("Enter your Matrix ID") + + Component.onCompleted: { + LoginHelper.matrixId = "" + } + + Kirigami.FormLayout { + QQC2.TextField { + id: matrixIdField + Kirigami.FormData.label: i18n("Matrix ID:") + placeholderText: "@user:matrix.org" + onTextChanged: { + if(acceptableInput) { + LoginHelper.matrixId = text + } + } + + Component.onCompleted: { + matrixIdField.forceActiveFocus() + } + + Keys.onReturnPressed: { + login.action.trigger() + } + + validator: RegularExpressionValidator { + regularExpression: /^\@?[a-zA-Z0-9\._=\-/]+\:[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*\.[a-zA-Z]+(:[0-9]+)?$/ + } + } + } + + action: Kirigami.Action { + text: LoginHelper.testing && matrixIdField.acceptableInput ? i18n("Loading") : i18nc("@action:button", "Continue") + onTriggered: { + if (LoginHelper.supportsSso && LoginHelper.supportsPassword) { + processed("qrc:/imports/NeoChat/Component/Login/LoginMethod.qml"); + } else if (LoginHelper.supportsPassword) { + processed("qrc:/imports/NeoChat/Component/Login/Password.qml"); + } else { + processed("qrc:/imports/NeoChat/Component/Login/Sso.qml"); + } + } + enabled: LoginHelper.homeserverReachable + } +} diff --git a/imports/NeoChat/Component/Login/LoginMethod.qml b/imports/NeoChat/Component/Login/LoginMethod.qml new file mode 100644 index 000000000..2d47c7d12 --- /dev/null +++ b/imports/NeoChat/Component/Login/LoginMethod.qml @@ -0,0 +1,35 @@ +/** + * SPDX-FileCopyrightText: 2020 Tobias Fella + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.14 +import QtQuick.Controls 2.14 as Controls +import QtQuick.Layouts 1.14 +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.neochat 1.0 +import NeoChat.Component.Login 1.0 + +LoginStep { + id: loginMethod + + title: i18n("Login Methods") + + Layout.alignment: Qt.AlignHCenter + + Controls.Button { + Layout.alignment: Qt.AlignHCenter + text: i18n("Login with password") + Layout.preferredWidth: Kirigami.Units.gridUnit * 12 + onClicked: processed("qrc:/imports/NeoChat/Component/Login/Password.qml") + } + + Controls.Button { + Layout.alignment: Qt.AlignHCenter + text: i18n("Login with single sign-on") + Layout.preferredWidth: Kirigami.Units.gridUnit * 12 + onClicked: processed("qrc:/imports/NeoChat/Component/Login/Sso.qml") + } +} diff --git a/imports/NeoChat/Component/Login/LoginRegister.qml b/imports/NeoChat/Component/Login/LoginRegister.qml new file mode 100644 index 000000000..8a7e0ff9c --- /dev/null +++ b/imports/NeoChat/Component/Login/LoginRegister.qml @@ -0,0 +1,35 @@ +/** + * SPDX-FileCopyrightText: 2020 Tobias Fella + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.14 +import QtQuick.Controls 2.14 as Controls +import QtQuick.Layouts 1.14 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.neochat 1.0 + +import NeoChat.Component.Login 1.0 + +LoginStep { + id: loginRegister + + Layout.alignment: Qt.AlignHCenter + + Controls.Button { + Layout.alignment: Qt.AlignHCenter + text: i18n("Login") + Layout.preferredWidth: Kirigami.Units.gridUnit * 12 + onClicked: processed("qrc:/imports/NeoChat/Component/Login/Login.qml") + } + + Controls.Button { + Layout.alignment: Qt.AlignHCenter + text: i18n("Register") + Layout.preferredWidth: Kirigami.Units.gridUnit * 12 + onClicked: processed("qrc:/imports/NeoChat/Component/Login/Homeserver.qml") + } +} diff --git a/imports/NeoChat/Component/Login/LoginStep.qml b/imports/NeoChat/Component/Login/LoginStep.qml new file mode 100644 index 000000000..3958e17c1 --- /dev/null +++ b/imports/NeoChat/Component/Login/LoginStep.qml @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2020 Carl Schwan +// +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 + +/// Step for the login/registration flow +ColumnLayout { + + property string title: i18n("Welcome") + property string message: i18n("Welcome") + property bool showContinueButton: false + property bool showBackButton: false + property bool acceptable: false + property string previousUrl: "" + + /// Process this module, this is called by the continue button. + /// Should call \sa processed when it finish successfully. + property Action action: null + + /// Called when switching to the next step. + signal processed(url nextUrl) + + signal showMessage(string message) + +} diff --git a/imports/NeoChat/Component/Login/Password.qml b/imports/NeoChat/Component/Login/Password.qml new file mode 100644 index 000000000..c52d8fef2 --- /dev/null +++ b/imports/NeoChat/Component/Login/Password.qml @@ -0,0 +1,56 @@ +/** + * SPDX-FileCopyrightText: 2020 Tobias Fella + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +import QtQuick 2.15 +import QtQuick.Controls 2.12 as QQC2 +import QtQuick.Layouts 1.12 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.neochat 1.0 +import NeoChat.Component 1.0 + + +LoginStep { + id: password + + title: i18nc("@title", "Password") + message: i18n("Enter your password") + showContinueButton: true + showBackButton: true + previousUrl: LoginHelper.isLoggingIn ? "" : LoginHelper.supportsSso ? "qrc:/imports/NeoChat/Component/Login/LoginMethod.qml" : "qrc:/imports/NeoChat/Component/Login/Login.qml" + + action: Kirigami.Action { + text: i18nc("@action:button", "Login") + enabled: passwordField.text.length > 0 && !LoginHelper.isLoggingIn + onTriggered: { + LoginHelper.login(); + } + } + + Connections { + target: LoginHelper + function onConnected() { + processed("qrc:/imports/NeoChat/Component/Login/Loading.qml") + } + } + + Kirigami.FormLayout { + Kirigami.PasswordField { + id: passwordField + onTextChanged: LoginHelper.password = text + enabled: !LoginHelper.isLoggingIn + + Component.onCompleted: { + passwordField.forceActiveFocus() + } + + Keys.onReturnPressed: { + password.action.trigger() + } + } + } +} diff --git a/imports/NeoChat/Component/Login/Sso.qml b/imports/NeoChat/Component/Login/Sso.qml new file mode 100644 index 000000000..36f6c7e3b --- /dev/null +++ b/imports/NeoChat/Component/Login/Sso.qml @@ -0,0 +1,42 @@ + +/** + * SPDX-FileCopyrightText: 2020 Tobias Fella + * + * SPDX-License-Identifier: GPL-3.0-only + */ +import QtQuick 2.15 +import QtQuick.Controls 2.12 as QQC2 +import QtQuick.Layouts 1.12 + +import org.kde.neochat 1.0 + +import NeoChat.Component 1.0 + +import org.kde.kirigami 2.12 as Kirigami + +LoginStep { + id: root + + title: i18nc("@title", "Login") + message: i18n("Login with single sign-on") + + Kirigami.FormLayout { + Connections { + target: LoginHelper + onSsoUrlChanged: { + Qt.openUrlExternally(LoginHelper.ssoUrl) + } + onConnected: proccessed("qrc:/imports/NeoChat/Component/Login/Loading.qml") + } + + QQC2.Button { + text: i18n("Login") + onClicked: { + LoginHelper.loginWithSso() + root.showMessage(i18n("Complete the authentification steps in your browser")) + } + Component.onCompleted: forceActiveFocus() + Keys.onReturnPressed: clicked() + } + } +} diff --git a/imports/NeoChat/Component/Login/qmldir b/imports/NeoChat/Component/Login/qmldir new file mode 100644 index 000000000..8769eb0c4 --- /dev/null +++ b/imports/NeoChat/Component/Login/qmldir @@ -0,0 +1,7 @@ +module NeoChat.Component.Login +Login 1.0 Login.qml +Password 1.0 Password.qml +LoginRegister 1.0 LoginRegister.qml +Loading 1.0 Loading.qml +LoginMethod 1.0 LoginMethod.qml +LoginStep 1.0 LoginStep.qml diff --git a/imports/NeoChat/Page/AccountsPage.qml b/imports/NeoChat/Page/AccountsPage.qml index dd92e05f3..6ee23a770 100644 --- a/imports/NeoChat/Page/AccountsPage.qml +++ b/imports/NeoChat/Page/AccountsPage.qml @@ -77,7 +77,7 @@ Kirigami.ScrollablePage { actions.main: Kirigami.Action { text: i18n("Add an account") iconName: "list-add-user" - onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/LoginPage.qml") + onTriggered: pageStack.layers.push("qrc:/imports/NeoChat/Page/WelcomePage.qml") } Kirigami.OverlaySheet { diff --git a/imports/NeoChat/Page/LoginPage.qml b/imports/NeoChat/Page/LoginPage.qml deleted file mode 100644 index 43086c478..000000000 --- a/imports/NeoChat/Page/LoginPage.qml +++ /dev/null @@ -1,97 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2019 Black Hat - * SPDX-FileCopyrightText: 2020 Carl Schwan - * - * SPDX-License-Identifier: GPL-3.0-only - */ -import QtQuick 2.12 -import QtQuick.Controls 2.12 as QQC2 -import QtQuick.Layouts 1.12 - -import org.kde.neochat 1.0 - -import NeoChat.Component 1.0 - -import org.kde.kirigami 2.12 as Kirigami - -Kirigami.ScrollablePage { - id: root - - title: i18n("Login") - - header: QQC2.Control { - padding: Kirigami.Units.smallSpacing - contentItem: Kirigami.InlineMessage { - id: inlineMessage - visible: false - showCloseButton: true - } - } - - Kirigami.FormLayout { - id: formLayout - QQC2.TextField { - id: serverField - Kirigami.FormData.label: i18n("Server Address") - text: "https://matrix.org" - onAccepted: usernameField.forceActiveFocus() - } - QQC2.TextField { - id: usernameField - Kirigami.FormData.label: i18n("Username") - onAccepted: passwordField.forceActiveFocus() - } - Kirigami.PasswordField { - id: passwordField - Kirigami.FormData.label: i18n("Password") - onAccepted: accessTokenField.forceActiveFocus() - } - QQC2.TextField { - id: accessTokenField - Kirigami.FormData.label: i18n("Access Token (Optional)") - onAccepted: deviceNameField.forceActiveFocus() - } - QQC2.TextField { - id: deviceNameField - Kirigami.FormData.label: i18n("Device Name (Optional)") - onAccepted: doLogin() - } - RowLayout { - QQC2.Button { - visible: Controller.accountCount > 0 - text: i18n("Cancel") - onClicked: { - pageStack.layers.clear(); - } - } - QQC2.Button { - text: i18n("Login") - onClicked: doLogin() - } - } - - Connections { - target: Controller - function onErrorOccured(error, detail) { - inlineMessage.type = Kirigami.MessageType.Error; - if (detail && detail.length !== 0) { - inlineMessage.text = i18n("%1: %2", error, detail); - } else { - inlineMessage.text = error; - } - inlineMessage.visible = true; - } - } - } - - function doLogin() { - inlineMessage.text = i18n("Loading, this might take up to 10 seconds."); - inlineMessage.type = Kirigami.MessageType.Information - inlineMessage.visible = true; - if (accessTokenField.text.length > 0) { - Controller.loginWithAccessToken(serverField.text.trim(), usernameField.text.trim(), accessTokenField.text, deviceNameField.text.trim()); - } else { - Controller.loginWithCredentials(serverField.text.trim(), usernameField.text.trim(), passwordField.text, deviceNameField.text.trim()); - } - } -} diff --git a/imports/NeoChat/Page/WelcomePage.qml b/imports/NeoChat/Page/WelcomePage.qml new file mode 100644 index 000000000..dc106f0df --- /dev/null +++ b/imports/NeoChat/Page/WelcomePage.qml @@ -0,0 +1,101 @@ +/** + * SPDX-FileCopyrightText: 2020 Tobias Fella + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +import QtQuick 2.14 +import QtQuick.Controls 2.14 as Controls +import QtQuick.Layouts 1.14 + +import org.kde.kirigami 2.12 as Kirigami + +import org.kde.neochat 1.0 + +import NeoChat.Component.Login 1.0 + +Kirigami.ScrollablePage { + id: welcomePage + + property alias currentStep: module.item + + title: module.item.title ?? i18n("Welcome") + + header: Controls.Control { + contentItem: Kirigami.InlineMessage { + id: headerMessage + type: Kirigami.MessageType.Error + showCloseButton: true + visible: false + } + } + + Component.onCompleted: LoginHelper.init() + + Connections { + target: LoginHelper + onErrorOccured: { + headerMessage.text = message; + headerMessage.visible = true; + headerMessage.type = Kirigami.MessageType.Error; + } + } + + ColumnLayout { + Kirigami.Icon { + source: "org.kde.neochat" + Layout.fillWidth: true + Layout.preferredHeight: Kirigami.Units.gridUnit * 16 + } + Controls.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font.pixelSize: 25 + text: module.item.message ?? module.item.title ?? i18n("Welcome to Matrix") + } + + Loader { + id: module + Layout.alignment: Qt.AlignHCenter + source: "qrc:/imports/NeoChat/Component/Login/Login.qml" + onSourceChanged: { + headerMessage.visible = false + headerMessage.text = "" + } + } + RowLayout { + Layout.alignment: Qt.AlignHCenter + + Controls.Button { + text: i18nc("@action:button", "Back") + + enabled: welcomePage.currentStep.previousUrl !== "" + visible: welcomePage.currentStep.showBackButton + Layout.alignment: Qt.AlignHCenter + onClicked: { + module.source = welcomePage.currentStep.previousUrl + } + } + + Controls.Button { + id: continueButton + enabled: welcomePage.currentStep.acceptable + visible: welcomePage.currentStep.showContinueButton + action: welcomePage.currentStep.action + } + } + + Connections { + target: currentStep + + function onProcessed(nextUrl) { + module.source = nextUrl; + } + function onShowMessage(message) { + headerMessage.text = message; + headerMessage.visible = true; + headerMessage.type = Kirigami.MessageType.Information; + } + } + } +} diff --git a/imports/NeoChat/Page/qmldir b/imports/NeoChat/Page/qmldir index 9bea80824..c2ce20767 100644 --- a/imports/NeoChat/Page/qmldir +++ b/imports/NeoChat/Page/qmldir @@ -1,6 +1,5 @@ module NeoChat.Page LoadingPage 1.0 LoadingPage.qml -LoginPage 1.0 LoginPage.qml RoomListPage 1.0 RoomListPage.qml RoomPage 1.0 RoomPage.qml RoomWindow 1.0 RoomWindow.qml diff --git a/qml/main.qml b/qml/main.qml index dbdfdcdd1..764a4b3c7 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -246,6 +246,12 @@ Kirigami.ApplicationWindow { activeConnection: Controller.activeConnection } } + Connections { + target: LoginHelper + function onInitialSyncFinished() { + roomManager.roomList = pageStack.replace(roomListComponent); + } + } Connections { target: Controller @@ -255,23 +261,13 @@ Kirigami.ApplicationWindow { return; } if (Controller.accountCount === 0) { - pageStack.replace("qrc:/imports/NeoChat/Page/LoginPage.qml", {}); + pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml", {}); } else { roomManager.roomList = pageStack.replace(roomListComponent, {'activeConnection': Controller.activeConnection}); roomManager.loadInitialRoom(); } } - function onConnectionAdded() { - if (Controller.accountCount === 1) { - if (Controller.busy) { - pageStack.replace("qrc:/imports/NeoChat/Page/LoadingPage.qml"); - } else { - roomManager.roomList = pageStack.replace(roomListComponent); - } - } - } - function onBusyChanged() { if(!Controller.busy && roomManager.roomList === null) { roomManager.roomList = pageStack.replace(roomListComponent); @@ -286,7 +282,7 @@ Kirigami.ApplicationWindow { function onConnectionDropped() { if (Controller.accountCount === 0) { pageStack.clear(); - pageStack.replace("qrc:/imports/NeoChat/Page/LoginPage.qml"); + pageStack.replace("qrc:/imports/NeoChat/Page/WelcomePage.qml"); } } diff --git a/res.qrc b/res.qrc index bbe5e7403..8d980bce0 100644 --- a/res.qrc +++ b/res.qrc @@ -2,7 +2,6 @@ qml/main.qml imports/NeoChat/Page/qmldir - imports/NeoChat/Page/LoginPage.qml imports/NeoChat/Page/LoadingPage.qml imports/NeoChat/Page/RoomListPage.qml imports/NeoChat/Page/RoomPage.qml @@ -14,6 +13,7 @@ imports/NeoChat/Page/StartChatPage.qml imports/NeoChat/Page/ImageEditorPage.qml imports/NeoChat/Page/DevicesPage.qml + imports/NeoChat/Page/WelcomePage.qml imports/NeoChat/Component/qmldir imports/NeoChat/Component/ChatTextInput.qml imports/NeoChat/Component/AutoMouseArea.qml @@ -32,6 +32,15 @@ imports/NeoChat/Component/Timeline/AudioDelegate.qml imports/NeoChat/Component/Timeline/FileDelegate.qml imports/NeoChat/Component/Timeline/ImageDelegate.qml + imports/NeoChat/Component/Login/qmldir + imports/NeoChat/Component/Login/LoginStep.qml + imports/NeoChat/Component/Login/Login.qml + imports/NeoChat/Component/Login/Password.qml + imports/NeoChat/Component/Login/LoginRegister.qml + imports/NeoChat/Component/Login/Loading.qml + imports/NeoChat/Component/Login/Homeserver.qml + imports/NeoChat/Component/Login/LoginMethod.qml + imports/NeoChat/Component/Login/Sso.qml imports/NeoChat/Setting/Setting.qml imports/NeoChat/Setting/qmldir imports/NeoChat/Setting/Palette.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bc002367d..fb32360d2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,7 @@ add_executable(neochat chatdocumenthandler.cpp devicesmodel.cpp filetypesingleton.cpp + login.cpp ../res.qrc ) diff --git a/src/controller.cpp b/src/controller.cpp index c15b0b1fc..cc61c84af 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -91,50 +91,6 @@ inline QString accessTokenFileName(const AccountSettings &account) return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + '/' + fileName; } -void Controller::loginWithCredentials(const QString &serverAddr, const QString &user, const QString &pass, QString deviceName) -{ - if (user.isEmpty() || pass.isEmpty()) { - return; - } - - if (deviceName.isEmpty()) { - deviceName = "NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture(); - } - - auto conn = new Connection(this); - const QUrl serverUrl = QUrl::fromUserInput(serverAddr); - // we are using a fake mixd since resolveServer just set the homeserver url :sigh: - conn->resolveServer("@username:" + serverUrl.host() + ":" + QString::number(serverUrl.port(443))); - - connect(conn, &Connection::loginFlowsChanged, this, [this, user, conn, pass, deviceName]() { - conn->loginWithPassword(user, pass, deviceName, ""); - connect(conn, &Connection::connected, this, [this, conn, deviceName] { - AccountSettings account(conn->userId()); - account.setKeepLoggedIn(true); - account.clearAccessToken(); // Drop the legacy - just in case - account.setHomeserver(conn->homeserver()); - account.setDeviceId(conn->deviceId()); - account.setDeviceName(deviceName); - if (!saveAccessTokenToKeyChain(account, conn->accessToken())) { - qWarning() << "Couldn't save access token"; - } - account.sync(); - addConnection(conn); - setActiveConnection(conn); - }); - connect(conn, &Connection::networkError, [=](QString error, const QString &, int, int) { - Q_EMIT globalErrorOccured(i18n("Network Error"), std::move(error)); - }); - connect(conn, &Connection::loginError, [=](QString error, const QString &) { - Q_EMIT errorOccured(i18n("Login Failed"), std::move(error)); - }); - }); - - connect(conn, &Connection::resolveError, this, [=](QString error) { - Q_EMIT globalErrorOccured(i18n("Network Error"), std::move(error)); - }); -} - void Controller::loginWithAccessToken(const QString &serverAddr, const QString &user, const QString &token, const QString &deviceName) { if (user.isEmpty() || token.isEmpty()) { @@ -551,4 +507,5 @@ NeochatDeleteDeviceJob::NeochatDeleteDeviceJob(const QString &deviceId, const Om QJsonObject _data; addParam(_data, QStringLiteral("auth"), auth); setRequestData(std::move(_data)); + } diff --git a/src/controller.h b/src/controller.h index a20b764ca..0c74b807f 100644 --- a/src/controller.h +++ b/src/controller.h @@ -45,7 +45,6 @@ public: void addConnection(Connection *c); void dropConnection(Connection *c); - Q_INVOKABLE void loginWithCredentials(const QString &, const QString &, const QString &, QString); Q_INVOKABLE void loginWithAccessToken(const QString &, const QString &, const QString &, const QString &); Q_INVOKABLE void changePassword(Quotient::Connection *connection, const QString ¤tPassword, const QString &newPassword); @@ -61,6 +60,9 @@ public: void setAboutData(const KAboutData &aboutData); [[nodiscard]] KAboutData aboutData() const; + bool saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken); + bool saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken); + enum PasswordStatus { Success, Wrong, @@ -79,8 +81,6 @@ private: static QByteArray loadAccessTokenFromFile(const AccountSettings &account); QByteArray loadAccessTokenFromKeyChain(const AccountSettings &account); - bool saveAccessTokenToFile(const AccountSettings &account, const QByteArray &accessToken); - bool saveAccessTokenToKeyChain(const AccountSettings &account, const QByteArray &accessToken); void loadSettings(); void saveSettings() const; @@ -110,6 +110,7 @@ Q_SIGNALS: void showWindow(); void openRoom(NeoChatRoom *room); void userConsentRequired(QUrl url); + void testConnectionResult(const QString &connection, bool usable); public Q_SLOTS: void logout(Quotient::Connection *conn, bool serverSideLogout); diff --git a/src/login.cpp b/src/login.cpp new file mode 100644 index 000000000..3966a65a5 --- /dev/null +++ b/src/login.cpp @@ -0,0 +1,206 @@ +/** + * SPDX-FileCopyrightText: 2020 Tobias Fella + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "login.h" +#include "connection.h" +#include "controller.h" + +#include + +#include + +Login::Login(QObject *parent) + : QObject(parent) +{ + init(); +} + +void Login::init() +{ + m_homeserverReachable = false; + m_connection = nullptr; + m_matrixId = QString(); + m_password = QString(); + m_deviceName = QString(); + m_supportsSso = false; + m_supportsPassword = false; + m_ssoUrl = QUrl(); + + connect(this, &Login::matrixIdChanged, this, [=](){ + setHomeserverReachable(false); + + if (m_connection) { + delete m_connection; + m_connection = nullptr; + } + + if(m_matrixId == "@") { + return; + } + + m_testing = true; + Q_EMIT testingChanged(); + m_connection = new Connection(this); + m_connection->resolveServer(m_matrixId); + connect(m_connection, &Connection::loginFlowsChanged, this, [=](){ + setHomeserverReachable(true); + m_testing = false; + Q_EMIT testingChanged(); + m_supportsSso = m_connection->supportsSso(); + m_supportsPassword = m_connection->supportsPasswordAuth(); + Q_EMIT loginFlowsChanged(); + }); + }); +} + +void Login::setHomeserverReachable(bool reachable) +{ + m_homeserverReachable = reachable; + Q_EMIT homeserverReachableChanged(); +} + +bool Login::homeserverReachable() const +{ + return m_homeserverReachable; +} + +QString Login::matrixId() const +{ + return m_matrixId; +} + +void Login::setMatrixId(const QString &matrixId) +{ + m_matrixId = matrixId; + if(!m_matrixId.startsWith('@')) { + m_matrixId.prepend('@'); + } + Q_EMIT matrixIdChanged(); +} + +QString Login::password() const +{ + return m_password; +} + +void Login::setPassword(const QString &password) +{ + m_password = password; + Q_EMIT passwordChanged(); +} + +QString Login::deviceName() const +{ + return m_deviceName; +} + +void Login::setDeviceName(const QString &deviceName) +{ + m_deviceName = deviceName; + Q_EMIT deviceNameChanged(); +} + +void Login::login() +{ + m_isLoggingIn = true; + Q_EMIT isLoggingInChanged(); + + setDeviceName("NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture()); + + m_connection = new Connection(this); + m_connection->resolveServer(m_matrixId); + + connect(m_connection, &Connection::loginFlowsChanged, this, [=]() { + m_connection->loginWithPassword(m_matrixId, m_password, m_deviceName, QString()); + connect(m_connection, &Connection::connected, this, [=] { + Q_EMIT connected(); + m_isLoggingIn = false; + Q_EMIT isLoggingInChanged(); + AccountSettings account(m_connection->userId()); + account.setKeepLoggedIn(true); + account.clearAccessToken(); // Drop the legacy - just in case + account.setHomeserver(m_connection->homeserver()); + account.setDeviceId(m_connection->deviceId()); + account.setDeviceName(m_deviceName); + if (!Controller::instance().saveAccessTokenToKeyChain(account, m_connection->accessToken())) { + qWarning() << "Couldn't save access token"; + } + account.sync(); + Controller::instance().addConnection(m_connection); + Controller::instance().setActiveConnection(m_connection); + }); + connect(m_connection, &Connection::networkError, [=](QString error, const QString &, int, int) { + Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error)); + m_isLoggingIn = false; + Q_EMIT isLoggingInChanged(); + }); + connect(m_connection, &Connection::loginError, [=](QString error, const QString &) { + Q_EMIT errorOccured(i18n("Login Failed: %1", error)); + m_isLoggingIn = false; + Q_EMIT isLoggingInChanged(); + }); + }); + + connect(m_connection, &Connection::resolveError, this, [=](QString error) { + Q_EMIT Controller::instance().globalErrorOccured(i18n("Network Error"), std::move(error)); + }); + + connect(m_connection, &Connection::syncDone, this, [=]() { + Q_EMIT initialSyncFinished(); + disconnect(m_connection, &Connection::syncDone, this, nullptr); + }); +} + +bool Login::supportsPassword() const +{ + return m_supportsPassword; +} + +bool Login::supportsSso() const +{ + return m_supportsSso; +} + +QUrl Login::ssoUrl() const +{ + return m_ssoUrl; +} + +void Login::loginWithSso() +{ + SsoSession *session = m_connection->prepareForSso("NeoChat " + QSysInfo::machineHostName() + " " + QSysInfo::productType() + " " + QSysInfo::productVersion() + " " + QSysInfo::currentCpuArchitecture()); + m_ssoUrl = session->ssoUrl(); + Q_EMIT ssoUrlChanged(); + connect(m_connection, &Connection::connected, [=](){ + Q_EMIT connected(); + AccountSettings account(m_connection->userId()); + account.setKeepLoggedIn(true); + account.clearAccessToken(); // Drop the legacy - just in case + account.setHomeserver(m_connection->homeserver()); + account.setDeviceId(m_connection->deviceId()); + account.setDeviceName(m_deviceName); + if (!Controller::instance().saveAccessTokenToKeyChain(account, m_connection->accessToken())) { + qWarning() << "Couldn't save access token"; + } + account.sync(); + Controller::instance().addConnection(m_connection); + Controller::instance().setActiveConnection(m_connection); + }); + connect(m_connection, &Connection::syncDone, this, [=]() { + Q_EMIT initialSyncFinished(); + disconnect(m_connection, &Connection::syncDone, this, nullptr); + }); +} + +bool Login::testing() const +{ + return m_testing; +} + +bool Login::isLoggingIn() const +{ + return m_isLoggingIn; +} diff --git a/src/login.h b/src/login.h new file mode 100644 index 000000000..1e42d03be --- /dev/null +++ b/src/login.h @@ -0,0 +1,85 @@ +/** + * SPDX-FileCopyrightText: 2020 Tobias Fella + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include + +#include "csapi/wellknown.h" +#include "connection.h" + +using namespace Quotient; + +class Login : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool homeserverReachable READ homeserverReachable NOTIFY homeserverReachableChanged) + Q_PROPERTY(bool testing READ testing NOTIFY testingChanged) + Q_PROPERTY(QString matrixId READ matrixId WRITE setMatrixId NOTIFY matrixIdChanged) + Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged) + Q_PROPERTY(QString deviceName READ deviceName WRITE setDeviceName NOTIFY deviceNameChanged) + Q_PROPERTY(bool supportsSso READ supportsSso NOTIFY loginFlowsChanged STORED false) + Q_PROPERTY(bool supportsPassword READ supportsPassword NOTIFY loginFlowsChanged STORED false) + Q_PROPERTY(QUrl ssoUrl READ ssoUrl NOTIFY ssoUrlChanged) + Q_PROPERTY(bool isLoggingIn READ isLoggingIn NOTIFY isLoggingInChanged) + +public: + explicit Login(QObject *parent = nullptr); + + Q_INVOKABLE void init(); + + bool homeserverReachable() const; + + QString matrixId() const; + void setMatrixId(const QString &matrixId); + + QString password() const; + void setPassword(const QString &password); + + QString deviceName() const; + void setDeviceName(const QString &deviceName); + + bool supportsPassword() const; + bool supportsSso() const; + + bool testing() const; + + QUrl ssoUrl() const; + + bool isLoggingIn() const; + + Q_INVOKABLE void login(); + Q_INVOKABLE void loginWithSso(); + +Q_SIGNALS: + void homeserverReachableChanged(); + void testHomeserverFinished(); + void matrixIdChanged(); + void passwordChanged(); + void deviceNameChanged(); + void initialSyncFinished(); + void loginFlowsChanged(); + void ssoUrlChanged(); + void connected(); + void errorOccured(QString message); + void testingChanged(); + void isLoggingInChanged(); + +private: + void setHomeserverReachable(bool reachable); + + bool m_homeserverReachable; + QString m_matrixId; + QString m_password; + QString m_deviceName; + bool m_supportsSso = false; + bool m_supportsPassword = false; + Connection *m_connection = nullptr; + QUrl m_ssoUrl; + bool m_testing; + bool m_isLoggingIn = false; +}; diff --git a/src/main.cpp b/src/main.cpp index 0113d70c0..659f18c8c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -32,6 +32,7 @@ #include "devicesmodel.h" #include "emojimodel.h" #include "filetypesingleton.h" +#include "login.h" #include "matriximageprovider.h" #include "messageeventmodel.h" #include "messagefiltermodel.h" @@ -98,10 +99,13 @@ int main(int argc, char *argv[]) auto config = NeoChatConfig::self(); FileTypeSingleton fileTypeSingleton; + Login *login = new Login(); + qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Controller", &Controller::instance()); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Clipboard", &clipboard); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Config", config); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "FileType", &fileTypeSingleton); + qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "LoginHelper", login); qmlRegisterType("org.kde.neochat", 1, 0, "AccountListModel"); qmlRegisterType("org.kde.neochat", 1, 0, "ActionsHandler"); qmlRegisterType("org.kde.neochat", 1, 0, "ChatDocumentHandler");