mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 07:18:47 +00:00
Add entry archiving features and update menus
This commit is contained in:
14
src/main.py
14
src/main.py
@@ -276,9 +276,7 @@ def print_matches(
|
||||
print(color_text(f" Username: {username}", "index"))
|
||||
if url:
|
||||
print(color_text(f" URL: {url}", "index"))
|
||||
print(
|
||||
color_text(f" Blacklisted: {'Yes' if blacklisted else 'No'}", "index")
|
||||
)
|
||||
print(color_text(f" Archived: {'Yes' if blacklisted else 'No'}", "index"))
|
||||
print("-" * 40)
|
||||
|
||||
|
||||
@@ -752,6 +750,8 @@ def display_menu(
|
||||
5. Modify an Existing Entry
|
||||
6. 2FA Codes
|
||||
7. Settings
|
||||
8. Archive Entry
|
||||
9. View Archived Entries
|
||||
"""
|
||||
display_fn = getattr(password_manager, "display_stats", None)
|
||||
if callable(display_fn):
|
||||
@@ -781,7 +781,7 @@ def display_menu(
|
||||
print(color_text(menu, "menu"))
|
||||
try:
|
||||
choice = timed_input(
|
||||
"Enter your choice (1-7) or press Enter to exit: ",
|
||||
"Enter your choice (1-9) or press Enter to exit: ",
|
||||
inactivity_timeout,
|
||||
).strip()
|
||||
except TimeoutError:
|
||||
@@ -856,6 +856,12 @@ def display_menu(
|
||||
elif choice == "7":
|
||||
password_manager.update_activity()
|
||||
handle_settings(password_manager)
|
||||
elif choice == "8":
|
||||
password_manager.update_activity()
|
||||
password_manager.handle_archive_entry()
|
||||
elif choice == "9":
|
||||
password_manager.update_activity()
|
||||
password_manager.handle_view_archived_entries()
|
||||
else:
|
||||
print(colored("Invalid choice. Please select a valid option.", "red"))
|
||||
|
||||
|
@@ -630,9 +630,17 @@ class EntryManager:
|
||||
self.modify_entry(index, archived=False)
|
||||
|
||||
def list_entries(
|
||||
self, sort_by: str = "index", filter_kind: str | None = None
|
||||
self,
|
||||
sort_by: str = "index",
|
||||
filter_kind: str | None = None,
|
||||
*,
|
||||
include_archived: bool = False,
|
||||
) -> List[Tuple[int, str, Optional[str], Optional[str], bool]]:
|
||||
"""List entries in the index with optional sorting and filtering."""
|
||||
"""List entries in the index with optional sorting and filtering.
|
||||
|
||||
By default archived entries are omitted unless ``include_archived`` is
|
||||
``True``.
|
||||
"""
|
||||
try:
|
||||
data = self.vault.load_index()
|
||||
entries_data = data.get("entries", {})
|
||||
@@ -662,6 +670,10 @@ class EntryManager:
|
||||
!= filter_kind
|
||||
):
|
||||
continue
|
||||
if not include_archived and entry.get(
|
||||
"archived", entry.get("blacklisted", False)
|
||||
):
|
||||
continue
|
||||
filtered_items.append((int(idx_str), entry))
|
||||
|
||||
entries: List[Tuple[int, str, Optional[str], Optional[str], bool]] = []
|
||||
@@ -708,7 +720,7 @@ class EntryManager:
|
||||
print(colored(f" URL: {entry.get('url') or 'N/A'}", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f" Blacklisted: {'Yes' if entry.get('archived', entry.get('blacklisted', False)) else 'No'}",
|
||||
f" Archived: {'Yes' if entry.get('archived', entry.get('blacklisted', False)) else 'No'}",
|
||||
"cyan",
|
||||
)
|
||||
)
|
||||
@@ -880,11 +892,19 @@ class EntryManager:
|
||||
)
|
||||
|
||||
def list_all_entries(
|
||||
self, sort_by: str = "index", filter_kind: str | None = None
|
||||
self,
|
||||
sort_by: str = "index",
|
||||
filter_kind: str | None = None,
|
||||
*,
|
||||
include_archived: bool = False,
|
||||
) -> None:
|
||||
"""Display all entries using :meth:`list_entries`."""
|
||||
try:
|
||||
entries = self.list_entries(sort_by=sort_by, filter_kind=filter_kind)
|
||||
entries = self.list_entries(
|
||||
sort_by=sort_by,
|
||||
filter_kind=filter_kind,
|
||||
include_archived=include_archived,
|
||||
)
|
||||
if not entries:
|
||||
print(colored("No entries to display.", "yellow"))
|
||||
return
|
||||
@@ -896,9 +916,7 @@ class EntryManager:
|
||||
print(colored(f" Label: {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(colored(f" Archived: {'Yes' if blacklisted else 'No'}", "cyan"))
|
||||
print("-" * 40)
|
||||
|
||||
except Exception as e:
|
||||
@@ -907,7 +925,10 @@ class EntryManager:
|
||||
return
|
||||
|
||||
def get_entry_summaries(
|
||||
self, filter_kind: str | None = None
|
||||
self,
|
||||
filter_kind: str | None = None,
|
||||
*,
|
||||
include_archived: bool = False,
|
||||
) -> list[tuple[int, str, str]]:
|
||||
"""Return a list of entry index, type, and display labels."""
|
||||
try:
|
||||
@@ -919,6 +940,10 @@ class EntryManager:
|
||||
etype = entry.get("type", entry.get("kind", EntryType.PASSWORD.value))
|
||||
if filter_kind and etype != filter_kind:
|
||||
continue
|
||||
if not include_archived and entry.get(
|
||||
"archived", entry.get("blacklisted", False)
|
||||
):
|
||||
continue
|
||||
if etype == EntryType.PASSWORD.value:
|
||||
label = entry.get("label", entry.get("website", ""))
|
||||
else:
|
||||
|
@@ -1561,7 +1561,7 @@ class PasswordManager:
|
||||
if blacklisted:
|
||||
print(
|
||||
colored(
|
||||
f"Warning: This password is blacklisted and should not be used.",
|
||||
f"Warning: This password is archived and should not be used.",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
@@ -1589,7 +1589,7 @@ class PasswordManager:
|
||||
print(colored(f"Associated URL: {url or 'N/A'}", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f"Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}",
|
||||
f"Archived Status: {'Archived' if blacklisted else 'Active'}",
|
||||
"cyan",
|
||||
)
|
||||
)
|
||||
@@ -1673,7 +1673,7 @@ class PasswordManager:
|
||||
print(colored(f"Current Digits: {digits}", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f"Current Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}",
|
||||
f"Current Archived Status: {'Archived' if blacklisted else 'Active'}",
|
||||
"cyan",
|
||||
)
|
||||
)
|
||||
@@ -1708,7 +1708,7 @@ class PasswordManager:
|
||||
)
|
||||
blacklist_input = (
|
||||
input(
|
||||
f'Is this 2FA code blacklisted? (Y/N, current: {"Y" if blacklisted else "N"}): '
|
||||
f'Archive this 2FA code? (Y/N, current: {"Y" if blacklisted else "N"}): '
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
@@ -1722,7 +1722,7 @@ class PasswordManager:
|
||||
else:
|
||||
print(
|
||||
colored(
|
||||
"Invalid input for blacklist status. Keeping the current status.",
|
||||
"Invalid input for archived status. Keeping the current status.",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
@@ -1776,7 +1776,7 @@ class PasswordManager:
|
||||
print(colored(f"Current URL: {url or 'N/A'}", "cyan"))
|
||||
print(
|
||||
colored(
|
||||
f"Current Blacklist Status: {'Blacklisted' if blacklisted else 'Not Blacklisted'}",
|
||||
f"Current Archived Status: {'Archived' if blacklisted else 'Active'}",
|
||||
"cyan",
|
||||
)
|
||||
)
|
||||
@@ -1802,7 +1802,7 @@ class PasswordManager:
|
||||
)
|
||||
blacklist_input = (
|
||||
input(
|
||||
f'Is this password blacklisted? (Y/N, current: {"Y" if blacklisted else "N"}): '
|
||||
f'Archive this password? (Y/N, current: {"Y" if blacklisted else "N"}): '
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
@@ -1816,7 +1816,7 @@ class PasswordManager:
|
||||
else:
|
||||
print(
|
||||
colored(
|
||||
"Invalid input for blacklist status. Keeping the current status.",
|
||||
"Invalid input for archived status. Keeping the current status.",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
@@ -1997,7 +1997,10 @@ class PasswordManager:
|
||||
print(color_text(f" Username: {username or 'N/A'}", "index"))
|
||||
print(color_text(f" URL: {url or 'N/A'}", "index"))
|
||||
print(
|
||||
color_text(f" Blacklisted: {'Yes' if blacklisted else 'No'}", "index")
|
||||
color_text(
|
||||
f" Archived: {'Yes' if blacklisted else 'No'}",
|
||||
"index",
|
||||
)
|
||||
)
|
||||
print("-" * 40)
|
||||
|
||||
@@ -2038,7 +2041,9 @@ class PasswordManager:
|
||||
print(colored("Invalid choice.", "red"))
|
||||
continue
|
||||
|
||||
summaries = self.entry_manager.get_entry_summaries(filter_kind)
|
||||
summaries = self.entry_manager.get_entry_summaries(
|
||||
filter_kind, include_archived=False
|
||||
)
|
||||
if not summaries:
|
||||
continue
|
||||
while True:
|
||||
@@ -2103,6 +2108,59 @@ class PasswordManager:
|
||||
logging.error(f"Error during entry deletion: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to delete entry: {e}", "red"))
|
||||
|
||||
def handle_archive_entry(self) -> None:
|
||||
"""Archive an entry without deleting it."""
|
||||
try:
|
||||
index_input = input(
|
||||
"Enter the index number of the entry to archive: "
|
||||
).strip()
|
||||
if not index_input.isdigit():
|
||||
print(colored("Error: Index must be a number.", "red"))
|
||||
return
|
||||
index = int(index_input)
|
||||
self.entry_manager.archive_entry(index)
|
||||
self.is_dirty = True
|
||||
self.last_update = time.time()
|
||||
except Exception as e:
|
||||
logging.error(f"Error archiving entry: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to archive entry: {e}", "red"))
|
||||
|
||||
def handle_view_archived_entries(self) -> None:
|
||||
"""Display archived entries and optionally restore one."""
|
||||
try:
|
||||
archived = self.entry_manager.list_entries(include_archived=True)
|
||||
archived = [e for e in archived if e[4]]
|
||||
if not archived:
|
||||
print(colored("No archived entries found.", "yellow"))
|
||||
return
|
||||
while True:
|
||||
clear_and_print_fingerprint(
|
||||
getattr(self, "current_fingerprint", None),
|
||||
"Main Menu > Archived Entries",
|
||||
)
|
||||
print(colored("\n[+] Archived Entries:\n", "green"))
|
||||
for idx, label, username, url, _ in archived:
|
||||
print(colored(f"{idx}. {label}", "cyan"))
|
||||
idx_input = input(
|
||||
"Enter index to restore or press Enter to go back: "
|
||||
).strip()
|
||||
if not idx_input:
|
||||
break
|
||||
if not idx_input.isdigit():
|
||||
print(colored("Invalid index.", "red"))
|
||||
continue
|
||||
restore_index = int(idx_input)
|
||||
self.entry_manager.restore_entry(restore_index)
|
||||
self.is_dirty = True
|
||||
self.last_update = time.time()
|
||||
archived = [e for e in archived if e[0] != restore_index]
|
||||
if not archived:
|
||||
print(colored("All entries restored.", "green"))
|
||||
break
|
||||
except Exception as e:
|
||||
logging.error(f"Error viewing archived entries: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to view archived entries: {e}", "red"))
|
||||
|
||||
def handle_display_totp_codes(self) -> None:
|
||||
"""Display all stored TOTP codes with a countdown progress bar."""
|
||||
try:
|
||||
|
@@ -64,7 +64,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", ""])
|
||||
inputs = iter(["10", ""])
|
||||
monkeypatch.setattr(main, "timed_input", lambda *_: next(inputs))
|
||||
with pytest.raises(SystemExit):
|
||||
main.display_menu(pm, sync_interval=1000, inactivity_timeout=1000)
|
||||
|
Reference in New Issue
Block a user