"""clients view factory.""" # pyright: reportUnusedFunction=false import ipaddress import logging import uuid from typing import Annotated from fastapi import APIRouter, Depends, Form, Request, Response from pydantic import BaseModel, IPvAnyAddress, IPvAnyNetwork from sshecret.backend import ClientFilter from sshecret.backend.models import FilterType from sshecret.crypto import validate_public_key from sshecret_admin.auth import User from sshecret_admin.services import AdminBackend from ..dependencies import FrontendDependencies LOG = logging.getLogger(__name__) class ClientUpdate(BaseModel): id: uuid.UUID name: str description: str public_key: str sources: str | None = None class ClientCreate(BaseModel): name: str public_key: str description: str | None sources: str | None def create_router(dependencies: FrontendDependencies) -> APIRouter: """Create clients router.""" app = APIRouter() templates = dependencies.templates @app.get("/clients") async def get_clients( request: Request, current_user: Annotated[User, Depends(dependencies.get_user_from_access_token)], admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)], ) -> Response: """Get clients.""" clients = await admin.get_clients() LOG.info("Clients %r", clients) return templates.TemplateResponse( request, "clients/index.html.j2", { "page_title": "Clients", "clients": clients, "user": current_user.username, }, ) @app.post("/clients/query") async def query_clients( request: Request, _current_user: Annotated[ User, Depends(dependencies.get_user_from_access_token) ], admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)], query: Annotated[str, Form()], ) -> 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) return templates.TemplateResponse( request, "clients/inner.html.j2", { "clients": clients, }, ) @app.put("/clients/{id}") async def update_client( request: Request, id: str, _current_user: Annotated[ User, Depends(dependencies.get_user_from_access_token) ], admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)], client: Annotated[ClientUpdate, Form()], ): """Update a client.""" original_client = await admin.get_client(id) if not original_client: return templates.TemplateResponse( request, "fragments/error.html", {"message": "Client not found"} ) sources: list[IPvAnyAddress | IPvAnyNetwork] = [] if client.sources: source_str = client.sources.split(",") for source in source_str: if "/" in source: sources.append(ipaddress.ip_network(source.strip())) else: sources.append(ipaddress.ip_address(source.strip())) client_fields = client.model_dump(exclude_unset=True) del client_fields["sources"] if sources: client_fields["policies"] = sources LOG.info("Fields: %r", client_fields) updated_client = original_client.model_copy(update=client_fields) await admin.update_client(updated_client) clients = await admin.get_clients() headers = {"Hx-Refresh": "true"} return templates.TemplateResponse( request, "clients/inner.html.j2", { "clients": clients, }, headers=headers, ) @app.delete("/clients/{id}") async def delete_client( request: Request, id: str, _current_user: Annotated[ User, Depends(dependencies.get_user_from_access_token) ], admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)], ) -> Response: """Delete a client.""" await admin.delete_client(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/") async def create_client( request: Request, _current_user: Annotated[ User, Depends(dependencies.get_user_from_access_token) ], admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)], client: Annotated[ClientCreate, Form()], ) -> Response: """Create client.""" 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, }, headers=headers, ) @app.post("/clients/validate/source") async def validate_client_source( request: Request, _current_user: Annotated[ User, Depends(dependencies.get_user_from_access_token) ], sources: Annotated[str, Form()], ) -> Response: """Validate source.""" source_str = sources.split(",") for source in source_str: if "/" in source: try: _network = ipaddress.ip_network(source.strip()) except Exception: return templates.TemplateResponse( request, "/clients/field_invalid.html.j2", {"explanation": f"Invalid network {source.strip()}"}, ) else: try: _address = ipaddress.ip_address(source.strip()) except Exception: return templates.TemplateResponse( request, "/clients/field_invalid.html.j2", {"explanation": f"Invalid address {source.strip()}"}, ) return templates.TemplateResponse( request, "/clients/field_valid.html.j2", ) @app.post("/clients/validate/public_key") async def validate_client_public_key( request: Request, _current_user: Annotated[ User, Depends(dependencies.get_user_from_access_token) ], public_key: Annotated[str, Form()], ) -> Response: """Validate source.""" if validate_public_key(public_key.rstrip()): return templates.TemplateResponse( request, "/clients/field_valid.html.j2", ) return templates.TemplateResponse( request, "/clients/field_invalid.html.j2", {"explanation": "Invalid value. Not a valid SSH RSA Public Key."}, ) return app