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 title %}Client {{ client.name }}{% endblock %}
{% block master %} {% block master %}
{% include '/clients/partials/tree.html.j2' %} <div id="client-tree">
{% include '/clients/partials/tree.html.j2' %}
</div>
{% endblock %} {% endblock %}

View File

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

View File

@ -85,94 +85,9 @@
</div> </div>
</sl-tab-panel> </sl-tab-panel>
<sl-tab-panel name="events"> <sl-tab-panel name="events">
<div id="client-audit-events">
<table class="min-w-full lg:table-fixed divide-y divide-gray-200 dark:divide-gray-600" id="last-audit-events"> {% include '/clients/partials/client_events.html.j2' %}
<thead class="bg-gray-50 dark:bg-gray-700"> </div>
<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>
</sl-tab-panel> </sl-tab-panel>
</sl-tab-group> </sl-tab-group>
</div> </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>
</div> </div>
</sl-details> </sl-details>
{% if groups.groups %} {% if flat_groups.groups %}
<sl-details summary="Group"> <sl-details summary="Group">
<form <form
hx-put="/secrets/set-group/{{ secret.name }}" hx-put="/secrets/set-group/{{ secret.name }}"
@ -98,7 +98,7 @@
<div class="relative w-full"> <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"> <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> <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> <option value="{{ group.group_name }}" {% if group.name == secret.group -%}selected{% endif %}>{{ group.path }}</option>
{% endfor %} {% endfor %}
</select> </select>

View File

@ -133,6 +133,10 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
token_data: dict[str, str] = {"sub": user.username} token_data: dict[str, str] = {"sub": user.username}
access_token = create_access_token(dependencies.settings, data=token_data) access_token = create_access_token(dependencies.settings, data=token_data)
refresh_token = create_refresh_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 = RedirectResponse(url=next, status_code=status.HTTP_302_FOUND)
response.set_cookie( response.set_cookie(
"access_token", "access_token",

View File

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

View File

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