update api endpoints to use post body rather than url based handlers

This commit is contained in:
Evan Jarrett
2026-01-17 17:46:10 -06:00
parent faf63d8344
commit 0358e2e5ad
8 changed files with 83 additions and 30 deletions
+23 -7
View File
@@ -2,6 +2,7 @@ package handlers
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"html/template"
@@ -11,7 +12,6 @@ import (
"atcr.io/pkg/appview/db"
"atcr.io/pkg/appview/middleware"
"atcr.io/pkg/atproto"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
@@ -20,6 +20,12 @@ type StarRepositoryHandler struct {
BaseUIHandler
}
// starRequest is the JSON body for star/unstar requests
type starRequest struct {
Handle string `json:"handle"`
Repo string `json:"repo"`
}
func (h *StarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Get authenticated user from middleware
user := middleware.GetUser(r)
@@ -28,9 +34,14 @@ func (h *StarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
return
}
// Extract parameters
handle := chi.URLParam(r, "handle")
repository := chi.URLParam(r, "repository")
// Parse JSON body
var req starRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
handle := req.Handle
repository := req.Repo
// Resolve owner's handle to DID
ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle)
@@ -93,9 +104,14 @@ func (h *UnstarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
return
}
// Extract parameters
handle := chi.URLParam(r, "handle")
repository := chi.URLParam(r, "repository")
// Parse JSON body
var req starRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
handle := req.Handle
repository := req.Repo
// Resolve owner's handle to DID
ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle)
+37 -8
View File
@@ -12,10 +12,15 @@ import (
"atcr.io/pkg/appview/db"
"atcr.io/pkg/appview/middleware"
"atcr.io/pkg/atproto"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
// deleteTagRequest is the JSON body for delete tag requests
type deleteTagRequest struct {
Repo string `json:"repo"`
Tag string `json:"tag"`
}
// DeleteTagHandler handles deleting a tag
type DeleteTagHandler struct {
BaseUIHandler
@@ -28,8 +33,14 @@ func (h *DeleteTagHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
repo := chi.URLParam(r, "repository")
tag := chi.URLParam(r, "tag")
// Parse JSON body
var req deleteTagRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
repo := req.Repo
tag := req.Tag
// Create ATProto client with session provider (uses DoWithSession for DPoP nonce safety)
pdsClient := atproto.NewClientWithSessionProvider(user.PDSEndpoint, user.DID, h.Refresher)
@@ -59,6 +70,13 @@ func (h *DeleteTagHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
// deleteManifestRequest is the JSON body for delete manifest requests
type deleteManifestRequest struct {
Repo string `json:"repo"`
Digest string `json:"digest"`
Confirm bool `json:"confirm"`
}
// DeleteManifestHandler handles deleting a manifest
type DeleteManifestHandler struct {
BaseUIHandler
@@ -71,9 +89,15 @@ func (h *DeleteManifestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
return
}
repo := chi.URLParam(r, "repository")
digest := chi.URLParam(r, "digest")
confirmed := r.URL.Query().Get("confirm") == "true"
// Parse JSON body
var req deleteManifestRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
repo := req.Repo
digest := req.Digest
confirmed := req.Confirm
// Check if manifest is tagged
tagged, err := db.IsManifestTagged(h.DB, user.DID, repo, digest)
@@ -174,14 +198,19 @@ func (h *UploadAvatarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
return
}
repo := chi.URLParam(r, "repository")
// Parse multipart form (max 3MB to match lexicon maxSize)
if err := r.ParseMultipartForm(3 << 20); err != nil {
http.Error(w, "File too large (max 3MB)", http.StatusBadRequest)
return
}
// Get repo from form field
repo := r.FormValue("repo")
if repo == "" {
http.Error(w, "Missing repo field", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("avatar")
if err != nil {
http.Error(w, "No file provided", http.StatusBadRequest)
File diff suppressed because one or more lines are too long
+5 -5
View File
@@ -96,11 +96,11 @@ func RegisterUIRoutes(router chi.Router, deps UIDependencies) {
).ServeHTTP)
// API routes for stars (require authentication)
router.Post("/api/stars/{handle}/{repository}", middleware.RequireAuth(deps.SessionStore, deps.Database)(
router.Post("/api/stars", middleware.RequireAuth(deps.SessionStore, deps.Database)(
&uihandlers.StarRepositoryHandler{BaseUIHandler: base},
).ServeHTTP)
router.Delete("/api/stars/{handle}/{repository}", middleware.RequireAuth(deps.SessionStore, deps.Database)(
router.Delete("/api/stars", middleware.RequireAuth(deps.SessionStore, deps.Database)(
&uihandlers.UnstarRepositoryHandler{BaseUIHandler: base},
).ServeHTTP)
@@ -128,9 +128,9 @@ func RegisterUIRoutes(router chi.Router, deps UIDependencies) {
r.Get("/api/storage", (&uihandlers.StorageHandler{BaseUIHandler: base}).ServeHTTP)
r.Post("/api/profile/default-hold", (&uihandlers.UpdateDefaultHoldHandler{BaseUIHandler: base}).ServeHTTP)
r.Delete("/api/images/{repository}/tags/{tag}", (&uihandlers.DeleteTagHandler{BaseUIHandler: base}).ServeHTTP)
r.Delete("/api/images/{repository}/manifests/{digest}", (&uihandlers.DeleteManifestHandler{BaseUIHandler: base}).ServeHTTP)
r.Post("/api/images/{repository}/avatar", (&uihandlers.UploadAvatarHandler{BaseUIHandler: base}).ServeHTTP)
r.Delete("/api/tags", (&uihandlers.DeleteTagHandler{BaseUIHandler: base}).ServeHTTP)
r.Delete("/api/manifests", (&uihandlers.DeleteManifestHandler{BaseUIHandler: base}).ServeHTTP)
r.Post("/api/avatar", (&uihandlers.UploadAvatarHandler{BaseUIHandler: base}).ServeHTTP)
// Device approval page (authenticated)
r.Get("/device", (&uihandlers.DeviceApprovalPageHandler{BaseUIHandler: base}).ServeHTTP)
+6 -2
View File
@@ -253,9 +253,11 @@ document.addEventListener('DOMContentLoaded', () => {
async function deleteManifest(repository, digest, sanitizedId) {
try {
// First, try to delete without confirmation
const response = await fetch(`/api/images/${repository}/manifests/${digest}`, {
const response = await fetch('/api/manifests', {
method: 'DELETE',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ repo: repository, digest: digest, confirm: false }),
});
if (response.status === 409) {
@@ -314,9 +316,11 @@ async function confirmManifestDelete(repository, digest, sanitizedId) {
confirmBtn.textContent = 'Deleting...';
// Delete with confirmation
const response = await fetch(`/api/images/${repository}/manifests/${digest}?confirm=true`, {
const response = await fetch('/api/manifests', {
method: 'DELETE',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ repo: repository, digest: digest, confirm: true }),
});
if (response.ok) {
@@ -18,9 +18,11 @@
<label class="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 hover:opacity-100 transition-opacity cursor-pointer rounded-lg" for="avatar-upload" aria-label="Upload repository icon">
<i data-lucide="plus" class="size-8 text-white"></i>
</label>
<input type="hidden" id="avatar-repo" name="repo" value="{{ .RepositoryName }}">
<input type="file" id="avatar-upload" name="avatar"
accept="image/png,image/jpeg,image/webp"
hx-post="/api/images/{{ .RepositoryName }}/avatar"
hx-post="/api/avatar"
hx-include="#avatar-repo"
hx-encoding="multipart/form-data"
hx-swap="outerHTML"
hx-target="#repo-avatar"
+3 -2
View File
@@ -11,10 +11,11 @@
<button class="btn btn-sm gap-2 btn-ghost group border border-transparent hover:border-primary{{ if .IsStarred }} border-amber-400!{{ end }}"
id="star-btn"
{{ if .IsStarred }}
hx-delete="/api/stars/{{ .Handle }}/{{ .Repository }}"
hx-delete="/api/stars"
{{ else }}
hx-post="/api/stars/{{ .Handle }}/{{ .Repository }}"
hx-post="/api/stars"
{{ end }}
hx-vals='{"handle": "{{ .Handle }}", "repo": "{{ .Repository }}"}'
hx-swap="outerHTML"
hx-on::before-request="this.disabled=true"
hx-on::after-request="if(event.detail.xhr.status===401) window.location='/auth/oauth/login'"
+3 -2
View File
@@ -170,7 +170,8 @@
</time>
{{ if $.IsOwner }}
<button class="btn btn-ghost btn-sm text-error"
hx-delete="/api/images/{{ $.Repository.Name }}/tags/{{ .Tag.Tag }}"
hx-delete="/api/tags"
hx-vals='{"repo": "{{ $.Repository.Name }}", "tag": "{{ .Tag.Tag }}"}'
hx-confirm="Delete tag {{ .Tag.Tag }}?"
hx-target="#tag-{{ sanitizeID .Tag.Tag }}"
hx-swap="outerHTML"
@@ -257,7 +258,7 @@
{{ if $.IsOwner }}
<button class="btn btn-ghost btn-sm text-error"
onclick="deleteManifest('{{ $.Repository.Name }}', '{{ .Manifest.Digest }}', '{{ sanitizeID .Manifest.Digest }}')"
aria-label="Delete manifest {{ .Manifest.Digest | truncateDigest }}">
aria-label="Delete manifest {{ truncateDigest .Manifest.Digest 16 }}">
<i data-lucide="trash-2" class="size-4"></i>
</button>
{{ end }}