Preserve history when navigating the secrets page
This commit is contained in:
@ -4,6 +4,12 @@
|
|||||||
class="tree-entry-item"
|
class="tree-entry-item"
|
||||||
data-type="entry"
|
data-type="entry"
|
||||||
data-name="{{ entry.name }}"
|
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>
|
<sl-icon name="shield"> </sl-icon>
|
||||||
<span class="px-2">{{ entry.name }}</span>
|
<span class="px-2">{{ entry.name }}</span>
|
||||||
@ -16,6 +22,13 @@
|
|||||||
class="secret-group-list-item"
|
class="secret-group-list-item"
|
||||||
data-type="group"
|
data-type="group"
|
||||||
data-name="{{ group.group_name }}"
|
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>
|
<sl-icon name="folder"> </sl-icon>
|
||||||
<span class="px-2">{{ group.group_name }}</span>
|
<span class="px-2">{{ group.group_name }}</span>
|
||||||
@ -81,7 +94,22 @@
|
|||||||
|
|
||||||
</div>
|
</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">
|
<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">
|
||||||
|
{% 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' %}
|
{% include '/secrets/partials/default_detail.html.j2' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -99,17 +127,19 @@
|
|||||||
|
|
||||||
const type = selectedEl.dataset.type;
|
const type = selectedEl.dataset.type;
|
||||||
const name = selectedEl.dataset.name;
|
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;
|
if (!type || !name) return;
|
||||||
|
|
||||||
let url = '';
|
let url = '';
|
||||||
if (type === 'entry') {
|
if (type === 'entry') {
|
||||||
url = `/secrets/partial/secret/${encodeURIComponent(name)}`;
|
url = `/secrets/secret/${encodeURIComponent(name)}`;
|
||||||
} else if (type === 'group') {
|
} 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') {
|
} else if (type == 'root') {
|
||||||
url = `/secrets/partial/root_group`;
|
url = `/secrets/group/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
|
|||||||
@ -64,7 +64,6 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
|||||||
current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
|
current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
|
||||||
):
|
):
|
||||||
groups = await admin.get_secret_groups()
|
groups = await admin.get_secret_groups()
|
||||||
LOG.info("Groups: %r", groups)
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"secrets/index.html.j2",
|
"secrets/index.html.j2",
|
||||||
@ -85,12 +84,13 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
|||||||
request,
|
request,
|
||||||
"secrets/partials/edit_root.html.j2",
|
"secrets/partials/edit_root.html.j2",
|
||||||
{
|
{
|
||||||
|
"group_path_nodes": [],
|
||||||
"clients": clients,
|
"clients": clients,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/secrets/partial/secret/{name}")
|
@app.get("/secrets/partial/secret/{name}")
|
||||||
async def get_secret_tree_detail(
|
async def get_secret_tree_detail_partial(
|
||||||
request: Request,
|
request: Request,
|
||||||
name: str,
|
name: str,
|
||||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
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}")
|
@app.get("/secrets/partial/group/{name}")
|
||||||
async def get_group_details(
|
async def get_group_details(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@ -411,6 +411,18 @@ class AdminBackend:
|
|||||||
return matches.groups[0]
|
return matches.groups[0]
|
||||||
return None
|
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:
|
async def get_secret(self, name: str) -> SecretView | None:
|
||||||
"""Get secrets from backend."""
|
"""Get secrets from backend."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -32,7 +32,9 @@ def create_password_db(location: Path, password: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _kp_group_to_secret_group(
|
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:
|
) -> SecretGroup:
|
||||||
"""Convert keepass group to secret group dataclass."""
|
"""Convert keepass group to secret group dataclass."""
|
||||||
group_name = cast(str, kp_group.name)
|
group_name = cast(str, kp_group.name)
|
||||||
@ -143,8 +145,9 @@ class PasswordContext:
|
|||||||
return None
|
return None
|
||||||
return str(entry.group.name)
|
return str(entry.group.name)
|
||||||
|
|
||||||
|
def get_secret_groups(
|
||||||
def get_secret_groups(self, pattern: str | None = None, regex: bool = True) -> list[SecretGroup]:
|
self, pattern: str | None = None, regex: bool = True
|
||||||
|
) -> list[SecretGroup]:
|
||||||
"""Get secret groups.
|
"""Get secret groups.
|
||||||
|
|
||||||
A regex pattern may be provided to filter groups.
|
A regex pattern may be provided to filter groups.
|
||||||
@ -160,7 +163,9 @@ class PasswordContext:
|
|||||||
secret_groups = [_kp_group_to_secret_group(group) for group in groups]
|
secret_groups = [_kp_group_to_secret_group(group) for group in groups]
|
||||||
return secret_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."""
|
"""Get a flat list of groups."""
|
||||||
if pattern:
|
if pattern:
|
||||||
return self.get_secret_groups(pattern, regex)
|
return self.get_secret_groups(pattern, regex)
|
||||||
@ -169,6 +174,24 @@ class PasswordContext:
|
|||||||
secret_groups = [_kp_group_to_secret_group(group) for group in groups]
|
secret_groups = [_kp_group_to_secret_group(group) for group in groups]
|
||||||
return secret_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]:
|
def get_ungrouped_secrets(self) -> list[str]:
|
||||||
"""Get secrets without groups."""
|
"""Get secrets without groups."""
|
||||||
@ -193,7 +216,9 @@ class PasswordContext:
|
|||||||
f"Error: Cannot find a parent group named {parent_group}"
|
f"Error: Cannot find a parent group named {parent_group}"
|
||||||
)
|
)
|
||||||
kp_parent_group = query
|
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()
|
self.keepass.save()
|
||||||
|
|
||||||
def set_group_description(self, name: str, description: str) -> None:
|
def set_group_description(self, name: str, description: str) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user