mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 01:04:15 +00:00
update api endpoints to use post body rather than url based handlers
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Vendored
+3
-3
File diff suppressed because one or more lines are too long
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
Reference in New Issue
Block a user