Refactor frontend views
All checks were successful
Build and push image / build-containers (push) Successful in 10m14s
All checks were successful
Build and push image / build-containers (push) Successful in 10m14s
This commit is contained in:
@ -2,43 +2,20 @@
|
||||
|
||||
# pyright: reportUnusedFunction=false
|
||||
import logging
|
||||
import math
|
||||
from typing import Annotated, cast
|
||||
from fastapi import APIRouter, Depends, Request, Response
|
||||
from pydantic import BaseModel
|
||||
|
||||
from sshecret.backend import AuditFilter, Operation
|
||||
|
||||
from sshecret_admin.auth import LocalUserInfo
|
||||
from sshecret_admin.services import AdminBackend
|
||||
|
||||
from .common import PagingInfo
|
||||
from ..dependencies import FrontendDependencies
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PagingInfo(BaseModel):
|
||||
page: int
|
||||
limit: int
|
||||
total: int
|
||||
offset: int = 0
|
||||
|
||||
@property
|
||||
def first(self) -> int:
|
||||
"""The first result number."""
|
||||
return self.offset + 1
|
||||
|
||||
@property
|
||||
def last(self) -> int:
|
||||
"""Return the last result number."""
|
||||
return self.offset + self.limit
|
||||
|
||||
@property
|
||||
def total_pages(self) -> int:
|
||||
"""Return total pages."""
|
||||
return math.ceil(self.total / self.limit)
|
||||
|
||||
|
||||
def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
"""Create clients router."""
|
||||
|
||||
|
||||
@ -5,11 +5,12 @@ import ipaddress
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
from fastapi import APIRouter, Depends, Form, Request, Response
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Query, Request, Response
|
||||
from pydantic import BaseModel, IPvAnyAddress, IPvAnyNetwork
|
||||
from sshecret_admin.frontend.views.common import PagingInfo
|
||||
|
||||
from sshecret.backend import ClientFilter
|
||||
from sshecret.backend.models import FilterType
|
||||
from sshecret.backend.models import Client, ClientQueryResult, FilterType
|
||||
from sshecret.crypto import validate_public_key
|
||||
from sshecret_admin.auth import LocalUserInfo
|
||||
from sshecret_admin.services import AdminBackend
|
||||
@ -18,6 +19,8 @@ from ..dependencies import FrontendDependencies
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CLIENTS_PER_PAGE = 20
|
||||
|
||||
|
||||
class ClientUpdate(BaseModel):
|
||||
id: uuid.UUID
|
||||
@ -34,6 +37,38 @@ class ClientCreate(BaseModel):
|
||||
sources: str | None
|
||||
|
||||
|
||||
class LocatedClient(BaseModel):
|
||||
"""A located client."""
|
||||
|
||||
client: Client
|
||||
results: ClientQueryResult
|
||||
pages: PagingInfo
|
||||
|
||||
|
||||
async def locate_client(admin: AdminBackend, client_id: str) -> LocatedClient | None:
|
||||
"""Locate a client in a paginated dataset."""
|
||||
offset = 0
|
||||
page = 1
|
||||
total_clients = await admin.get_client_count()
|
||||
while offset < total_clients:
|
||||
filter = ClientFilter(limit=CLIENTS_PER_PAGE, offset=offset)
|
||||
results = await admin.query_clients(filter)
|
||||
matches = [client for client in results.clients if str(client.id) == client_id]
|
||||
if matches:
|
||||
client = matches[0]
|
||||
pages = PagingInfo(
|
||||
page=page,
|
||||
limit=CLIENTS_PER_PAGE,
|
||||
total=results.total_results,
|
||||
offset=offset,
|
||||
)
|
||||
return LocatedClient(client=client, results=results, pages=pages)
|
||||
offset += CLIENTS_PER_PAGE
|
||||
page += 1
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
"""Create clients router."""
|
||||
|
||||
@ -41,45 +76,116 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
|
||||
templates = dependencies.templates
|
||||
|
||||
@app.get("/clients")
|
||||
async def get_clients(
|
||||
@app.get("/clients/")
|
||||
async def get_client_tree(
|
||||
request: Request,
|
||||
current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> Response:
|
||||
"""Get clients."""
|
||||
clients = await admin.get_clients()
|
||||
LOG.info("Clients %r", clients)
|
||||
"""Get client tree view."""
|
||||
page = 1
|
||||
per_page = CLIENTS_PER_PAGE
|
||||
offset = 0
|
||||
|
||||
client_filter = ClientFilter(offset=offset, limit=per_page)
|
||||
results = await admin.query_clients(client_filter)
|
||||
paginate = PagingInfo(
|
||||
page=page, limit=per_page, total=results.total_results, offset=offset
|
||||
)
|
||||
|
||||
LOG.info("Results %r", results)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"clients/index.html.j2",
|
||||
{
|
||||
"page_title": "Clients",
|
||||
"clients": clients,
|
||||
"offset": offset,
|
||||
"pages": paginate,
|
||||
"clients": results.clients,
|
||||
"user": current_user,
|
||||
"results": results,
|
||||
},
|
||||
)
|
||||
|
||||
@app.post("/clients/query")
|
||||
async def query_clients(
|
||||
@app.get("/clients/page/{page}")
|
||||
async def get_client_page(
|
||||
request: Request,
|
||||
current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
query: Annotated[str, Form()],
|
||||
page: int,
|
||||
) -> Response:
|
||||
"""Query for a client."""
|
||||
query_filter: ClientFilter | None = None
|
||||
if query:
|
||||
name = f"%{query}%"
|
||||
query_filter = ClientFilter(name=name, filter_name=FilterType.LIKE)
|
||||
clients = await admin.get_clients(query_filter)
|
||||
"""Get client tree view."""
|
||||
per_page = CLIENTS_PER_PAGE
|
||||
offset = 0
|
||||
if page > 1:
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
client_filter = ClientFilter(offset=offset, limit=per_page)
|
||||
results = await admin.query_clients(client_filter)
|
||||
paginate = PagingInfo(
|
||||
page=page,
|
||||
limit=per_page,
|
||||
offset=offset,
|
||||
total=results.total_results,
|
||||
)
|
||||
LOG.info("Results %r", results)
|
||||
template = "clients/index.html.j2"
|
||||
if request.headers.get("HX-Request"):
|
||||
# This is a HTMX request.
|
||||
template = "clients/partials/tree.html.j2"
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"clients/inner.html.j2",
|
||||
template,
|
||||
{
|
||||
"clients": clients,
|
||||
"page_title": "Clients",
|
||||
"offset": offset,
|
||||
"last_num": offset + per_page,
|
||||
"pages": paginate,
|
||||
"clients": results.clients,
|
||||
"user": current_user,
|
||||
"results": results,
|
||||
},
|
||||
)
|
||||
|
||||
@app.get("/clients/client/{id}")
|
||||
async def get_client(
|
||||
request: Request,
|
||||
current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
id: str,
|
||||
) -> Response:
|
||||
"""Fetch a client."""
|
||||
|
||||
results = await locate_client(admin, id)
|
||||
|
||||
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
|
||||
)
|
||||
template = "clients/client.html.j2"
|
||||
|
||||
headers: dict[str, str] = {}
|
||||
if request.headers.get("HX-Request"):
|
||||
headers["HX-Push-Url"] = request.url.path
|
||||
template = "clients/partials/client_details.html.j2"
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
template,
|
||||
{
|
||||
"page_title": f"Client {results.client.name}",
|
||||
"pages": results.pages,
|
||||
"clients": results.results.clients,
|
||||
"client": results.client,
|
||||
"user": current_user,
|
||||
"results": results.results,
|
||||
"events": events,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
@app.put("/clients/{id}")
|
||||
async def update_client(
|
||||
request: Request,
|
||||
@ -111,36 +217,17 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
LOG.info("Fields: %r", client_fields)
|
||||
updated_client = original_client.model_copy(update=client_fields)
|
||||
|
||||
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)
|
||||
|
||||
clients = await admin.get_clients()
|
||||
headers = {"Hx-Refresh": "true"}
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"clients/inner.html.j2",
|
||||
"clients/partials/client_details.html.j2",
|
||||
{
|
||||
"clients": clients,
|
||||
"client": final_client,
|
||||
"events": events,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
@app.delete("/clients/{id}")
|
||||
async def delete_client(
|
||||
request: Request,
|
||||
id: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> Response:
|
||||
"""Delete a client."""
|
||||
await admin.delete_client(("id", id))
|
||||
clients = await admin.get_clients()
|
||||
headers = {"Hx-Refresh": "true"}
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"clients/inner.html.j2",
|
||||
{
|
||||
"clients": clients,
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
@app.post("/clients/")
|
||||
@ -153,20 +240,22 @@ 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(
|
||||
client.name, client.public_key.rstrip(), client.description, sources
|
||||
)
|
||||
clients = await admin.get_clients()
|
||||
|
||||
headers = {"Hx-Refresh": "true"}
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"clients/inner.html.j2",
|
||||
{
|
||||
"clients": clients,
|
||||
},
|
||||
return Response(
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
@app.delete("/clients/{id}")
|
||||
async def delete_client(
|
||||
id: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> Response:
|
||||
"""Delete a client."""
|
||||
await admin.delete_client(("id", id))
|
||||
headers = {"Hx-Refresh": "true"}
|
||||
return Response(headers=headers)
|
||||
|
||||
@app.post("/clients/validate/source")
|
||||
async def validate_client_source(
|
||||
request: Request,
|
||||
@ -215,4 +304,38 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
|
||||
{"explanation": "Invalid value. Not a valid SSH RSA Public Key."},
|
||||
)
|
||||
|
||||
@app.post("/clients/query")
|
||||
async def query_clients(
|
||||
request: Request,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
query: Annotated[str, Form()],
|
||||
) -> Response:
|
||||
"""Query for a client."""
|
||||
query_filter = ClientFilter(limit=CLIENTS_PER_PAGE)
|
||||
if query:
|
||||
name = f"%{query}%"
|
||||
query_filter = ClientFilter(
|
||||
name=name, filter_name=FilterType.LIKE, limit=CLIENTS_PER_PAGE
|
||||
)
|
||||
results = await admin.query_clients(query_filter)
|
||||
pages: PagingInfo | None = None
|
||||
if not query:
|
||||
pages = PagingInfo(
|
||||
page=1, limit=CLIENTS_PER_PAGE, offset=0, total=results.total_results
|
||||
)
|
||||
|
||||
more_results: int | None = None
|
||||
if query and results.total_results > CLIENTS_PER_PAGE:
|
||||
more_results = results.total_results - CLIENTS_PER_PAGE
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"clients/partials/tree_items.html.j2",
|
||||
{
|
||||
"clients": results.clients,
|
||||
"pages": pages,
|
||||
"results": results,
|
||||
"more_results": more_results,
|
||||
},
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
"""Common utilities."""
|
||||
|
||||
import math
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PagingInfo(BaseModel):
|
||||
page: int
|
||||
limit: int
|
||||
total: int
|
||||
offset: int = 0
|
||||
|
||||
@property
|
||||
def first(self) -> int:
|
||||
"""The first result number."""
|
||||
return self.offset + 1
|
||||
|
||||
@property
|
||||
def last(self) -> int:
|
||||
"""Return the last result number."""
|
||||
return self.offset + self.limit
|
||||
|
||||
@property
|
||||
def total_pages(self) -> int:
|
||||
"""Return total pages."""
|
||||
return math.ceil(self.total / self.limit)
|
||||
|
||||
@property
|
||||
def pages(self) -> list[int]:
|
||||
"""Return all page numbers."""
|
||||
return [page for page in range(1, self.total_pages + 1)]
|
||||
|
||||
@property
|
||||
def is_last(self) -> bool:
|
||||
"""Is this the last page?"""
|
||||
return self.page == self.total_pages
|
||||
|
||||
@property
|
||||
def is_first(self) -> bool:
|
||||
"""Is this the first page?"""
|
||||
return self.page == 1
|
||||
Reference in New Issue
Block a user