mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 23:38:49 +00:00
234 lines
9.0 KiB
Python
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()
|