Adding object detail page.

This commit is contained in:
Michael Woods
2025-12-26 14:29:27 -05:00
parent 6dfaaa76d4
commit 7d99eecc61
2 changed files with 143 additions and 1 deletions

View File

@@ -1,10 +1,15 @@
from fastapi import APIRouter, Depends, Request, Form, File, UploadFile from fastapi import APIRouter, Depends, Request, Form, File, UploadFile
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from uuid import UUID
import base64
from packetserver.http.dependencies import get_current_http_user from packetserver.http.dependencies import get_current_http_user
from packetserver.http.auth import HttpUser from packetserver.http.auth import HttpUser
from packetserver.http.server import templates from packetserver.http.server import templates
from packetserver.http.routers.objects import router as api_router # to call internal endpoints from packetserver.http.routers.objects import router as api_router # to call internal endpoints
from packetserver.http.database import DbDependency from packetserver.http.database import DbDependency
from packetserver.http.routers.objects import get_object_metadata as api_get_metadata
router = APIRouter(tags=["objects_html"]) router = APIRouter(tags=["objects_html"])
@@ -28,4 +33,55 @@ async def objects_page(
"current_user": current_user.username, "current_user": current_user.username,
"objects": objects "objects": objects
} }
) )
@router.get("/objects/{uuid}", response_class=HTMLResponse)
async def object_detail_page(
request: Request,
uuid: UUID,
db: DbDependency,
current_user: HttpUser = Depends(get_current_http_user)
):
# Call the existing metadata API function
obj = api_get_metadata(uuid=uuid, db=db, current_user=current_user)
return templates.TemplateResponse(
"object_detail.html",
{
"request": request,
"current_user": current_user.username,
"obj": obj
}
)
@router.post("/objects/{uuid}")
async def update_object(
db: DbDependency,
uuid: UUID,
request: Request,
name: str = Form(None),
private: str = Form("off"), # checkbox sends "on" if checked
new_text: str = Form(None),
new_file: UploadFile = File(None),
new_base64: str = Form(None),
current_user: HttpUser = Depends(get_current_http_user)
):
payload = {}
if name is not None:
payload["name"] = name
payload["private"] = (private == "on")
if new_text is not None and new_text.strip():
payload["data_text"] = new_text.strip()
elif new_file and new_file.filename:
content = await new_file.read()
payload["data_base64"] = base64.b64encode(content).decode('ascii')
elif new_base64 and new_base64.strip():
payload["data_base64"] = new_base64.strip()
# Call the PATCH API internally (simple requests or direct function call)
from packetserver.http.routers.objects import update_object as api_update
await api_update(uuid=uuid, payload=ObjectUpdate(**payload), db=db, current_user=current_user)
# Redirect back to the detail page (or /objects list)
return RedirectResponse(url=f"/objects/{uuid}", status_code=303)

View File

@@ -0,0 +1,86 @@
{% extends "base.html" %}
{% block title %}{{ obj.name }} - Edit Object{% endblock %}
{% block content %}
<div class="row">
<div class="col-lg-8">
<h2>Edit Object: {{ obj.name }}</h2>
<div class="card mb-4">
<div class="card-body">
<form method="post" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" name="name" class="form-control" value="{{ obj.name }}">
</div>
<div class="form-check mb-3">
<input type="checkbox" name="private" class="form-check-input" {% if obj.private %}checked{% endif %}>
<label class="form-check-label">Private (only you can see/download)</label>
</div>
<hr>
<h5>Replace Content</h5>
<div class="mb-3">
<label class="form-label">New Text Content (forces text type)</label>
<textarea name="new_text" class="form-control" rows="8" placeholder="Paste new text here to overwrite as text object"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Or Upload New File (forces binary)</label>
<input type="file" name="new_file" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">Or Paste Base64 (advanced, forces binary)</label>
<textarea name="new_base64" class="form-control" rows="4" placeholder="Paste base64-encoded data"></textarea>
</div>
<button type="submit" class="btn btn-success me-2">Save Changes</button>
<a href="/objects" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
<div class="card">
<div class="card-body">
<h5>Danger Zone</h5>
<button type="button" class="btn btn-danger" onclick="deleteObject('{{ obj.uuid }}', '{{ obj.name | e }}')">Delete This Object Permanently</button>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">Current Details</div>
<ul class="list-group list-group-flush">
<li class="list-group-item"><strong>UUID:</strong> {{ obj.uuid }}</li>
<li class="list-group-item"><strong>Size:</strong> {{ obj.size }} bytes</li>
<li class="list-group-item"><strong>Type:</strong> {% if obj.binary %}Binary{% else %}Text{% endif %}</li>
<li class="list-group-item"><strong>Uploaded:</strong> {{ obj.created_at.strftime('%b %d, %Y') }}</li>
<li class="list-group-item"><strong>Modified:</strong> {{ obj.modified_at.strftime('%b %d, %Y') }}</li>
</ul>
<div class="card-body">
<a href="/api/v1/objects/{{ obj.uuid }}/download" class="btn btn-primary w-100 mb-2">Download Current Version</a>
{% if not obj.binary %}
<a href="/api/v1/objects/{{ obj.uuid }}/text" class="btn btn-outline-info w-100" target="_blank">View as Text</a>
{% endif %}
</div>
</div>
</div>
</div>
<script>
async function deleteObject(uuid, name) {
if (!confirm(`Permanently delete "${name}"? This cannot be undone.`)) return;
const response = await fetch(`/api/v1/objects/${uuid}`, { method: 'DELETE' });
if (response.ok) {
window.location.href = '/objects';
} else {
alert('Delete failed');
}
}
</script>
{% endblock %}