Update views

This commit is contained in:
2025-06-19 19:44:33 +02:00
parent 1cde31a023
commit 4a5874d4f8
9 changed files with 252 additions and 94 deletions

View File

@ -0,0 +1,23 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% block title %}Sshecret Admin{% endblock %}</title>
{% block head %}
{% include 'base/partials/stylesheets.html.j2' %}
{% endblock %}
</head>
<body class="bg-gray-50 text-gray-900 min-h-screen flex flex-col">
<main id="content" class="flex-1 overflow-y-auto" hx-target="this" hx-swap="innerHTML">
<div class="" id="maincontent">
{% block content %}{% endblock %}
</div>
</main>
{% block scripts %}
{% include 'base/partials/scripts.html.j2' %}
{% endblock %}
{% block local_scripts %}
{% endblock %}
</body>
</html>

View File

@ -2,7 +2,9 @@
{% block title %}Client {{ client.name }}{% endblock %}
{% block master %}
{% include '/clients/partials/tree.html.j2' %}
<div id="client-tree">
{% include '/clients/partials/tree.html.j2' %}
</div>
{% endblock %}

View File

@ -2,7 +2,9 @@
{% block title %}Clients{% endblock %}
{% block master %}
{% include '/clients/partials/tree.html.j2' %}
<div id="client-tree">
{% include '/clients/partials/tree.html.j2' %}
</div>
{% endblock %}

View File

@ -85,94 +85,9 @@
</div>
</sl-tab-panel>
<sl-tab-panel name="events">
<table class="min-w-full lg:table-fixed divide-y divide-gray-200 dark:divide-gray-600" id="last-audit-events">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Timestamp</th>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Subsystem</th>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Message</th>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Origin</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800">
{% for entry in events.results | list %}
<tr class="{{ loop.cycle('', 'bg-gray-50 dark:bg-gray-700 ') }}hover:bg-gray-100 dark:hover:bg-gray-700" id="login-entry-{{ entry.id }}">
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
<p>{{ entry.timestamp }}<button data-popover-target="popover-audit-entry-{{ entry.id }}" data-popover-placement="bottom-end" type="button"><svg class="w-4 h-4 ms-2 text-gray-400 hover:text-gray-500" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"></path>
</svg><span class="sr-only">Show information</span></button></p>
<div data-popover id="popover-audit-entry-{{entry.id}}" role="tooltip" class="absolute z-10 invisible inline-block text-sm text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-xs opacity-0 w-80 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400">
<dl class="max-w-md text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700 px-2 py-2">
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">ID</dt>
<dd class="text-xs font-semibold">{{ entry.id }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Subsystem</dt>
<dd class="text-xs font-semibold">{{ entry.subsystem }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Timestamp</dt>
<dd class="text-xs font-semibold">{{ entry.timestamp }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Operation</dt>
<dd class="text-xs font-semibold">{{ entry.operation }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Client ID</dt>
<dd class="text-xs font-semibold">{{ entry.client_id }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Client Name</dt>
<dd class="text-xs font-semibold">{{ entry.client_name }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Secret ID</dt>
<dd class="text-xs font-semibold">{{ entry.secret_id }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Secret Name</dt>
<dd class="text-xs font-semibold">{{ entry.secret_name }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Message</dt>
<dd class="text-xs font-semibold">{{ entry.message }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Origin</dt>
<dd class="text-xs font-semibold">{{ entry.origin }}</dd>
</div>
{% if entry.data %}
{% for key, value in entry.data.items() %}
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">{{ key | capitalize }}</dt>
<dd class="text-xs font-semibold">{{ value }}</dd>
</div>
{% endfor %}
{% endif %}
</dl>
</div>
</td>
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
{{ entry.subsystem }}
</td>
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
{{ entry.message }}
</td>
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
{{ entry.origin }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div id="client-audit-events">
{% include '/clients/partials/client_events.html.j2' %}
</div>
</sl-tab-panel>
</sl-tab-group>
</div>

View File

@ -0,0 +1,157 @@
<table class="min-w-full lg:table-fixed divide-y divide-gray-200 dark:divide-gray-600" id="last-audit-events">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Timestamp</th>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Subsystem</th>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Message</th>
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">Origin</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800" id="last-audit-events-body">
{% for entry in events.results | list %}
<tr class="{{ loop.cycle('', 'bg-gray-50 dark:bg-gray-700 ') }}hover:bg-gray-100 dark:hover:bg-gray-700" id="login-entry-{{ entry.id }}">
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
<p>{{ entry.timestamp }}<button data-popover-target="popover-audit-entry-{{ entry.id }}" data-popover-placement="bottom-end" type="button"><svg class="w-4 h-4 ms-2 text-gray-400 hover:text-gray-500" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"></path>
</svg><span class="sr-only">Show information</span></button></p>
<div data-popover id="popover-audit-entry-{{entry.id}}" role="tooltip" class="absolute z-10 invisible inline-block text-sm text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-xs opacity-0 w-80 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400">
<dl class="max-w-md text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700 px-2 py-2">
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">ID</dt>
<dd class="text-xs font-semibold">{{ entry.id }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Subsystem</dt>
<dd class="text-xs font-semibold">{{ entry.subsystem }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Timestamp</dt>
<dd class="text-xs font-semibold">{{ entry.timestamp }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Operation</dt>
<dd class="text-xs font-semibold">{{ entry.operation }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Client ID</dt>
<dd class="text-xs font-semibold">{{ entry.client_id }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Client Name</dt>
<dd class="text-xs font-semibold">{{ entry.client_name }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Secret ID</dt>
<dd class="text-xs font-semibold">{{ entry.secret_id }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Secret Name</dt>
<dd class="text-xs font-semibold">{{ entry.secret_name }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Message</dt>
<dd class="text-xs font-semibold">{{ entry.message }}</dd>
</div>
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Origin</dt>
<dd class="text-xs font-semibold">{{ entry.origin }}</dd>
</div>
{% if entry.data %}
{% for key, value in entry.data.items() %}
<div class="flex flex-col pb-3">
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">{{ key | capitalize }}</dt>
<dd class="text-xs font-semibold">{{ value }}</dd>
</div>
{% endfor %}
{% endif %}
</dl>
</div>
</td>
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
{{ entry.subsystem }}
</td>
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
{{ entry.message }}
</td>
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
{{ entry.origin }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div
class="sticky bottom-0 right-0 items-center w-full p-4 bg-white border-t border-gray-200 sm:flex sm:justify-between dark:bg-gray-800 dark:border-gray-700"
>
<div class="flex items-center mb-4 sm:mb-0">
<span class="text-sm font-normal text-gray-500 dark:text-gray-400"
>Showing
{% if events_paging.total < events_paging.last %}
<span class="font-semibold text-gray-900 dark:text-white">{{events_paging.first }}-{{ events_paging.total}}</span> of
{% else %}
<span class="font-semibold text-gray-900 dark:text-white">{{events_paging.first }}-{{ events_paging.last}}</span> of
{% endif %}
<span class="font-semibold text-gray-900 dark:text-white"
>{{ events_paging.total }}</span
></span
>
</div>
<div class="flex items-center space-x-3">
<div class="flex space-x-1">
<button
{% if events_paging.page == 1 %}
class="px-3 py-1 min-w-9 min-h-9 text-sm font-normal text-gray-900 bg-white border border-gray-300 rounded hover:bg-gray-100 focus:ring-4 focus:ring-primary-300 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 transition duration-200 ease"
disabled=""
{% else %}
class="px-3 py-1 min-w-9 min-h-9 text-sm font-normal text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800 transition duration-200 ease"
hx-get="/clients/client/{{ client.id }}/events/{{ events_paging.page - 1 }}"
hx-target="#client-audit-events"
{% endif %}
>
Prev
</button>
{% for n in range(events_paging.total_pages) %}
{% set p = n + 1 %}
{% if p == events_paging.page %}
<button
class="px-3 py-1 min-w-9 min-h-9 text-sm font-normal text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800 transition duration-200 ease">
{{ p }}
</button>
{% else %}
<button
class="px-3 py-1 min-w-9 min-h-9 text-sm font-normal text-gray-900 bg-white border border-gray-300 rounded hover:bg-gray-100 focus:ring-4 focus:ring-primary-300 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 transition duration-200 ease"
hx-get="/clients/client/{{client.id}}/events/{{ p }}"
hx-target="#client-audit-events"
>
{{ p }}
</button>
{% endif %}
{% endfor %}
<button
{% if events_paging.page < events_paging.total_pages %}
class="px-3 py-1 min-w-9 min-h-9 text-sm font-normal text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800 transition duration-200 ease"
hx-get="/clients/client/{{ client.id }}/events/{{ events_paging.page + 1 }}"
hx-target="#client-audit-events"
{% else %}
class="px-3 py-1 min-w-9 min-h-9 text-sm font-normal text-gray-900 bg-white border border-gray-300 rounded hover:bg-gray-100 focus:ring-4 focus:ring-primary-300 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 transition duration-200 ease"
disabled=""
{% endif %}
>
Next
</button>
</div>
</div>

View File

@ -86,7 +86,7 @@
</div>
</div>
</sl-details>
{% if groups.groups %}
{% if flat_groups.groups %}
<sl-details summary="Group">
<form
hx-put="/secrets/set-group/{{ secret.name }}"
@ -98,7 +98,7 @@
<div class="relative w-full">
<select id="group_name" name="group_name" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option value="__ROOT">Ungrouped</option>
{% for group in groups.groups %}
{% for group in flat_groups.groups %}
<option value="{{ group.group_name }}" {% if group.name == secret.group -%}selected{% endif %}>{{ group.path }}</option>
{% endfor %}
</select>

View File

@ -133,6 +133,10 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
token_data: dict[str, str] = {"sub": user.username}
access_token = create_access_token(dependencies.settings, data=token_data)
refresh_token = create_refresh_token(dependencies.settings, data=token_data)
if next == "/refresh":
# Don't redirect from login to refresh. Send to dashboard instead.
next = "/"
response = RedirectResponse(url=next, status_code=status.HTTP_302_FOUND)
response.set_cookie(
"access_token",

View File

@ -6,6 +6,7 @@ import logging
import uuid
from typing import Annotated
from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request, Response
from fastapi.responses import RedirectResponse
from pydantic import BaseModel, IPvAnyAddress, IPvAnyNetwork
from sshecret_admin.frontend.views.common import PagingInfo
@ -20,6 +21,7 @@ from ..dependencies import FrontendDependencies
LOG = logging.getLogger(__name__)
CLIENTS_PER_PAGE = 20
EVENTS_PER_PAGE = 20
class ClientUpdate(BaseModel):
@ -165,7 +167,7 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
if not results:
raise HTTPException(status_code=404, detail="Client not found.")
events = await admin.get_audit_log_detailed(
limit=10, client_name=results.client.name
limit=EVENTS_PER_PAGE, client_name=results.client.name
)
template = "clients/client.html.j2"
@ -179,6 +181,9 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
headers["HX-Push-Url"] = request.url.path
template = "clients/partials/client_details.html.j2"
events_paging = PagingInfo(
page=1, limit=EVENTS_PER_PAGE, total=events.total, offset=0
)
return templates.TemplateResponse(
request,
template,
@ -191,10 +196,45 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
"user": current_user,
"results": results.results,
"events": events,
"events_paging": events_paging,
},
headers=headers,
)
@app.get("/clients/client/{id}/events/{page}")
async def get_client_events(
request: Request,
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
id: str,
page: int,
) -> Response:
"""Get more events for a client."""
if not "HX-Request" in request.headers:
return RedirectResponse(url=f"/clients/client/{id}")
client = await admin.get_client(("id", id))
if not client:
raise HTTPException(status_code=404, detail="Client not found.")
offset = 0
if page > 1:
offset = (page - 1) * EVENTS_PER_PAGE
events = await admin.get_audit_log_detailed(
limit=EVENTS_PER_PAGE, client_name=client.name, offset=offset
)
events_paging = PagingInfo(
page=page, limit=EVENTS_PER_PAGE, total=events.total, offset=offset
)
return templates.TemplateResponse(
request,
"clients/partials/client_events.html.j2",
{
"events": events,
"client": client,
"events_paging": events_paging,
},
)
@app.put("/clients/{id}")
async def update_client(
request: Request,
@ -228,7 +268,13 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
final_client = await admin.update_client(updated_client)
events = await admin.get_audit_log_detailed(limit=10, client_name=client.name)
events = await admin.get_audit_log_detailed(
limit=EVENTS_PER_PAGE, client_name=client.name
)
events_paging = PagingInfo(
page=1, limit=EVENTS_PER_PAGE, total=events.total, offset=0
)
return templates.TemplateResponse(
request,
@ -236,6 +282,7 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
{
"client": final_client,
"events": events,
"events_paging": events_paging,
},
)
@ -249,6 +296,12 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
sources: list[str] | None = None
if client.sources:
sources = [source.strip() for source in client.sources.split(",")]
await admin.create_client(
name=client.name,
public_key=client.public_key,
description=client.description,
sources=sources,
)
headers = {"Hx-Refresh": "true"}
return Response(

View File

@ -173,6 +173,7 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
"""Get secret detail."""
secret = await admin.get_secret(name)
groups = await admin.get_secret_groups()
flat_groups = await admin.get_secret_groups(flat=True)
events = await admin.get_audit_log_detailed(limit=10, secret_name=name)
if not secret:
@ -183,6 +184,7 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
context: dict[str, Any] = {
"secret": secret,
"groups": groups,
"flat_groups": flat_groups,
"events": events,
"secret_page": True,
}