438 lines
15 KiB
QML
438 lines
15 KiB
QML
import QtQuick 2.15
|
|
import QtQuick.Controls 2.15
|
|
import QtQuick.Layouts 1.15
|
|
|
|
import utils 1.0
|
|
|
|
import StatusQ.Controls 0.1
|
|
import StatusQ.Core 0.1
|
|
import StatusQ.Core.Theme 0.1
|
|
import StatusQ.Components 0.1
|
|
import StatusQ.Popups 0.1
|
|
|
|
ColumnLayout {
|
|
id: root
|
|
|
|
readonly property bool ready: newPswInput.text.length >= Constants.minPasswordLength && newPswInput.text === confirmPswInput.text && errorTxt.text === ""
|
|
|
|
property bool createNewPsw: true
|
|
property string title: createNewPsw ? qsTr("Create a password") : qsTr("Change your password")
|
|
property bool titleVisible: true
|
|
property real titleSize: 22
|
|
property string introText: {
|
|
if (createNewPsw) {
|
|
return qsTr("Create a password to unlock Status on this device & sign transactions.")
|
|
}
|
|
|
|
return qsTr("Change password used to unlock Status on this device & sign transactions.")
|
|
}
|
|
property string recoverText: qsTr("You will not be able to recover this password if it is lost.")
|
|
property string strengthenText: qsTr("Minimum %n character(s). To strengthen your password consider including:", "", Constants.minPasswordLength)
|
|
property bool highSizeIntro: false
|
|
|
|
property int contentAlignment: Qt.AlignHCenter
|
|
|
|
property var passwordStrengthScoreFunction: function () {}
|
|
|
|
readonly property int zBehind: 1
|
|
readonly property int zFront: 100
|
|
|
|
property alias currentPswText: currentPswInput.text
|
|
property alias newPswText: newPswInput.text
|
|
property alias confirmationPswText: confirmPswInput.text
|
|
property alias errorMsgText: errorTxt.text
|
|
|
|
signal returnPressed()
|
|
|
|
function forceNewPswInputFocus() { newPswInput.forceActiveFocus(Qt.MouseFocusReason) }
|
|
|
|
function reset() {
|
|
newPswInput.text = ""
|
|
currentPswInput.text = ""
|
|
confirmPswInput.text = ""
|
|
errorTxt.text = ""
|
|
strengthInditactor.strength = StatusPasswordStrengthIndicator.Strength.None
|
|
|
|
// Update focus:
|
|
if(root.createNewPsw)
|
|
newPswInput.forceActiveFocus(Qt.MouseFocusReason)
|
|
else
|
|
currentPswInput.forceActiveFocus(Qt.MouseFocusReason)
|
|
}
|
|
|
|
function checkPasswordMatches(onlyIfConfirmPasswordHasFocus = true) {
|
|
if (confirmPswInput.text.length === 0) {
|
|
return
|
|
}
|
|
|
|
if (onlyIfConfirmPasswordHasFocus && !confirmPswInput.focus) {
|
|
return
|
|
}
|
|
|
|
if(newPswInput.text.length >= Constants.minPasswordLength) {
|
|
if(confirmPswInput.text !== newPswInput.text) {
|
|
errorTxt.text = qsTr("Passwords don't match")
|
|
}
|
|
}
|
|
}
|
|
|
|
QtObject {
|
|
id: d
|
|
|
|
property bool containsLower: false
|
|
property bool containsUpper: false
|
|
property bool containsNumbers: false
|
|
property bool containsSymbols: false
|
|
|
|
readonly property var validatorRegexp: /^[!-~]+$/
|
|
readonly property string validatorErrMessage: qsTr("Only ASCII letters, numbers, and symbols are allowed")
|
|
readonly property string passTooLongErrMessage: qsTr("Maximum %n character(s)", "", Constants.maxPasswordLength)
|
|
|
|
// Password strength categorization / validation
|
|
function lowerCaseValidator(text) { return (/[a-z]/.test(text)) }
|
|
function upperCaseValidator(text) { return (/[A-Z]/.test(text)) }
|
|
function numbersValidator(text) { return (/\d/.test(text)) }
|
|
// That includes NOT extended ASCII printable symbols less space:
|
|
function symbolsValidator(text) { return (/[!-\/:-@[-`{-~]/.test(text)) }
|
|
|
|
function validateCharacterSet(text) {
|
|
if(!(d.validatorRegexp).test(text)) {
|
|
errorTxt.text = d.validatorErrMessage
|
|
return false
|
|
}
|
|
if(text.length > Constants.maxPasswordLength) {
|
|
errorTxt.text = d.passTooLongErrMessage
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Used to convert strength from a given score to a specific category
|
|
function convertStrength(score) {
|
|
var strength = StatusPasswordStrengthIndicator.Strength.None
|
|
switch(score) {
|
|
case 0: strength = StatusPasswordStrengthIndicator.Strength.VeryWeak; break
|
|
case 1: strength = StatusPasswordStrengthIndicator.Strength.Weak; break
|
|
case 2: strength = StatusPasswordStrengthIndicator.Strength.SoSo; break
|
|
case 3: strength = StatusPasswordStrengthIndicator.Strength.Good; break
|
|
case 4: strength = StatusPasswordStrengthIndicator.Strength.Great; break
|
|
}
|
|
if(strength > 4)
|
|
strength = StatusPasswordStrengthIndicator.Strength.Great
|
|
return strength
|
|
}
|
|
|
|
// Password validation / error message selection:
|
|
function passwordValidation() {
|
|
// 3 rules to validate:
|
|
// * Password is in pwnd passwords database
|
|
if(isInPwndDatabase())
|
|
errorTxt.text = qsTr("Password pwned, shouldn't be used")
|
|
|
|
// * Common password
|
|
else if(isCommonPassword())
|
|
errorTxt.text = qsTr("Common password, shouldn't be used")
|
|
|
|
// * Password too short
|
|
else if(isTooShort())
|
|
errorTxt.text = qsTr("Minimum %n character(s)", "", Constants.minPasswordLength)
|
|
}
|
|
|
|
function isInPwndDatabase() {
|
|
// "TODO - Nice To Have: Pwnd password validation NOT implemented yet! "
|
|
return false
|
|
}
|
|
|
|
function isCommonPassword() {
|
|
// "TODO - Nice To Have: Common password validation NOT implemented yet! "
|
|
return false
|
|
}
|
|
|
|
function isTooShort() { return newPswInput.text.length < Constants.minPasswordLength }
|
|
}
|
|
|
|
implicitWidth: 460
|
|
spacing: Theme.bigPadding
|
|
z: root.zFront
|
|
|
|
StatusBaseText {
|
|
Layout.alignment: root.contentAlignment
|
|
visible: root.titleVisible
|
|
text: root.title
|
|
font.pixelSize: root.titleSize
|
|
font.bold: true
|
|
color: Theme.palette.directColor1
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: introColumn
|
|
|
|
Layout.fillWidth: true
|
|
Layout.alignment: root.contentAlignment
|
|
spacing: 4
|
|
|
|
StatusBaseText {
|
|
Layout.fillWidth: true
|
|
Layout.alignment: root.contentAlignment
|
|
|
|
text: root.introText
|
|
horizontalAlignment: root.contentAlignment
|
|
font.pixelSize: root.highSizeIntro ? Theme.primaryTextFontSize : Theme.tertiaryTextFontSize
|
|
wrapMode: Text.WordWrap
|
|
color: Theme.palette.baseColor1
|
|
}
|
|
|
|
StatusBaseText {
|
|
Layout.fillWidth: true
|
|
Layout.alignment: root.contentAlignment
|
|
|
|
text: root.recoverText
|
|
horizontalAlignment: root.contentAlignment
|
|
font.pixelSize: root.highSizeIntro ? Theme.primaryTextFontSize : Theme.tertiaryTextFontSize
|
|
wrapMode: Text.WordWrap
|
|
color: Theme.palette.dangerColor1
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
visible: !root.createNewPsw
|
|
|
|
StatusBaseText {
|
|
text: qsTr("Current password")
|
|
}
|
|
|
|
StatusPasswordInput {
|
|
id: currentPswInput
|
|
objectName: "passwordViewCurrentPassword"
|
|
|
|
property bool showPassword
|
|
|
|
z: root.zFront
|
|
Layout.fillWidth: true
|
|
Layout.alignment: root.contentAlignment
|
|
placeholderText: qsTr("Enter current password")
|
|
echoMode: showPassword ? TextInput.Normal : TextInput.Password
|
|
rightPadding: showHideCurrentIcon.width + showHideCurrentIcon.anchors.rightMargin + Theme.padding / 2
|
|
onAccepted: root.returnPressed()
|
|
|
|
StatusFlatRoundButton {
|
|
id: showHideCurrentIcon
|
|
visible: currentPswInput.text !== ""
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 16
|
|
width: 24
|
|
height: 24
|
|
icon.name: currentPswInput.showPassword ? "hide" : "show"
|
|
icon.color: Theme.palette.baseColor1
|
|
|
|
onClicked: currentPswInput.showPassword = !currentPswInput.showPassword
|
|
}
|
|
}
|
|
}
|
|
|
|
StatusModalDivider {
|
|
visible: !root.createNewPsw
|
|
Layout.fillWidth: true
|
|
Layout.alignment: root.contentAlignment
|
|
}
|
|
|
|
ColumnLayout {
|
|
z: root.zFront
|
|
Layout.fillWidth: true
|
|
Layout.alignment: root.contentAlignment
|
|
|
|
StatusBaseText {
|
|
text: qsTr("New password")
|
|
}
|
|
|
|
StatusPasswordInput {
|
|
id: newPswInput
|
|
objectName: "passwordViewNewPassword"
|
|
|
|
property bool showPassword
|
|
|
|
Layout.alignment: root.contentAlignment
|
|
Layout.fillWidth: true
|
|
placeholderText: qsTr("Enter new password")
|
|
echoMode: showPassword ? TextInput.Normal : TextInput.Password
|
|
rightPadding: showHideNewIcon.width + showHideNewIcon.anchors.rightMargin + Theme.padding / 2
|
|
|
|
onTextChanged: {
|
|
// Update password checkers
|
|
errorTxt.text = ""
|
|
// Update strength indicator:
|
|
strengthInditactor.strength = d.convertStrength(root.passwordStrengthScoreFunction(newPswInput.text))
|
|
|
|
d.containsLower = d.lowerCaseValidator(text)
|
|
d.containsUpper = d.upperCaseValidator(text)
|
|
d.containsNumbers = d.numbersValidator(text)
|
|
d.containsSymbols = d.symbolsValidator(text)
|
|
|
|
if(!d.validateCharacterSet(text)) return
|
|
|
|
if (text.length === confirmPswInput.text.length) {
|
|
root.checkPasswordMatches(false)
|
|
}
|
|
}
|
|
onAccepted: root.returnPressed()
|
|
|
|
StatusFlatRoundButton {
|
|
id: showHideNewIcon
|
|
visible: newPswInput.text !== ""
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 16
|
|
width: 24
|
|
height: 24
|
|
icon.name: newPswInput.showPassword ? "hide" : "show"
|
|
icon.color: Theme.palette.baseColor1
|
|
|
|
onClicked: newPswInput.showPassword = !newPswInput.showPassword
|
|
}
|
|
}
|
|
|
|
StatusPasswordStrengthIndicator {
|
|
id: strengthInditactor
|
|
Layout.fillWidth: true
|
|
value: Math.min(Constants.minPasswordLength, newPswInput.text.length)
|
|
from: 0
|
|
to: Constants.minPasswordLength
|
|
labelVeryWeak: qsTr("Very weak")
|
|
labelWeak: qsTr("Weak")
|
|
labelSoso: qsTr("So-so")
|
|
labelGood: qsTr("Good")
|
|
labelGreat: qsTr("Great")
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.minimumHeight: 80
|
|
border.color: Theme.palette.baseColor2
|
|
border.width: 1
|
|
color: "transparent"
|
|
radius: Theme.radius
|
|
implicitHeight: strengthColumn.implicitHeight
|
|
implicitWidth: strengthColumn.implicitWidth
|
|
|
|
ColumnLayout {
|
|
id: strengthColumn
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.padding
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Theme.padding
|
|
|
|
StatusBaseText {
|
|
id: strengthenTxt
|
|
Layout.fillHeight: true
|
|
Layout.alignment: Qt.AlignHCenter
|
|
wrapMode: Text.WordWrap
|
|
text: root.strengthenText
|
|
font.pixelSize: 12
|
|
color: Theme.palette.baseColor1
|
|
clip: true
|
|
}
|
|
|
|
RowLayout {
|
|
spacing: Theme.padding
|
|
Layout.alignment: Qt.AlignHCenter
|
|
|
|
StatusBaseText {
|
|
id: lowerCaseTxt
|
|
text: "• " + qsTr("Lower case")
|
|
font.pixelSize: 12
|
|
color: d.containsLower ? Theme.palette.successColor1 : Theme.palette.baseColor1
|
|
}
|
|
|
|
StatusBaseText {
|
|
id: upperCaseTxt
|
|
text: "• " + qsTr("Upper case")
|
|
font.pixelSize: 12
|
|
color: d.containsUpper ? Theme.palette.successColor1 : Theme.palette.baseColor1
|
|
}
|
|
|
|
StatusBaseText {
|
|
id: numbersTxt
|
|
text: "• " + qsTr("Numbers")
|
|
font.pixelSize: 12
|
|
color: d.containsNumbers ? Theme.palette.successColor1 : Theme.palette.baseColor1
|
|
}
|
|
|
|
StatusBaseText {
|
|
id: symbolsTxt
|
|
text: "• " + qsTr("Symbols")
|
|
font.pixelSize: 12
|
|
color: d.containsSymbols ? Theme.palette.successColor1 : Theme.palette.baseColor1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
StatusBaseText {
|
|
text: qsTr("Confirm new password")
|
|
}
|
|
|
|
StatusPasswordInput {
|
|
id: confirmPswInput
|
|
objectName: "passwordViewNewPasswordConfirm"
|
|
|
|
property bool showPassword
|
|
|
|
z: root.zFront
|
|
Layout.fillWidth: true
|
|
Layout.alignment: root.contentAlignment
|
|
placeholderText: qsTr("Enter new password")
|
|
echoMode: showPassword ? TextInput.Normal : TextInput.Password
|
|
rightPadding: showHideConfirmIcon.width + showHideConfirmIcon.anchors.rightMargin + Theme.padding / 2
|
|
|
|
onTextChanged: {
|
|
errorTxt.text = ""
|
|
|
|
if(!d.validateCharacterSet(newPswInput.text)) return
|
|
|
|
d.passwordValidation();
|
|
if(text.length === newPswInput.text.length) {
|
|
root.checkPasswordMatches()
|
|
}
|
|
}
|
|
|
|
onFocusChanged: {
|
|
// When clicking into the confirmation input, validate if new password:
|
|
if(focus) {
|
|
d.passwordValidation()
|
|
}
|
|
// When leaving the confirmation input because of the button or other input component is focused, check if password matches
|
|
else {
|
|
root.checkPasswordMatches(false)
|
|
}
|
|
}
|
|
onAccepted: root.returnPressed()
|
|
|
|
StatusFlatRoundButton {
|
|
id: showHideConfirmIcon
|
|
visible: confirmPswInput.text !== ""
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 16
|
|
width: 24
|
|
height: 24
|
|
icon.name: confirmPswInput.showPassword ? "hide" : "show"
|
|
icon.color: Theme.palette.baseColor1
|
|
|
|
onClicked: confirmPswInput.showPassword = !confirmPswInput.showPassword
|
|
}
|
|
}
|
|
}
|
|
|
|
StatusBaseText {
|
|
id: errorTxt
|
|
Layout.alignment: root.contentAlignment
|
|
Layout.fillHeight: true
|
|
font.pixelSize: 12
|
|
color: Theme.palette.dangerColor1
|
|
}
|
|
}
|