Refactor and update input validation

This change fixes input validation for the following cases:

 - Check that entered account names are still available
 - Working validation for time steps (input mask was completely broken)
 - Allow longer tokens: liboath is no longer used, Keysmith can handle it

Additionally the QML code is refactored significantly:

 - Extracted the main accounts overview page
 - Extracted the add an account page
 - Completed the internal renaming of "Oath" to "Keysmith" for QML types
master
Johan Ouwerkerk 2020-03-25 20:19:26 +01:00
parent 2a9c80fff5
commit 768ccdba97
9 changed files with 139 additions and 184 deletions

View File

@ -0,0 +1,52 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
import QtQuick 2.1
import QtQuick.Layouts 1.2
import QtQuick.Controls 2.0 as Controls
import org.kde.kirigami 2.8 as Kirigami
import Keysmith.Application 1.0
import Keysmith.Models 1.0 as Models
Kirigami.ScrollablePage {
id: root
/*
* Explicitly opt-out of scroll-to-refresh/drag-to-refresh behaviour
* Underlying model implementations don't offer the hooks for that.
*/
supportsRefreshing: false
title: i18nc("@title:window", "Accounts")
signal accountWanted
property bool addActionEnabled : true
property Models.AccountListModel accounts: Keysmith.accountListModel()
Component {
id: mainListDelegate
AccountEntryView {
account: model.account
}
}
ListView {
id: mainList
model: accounts
delegate: Kirigami.DelegateRecycler {
width: parent ? parent.width : implicitWidth
sourceComponent: mainListDelegate
}
}
actions.main: Kirigami.Action {
id: addAction
text: i18n("Add")
iconName: "list-add"
visible: addActionEnabled
onTriggered: {
root.accountWanted();
}
}
}

View File

@ -0,0 +1,52 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
import QtQuick 2.1
import QtQuick.Layouts 1.2
import QtQuick.Controls 2.0 as Controls
import org.kde.kirigami 2.8 as Kirigami
import Keysmith.Application 1.0
import Keysmith.Models 1.0 as Models
import Keysmith.Validators 1.0 as Validators
Kirigami.Page {
id: root
title: i18nc("@title:window", "Add new account")
signal dismissed
property Models.AccountListModel accounts: Keysmith.accountListModel()
ColumnLayout {
anchors {
horizontalCenter: parent.horizontalCenter
}
Kirigami.FormLayout {
Controls.TextField {
id: accountName
Kirigami.FormData.label: i18nc("@label:textbox", "Account Name:")
validator: Validators.AccountNameValidator {
accounts: root.accounts
}
}
}
TokenDetailsForm {
id: tokenDetails
}
}
actions.main: Kirigami.Action {
text: i18n("Add")
iconName: "answer-correct"
onTriggered: {
if (tokenDetails.isTotp) {
accounts.addTotp(accountName.text, tokenDetails.secret, parseInt(tokenDetails.timeStep), tokenDetails.tokenLength);
}
if (tokenDetails.isHotp) {
accounts.addHotp(accountName.text, tokenDetails.secret, parseInt(tokenDetails.counter), tokenDetails.tokenLength);
}
root.dismissed();
}
}
}

View File

@ -1,25 +1,10 @@
/* /*
* Copyright 2019 Johan Ouwerkerk <jm.ouwerkerk@gmail.com> * SPDX-License-Identifier: GPL-3.0-or-later
* * SPDX-FileCopyrightText: 2019 Bhushan Shah <bshah@kde.org>
* This program is free software; you can redistribute it and/or * SPDX-FileCopyrightText: 2019-2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
* 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 any later version accepted by the membership of
* KDE e.V. (or its successor approved by the membership of KDE
* e.V.), which shall act as a proxy defined in Section 14 of
* version 3 of the license.
*
* 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 <https://www.gnu.org/licenses/>.
*
*/ */
import Oath.Validators 1.0 as Validators import Keysmith.Validators 1.0 as Validators
import QtQuick 2.1 import QtQuick 2.1
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import QtQuick.Controls 2.0 as Controls import QtQuick.Controls 2.0 as Controls
@ -63,7 +48,9 @@ Kirigami.FormLayout {
Kirigami.FormData.label: i18nc("@label:textbox", "Timer:") Kirigami.FormData.label: i18nc("@label:textbox", "Timer:")
enabled: totpRadio.checked enabled: totpRadio.checked
text: "30" text: "30"
inputMask: "0009" validator: IntValidator {
bottom: 1
}
inputMethodHints: Qt.ImhDigitsOnly inputMethodHints: Qt.ImhDigitsOnly
} }
Controls.TextField { Controls.TextField {
@ -77,8 +64,9 @@ Kirigami.FormLayout {
inputMethodHints: Qt.ImhDigitsOnly inputMethodHints: Qt.ImhDigitsOnly
} }
/* /*
* The liboath API is documented to support tokens which are * OATH tokens are derived from a 32bit value, base-10 encoded.
* 6, 7 or 8 characters long only. * Meaning tokens should not be longer than 10 digits max.
* In addition tokens must be 6 digits long at minimum.
* *
* Make a virtue of it by offering a spinner for better UX * Make a virtue of it by offering a spinner for better UX
*/ */
@ -86,7 +74,7 @@ Kirigami.FormLayout {
id: pinLengthField id: pinLengthField
Kirigami.FormData.label: i18nc("@label:spinbox", "Token length:") Kirigami.FormData.label: i18nc("@label:spinbox", "Token length:")
from: 6 from: 6
to: 8 to: 10
value: 6 value: 6
} }
} }

View File

@ -1,27 +1,11 @@
/* /*
* Copyright 2019 Bhushan Shah <bshah@kde.org> * SPDX-License-Identifier: GPL-3.0-or-later
* * SPDX-FileCopyrightText: 2019 Bhushan Shah <bshah@kde.org>
* This program is free software; you can redistribute it and/or * SPDX-FileCopyrightText: 2019-2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
* 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 any later version accepted by the membership of
* KDE e.V. (or its successor approved by the membership of KDE
* e.V.), which shall act as a proxy defined in Section 14 of
* version 3 of the license.
*
* 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 <https://www.gnu.org/licenses/>.
*
*/ */
import Keysmith.Application 1.0 import Keysmith.Application 1.0
import Keysmith.Models 1.0 as Models import Keysmith.Models 1.0 as Models
import Oath.Validators 1.0 as Validators
import QtQuick 2.1 import QtQuick 2.1
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
@ -31,90 +15,30 @@ import org.kde.kirigami 2.4 as Kirigami
Kirigami.ApplicationWindow { Kirigami.ApplicationWindow {
id: root id: root
pageStack.initialPage: mainPageComponent pageStack.initialPage: accountsOverviewPage
property bool addActionEnabled: true property bool addActionEnabled: true
property Models.AccountListModel accounts: Keysmith.accountListModel() property Models.AccountListModel accounts: Keysmith.accountListModel()
Kirigami.Action {
id: addAction
text: i18n("Add")
iconName: "list-add"
visible: addActionEnabled
onTriggered: {
pageStack.push(addPageComponent);
addActionEnabled = false
}
}
Component { Component {
id: mainListDelegate id: accountsOverviewPage
AccountEntryView { AccountsOverview {
account: model.account accounts: root.accounts
} addActionEnabled: root.addActionEnabled
} onAccountWanted: {
pageStack.push(addPageComponent);
Component { root.addActionEnabled = false;
id: mainPageComponent
Kirigami.ScrollablePage {
title: i18nc("@title:window", "Accounts")
actions.main: addAction
/*
* Explicitly opt-out of scroll-to-refresh/drag-to-refresh behaviour
* Underlying model implementations don't offer the hooks for that.
*/
supportsRefreshing: false
ListView {
id: mainList
model: accounts
delegate: Kirigami.DelegateRecycler {
width: parent ? parent.width : implicitWidth
sourceComponent: mainListDelegate
}
} }
} }
} }
Component { Component {
id: addPageComponent id: addPageComponent
Kirigami.Page { AddAccount {
title: i18nc("@title:window", "Add new account") accounts: root.accounts
actions.main: Kirigami.Action { onDismissed: {
text: i18n("Add") pageStack.pop();
iconName: "answer-correct" addActionEnabled = true;
onTriggered: {
if (tokenDetails.isTotp) {
accounts.addTotp(accountName.text, tokenDetails.secret, parseInt(tokenDetails.timeStep), tokenDetails.tokenLength);
}
if (tokenDetails.isHotp) {
accounts.addHotp(accountName.text, tokenDetails.secret, parseInt(tokenDetails.counter), tokenDetails.tokenLength);
}
pageStack.pop();
addActionEnabled = true;
}
}
ColumnLayout {
id: addFormLayout
anchors {
horizontalCenter: parent.horizontalCenter
}
Kirigami.FormLayout {
Controls.TextField {
id: accountName
Kirigami.FormData.label: i18nc("@label:textbox", "Account Name:")
validator: Validators.AccountNameValidator {
id: nameValidator
}
}
}
TokenDetailsForm {
id: tokenDetails
}
} }
} }
} }

View File

@ -29,7 +29,8 @@
#include "app/keysmith.h" #include "app/keysmith.h"
#include "model/accounts.h" #include "model/accounts.h"
#include "../validators/qmlsupport.h" #include "validators/countervalidator.h"
#include "validators/secretvalidator.h"
Q_DECL_EXPORT int main(int argc, char *argv[]) Q_DECL_EXPORT int main(int argc, char *argv[])
{ {
@ -45,9 +46,11 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
engine.rootContext()->setContextObject(new KLocalizedContext(&engine)); engine.rootContext()->setContextObject(new KLocalizedContext(&engine));
validators::registerValidatorTypes();
qmlRegisterUncreatableType<model::SimpleAccountListModel>("Keysmith.Models", 1, 0, "AccountListModel", "Use the Keysmith singleton to obtain an AccountListModel"); qmlRegisterUncreatableType<model::SimpleAccountListModel>("Keysmith.Models", 1, 0, "AccountListModel", "Use the Keysmith singleton to obtain an AccountListModel");
qmlRegisterUncreatableType<model::AccountView>("Keysmith.Models", 1, 0, "Account", "Use an AccountListModel from the Keysmith singleton to obtain an Account"); qmlRegisterUncreatableType<model::AccountView>("Keysmith.Models", 1, 0, "Account", "Use an AccountListModel from the Keysmith singleton to obtain an Account");
qmlRegisterType<model::AccountNameValidator>("Keysmith.Validators", 1, 0, "AccountNameValidator");
qmlRegisterType<validators::Base32Validator>("Keysmith.Validators", 1, 0, "Base32SecretValidator");
qmlRegisterType<validators::UnsignedLongValidator>("Keysmith.Validators", 1, 0, "HOTPCounterValidator");
qmlRegisterSingletonType<app::Keysmith>("Keysmith.Application", 1, 0, "Keysmith", [](QQmlEngine *qml, QJSEngine *js) -> QObject * qmlRegisterSingletonType<app::Keysmith>("Keysmith.Application", 1, 0, "Keysmith", [](QQmlEngine *qml, QJSEngine *js) -> QObject *
{ {
Q_UNUSED(qml); Q_UNUSED(qml);

View File

@ -3,5 +3,7 @@
<file alias="main.qml">contents/ui/main.qml</file> <file alias="main.qml">contents/ui/main.qml</file>
<file alias="TokenDetailsForm.qml">contents/ui/TokenDetailsForm.qml</file> <file alias="TokenDetailsForm.qml">contents/ui/TokenDetailsForm.qml</file>
<file alias="AccountEntryView.qml">contents/ui/AccountEntryView.qml</file> <file alias="AccountEntryView.qml">contents/ui/AccountEntryView.qml</file>
<file alias="AccountsOverview.qml">contents/ui/AccountsOverview.qml</file>
<file alias="AddAccount.qml">contents/ui/AddAccount.qml</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -2,12 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2019-2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com> # SPDX-FileCopyrightText: 2019-2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
# #
# This directory contains (custom) validators to support form field validation in the QML UI
# For the QML bindings see: qmlsupport.cpp
#
set(validator_SRCS set(validator_SRCS
qmlsupport.cpp
countervalidator.cpp countervalidator.cpp
namevalidator.cpp namevalidator.cpp
secretvalidator.cpp secretvalidator.cpp
@ -15,4 +11,4 @@ set(validator_SRCS
) )
add_library(validator_lib STATIC ${validator_SRCS}) add_library(validator_lib STATIC ${validator_SRCS})
target_link_libraries(validator_lib Qt5::Core Qt5::Qml Qt5::Gui base32_lib) target_link_libraries(validator_lib Qt5::Core Qt5::Gui base32_lib)

View File

@ -1,35 +0,0 @@
/*****************************************************************************
* Copyright: 2019 Johan Ouwerkerk <jm.ouwerkerk@gmail.com> *
* *
* This project 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 project 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 "qmlsupport.h"
#include "countervalidator.h"
#include "namevalidator.h"
#include "secretvalidator.h"
#include <QtQml>
namespace validators
{
void registerValidatorTypes(void)
{
qmlRegisterType<validators::NameValidator>("Oath.Validators", 1, 0, "AccountNameValidator");
qmlRegisterType<validators::UnsignedLongValidator>("Oath.Validators", 1, 0, "HOTPCounterValidator");
qmlRegisterType<validators::Base32Validator>("Oath.Validators", 1, 0, "Base32SecretValidator");
}
}

View File

@ -1,27 +0,0 @@
/*****************************************************************************
* Copyright: 2019 Johan Ouwerkerk <jm.ouwerkerk@gmail.com> *
* *
* This project 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 project 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/>. *
* *
****************************************************************************/
#ifndef VALIDATOR_QML_SUPPORT_H
#define VALIDATOR_QML_SUPPORT_H
namespace validators
{
void registerValidatorTypes(void);
}
#endif