mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 15:28:44 +00:00
Merge pull request #271 from PR0M3TH3AN/codex/add--list-entries--item-to-main-menu
Add list entries feature
This commit is contained in:
22
src/main.py
22
src/main.py
@@ -701,10 +701,11 @@ def display_menu(
|
||||
1. Add Entry
|
||||
2. Retrieve Entry
|
||||
3. Search Entries
|
||||
4. Modify an Existing Entry
|
||||
5. 2FA Codes
|
||||
6. Settings
|
||||
7. Exit
|
||||
4. List Entries
|
||||
5. Modify an Existing Entry
|
||||
6. 2FA Codes
|
||||
7. Settings
|
||||
8. Exit
|
||||
"""
|
||||
display_fn = getattr(password_manager, "display_stats", None)
|
||||
if callable(display_fn):
|
||||
@@ -729,7 +730,7 @@ def display_menu(
|
||||
print(colored(menu, "cyan"))
|
||||
try:
|
||||
choice = timed_input(
|
||||
"Enter your choice (1-7): ", inactivity_timeout
|
||||
"Enter your choice (1-8): ", inactivity_timeout
|
||||
).strip()
|
||||
except TimeoutError:
|
||||
print(colored("Session timed out. Vault locked.", "yellow"))
|
||||
@@ -740,7 +741,7 @@ def display_menu(
|
||||
if not choice:
|
||||
print(
|
||||
colored(
|
||||
"No input detected. Please enter a number between 1 and 7.",
|
||||
"No input detected. Please enter a number between 1 and 8.",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
@@ -787,14 +788,17 @@ def display_menu(
|
||||
password_manager.handle_search_entries()
|
||||
elif choice == "4":
|
||||
password_manager.update_activity()
|
||||
password_manager.handle_modify_entry()
|
||||
password_manager.handle_list_entries()
|
||||
elif choice == "5":
|
||||
password_manager.update_activity()
|
||||
password_manager.handle_display_totp_codes()
|
||||
password_manager.handle_modify_entry()
|
||||
elif choice == "6":
|
||||
password_manager.update_activity()
|
||||
handle_settings(password_manager)
|
||||
password_manager.handle_display_totp_codes()
|
||||
elif choice == "7":
|
||||
password_manager.update_activity()
|
||||
handle_settings(password_manager)
|
||||
elif choice == "8":
|
||||
logging.info("Exiting the program.")
|
||||
print(colored("Exiting the program.", "green"))
|
||||
password_manager.nostr_client.close_client_pool()
|
||||
|
@@ -841,3 +841,39 @@ class EntryManager:
|
||||
logger.error(f"Failed to list all entries: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to list all entries: {e}", "red"))
|
||||
return
|
||||
|
||||
def get_entry_summaries(
|
||||
self, filter_kind: str | None = None
|
||||
) -> list[tuple[int, str]]:
|
||||
"""Return a list of entry index and display labels."""
|
||||
try:
|
||||
data = self.vault.load_index()
|
||||
entries_data = data.get("entries", {})
|
||||
|
||||
summaries: list[tuple[int, str]] = []
|
||||
for idx_str, entry in entries_data.items():
|
||||
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
|
||||
if filter_kind and etype != filter_kind:
|
||||
continue
|
||||
if etype == EntryType.PASSWORD.value:
|
||||
label = entry.get("website", "")
|
||||
elif etype == EntryType.TOTP.value:
|
||||
label = entry.get("label", "")
|
||||
elif etype == EntryType.SSH.value:
|
||||
label = "SSH Key"
|
||||
elif etype == EntryType.SEED.value:
|
||||
label = "Seed Phrase"
|
||||
elif etype == EntryType.NOSTR.value:
|
||||
label = entry.get("label", "Nostr Key")
|
||||
elif etype == EntryType.PGP.value:
|
||||
label = "PGP Key"
|
||||
else:
|
||||
label = etype
|
||||
summaries.append((int(idx_str), label))
|
||||
|
||||
summaries.sort(key=lambda x: x[0])
|
||||
return summaries
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get entry summaries: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to get entry summaries: {e}", "red"))
|
||||
return []
|
||||
|
@@ -1752,93 +1752,126 @@ class PasswordManager:
|
||||
|
||||
print(colored("\n[+] Search Results:\n", "green"))
|
||||
for match in results:
|
||||
index, website, username, url, blacklisted = match
|
||||
entry = self.entry_manager.retrieve_entry(index)
|
||||
if not entry:
|
||||
continue
|
||||
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
|
||||
print(colored(f"Index: {index}", "cyan"))
|
||||
if etype == EntryType.TOTP.value:
|
||||
print(colored(f" Label: {entry.get('label', website)}", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f" Derivation Index: {entry.get('index', index)}", "cyan"
|
||||
)
|
||||
)
|
||||
print(
|
||||
colored(
|
||||
f" Period: {entry.get('period', 30)}s Digits: {entry.get('digits', 6)}",
|
||||
"cyan",
|
||||
)
|
||||
)
|
||||
notes = entry.get("notes", "")
|
||||
if notes:
|
||||
print(colored(f" Notes: {notes}", "cyan"))
|
||||
elif etype == EntryType.SEED.value:
|
||||
print(colored(" Type: Seed Phrase", "cyan"))
|
||||
print(colored(f" Words: {entry.get('words', 24)}", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f" Derivation Index: {entry.get('index', index)}", "cyan"
|
||||
)
|
||||
)
|
||||
notes = entry.get("notes", "")
|
||||
if notes:
|
||||
print(colored(f" Notes: {notes}", "cyan"))
|
||||
elif etype == EntryType.SSH.value:
|
||||
print(colored(" Type: SSH Key", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f" Derivation Index: {entry.get('index', index)}", "cyan"
|
||||
)
|
||||
)
|
||||
notes = entry.get("notes", "")
|
||||
if notes:
|
||||
print(colored(f" Notes: {notes}", "cyan"))
|
||||
elif etype == EntryType.PGP.value:
|
||||
print(colored(" Type: PGP Key", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f" Key Type: {entry.get('key_type', 'ed25519')}", "cyan"
|
||||
)
|
||||
)
|
||||
uid = entry.get("user_id", "")
|
||||
if uid:
|
||||
print(colored(f" User ID: {uid}", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f" Derivation Index: {entry.get('index', index)}", "cyan"
|
||||
)
|
||||
)
|
||||
notes = entry.get("notes", "")
|
||||
if notes:
|
||||
print(colored(f" Notes: {notes}", "cyan"))
|
||||
elif etype == EntryType.NOSTR.value:
|
||||
print(colored(" Type: Nostr Key", "cyan"))
|
||||
print(colored(f" Label: {entry.get('label', '')}", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f" Derivation Index: {entry.get('index', index)}", "cyan"
|
||||
)
|
||||
)
|
||||
notes = entry.get("notes", "")
|
||||
if notes:
|
||||
print(colored(f" Notes: {notes}", "cyan"))
|
||||
else:
|
||||
print(colored(f" Website: {website}", "cyan"))
|
||||
print(colored(f" Username: {username or 'N/A'}", "cyan"))
|
||||
print(colored(f" URL: {url or 'N/A'}", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f" Blacklisted: {'Yes' if blacklisted else 'No'}", "cyan"
|
||||
)
|
||||
)
|
||||
print("-" * 40)
|
||||
|
||||
self.display_entry_details(match[0])
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to search entries: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to search entries: {e}", "red"))
|
||||
|
||||
def display_entry_details(self, index: int) -> None:
|
||||
"""Print detailed information for a single entry."""
|
||||
entry = self.entry_manager.retrieve_entry(index)
|
||||
if not entry:
|
||||
return
|
||||
|
||||
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
|
||||
print(colored(f"Index: {index}", "cyan"))
|
||||
if etype == EntryType.TOTP.value:
|
||||
print(colored(f" Label: {entry.get('label', '')}", "cyan"))
|
||||
print(colored(f" Derivation Index: {entry.get('index', index)}", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f" Period: {entry.get('period', 30)}s Digits: {entry.get('digits', 6)}",
|
||||
"cyan",
|
||||
)
|
||||
)
|
||||
notes = entry.get("notes", "")
|
||||
if notes:
|
||||
print(colored(f" Notes: {notes}", "cyan"))
|
||||
elif etype == EntryType.SEED.value:
|
||||
print(colored(" Type: Seed Phrase", "cyan"))
|
||||
print(colored(f" Words: {entry.get('words', 24)}", "cyan"))
|
||||
print(colored(f" Derivation Index: {entry.get('index', index)}", "cyan"))
|
||||
notes = entry.get("notes", "")
|
||||
if notes:
|
||||
print(colored(f" Notes: {notes}", "cyan"))
|
||||
elif etype == EntryType.SSH.value:
|
||||
print(colored(" Type: SSH Key", "cyan"))
|
||||
print(colored(f" Derivation Index: {entry.get('index', index)}", "cyan"))
|
||||
notes = entry.get("notes", "")
|
||||
if notes:
|
||||
print(colored(f" Notes: {notes}", "cyan"))
|
||||
elif etype == EntryType.PGP.value:
|
||||
print(colored(" Type: PGP Key", "cyan"))
|
||||
print(colored(f" Key Type: {entry.get('key_type', 'ed25519')}", "cyan"))
|
||||
uid = entry.get("user_id", "")
|
||||
if uid:
|
||||
print(colored(f" User ID: {uid}", "cyan"))
|
||||
print(colored(f" Derivation Index: {entry.get('index', index)}", "cyan"))
|
||||
notes = entry.get("notes", "")
|
||||
if notes:
|
||||
print(colored(f" Notes: {notes}", "cyan"))
|
||||
elif etype == EntryType.NOSTR.value:
|
||||
print(colored(" Type: Nostr Key", "cyan"))
|
||||
print(colored(f" Label: {entry.get('label', '')}", "cyan"))
|
||||
print(colored(f" Derivation Index: {entry.get('index', index)}", "cyan"))
|
||||
notes = entry.get("notes", "")
|
||||
if notes:
|
||||
print(colored(f" Notes: {notes}", "cyan"))
|
||||
else:
|
||||
website = entry.get("website", "")
|
||||
username = entry.get("username", "")
|
||||
url = entry.get("url", "")
|
||||
blacklisted = entry.get("blacklisted", False)
|
||||
print(colored(f" Website: {website}", "cyan"))
|
||||
print(colored(f" Username: {username or 'N/A'}", "cyan"))
|
||||
print(colored(f" URL: {url or 'N/A'}", "cyan"))
|
||||
print(colored(f" Blacklisted: {'Yes' if blacklisted else 'No'}", "cyan"))
|
||||
print("-" * 40)
|
||||
|
||||
def handle_list_entries(self) -> None:
|
||||
"""List entries and optionally show details."""
|
||||
try:
|
||||
while True:
|
||||
print("\nList Entries:")
|
||||
print("1. All")
|
||||
print("2. Passwords")
|
||||
print("3. 2FA (TOTP)")
|
||||
print("4. SSH Key")
|
||||
print("5. Seed Phrase")
|
||||
print("6. Nostr Key Pair")
|
||||
print("7. PGP")
|
||||
print("8. Back")
|
||||
choice = input("Select entry type: ").strip()
|
||||
if choice == "1":
|
||||
filter_kind = None
|
||||
elif choice == "2":
|
||||
filter_kind = EntryType.PASSWORD.value
|
||||
elif choice == "3":
|
||||
filter_kind = EntryType.TOTP.value
|
||||
elif choice == "4":
|
||||
filter_kind = EntryType.SSH.value
|
||||
elif choice == "5":
|
||||
filter_kind = EntryType.SEED.value
|
||||
elif choice == "6":
|
||||
filter_kind = EntryType.NOSTR.value
|
||||
elif choice == "7":
|
||||
filter_kind = EntryType.PGP.value
|
||||
elif choice == "8":
|
||||
return
|
||||
else:
|
||||
print(colored("Invalid choice.", "red"))
|
||||
continue
|
||||
|
||||
summaries = self.entry_manager.get_entry_summaries(filter_kind)
|
||||
if not summaries:
|
||||
continue
|
||||
print(colored("\n[+] Entries:\n", "green"))
|
||||
for idx, label in summaries:
|
||||
print(colored(f"{idx}. {label}", "cyan"))
|
||||
idx_input = input(
|
||||
"Enter index to view details or press Enter to return: "
|
||||
).strip()
|
||||
if not idx_input:
|
||||
return
|
||||
if not idx_input.isdigit():
|
||||
print(colored("Invalid index.", "red"))
|
||||
continue
|
||||
self.display_entry_details(int(idx_input))
|
||||
return
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to list entries: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to list entries: {e}", "red"))
|
||||
|
||||
def delete_entry(self) -> None:
|
||||
"""Deletes an entry from the password index."""
|
||||
try:
|
||||
|
@@ -31,7 +31,7 @@ def test_auto_sync_triggers_post(monkeypatch):
|
||||
called = True
|
||||
|
||||
monkeypatch.setattr(main, "handle_post_to_nostr", fake_post)
|
||||
monkeypatch.setattr(main, "timed_input", lambda *_: "7")
|
||||
monkeypatch.setattr(main, "timed_input", lambda *_: "8")
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
main.display_menu(pm, sync_interval=0.1)
|
||||
|
@@ -52,7 +52,7 @@ def _make_pm(called, locked=None):
|
||||
def test_empty_and_non_numeric_choice(monkeypatch, capsys):
|
||||
called = {"add": False, "retrieve": False, "modify": False}
|
||||
pm, _ = _make_pm(called)
|
||||
inputs = iter(["", "abc", "7"])
|
||||
inputs = iter(["", "abc", "8"])
|
||||
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
||||
with pytest.raises(SystemExit):
|
||||
main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000)
|
||||
@@ -65,7 +65,7 @@ def test_empty_and_non_numeric_choice(monkeypatch, capsys):
|
||||
def test_out_of_range_menu(monkeypatch, capsys):
|
||||
called = {"add": False, "retrieve": False, "modify": False}
|
||||
pm, _ = _make_pm(called)
|
||||
inputs = iter(["9", "7"])
|
||||
inputs = iter(["9", "8"])
|
||||
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
||||
with pytest.raises(SystemExit):
|
||||
main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000)
|
||||
@@ -77,7 +77,7 @@ def test_out_of_range_menu(monkeypatch, capsys):
|
||||
def test_invalid_add_entry_submenu(monkeypatch, capsys):
|
||||
called = {"add": False, "retrieve": False, "modify": False}
|
||||
pm, _ = _make_pm(called)
|
||||
inputs = iter(["1", "8", "7", "7"])
|
||||
inputs = iter(["1", "8", "7", "8"])
|
||||
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
||||
monkeypatch.setattr("builtins.input", lambda *_: next(inputs))
|
||||
with pytest.raises(SystemExit):
|
||||
@@ -92,7 +92,7 @@ def test_inactivity_timeout_loop(monkeypatch, capsys):
|
||||
pm, locked = _make_pm(called)
|
||||
pm.last_activity = 0
|
||||
monkeypatch.setattr(time, "time", lambda: 100.0)
|
||||
monkeypatch.setattr(main, "timed_input", lambda *_: "7")
|
||||
monkeypatch.setattr(main, "timed_input", lambda *_: "8")
|
||||
with pytest.raises(SystemExit):
|
||||
main.display_menu(pm, sync_interval=1000, inactivity_timeout=0.1)
|
||||
out = capsys.readouterr().out
|
||||
|
@@ -36,7 +36,7 @@ def test_inactivity_triggers_lock(monkeypatch):
|
||||
unlock_vault=unlock_vault,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(main, "timed_input", lambda *_: "7")
|
||||
monkeypatch.setattr(main, "timed_input", lambda *_: "8")
|
||||
|
||||
with pytest.raises(SystemExit):
|
||||
main.display_menu(pm, sync_interval=1000, inactivity_timeout=0.1)
|
||||
@@ -72,7 +72,7 @@ def test_input_timeout_triggers_lock(monkeypatch):
|
||||
unlock_vault=unlock_vault,
|
||||
)
|
||||
|
||||
responses = iter([TimeoutError(), "7"])
|
||||
responses = iter([TimeoutError(), "8"])
|
||||
|
||||
def fake_input(*_args, **_kwargs):
|
||||
val = next(responses)
|
||||
|
43
src/tests/test_manager_list_entries.py
Normal file
43
src/tests/test_manager_list_entries.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from types import SimpleNamespace
|
||||
|
||||
from helpers import create_vault, TEST_SEED, TEST_PASSWORD
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.append(str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from password_manager.entry_management import EntryManager
|
||||
from password_manager.backup import BackupManager
|
||||
from password_manager.manager import PasswordManager, EncryptionMode
|
||||
from password_manager.config_manager import ConfigManager
|
||||
|
||||
|
||||
def test_handle_list_entries(monkeypatch, capsys):
|
||||
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.nostr_client = SimpleNamespace()
|
||||
pm.fingerprint_dir = tmp_path
|
||||
|
||||
entry_mgr.add_totp("Example", TEST_SEED)
|
||||
entry_mgr.add_entry("example.com", 12)
|
||||
|
||||
inputs = iter(["1", ""]) # list all, then exit
|
||||
monkeypatch.setattr("builtins.input", lambda *_: next(inputs))
|
||||
|
||||
pm.handle_list_entries()
|
||||
out = capsys.readouterr().out
|
||||
assert "Example" in out
|
||||
assert "example.com" in out
|
@@ -30,7 +30,7 @@ def _make_pm(calls):
|
||||
def test_menu_totp_option(monkeypatch):
|
||||
calls = []
|
||||
pm = _make_pm(calls)
|
||||
inputs = iter(["5", "7"])
|
||||
inputs = iter(["6", "8"])
|
||||
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
||||
monkeypatch.setattr(main, "handle_settings", lambda *_: None)
|
||||
with pytest.raises(SystemExit):
|
||||
@@ -41,7 +41,7 @@ def test_menu_totp_option(monkeypatch):
|
||||
def test_menu_settings_option(monkeypatch):
|
||||
calls = []
|
||||
pm = _make_pm(calls)
|
||||
inputs = iter(["6", "7"])
|
||||
inputs = iter(["7", "8"])
|
||||
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
||||
monkeypatch.setattr(main, "handle_settings", lambda *_: calls.append("settings"))
|
||||
with pytest.raises(SystemExit):
|
||||
|
@@ -30,7 +30,7 @@ def _make_pm(called):
|
||||
def test_menu_search_option(monkeypatch):
|
||||
called = []
|
||||
pm = _make_pm(called)
|
||||
inputs = iter(["3", "7"])
|
||||
inputs = iter(["3", "8"])
|
||||
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
||||
monkeypatch.setattr("builtins.input", lambda *_: "query")
|
||||
with pytest.raises(SystemExit):
|
||||
|
Reference in New Issue
Block a user