Preserve history when navigating the secrets page
This commit is contained in:
@ -4,6 +4,12 @@
|
||||
class="tree-entry-item"
|
||||
data-type="entry"
|
||||
data-name="{{ entry.name }}"
|
||||
data-group-path="/"
|
||||
{% if secret | default(false) %}
|
||||
{% if secret.name == entry.name %}
|
||||
selected=""
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
>
|
||||
<sl-icon name="shield"> </sl-icon>
|
||||
<span class="px-2">{{ entry.name }}</span>
|
||||
@ -16,6 +22,13 @@
|
||||
class="secret-group-list-item"
|
||||
data-type="group"
|
||||
data-name="{{ group.group_name }}"
|
||||
data-group-path="{{ group.path }}"
|
||||
{% if group_path_nodes | default(false) %}
|
||||
{% if group.group_name in group_path_nodes %}
|
||||
expanded=""
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
>
|
||||
<sl-icon name="folder"> </sl-icon>
|
||||
<span class="px-2">{{ group.group_name }}</span>
|
||||
@ -81,7 +94,22 @@
|
||||
|
||||
</div>
|
||||
<div class="2xl:col-span-2 xl:col-span-2 p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
{% include '/secrets/partials/default_detail.html.j2' %}
|
||||
{% if group_page | default(false) %}
|
||||
<div class="w-full" id="secretdetails">
|
||||
{% include '/secrets/partials/group_detail.html.j2' %}
|
||||
</div>
|
||||
{% elif root_group_page | default(false) %}
|
||||
<div class="w-full" id="secretdetails">
|
||||
{% include '/secrets/partials/edit_root.html.j2' %}
|
||||
</div>
|
||||
{% elif secret_page | default(false) %}
|
||||
<div class="w-full" id="secretdetails">
|
||||
{% include '/secrets/partials/tree_detail.html.j2' %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% include '/secrets/partials/default_detail.html.j2' %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -99,17 +127,19 @@
|
||||
|
||||
const type = selectedEl.dataset.type;
|
||||
const name = selectedEl.dataset.name;
|
||||
console.log(`Event on ${type} ${name}`);
|
||||
const groupPath = selectedEl.dataset.groupPath;
|
||||
console.log(`Event on ${type} ${name} path: ${groupPath}`);
|
||||
|
||||
if (!type || !name) return;
|
||||
|
||||
let url = '';
|
||||
if (type === 'entry') {
|
||||
url = `/secrets/partial/secret/${encodeURIComponent(name)}`;
|
||||
url = `/secrets/secret/${encodeURIComponent(name)}`;
|
||||
} else if (type === 'group') {
|
||||
url = `/secrets/partial/group/${encodeURIComponent(name)}`;
|
||||
//url = `/secrets/partial/group/${encodeURIComponent(name)}`;
|
||||
url = `/secrets/group/${encodeURIComponent(groupPath)}`;
|
||||
} else if (type == 'root') {
|
||||
url = `/secrets/partial/root_group`;
|
||||
url = `/secrets/group/`;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
|
||||
@ -64,7 +64,6 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
|
||||
):
|
||||
groups = await admin.get_secret_groups()
|
||||
LOG.info("Groups: %r", groups)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"secrets/index.html.j2",
|
||||
@ -85,12 +84,13 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
request,
|
||||
"secrets/partials/edit_root.html.j2",
|
||||
{
|
||||
"group_path_nodes": [],
|
||||
"clients": clients,
|
||||
},
|
||||
)
|
||||
|
||||
@app.get("/secrets/partial/secret/{name}")
|
||||
async def get_secret_tree_detail(
|
||||
async def get_secret_tree_detail_partial(
|
||||
request: Request,
|
||||
name: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
@ -114,6 +114,118 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
},
|
||||
)
|
||||
|
||||
@app.get("/secrets/group/")
|
||||
async def show_root_group(
|
||||
request: Request,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
|
||||
):
|
||||
"""Show the root path."""
|
||||
clients = await admin.get_clients()
|
||||
context: dict[str, Any] = {
|
||||
"clients": clients,
|
||||
"root_group_page": True,
|
||||
}
|
||||
headers: dict[str, str] = {}
|
||||
if request.headers.get("HX-Request"):
|
||||
# This is a HTMX request.
|
||||
template_name = "secrets/partials/edit_root.html.j2"
|
||||
headers["HX-Push-Url"] = request.url.path
|
||||
else:
|
||||
groups = await admin.get_secret_groups()
|
||||
template_name = "secrets/index.html.j2"
|
||||
context["user"] = current_user
|
||||
context["groups"] = groups
|
||||
context["group_path_nodes"] = ["/"]
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request, template_name, context, headers=headers
|
||||
)
|
||||
|
||||
@app.get("/secrets/group/{group_path:path}")
|
||||
async def show_group(
|
||||
request: Request,
|
||||
group_path: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
|
||||
):
|
||||
"""Show a group."""
|
||||
group = await admin.get_secret_group_by_path(group_path)
|
||||
if not group:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Group not found"
|
||||
)
|
||||
clients = await admin.get_clients()
|
||||
|
||||
headers: dict[str, str] = {}
|
||||
context: dict[str, Any] = {
|
||||
"group_page": True,
|
||||
"name": group.group_name,
|
||||
"description": group.description,
|
||||
"clients": clients,
|
||||
}
|
||||
if request.headers.get("HX-Request"):
|
||||
# This is a HTMX request.
|
||||
template_name = "secrets/partials/group_detail.html.j2"
|
||||
headers["HX-Push-Url"] = request.url.path
|
||||
else:
|
||||
template_name = "secrets/index.html.j2"
|
||||
|
||||
groups = await admin.get_secret_groups()
|
||||
context["user"] = current_user
|
||||
context["groups"] = groups
|
||||
context["group_path_nodes"] = group.path.split("/")
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request, template_name, context, headers=headers
|
||||
)
|
||||
|
||||
@app.get("/secrets/secret/{name}")
|
||||
async def get_secret_tree_detail(
|
||||
request: Request,
|
||||
name: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
|
||||
):
|
||||
"""Get secret detail."""
|
||||
secret = await admin.get_secret(name)
|
||||
groups = await admin.get_secret_groups(flat=True)
|
||||
events = await admin.get_audit_log_detailed(limit=10, secret_name=name)
|
||||
|
||||
if not secret:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Secret not found"
|
||||
)
|
||||
|
||||
context: dict[str, Any] = {
|
||||
"secret": secret,
|
||||
"groups": groups,
|
||||
"events": events,
|
||||
"secret_page": True,
|
||||
}
|
||||
|
||||
headers: dict[str, str] = {}
|
||||
|
||||
if request.headers.get("HX-Request"):
|
||||
# This is a HTMX request.
|
||||
template_name = "secrets/partials/tree_detail.html.j2"
|
||||
headers["HX-Push-Url"] = request.url.path
|
||||
else:
|
||||
group_path = ["/"]
|
||||
if secret.group:
|
||||
group = await admin.get_secret_group(secret.group)
|
||||
if group:
|
||||
group_path = group.path.split("/")
|
||||
|
||||
template_name = "secrets/index.html.j2"
|
||||
context["user"] = current_user
|
||||
context["groups"] = groups
|
||||
context["group_path_nodes"] = group_path
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request, template_name, context, headers=headers
|
||||
)
|
||||
|
||||
@app.get("/secrets/partial/group/{name}")
|
||||
async def get_group_details(
|
||||
request: Request,
|
||||
|
||||
@ -411,6 +411,18 @@ class AdminBackend:
|
||||
return matches.groups[0]
|
||||
return None
|
||||
|
||||
async def get_secret_group_by_path(self, path: str) -> ClientSecretGroup | None:
|
||||
"""Get a group based on its path."""
|
||||
with self.password_manager() as password_manager:
|
||||
secret_group = password_manager.get_secret_group(path)
|
||||
|
||||
if not secret_group:
|
||||
return None
|
||||
|
||||
all_secrets = await self.backend.get_detailed_secrets()
|
||||
secrets_mapping = {secret.name: secret for secret in all_secrets}
|
||||
return add_clients_to_secret_group(secret_group, secrets_mapping)
|
||||
|
||||
async def get_secret(self, name: str) -> SecretView | None:
|
||||
"""Get secrets from backend."""
|
||||
try:
|
||||
|
||||
@ -32,7 +32,9 @@ def create_password_db(location: Path, password: str) -> None:
|
||||
|
||||
|
||||
def _kp_group_to_secret_group(
|
||||
kp_group: pykeepass.group.Group, parent: SecretGroup | None = None, depth: int | None = None
|
||||
kp_group: pykeepass.group.Group,
|
||||
parent: SecretGroup | None = None,
|
||||
depth: int | None = None,
|
||||
) -> SecretGroup:
|
||||
"""Convert keepass group to secret group dataclass."""
|
||||
group_name = cast(str, kp_group.name)
|
||||
@ -143,8 +145,9 @@ class PasswordContext:
|
||||
return None
|
||||
return str(entry.group.name)
|
||||
|
||||
|
||||
def get_secret_groups(self, pattern: str | None = None, regex: bool = True) -> list[SecretGroup]:
|
||||
def get_secret_groups(
|
||||
self, pattern: str | None = None, regex: bool = True
|
||||
) -> list[SecretGroup]:
|
||||
"""Get secret groups.
|
||||
|
||||
A regex pattern may be provided to filter groups.
|
||||
@ -160,15 +163,35 @@ class PasswordContext:
|
||||
secret_groups = [_kp_group_to_secret_group(group) for group in groups]
|
||||
return secret_groups
|
||||
|
||||
def get_secret_group_list(self, pattern: str | None = None, regex: bool = True) -> list[SecretGroup]:
|
||||
def get_secret_group_list(
|
||||
self, pattern: str | None = None, regex: bool = True
|
||||
) -> list[SecretGroup]:
|
||||
"""Get a flat list of groups."""
|
||||
if pattern:
|
||||
return self.get_secret_groups(pattern, regex)
|
||||
|
||||
groups = [ group for group in self.keepass.groups if not group.is_root_group ]
|
||||
groups = [group for group in self.keepass.groups if not group.is_root_group]
|
||||
secret_groups = [_kp_group_to_secret_group(group) for group in groups]
|
||||
return secret_groups
|
||||
|
||||
def get_secret_group(self, path: str) -> SecretGroup | None:
|
||||
"""Get a secret group by path."""
|
||||
elements = path.split("/")
|
||||
final_element = elements[-1]
|
||||
|
||||
current = self._root_group
|
||||
while elements:
|
||||
groupname = elements.pop(0)
|
||||
matches = [
|
||||
subgroup for subgroup in current.subgroups if subgroup.name == groupname
|
||||
]
|
||||
if matches:
|
||||
current = matches[0]
|
||||
else:
|
||||
return None
|
||||
if not current.is_root_group and current.name == final_element:
|
||||
return _kp_group_to_secret_group(current)
|
||||
return None
|
||||
|
||||
def get_ungrouped_secrets(self) -> list[str]:
|
||||
"""Get secrets without groups."""
|
||||
@ -193,7 +216,9 @@ class PasswordContext:
|
||||
f"Error: Cannot find a parent group named {parent_group}"
|
||||
)
|
||||
kp_parent_group = query
|
||||
self.keepass.add_group(destination_group=kp_parent_group, group_name=name, notes=description)
|
||||
self.keepass.add_group(
|
||||
destination_group=kp_parent_group, group_name=name, notes=description
|
||||
)
|
||||
self.keepass.save()
|
||||
|
||||
def set_group_description(self, name: str, description: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user