from typing import Optional, Annotated from injectable import autowired, injectable, Autowired from pykeepass import Group, Entry from app.api.models import EntryKind, EntrySimpleDTO, EntryComplexDTO, GroupDTO, NodeDTO, EntryNodeDTO from app.services import Passwords @injectable(singleton=True) class PasswordsController: @autowired def __init__(self, passwords: Annotated[Passwords, Autowired]): self._pw = passwords # Helpers def _group_by_path(self, path: Optional[str]) -> Group: if not path or path == "/": return self._pw.kp.root_group parts = [p for p in path.split("/") if p] g = self._pw.kp.root_group for p in parts: g = next((x for x in g.subgroups if x.name == p), None) if g is None: raise ValueError(f"Group not found: {path}") return g def _ensure_group(self, parent: Group, name: str) -> Group: g = next((x for x in parent.subgroups if x.name == name), None) if g: return g return self._pw.kp.add_group(parent, name) def _entry_by_path(self, path: str) -> Entry: # path: /Group1/Sub/Title parts = [p for p in path.split("/") if p] if not parts: raise ValueError("Invalid entry path") title = parts[-1] group_path = "/" + "/".join(parts[:-1]) if len(parts) > 1 else "/" g = self._group_by_path(group_path) e = next((x for x in g.entries if x.title == title), None) if not e: raise ValueError(f"Entry not found: {path}") return e def _node_of_group(self, g: Group) -> NodeDTO: raw_path = g.path path = "/" + "/".join(raw_path) if raw_path else "/" groups = [self._node_of_group(sg) for sg in g.subgroups] entries = [EntryNodeDTO( kind=EntryKind.complex if (e.username or e.url or e.notes) else EntryKind.simple, title=e.title, path=path.rstrip("/") + "/" + e.title ) for e in g.entries] return NodeDTO(name=g.name or "", path=path, groups=groups, entries=entries) # Tree def get_tree(self) -> NodeDTO: return self._node_of_group(self._pw.kp.root_group) # Groups def create_group(self, name: str, parent_path: Optional[str]) -> GroupDTO: parent = self._group_by_path(parent_path) g = self._ensure_group(parent, name) return GroupDTO(name=g.name, path=self._node_of_group(g).path) def rename_group(self, path: str, new_name: str) -> GroupDTO: g = self._group_by_path(path) g.name = new_name return GroupDTO(name=g.name, path=self._node_of_group(g).path) def delete_group(self, path: str) -> None: g = self._group_by_path(path) if g is self._pw.kp.root_group: raise ValueError("Cannot delete root group") self._pw.kp.delete_group(g) # Entries def create_entry_simple(self, parent_path: str, dto: EntrySimpleDTO) -> NodeDTO: parent = self._group_by_path(parent_path) title = dto.key password = dto.value self._pw.kp.add_entry(parent, title=title, username="", password=password) return self._node_of_group(parent) def create_entry_complex(self, parent_path: str, dto: EntryComplexDTO) -> NodeDTO: parent = self._group_by_path(parent_path) self._pw.kp.add_entry( parent, title=dto.title, username=dto.username or "", password=dto.password or "", url=dto.url or "", notes=dto.notes or "", ) return self._node_of_group(parent) def update_entry_simple(self, path: str, dto: EntrySimpleDTO) -> NodeDTO: e = self._entry_by_path(path) parent = e.group e.title = dto.key e.password = dto.value return self._node_of_group(parent) def update_entry_complex(self, path: str, dto: EntryComplexDTO) -> NodeDTO: e = self._entry_by_path(path) parent = e.group e.title = dto.title e.username = dto.username e.password = dto.password e.url = dto.url or "" e.notes = dto.notes or "" return self._node_of_group(parent) def move_entry(self, path: str, target_group_path: str) -> NodeDTO: e = self._entry_by_path(path) target = self._group_by_path(target_group_path) self._pw.kp.move_entry(e, target) return self._node_of_group(target) def delete_entry(self, path: str) -> None: e = self._entry_by_path(path) self._pw.kp.delete_entry(e) # Persist def save(self): self._pw.save()