feat: add quick password entry mode

This commit is contained in:
thePR0M3TH3AN
2025-08-02 16:43:43 -04:00
parent 7a8c0aef86
commit 7f503f0787
5 changed files with 173 additions and 55 deletions

View File

@@ -1464,6 +1464,72 @@ class PasswordManager:
parent_fingerprint=parent_fp,
child_fingerprint=child_fp,
)
def prompt_length() -> int | None:
length_input = input(
f"Enter desired password length (default {DEFAULT_PASSWORD_LENGTH}): "
).strip()
length = DEFAULT_PASSWORD_LENGTH
if length_input:
if not length_input.isdigit():
print(
colored("Error: Password length must be a number.", "red")
)
return None
length = int(length_input)
if not (MIN_PASSWORD_LENGTH <= length <= MAX_PASSWORD_LENGTH):
print(
colored(
f"Error: Password length must be between {MIN_PASSWORD_LENGTH} and {MAX_PASSWORD_LENGTH}.",
"red",
)
)
return None
return length
def finalize_entry(index: int, label: str, length: int) -> None:
# Mark database as dirty for background sync
self.is_dirty = True
self.last_update = time.time()
# Generate the password using the assigned index
entry = self.entry_manager.retrieve_entry(index)
password = self._generate_password_for_entry(entry, index, length)
# Provide user feedback
print(
colored(
f"\n[+] Password generated and indexed with ID {index}.\n",
"green",
)
)
if self.secret_mode_enabled:
copy_to_clipboard(password, self.clipboard_clear_delay)
print(
colored(
f"[+] Password copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
else:
print(colored(f"Password for {label}: {password}\n", "yellow"))
# Automatically push the updated encrypted index to Nostr so the
# latest changes are backed up remotely.
try:
self.start_background_vault_sync()
logging.info(
"Encrypted index posted to Nostr after entry addition."
)
except Exception as nostr_error:
logging.error(
f"Failed to post updated index to Nostr: {nostr_error}",
exc_info=True,
)
pause()
mode = input("Choose mode: [Q]uick or [A]dvanced? ").strip().lower()
website_name = input("Enter the label or website name: ").strip()
if not website_name:
print(colored("Error: Label cannot be empty.", "red"))
@@ -1471,6 +1537,29 @@ class PasswordManager:
username = input("Enter the username (optional): ").strip()
url = input("Enter the URL (optional): ").strip()
if mode.startswith("q"):
length = prompt_length()
if length is None:
return
include_special_input = (
input("Include special characters? (Y/n): ").strip().lower()
)
include_special_chars: bool | None = None
if include_special_input:
include_special_chars = include_special_input != "n"
index = self.entry_manager.add_entry(
website_name,
length,
username,
url,
include_special_chars=include_special_chars,
)
finalize_entry(index, website_name, length)
return
notes = input("Enter notes (optional): ").strip()
tags_input = input("Enter tags (comma-separated, optional): ").strip()
tags = (
@@ -1491,23 +1580,9 @@ class PasswordManager:
{"label": label, "value": value, "is_hidden": hidden}
)
length_input = input(
f"Enter desired password length (default {DEFAULT_PASSWORD_LENGTH}): "
).strip()
length = DEFAULT_PASSWORD_LENGTH
if length_input:
if not length_input.isdigit():
print(colored("Error: Password length must be a number.", "red"))
return
length = int(length_input)
if not (MIN_PASSWORD_LENGTH <= length <= MAX_PASSWORD_LENGTH):
print(
colored(
f"Error: Password length must be between {MIN_PASSWORD_LENGTH} and {MAX_PASSWORD_LENGTH}.",
"red",
)
)
return
length = prompt_length()
if length is None:
return
include_special_input = (
input("Include special characters? (Y/n): ").strip().lower()
@@ -1563,7 +1638,6 @@ class PasswordManager:
return
min_special = int(min_special_input) if min_special_input else None
# Add the entry to the index and get the assigned index
index = self.entry_manager.add_entry(
website_name,
length,
@@ -1583,43 +1657,7 @@ class PasswordManager:
min_special=min_special,
)
# Mark database as dirty for background sync
self.is_dirty = True
self.last_update = time.time()
# Generate the password using the assigned index
entry = self.entry_manager.retrieve_entry(index)
password = self._generate_password_for_entry(entry, index, length)
# Provide user feedback
print(
colored(
f"\n[+] Password generated and indexed with ID {index}.\n",
"green",
)
)
if self.secret_mode_enabled:
copy_to_clipboard(password, self.clipboard_clear_delay)
print(
colored(
f"[+] Password copied to clipboard. Will clear in {self.clipboard_clear_delay} seconds.",
"green",
)
)
else:
print(colored(f"Password for {website_name}: {password}\n", "yellow"))
# Automatically push the updated encrypted index to Nostr so the
# latest changes are backed up remotely.
try:
self.start_background_vault_sync()
logging.info("Encrypted index posted to Nostr after entry addition.")
except Exception as nostr_error:
logging.error(
f"Failed to post updated index to Nostr: {nostr_error}",
exc_info=True,
)
pause()
finalize_entry(index, website_name, length)
except Exception as e:
logging.error(f"Error during password generation: {e}", exc_info=True)

View File

@@ -45,6 +45,7 @@ def test_handle_add_password(monkeypatch, dummy_nostr_client, capsys):
inputs = iter(
[
"a", # advanced mode
"Example", # label
"", # username
"", # url
@@ -114,6 +115,7 @@ def test_handle_add_password_secret_mode(monkeypatch, dummy_nostr_client, capsys
inputs = iter(
[
"a", # advanced mode
"Example", # label
"", # username
"", # url
@@ -147,3 +149,62 @@ def test_handle_add_password_secret_mode(monkeypatch, dummy_nostr_client, capsys
assert f"pw-0-{DEFAULT_PASSWORD_LENGTH}" not in out
assert "copied to clipboard" in out
assert called == [(f"pw-0-{DEFAULT_PASSWORD_LENGTH}", 5)]
def test_handle_add_password_quick_mode(monkeypatch, dummy_nostr_client, capsys):
client, _relay = dummy_nostr_client
with TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
vault, enc_mgr = create_vault(tmp_path, TEST_SEED, TEST_PASSWORD)
cfg_mgr = ConfigManager(vault, tmp_path)
backup_mgr = BackupManager(tmp_path, cfg_mgr)
entry_mgr = EntryManager(vault, backup_mgr)
pm = PasswordManager.__new__(PasswordManager)
pm.encryption_mode = EncryptionMode.SEED_ONLY
pm.encryption_manager = enc_mgr
pm.vault = vault
pm.entry_manager = entry_mgr
pm.backup_manager = backup_mgr
pm.password_generator = FakePasswordGenerator()
pm.parent_seed = TEST_SEED
pm.nostr_client = client
pm.fingerprint_dir = tmp_path
pm.secret_mode_enabled = False
pm.is_dirty = False
inputs = iter(
[
"q", # quick mode
"Example", # label
"", # username
"", # url
"", # length (default)
"", # include special default
]
)
monkeypatch.setattr("builtins.input", lambda *a, **k: next(inputs))
monkeypatch.setattr("seedpass.core.manager.pause", lambda *a, **k: None)
monkeypatch.setattr(pm, "start_background_vault_sync", lambda *a, **k: None)
pm.handle_add_password()
out = capsys.readouterr().out
entries = entry_mgr.list_entries(verbose=False)
assert entries == [(0, "Example", "", "", False)]
entry = entry_mgr.retrieve_entry(0)
assert entry == {
"label": "Example",
"length": DEFAULT_PASSWORD_LENGTH,
"username": "",
"url": "",
"archived": False,
"type": "password",
"kind": "password",
"notes": "",
"custom_fields": [],
"tags": [],
}
assert f"pw-0-{DEFAULT_PASSWORD_LENGTH}" in out

View File

@@ -50,6 +50,7 @@ def test_manager_workflow(monkeypatch):
inputs = iter(
[
"a", # advanced mode
"example.com",
"", # username
"", # url