karl/app/web/passwords.py

127 lines
4.2 KiB
Python

from typing import Optional, List, 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()