Skip to content

Commit

Permalink
Introduced password input widget (#1662)
Browse files Browse the repository at this point in the history
Move existing code for password input widgets into common classes to increase maintainability and reusability alongside reducing redundancy. This implements a `PasswordLineEdit` that can show a red border when an invalid password is entered. It also features a button for showing/hiding the password entered. When combining two of these entries for setting a new password `PasswordInput` can be used from now on. It combines a form for entering and confirming a password with a label to show a message when there is an issue with the password. It also checks the entered password against some rules regarding its length. This PR replaces existing widgets for entering passwords with these two new widgets.

* src/vorta/views/partials/password_input.py : Implement common input widgets/classes

* src/vorta/views/repo_add_dialog.py : Use new widgets.
* src/vorta/assets/UI/repoadd.ui : ^^^
  • Loading branch information
jetchirag authored Jul 28, 2023
1 parent 157ac37 commit 25b4cc0
Show file tree
Hide file tree
Showing 6 changed files with 476 additions and 230 deletions.
68 changes: 1 addition & 67 deletions src/vorta/assets/UI/repoadd.ui
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@
<ui version="4.0">
<class>AddRepository</class>
<widget class="QDialog" name="AddRepository">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>583</width>
<height>338</height>
</rect>
</property>
<property name="modal">
<bool>true</bool>
</property>
Expand Down Expand Up @@ -82,7 +74,7 @@
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
<number>20</number>
</property>
<item row="0" column="1">
<widget class="QLabel" name="title">
Expand Down Expand Up @@ -169,47 +161,6 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Borg passphrase:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="passwordLineEdit">
<property name="enabled">
<bool>true</bool>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="confirmLabel">
<property name="text">
<string>Confirm passphrase:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="confirmLineEdit">
<property name="enabled">
<bool>true</bool>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="QLabel" name="passwordLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabWidgetPage2">
Expand Down Expand Up @@ -254,23 +205,6 @@
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="encryptionLabel">
<property name="text">
<string>Encryption:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="encryptionComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
Expand Down
16 changes: 0 additions & 16 deletions src/vorta/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from PyQt6.QtWidgets import QApplication, QFileDialog, QSystemTrayIcon

from vorta.borg._compatibility import BorgCompatibility
from vorta.i18n import trans_late
from vorta.log import logger
from vorta.network_status.abc import NetworkStatusMonitor

Expand Down Expand Up @@ -507,21 +506,6 @@ def is_system_tray_available():
return is_available


def validate_passwords(first_pass, second_pass):
'''Validates the password for borg, do not use on single fields'''
pass_equal = first_pass == second_pass
pass_long = len(first_pass) > 8

if not pass_long and not pass_equal:
return trans_late('utils', "Passwords must be identical and greater than 8 characters long.")
if not pass_equal:
return trans_late('utils', "Passwords must be identical.")
if not pass_long:
return trans_late('utils', "Passwords must be greater than 8 characters long.")

return ""


def search(key, iterable: Iterable, func: Callable = None) -> Tuple[int, Any]:
"""
Search for a key in an iterable.
Expand Down
172 changes: 172 additions & 0 deletions src/vorta/views/partials/password_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
from typing import Optional

from PyQt6.QtCore import QObject
from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import QFormLayout, QLabel, QLineEdit, QWidget

from vorta.i18n import translate
from vorta.views.utils import get_colored_icon


class PasswordLineEdit(QLineEdit):
def __init__(self, *, parent: Optional[QWidget] = None, show_visibility_button: bool = True) -> None:
super().__init__(parent)

self._show_visibility_button = show_visibility_button
self._error_state = False
self._visible = False

self.setEchoMode(QLineEdit.EchoMode.Password)

if self._show_visibility_button:
self.showHideAction = QAction(self.tr("Show password"), self)
self.showHideAction.setCheckable(True)
self.showHideAction.toggled.connect(self.toggle_visibility)
self.showHideAction.setIcon(get_colored_icon("eye"))
self.addAction(self.showHideAction, QLineEdit.ActionPosition.TrailingPosition)

def get_password(self) -> str:
"""Return password text"""
return self.text()

@property
def visible(self) -> bool:
"""Return password visibility"""
return self._visible

@visible.setter
def visible(self, value: bool) -> None:
"""Set password visibility"""
if not isinstance(value, bool):
raise TypeError("visible must be a boolean value")
self._visible = value
self.setEchoMode(QLineEdit.EchoMode.Normal if self._visible else QLineEdit.EchoMode.Password)

if self._show_visibility_button:
if self._visible:
self.showHideAction.setIcon(get_colored_icon("eye-slash"))
self.showHideAction.setText(self.tr("Hide password"))

else:
self.showHideAction.setIcon(get_colored_icon("eye"))
self.showHideAction.setText(self.tr("Show password"))

def toggle_visibility(self) -> None:
"""Toggle password visibility"""
self.visible = not self._visible

@property
def error_state(self) -> bool:
"""Return error state"""
return self._error_state

@error_state.setter
def error_state(self, error: bool) -> None:
"""Set error state and update style"""
self._error_state = error
if error:
self.setStyleSheet("QLineEdit { border: 2px solid red; }")
else:
self.setStyleSheet('')


class PasswordInput(QObject):
def __init__(self, *, parent=None, minimum_length: int = 9, show_error: bool = True, label: list = None) -> None:
super().__init__(parent)
self._minimum_length = minimum_length
self._show_error = show_error

if label:
self._label_password = QLabel(label[0])
self._label_confirm = QLabel(label[1])
else:
self._label_password = QLabel(self.tr("Enter passphrase:"))
self._label_confirm = QLabel(self.tr("Confirm passphrase:"))

# Create password line edits
self.passwordLineEdit = PasswordLineEdit()
self.confirmLineEdit = PasswordLineEdit()
self.validation_label = QLabel("")

self.passwordLineEdit.editingFinished.connect(self.on_editing_finished)
self.confirmLineEdit.textChanged.connect(self.validate)

def on_editing_finished(self) -> None:
self.passwordLineEdit.editingFinished.disconnect(self.on_editing_finished)
self.passwordLineEdit.textChanged.connect(self.validate)
self.validate()

def set_labels(self, label_1: str, label_2: str) -> None:
self._label_password.setText(label_1)
self._label_confirm.setText(label_2)

def set_error_label(self, text: str) -> None:
self.validation_label.setText(text)

def set_validation_enabled(self, enable: bool) -> None:
self._show_error = enable
self.passwordLineEdit.error_state = False
self.confirmLineEdit.error_state = False
if not enable:
self.set_error_label("")

def clear(self) -> None:
self.passwordLineEdit.clear()
self.confirmLineEdit.clear()
self.passwordLineEdit.error_state = False
self.confirmLineEdit.error_state = False
self.set_error_label("")

def get_password(self) -> str:
return self.passwordLineEdit.text()

def validate(self) -> bool:
if not self._show_error:
return True

first_pass = self.passwordLineEdit.text()
second_pass = self.confirmLineEdit.text()

pass_equal = first_pass == second_pass
pass_long = len(first_pass) >= self._minimum_length

self.passwordLineEdit.error_state = False
self.confirmLineEdit.error_state = False
self.set_error_label("")

if not pass_long and not pass_equal:
self.passwordLineEdit.error_state = True
self.confirmLineEdit.error_state = True
self.set_error_label(
translate('PasswordInput', "Passwords must be identical and atleast {0} characters long.").format(
self._minimum_length
)
)
elif not pass_equal:
self.confirmLineEdit.error_state = True
self.set_error_label(translate('PasswordInput', "Passwords must be identical."))
elif not pass_long:
self.passwordLineEdit.error_state = True
self.set_error_label(
translate('PasswordInput', "Passwords must be atleast {0} characters long.").format(
self._minimum_length
)
)

return not bool(self.validation_label.text())

def add_form_to_layout(self, form_layout: QFormLayout) -> None:
"""Adds form to layout"""
form_layout.addRow(self._label_password, self.passwordLineEdit)
form_layout.addRow(self._label_confirm, self.confirmLineEdit)
form_layout.addRow(self.validation_label)

def create_form_widget(self, parent: Optional[QWidget] = None) -> QWidget:
""" "Creates and Returns a new QWidget with form layout"""
widget = QWidget(parent=parent)
form_layout = QFormLayout(widget)
form_layout.setContentsMargins(0, 0, 0, 0)
form_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
self.add_form_to_layout(form_layout)
widget.setLayout(form_layout)
return widget
Loading

0 comments on commit 25b4cc0

Please sign in to comment.