Files
seedPass/src/seedpass/core/entry_service.py
2025-08-11 20:36:53 -04:00

234 lines
9.0 KiB
Python

from __future__ import annotations
import logging
import time
from typing import TYPE_CHECKING
from termcolor import colored
from constants import (
DEFAULT_PASSWORD_LENGTH,
MAX_PASSWORD_LENGTH,
MIN_PASSWORD_LENGTH,
)
import seedpass.core.manager as manager_module
from utils.terminal_utils import clear_header_with_notification, pause
if TYPE_CHECKING: # pragma: no cover - typing only
from .manager import PasswordManager
class EntryService:
"""Entry management operations for :class:`PasswordManager`."""
def __init__(self, manager: PasswordManager) -> None:
self.manager = manager
def handle_add_password(self) -> None:
pm = self.manager
try:
fp, parent_fp, child_fp = pm.header_fingerprint_args
clear_header_with_notification(
pm,
fp,
"Main Menu > Add Entry > Password",
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:
pm.is_dirty = True
pm.last_update = time.time()
entry = pm.entry_manager.retrieve_entry(index)
password = pm._generate_password_for_entry(entry, index, length)
print(
colored(
f"\n[+] Password generated and indexed with ID {index}.\n",
"green",
)
)
if pm.secret_mode_enabled:
if manager_module.copy_to_clipboard(
password, pm.clipboard_clear_delay
):
print(
colored(
f"[+] Password copied to clipboard. Will clear in {pm.clipboard_clear_delay} seconds.",
"green",
)
)
else:
print(colored(f"Password for {label}: {password}\n", "yellow"))
try:
pm.start_background_vault_sync()
logging.info(
"Encrypted index posted to Nostr after entry addition."
)
except Exception as nostr_error: # pragma: no cover - best effort
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"))
return
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 = pm.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 = (
[t.strip() for t in tags_input.split(",") if t.strip()]
if tags_input
else []
)
custom_fields: list[dict[str, object]] = []
while True:
add_field = input("Add custom field? (y/N): ").strip().lower()
if add_field != "y":
break
label = input(" Field label: ").strip()
value = input(" Field value: ").strip()
hidden = input(" Hidden field? (y/N): ").strip().lower() == "y"
custom_fields.append(
{"label": label, "value": value, "is_hidden": hidden}
)
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"
allowed_special_chars = input(
"Allowed special characters (leave blank for default): "
).strip()
if not allowed_special_chars:
allowed_special_chars = None
special_mode = input("Special character mode (safe/leave blank): ").strip()
if not special_mode:
special_mode = None
exclude_ambiguous_input = (
input("Exclude ambiguous characters? (y/N): ").strip().lower()
)
exclude_ambiguous: bool | None = None
if exclude_ambiguous_input:
exclude_ambiguous = exclude_ambiguous_input == "y"
min_uppercase_input = input(
"Minimum uppercase letters (blank for default): "
).strip()
if min_uppercase_input and not min_uppercase_input.isdigit():
print(colored("Error: Minimum uppercase must be a number.", "red"))
return
min_uppercase = int(min_uppercase_input) if min_uppercase_input else None
min_lowercase_input = input(
"Minimum lowercase letters (blank for default): "
).strip()
if min_lowercase_input and not min_lowercase_input.isdigit():
print(colored("Error: Minimum lowercase must be a number.", "red"))
return
min_lowercase = int(min_lowercase_input) if min_lowercase_input else None
min_digits_input = input("Minimum digits (blank for default): ").strip()
if min_digits_input and not min_digits_input.isdigit():
print(colored("Error: Minimum digits must be a number.", "red"))
return
min_digits = int(min_digits_input) if min_digits_input else None
min_special_input = input(
"Minimum special characters (blank for default): "
).strip()
if min_special_input and not min_special_input.isdigit():
print(colored("Error: Minimum special must be a number.", "red"))
return
min_special = int(min_special_input) if min_special_input else None
index = pm.entry_manager.add_entry(
website_name,
length,
username,
url,
archived=False,
notes=notes,
custom_fields=custom_fields,
tags=tags,
include_special_chars=include_special_chars,
allowed_special_chars=allowed_special_chars,
special_mode=special_mode,
exclude_ambiguous=exclude_ambiguous,
min_uppercase=min_uppercase,
min_lowercase=min_lowercase,
min_digits=min_digits,
min_special=min_special,
)
finalize_entry(index, website_name, length)
except Exception as e: # pragma: no cover - defensive
logging.error(f"Error during password generation: {e}", exc_info=True)
print(colored(f"Error: Failed to generate password: {e}", "red"))
pause()