mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-19 00:34:16 +00:00
use chi/render to simplify returned json
This commit is contained in:
@@ -26,12 +26,6 @@ linters:
|
||||
- errcheck
|
||||
|
||||
# TODO: fix issues and remove these paths one by one
|
||||
- path: pkg/auth
|
||||
linters:
|
||||
- errcheck
|
||||
- path: pkg/appview
|
||||
linters:
|
||||
- errcheck
|
||||
- path: cmd/credential-helper
|
||||
linters:
|
||||
- errcheck
|
||||
|
||||
@@ -14,11 +14,11 @@ steps:
|
||||
CGO_ENABLED: 1
|
||||
command: |
|
||||
go mod download
|
||||
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.2
|
||||
go generate ./...
|
||||
|
||||
- name: Run Linter
|
||||
environment:
|
||||
CGO_ENABLED: 1
|
||||
command: |
|
||||
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.7.2
|
||||
golangci-lint run ./...
|
||||
|
||||
@@ -9,6 +9,7 @@ require (
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/earthboundkid/versioninfo/v2 v2.24.1
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/goki/freetype v1.0.5
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -40,6 +41,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b // indirect
|
||||
github.com/ajg/form v1.5.1 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 // indirect
|
||||
|
||||
@@ -3,6 +3,8 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20221103172237-443f56ff4ba8/go.mod h
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b h1:5/++qT1/z812ZqBvqQt6ToRswSuPZ/B33m6xVHRzADU=
|
||||
github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b/go.mod h1:4+EPqMRApwwE/6yo6CxiHoSnBzjRr3jsqer7frxP8y4=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5 h1:iW0a5ljuFxkLGPNem5Ui+KBjFJzKg4Fv2fnxe4dvzpM=
|
||||
@@ -70,6 +72,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
|
||||
+10
-17
@@ -2,7 +2,6 @@ package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
"atcr.io/pkg/auth/oauth"
|
||||
"github.com/bluesky-social/indigo/atproto/identity"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
// StarRepositoryHandler handles starring a repository
|
||||
@@ -66,9 +66,8 @@ func (h *StarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
// Return success
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]bool{"starred": true})
|
||||
render.JSON(w, r, map[string]bool{"starred": true})
|
||||
}
|
||||
|
||||
// UnstarRepositoryHandler handles unstarring a repository
|
||||
@@ -122,8 +121,7 @@ func (h *UnstarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
// Return success
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]bool{"starred": false})
|
||||
render.JSON(w, r, map[string]bool{"starred": false})
|
||||
}
|
||||
|
||||
// CheckStarHandler checks if current user has starred a repository
|
||||
@@ -138,8 +136,7 @@ func (h *CheckStarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
user := middleware.GetUser(r)
|
||||
if user == nil {
|
||||
// Not authenticated - return not starred
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]bool{"starred": false})
|
||||
render.JSON(w, r, map[string]bool{"starred": false})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -166,16 +163,14 @@ func (h *CheckStarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if OAuth error - if so, invalidate sessions
|
||||
if err != nil && handleOAuthError(r.Context(), h.Refresher, user.DID, err) {
|
||||
// For a read operation, just return not starred instead of error
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]bool{"starred": false})
|
||||
render.JSON(w, r, map[string]bool{"starred": false})
|
||||
return
|
||||
}
|
||||
|
||||
starred := err == nil
|
||||
|
||||
// Return result
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]bool{"starred": starred})
|
||||
render.JSON(w, r, map[string]bool{"starred": starred})
|
||||
}
|
||||
|
||||
// GetStatsHandler returns repository statistics
|
||||
@@ -204,8 +199,7 @@ func (h *GetStatsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Return stats as JSON
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(stats)
|
||||
render.JSON(w, r, stats)
|
||||
}
|
||||
|
||||
// ManifestDetailHandler returns detailed manifest information including platforms
|
||||
@@ -240,8 +234,7 @@ func (h *ManifestDetailHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
// Return manifest as JSON
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(manifest)
|
||||
render.JSON(w, r, manifest)
|
||||
}
|
||||
|
||||
// CredentialHelperVersionResponse is the response for the credential helper version API
|
||||
@@ -297,7 +290,7 @@ func (h *CredentialHelperVersionHandler) ServeHTTP(w http.ResponseWriter, r *htt
|
||||
Checksums: h.Checksums,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
render.SetContentType(render.ContentTypeJSON)
|
||||
w.Header().Set("Cache-Control", "public, max-age=300") // Cache for 5 minutes
|
||||
json.NewEncoder(w).Encode(response)
|
||||
render.JSON(w, r, response)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
@@ -9,9 +8,9 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"atcr.io/pkg/appview/db"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
// DeviceCodeRequest is the request to start device authorization
|
||||
@@ -41,7 +40,7 @@ func (h *DeviceCodeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var req DeviceCodeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
if err := render.Decode(r, &req); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -73,8 +72,7 @@ func (h *DeviceCodeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
Interval: 5, // Poll every 5 seconds
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
render.JSON(w, r, resp)
|
||||
}
|
||||
|
||||
// DeviceTokenRequest is the request to poll for device authorization
|
||||
@@ -102,7 +100,7 @@ func (h *DeviceTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var req DeviceTokenRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
if err := render.Decode(r, &req); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -113,8 +111,7 @@ func (h *DeviceTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
resp := DeviceTokenResponse{
|
||||
Error: "expired_token",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
render.JSON(w, r, resp)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -124,8 +121,7 @@ func (h *DeviceTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
resp := DeviceTokenResponse{
|
||||
Error: "authorization_pending",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
render.JSON(w, r, resp)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -147,8 +143,7 @@ func (h *DeviceTokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
DID: *pending.ApprovedDID,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
render.JSON(w, r, resp)
|
||||
}
|
||||
|
||||
// DeviceApprovalPageHandler handles GET /device
|
||||
@@ -257,15 +252,14 @@ func (h *DeviceApproveHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
var req DeviceApproveRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
if err := render.Decode(r, &req); err != nil {
|
||||
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if !req.Approve {
|
||||
// User denied
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "denied"})
|
||||
render.JSON(w, r, map[string]string{"status": "denied"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -276,8 +270,7 @@ func (h *DeviceApproveHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
http.Error(w, fmt.Sprintf("failed to approve: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "approved"})
|
||||
render.JSON(w, r, map[string]string{"status": "approved"})
|
||||
}
|
||||
|
||||
// ListDevicesHandler handles GET /api/devices
|
||||
@@ -308,8 +301,7 @@ func (h *ListDevicesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Get devices for this user
|
||||
devices := h.Store.ListDevices(sess.DID)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(devices)
|
||||
render.JSON(w, r, devices)
|
||||
}
|
||||
|
||||
// RevokeDeviceHandler handles DELETE /api/devices/{id}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"atcr.io/pkg/atproto"
|
||||
"atcr.io/pkg/auth/oauth"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
// DeleteTagHandler handles deleting a tag
|
||||
@@ -93,9 +94,8 @@ func (h *DeleteManifestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
render.Status(r, http.StatusConflict)
|
||||
render.JSON(w, r, map[string]any{
|
||||
"error": "confirmation_required",
|
||||
"message": "This manifest has associated tags that will also be deleted",
|
||||
"tags": tags,
|
||||
@@ -266,6 +266,5 @@ func (h *UploadAvatarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// Return new avatar URL
|
||||
avatarURL := atproto.BlobCDNURL(user.DID, blobRef.Ref.Link)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"avatarURL": avatarURL})
|
||||
render.JSON(w, r, map[string]string{"avatarURL": avatarURL})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package token
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
"atcr.io/pkg/appview/db"
|
||||
"atcr.io/pkg/atproto"
|
||||
"atcr.io/pkg/auth"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
// PostAuthCallback is called after successful Basic Auth authentication.
|
||||
@@ -120,7 +120,7 @@ func sendOAuthSessionExpiredError(w http.ResponseWriter, r *http.Request) {
|
||||
Message: "OAuth session expired or invalidated. Please re-authenticate in your browser.",
|
||||
LoginURL: loginURL,
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
render.JSON(w, r, resp)
|
||||
}
|
||||
|
||||
// ServeHTTP handles the token request
|
||||
@@ -269,9 +269,5 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
IssuedAt: now.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to encode response: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
render.JSON(w, r, resp)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"atcr.io/pkg/atproto"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
// DashboardStats contains dashboard statistics
|
||||
@@ -122,11 +122,7 @@ func (ui *AdminUI) handleStatsAPI(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Otherwise return JSON
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(stats); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
render.JSON(w, r, stats)
|
||||
}
|
||||
|
||||
// UserUsage represents storage usage for a user
|
||||
@@ -194,9 +190,5 @@ func (ui *AdminUI) handleTopUsersAPI(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Otherwise return JSON
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(users); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
render.JSON(w, r, users)
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func (ui *AdminUI) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
token, err := ui.createSession(did, handle)
|
||||
if err != nil {
|
||||
slog.Error("failed to create session token", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
http.Error(w, "Failed to create session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
ui.setSessionCookie(w, r, token)
|
||||
|
||||
@@ -321,7 +321,7 @@ func (ui *AdminUI) handleCrewUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
if tier != "" {
|
||||
if err := ui.pds.UpdateCrewMemberTier(ctx, current.Member, tier); err != nil {
|
||||
slog.Error("failed to update crew member tier", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
http.Error(w, "Failed to update tier", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests for helper functions
|
||||
func TestNormalizeETag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
etag string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "etag without quotes",
|
||||
etag: "abc123",
|
||||
expected: "\"abc123\"",
|
||||
},
|
||||
{
|
||||
name: "etag already has quotes",
|
||||
etag: "\"abc123\"",
|
||||
expected: "\"abc123\"",
|
||||
},
|
||||
{
|
||||
name: "empty etag",
|
||||
etag: "",
|
||||
expected: "\"\"",
|
||||
},
|
||||
{
|
||||
name: "etag with special characters",
|
||||
etag: "abc-123_def",
|
||||
expected: "\"abc-123_def\"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := normalizeETag(tt.etag)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected %s, got %s", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Package oci provides HTTP helpers for OCI registry endpoints in the hold service.
|
||||
// It includes utilities for JSON encoding/decoding of request/response bodies
|
||||
// and standardized error responses for XRPC endpoints.
|
||||
package oci
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// DecodeJSON decodes JSON request body into the provided value
|
||||
// Returns an error if decoding fails
|
||||
func DecodeJSON(r *http.Request, v any) error {
|
||||
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
||||
return fmt.Errorf("invalid JSON body: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RespondJSON writes a JSON response with the given status code
|
||||
func RespondJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
// If encoding fails, we can't do much since headers are already sent
|
||||
// Log the error but don't try to send another response
|
||||
slog.Error("Failed to encode JSON response", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RespondError writes a JSON error response with the given status code and message
|
||||
func RespondError(w http.ResponseWriter, status int, message string) {
|
||||
RespondJSON(w, status, map[string]string{
|
||||
"error": message,
|
||||
})
|
||||
}
|
||||
@@ -227,3 +227,42 @@ func TestCleanupExpiredSessions(t *testing.T) {
|
||||
t.Error("Recent session should still exist")
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for helper functions
|
||||
func TestNormalizeETag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
etag string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "etag without quotes",
|
||||
etag: "abc123",
|
||||
expected: "\"abc123\"",
|
||||
},
|
||||
{
|
||||
name: "etag already has quotes",
|
||||
etag: "\"abc123\"",
|
||||
expected: "\"abc123\"",
|
||||
},
|
||||
{
|
||||
name: "empty etag",
|
||||
etag: "",
|
||||
expected: "\"\"",
|
||||
},
|
||||
{
|
||||
name: "etag with special characters",
|
||||
etag: "abc-123_def",
|
||||
expected: "\"abc-123_def\"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := normalizeETag(tt.etag)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected %s, got %s", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+56
-33
@@ -1,3 +1,4 @@
|
||||
// Package oci provides OCI registry endpoints for the hold service.
|
||||
package oci
|
||||
|
||||
import (
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"atcr.io/pkg/s3"
|
||||
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
// XRPCHandler handles OCI-specific XRPC endpoints for multipart uploads
|
||||
@@ -63,23 +65,26 @@ func (h *XRPCHandler) HandleInitiateUpload(w http.ResponseWriter, r *http.Reques
|
||||
Digest string `json:"digest"`
|
||||
}
|
||||
|
||||
if err := DecodeJSON(r, &req); err != nil {
|
||||
RespondError(w, http.StatusBadRequest, err.Error())
|
||||
if err := render.Decode(r, &req); err != nil {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Digest == "" {
|
||||
RespondError(w, http.StatusBadRequest, "digest is required")
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, map[string]string{"error": "digest is required"})
|
||||
return
|
||||
}
|
||||
|
||||
uploadID, _, err := h.StartMultipartUploadWithManager(r.Context(), req.Digest)
|
||||
if err != nil {
|
||||
RespondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to initiate upload: %v", err))
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
render.JSON(w, r, map[string]string{"error": fmt.Sprintf("failed to initiate upload: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
RespondJSON(w, http.StatusOK, map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"uploadId": uploadID,
|
||||
})
|
||||
}
|
||||
@@ -92,23 +97,26 @@ func (h *XRPCHandler) HandleGetPartUploadURL(w http.ResponseWriter, r *http.Requ
|
||||
PartNumber int `json:"partNumber"`
|
||||
}
|
||||
|
||||
if err := DecodeJSON(r, &req); err != nil {
|
||||
RespondError(w, http.StatusBadRequest, err.Error())
|
||||
if err := render.Decode(r, &req); err != nil {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.UploadID == "" || req.PartNumber == 0 {
|
||||
RespondError(w, http.StatusBadRequest, "uploadId and partNumber are required")
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, map[string]string{"error": "uploadId and partNumber are required"})
|
||||
return
|
||||
}
|
||||
|
||||
uploadInfo, err := h.GetPartUploadURL(r.Context(), req.UploadID, req.PartNumber)
|
||||
if err != nil {
|
||||
RespondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get part upload URL: %v", err))
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
render.JSON(w, r, map[string]string{"error": fmt.Sprintf("failed to get part upload URL: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
RespondJSON(w, http.StatusOK, uploadInfo)
|
||||
render.JSON(w, r, uploadInfo)
|
||||
}
|
||||
|
||||
// HandleUploadPart handles direct buffered part uploads
|
||||
@@ -118,29 +126,33 @@ func (h *XRPCHandler) HandleUploadPart(w http.ResponseWriter, r *http.Request) {
|
||||
partNumberStr := r.Header.Get("X-Part-Number")
|
||||
|
||||
if uploadID == "" || partNumberStr == "" {
|
||||
RespondError(w, http.StatusBadRequest, "X-Upload-Id and X-Part-Number headers are required")
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, map[string]string{"error": "X-Upload-Id and X-Part-Number headers are required"})
|
||||
return
|
||||
}
|
||||
|
||||
partNumber, err := strconv.Atoi(partNumberStr)
|
||||
if err != nil {
|
||||
RespondError(w, http.StatusBadRequest, fmt.Sprintf("invalid part number: %v", err))
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, map[string]string{"error": fmt.Sprintf("invalid part number: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
RespondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to read part data: %v", err))
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
render.JSON(w, r, map[string]string{"error": fmt.Sprintf("failed to read part data: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
etag, err := h.HandleBufferedPartUpload(r.Context(), uploadID, partNumber, data)
|
||||
if err != nil {
|
||||
RespondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to upload part: %v", err))
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
render.JSON(w, r, map[string]string{"error": fmt.Sprintf("failed to upload part: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
RespondJSON(w, http.StatusOK, map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"etag": etag,
|
||||
})
|
||||
}
|
||||
@@ -154,23 +166,26 @@ func (h *XRPCHandler) HandleCompleteUpload(w http.ResponseWriter, r *http.Reques
|
||||
Parts []PartInfo `json:"parts"`
|
||||
}
|
||||
|
||||
if err := DecodeJSON(r, &req); err != nil {
|
||||
RespondError(w, http.StatusBadRequest, err.Error())
|
||||
if err := render.Decode(r, &req); err != nil {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.UploadID == "" || req.Digest == "" || len(req.Parts) == 0 {
|
||||
RespondError(w, http.StatusBadRequest, "uploadId, digest, and parts are required")
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, map[string]string{"error": "uploadId, digest, and parts are required"})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.CompleteMultipartUploadWithManager(r.Context(), req.UploadID, req.Digest, req.Parts)
|
||||
if err != nil {
|
||||
RespondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to complete upload: %v", err))
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
render.JSON(w, r, map[string]string{"error": fmt.Sprintf("failed to complete upload: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
RespondJSON(w, http.StatusOK, map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"status": "completed",
|
||||
"digest": req.Digest,
|
||||
})
|
||||
@@ -183,23 +198,26 @@ func (h *XRPCHandler) HandleAbortUpload(w http.ResponseWriter, r *http.Request)
|
||||
UploadID string `json:"uploadId"`
|
||||
}
|
||||
|
||||
if err := DecodeJSON(r, &req); err != nil {
|
||||
RespondError(w, http.StatusBadRequest, err.Error())
|
||||
if err := render.Decode(r, &req); err != nil {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.UploadID == "" {
|
||||
RespondError(w, http.StatusBadRequest, "uploadId is required")
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, map[string]string{"error": "uploadId is required"})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.AbortMultipartUploadWithManager(r.Context(), req.UploadID)
|
||||
if err != nil {
|
||||
RespondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to abort upload: %v", err))
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
render.JSON(w, r, map[string]string{"error": fmt.Sprintf("failed to abort upload: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
RespondJSON(w, http.StatusOK, map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"status": "aborted",
|
||||
})
|
||||
}
|
||||
@@ -214,7 +232,8 @@ func (h *XRPCHandler) HandleNotifyManifest(w http.ResponseWriter, r *http.Reques
|
||||
// Validate service token (same auth as blob:write endpoints)
|
||||
validatedUser, err := pds.ValidateBlobWriteAccess(r, h.pds, h.httpClient)
|
||||
if err != nil {
|
||||
RespondError(w, http.StatusForbidden, fmt.Sprintf("authorization failed: %v", err))
|
||||
render.Status(r, http.StatusForbidden)
|
||||
render.JSON(w, r, map[string]string{"error": fmt.Sprintf("authorization failed: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -248,8 +267,9 @@ func (h *XRPCHandler) HandleNotifyManifest(w http.ResponseWriter, r *http.Reques
|
||||
} `json:"manifest"`
|
||||
}
|
||||
|
||||
if err := DecodeJSON(r, &req); err != nil {
|
||||
RespondError(w, http.StatusBadRequest, err.Error())
|
||||
if err := render.Decode(r, &req); err != nil {
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -261,7 +281,8 @@ func (h *XRPCHandler) HandleNotifyManifest(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
// Validate operation
|
||||
if operation != "push" && operation != "pull" {
|
||||
RespondError(w, http.StatusBadRequest, fmt.Sprintf("invalid operation: %s (must be 'push' or 'pull')", operation))
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, map[string]string{"error": fmt.Sprintf("invalid operation: %s (must be 'push' or 'pull')", operation)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -269,7 +290,8 @@ func (h *XRPCHandler) HandleNotifyManifest(w http.ResponseWriter, r *http.Reques
|
||||
// For pulls: userDID is the repo owner (for stats), but the token belongs to the puller
|
||||
// This allows anyone to pull from a public repo and have stats tracked under the owner
|
||||
if operation == "push" && req.UserDID != validatedUser.DID {
|
||||
RespondError(w, http.StatusForbidden, "user DID mismatch")
|
||||
render.Status(r, http.StatusForbidden)
|
||||
render.JSON(w, r, map[string]string{"error": "user DID mismatch"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -290,10 +312,11 @@ func (h *XRPCHandler) HandleNotifyManifest(w http.ResponseWriter, r *http.Reques
|
||||
"repository", req.Repository,
|
||||
"tag", req.Tag,
|
||||
)
|
||||
RespondError(w, http.StatusForbidden, fmt.Sprintf(
|
||||
render.Status(r, http.StatusForbidden)
|
||||
render.JSON(w, r, map[string]string{"error": fmt.Sprintf(
|
||||
"quota exceeded: current=%d bytes, limit=%d bytes. Delete images to free space.",
|
||||
stats.TotalSize, *stats.Limit,
|
||||
))
|
||||
)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -409,7 +432,7 @@ func (h *XRPCHandler) HandleNotifyManifest(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
RespondJSON(w, http.StatusOK, resp)
|
||||
render.JSON(w, r, resp)
|
||||
}
|
||||
|
||||
// requireBlobWriteAccess middleware - validates DPoP + OAuth and checks for blob:write permission
|
||||
|
||||
@@ -41,9 +41,6 @@ CREATE INDEX IF NOT EXISTS idx_records_collection_rkey ON records(collection, rk
|
||||
CREATE INDEX IF NOT EXISTS idx_records_collection_did ON records(collection, did);
|
||||
`
|
||||
|
||||
// Schema version for migration detection
|
||||
const recordsSchemaVersion = 2
|
||||
|
||||
// NewRecordsIndex creates or opens a records index
|
||||
// If the schema is outdated (missing did column), drops and rebuilds the table
|
||||
func NewRecordsIndex(dbPath string) (*RecordsIndex, error) {
|
||||
|
||||
+43
-175
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/bluesky-social/indigo/repo"
|
||||
"github.com/distribution/distribution/v3/registry/storage/driver"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-car"
|
||||
@@ -202,15 +203,9 @@ func (h *XRPCHandler) RegisterHandlers(r chi.Router) {
|
||||
|
||||
// HandleHealth returns health check information
|
||||
func (h *XRPCHandler) HandleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"version": "0.4.999",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// HandleDescribeServer returns server metadata
|
||||
@@ -223,17 +218,11 @@ func (h *XRPCHandler) HandleDescribeServer(w http.ResponseWriter, r *http.Reques
|
||||
hostname, _, _ = strings.Cut(hostname, "/") // Remove path
|
||||
hostname, _, _ = strings.Cut(hostname, ":") // Remove port
|
||||
|
||||
response := map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"did": h.pds.DID(),
|
||||
"availableUserDomains": []string{"." + hostname},
|
||||
"inviteCodeRequired": true, // Single-user PDS, no account creation
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// HandleResolveHandle resolves a handle to a DID
|
||||
@@ -256,15 +245,9 @@ func (h *XRPCHandler) HandleResolveHandle(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
// Return the DID
|
||||
response := map[string]string{
|
||||
render.JSON(w, r, map[string]string{
|
||||
"did": h.pds.DID(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// HandleGetProfile returns aggregated profile information
|
||||
@@ -296,13 +279,7 @@ func (h *XRPCHandler) HandleGetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Build profile response using shared function
|
||||
response := h.buildProfileResponse(r.Context())
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
render.JSON(w, r, h.buildProfileResponse(r.Context()))
|
||||
}
|
||||
|
||||
// HandleGetProfiles returns aggregated profile information for multiple actors
|
||||
@@ -348,15 +325,9 @@ func (h *XRPCHandler) HandleGetProfiles(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Return profiles array
|
||||
response := map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"profiles": profiles,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// buildProfileResponse builds a profile response map (shared by GetProfile and GetProfiles)
|
||||
@@ -441,19 +412,13 @@ func (h *XRPCHandler) HandleDescribeRepo(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Note: For did:web, the handle IS the DID (not just hostname)
|
||||
response := map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"did": h.pds.DID(),
|
||||
"handle": h.pds.DID(),
|
||||
"didDoc": didDoc,
|
||||
"collections": collections,
|
||||
"handleIsCorrect": true,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// HandleGetRecord retrieves a record from the repository
|
||||
@@ -490,17 +455,11 @@ func (h *XRPCHandler) HandleGetRecord(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"uri": fmt.Sprintf("at://%s/%s/%s", h.pds.DID(), collection, rkey),
|
||||
"cid": recordCID.String(),
|
||||
"value": recordValue,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// HandleListRecords lists records in a collection
|
||||
@@ -570,12 +529,7 @@ func (h *XRPCHandler) handleListRecordsIndexed(w http.ResponseWriter, r *http.Re
|
||||
|
||||
if !head.Defined() {
|
||||
// Empty repo, return empty list
|
||||
response := map[string]any{"records": []any{}}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
render.JSON(w, r, map[string]any{"records": []any{}})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -621,11 +575,7 @@ func (h *XRPCHandler) handleListRecordsIndexed(w http.ResponseWriter, r *http.Re
|
||||
response["cursor"] = nextCursor
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
render.JSON(w, r, response)
|
||||
}
|
||||
|
||||
// handleListRecordsMST uses the legacy MST-based listing (fallback for tests)
|
||||
@@ -645,12 +595,7 @@ func (h *XRPCHandler) handleListRecordsMST(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
if !head.Defined() {
|
||||
// Empty repo, return empty list
|
||||
response := map[string]any{"records": []any{}}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
render.JSON(w, r, map[string]any{"records": []any{}})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -758,11 +703,7 @@ func (h *XRPCHandler) handleListRecordsMST(w http.ResponseWriter, r *http.Reques
|
||||
response["cursor"] = nextCursor
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
render.JSON(w, r, response)
|
||||
}
|
||||
|
||||
// HandleDeleteRecord deletes a record from the repository
|
||||
@@ -831,15 +772,11 @@ func (h *XRPCHandler) HandleDeleteRecord(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
if !currentCID.Equals(swapRecordCID) {
|
||||
// Swap failed - record CID doesn't match
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
response := map[string]any{
|
||||
render.Status(r, http.StatusBadRequest)
|
||||
render.JSON(w, r, map[string]any{
|
||||
"error": "InvalidSwap",
|
||||
"message": "record CID does not match swapRecord",
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -875,18 +812,12 @@ func (h *XRPCHandler) HandleDeleteRecord(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Return commit response (per spec)
|
||||
response := map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"commit": map[string]any{
|
||||
"cid": head.String(),
|
||||
"rev": rev,
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// HandleSyncGetRecord returns a single record as a CAR file for sync
|
||||
@@ -941,8 +872,7 @@ func (h *XRPCHandler) HandleSyncGetRecord(w http.ResponseWriter, r *http.Request
|
||||
|
||||
// Write the CAR data to the response
|
||||
if _, err := w.Write(buf.Bytes()); err != nil {
|
||||
slog.Error("failed to write car to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
slog.Error("failed to write CAR to http response", "error", err, "path", r.URL.Path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1094,7 +1024,7 @@ func (h *XRPCHandler) HandleUploadBlob(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Return ATProto-compliant blob response
|
||||
response := map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"blob": map[string]any{
|
||||
"$type": "blob",
|
||||
"ref": map[string]any{
|
||||
@@ -1103,13 +1033,7 @@ func (h *XRPCHandler) HandleUploadBlob(w http.ResponseWriter, r *http.Request) {
|
||||
"mimeType": "application/octet-stream",
|
||||
"size": size,
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// HandleGetBlob routes blob requests to appropriate handlers based on blob type
|
||||
@@ -1181,14 +1105,9 @@ func (h *XRPCHandler) handleGetOCIBlob(w http.ResponseWriter, r *http.Request, d
|
||||
"url", presignedURL)
|
||||
|
||||
// Return JSON response with presigned URL (AppView expects this format)
|
||||
response := map[string]string{
|
||||
render.JSON(w, r, map[string]string{
|
||||
"url": presignedURL,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetATProtoBlob handles standard ATProto blob requests
|
||||
@@ -1238,14 +1157,7 @@ func (h *XRPCHandler) HandleListRepos(w http.ResponseWriter, r *http.Request) {
|
||||
head, err := h.pds.repomgr.GetRepoRoot(r.Context(), h.pds.uid)
|
||||
if err != nil {
|
||||
// If no repo exists yet, return empty list
|
||||
response := map[string]any{
|
||||
"repos": []any{},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
render.JSON(w, r, map[string]any{"repos": []any{}})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1253,14 +1165,7 @@ func (h *XRPCHandler) HandleListRepos(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil || rev == "" {
|
||||
// No commits yet, return empty list
|
||||
// Don't expose repos with no revision (empty/uninitialized)
|
||||
response := map[string]any{
|
||||
"repos": []any{},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
render.JSON(w, r, map[string]any{"repos": []any{}})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1273,15 +1178,9 @@ func (h *XRPCHandler) HandleListRepos(w http.ResponseWriter, r *http.Request) {
|
||||
},
|
||||
}
|
||||
|
||||
response := map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"repos": repos,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// HandleGetRepoStatus returns the hosting status for a repository
|
||||
@@ -1305,30 +1204,19 @@ func (h *XRPCHandler) HandleGetRepoStatus(w http.ResponseWriter, r *http.Request
|
||||
if err != nil || rev == "" {
|
||||
// Repo exists (DID matches) but no commits yet
|
||||
// Per ATProto spec, return active=true even if empty
|
||||
response := map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"did": did,
|
||||
"active": true,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Return status with revision
|
||||
response := map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"did": did,
|
||||
"active": true,
|
||||
"rev": rev,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// HandleDIDDocument returns the DID document
|
||||
@@ -1339,11 +1227,7 @@ func (h *XRPCHandler) HandleDIDDocument(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(doc); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
render.JSON(w, r, doc)
|
||||
}
|
||||
|
||||
// HandleAtprotoDID returns the DID for handle resolution
|
||||
@@ -1434,18 +1318,12 @@ func (h *XRPCHandler) HandleRequestCrew(w http.ResponseWriter, r *http.Request)
|
||||
slog.Debug("User is already a crew member",
|
||||
"did", user.DID,
|
||||
"rkey", member.Rkey)
|
||||
response := map[string]any{
|
||||
render.JSON(w, r, map[string]any{
|
||||
"uri": fmt.Sprintf("at://%s/%s/%s", h.pds.DID(), atproto.CrewCollection, member.Rkey),
|
||||
"cid": member.Cid.String(),
|
||||
"status": "already_member",
|
||||
"message": "User is already a crew member",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1470,18 +1348,12 @@ func (h *XRPCHandler) HandleRequestCrew(w http.ResponseWriter, r *http.Request)
|
||||
// Return success response
|
||||
// Note: rkey is generated by AddCrewMember (TID), we don't have direct access to it
|
||||
// For now, return just the CID. In production, AddCrewMember should return both CID and rkey
|
||||
response := map[string]any{
|
||||
render.Status(r, http.StatusCreated)
|
||||
render.JSON(w, r, map[string]any{
|
||||
"cid": recordCID.String(),
|
||||
"status": "created",
|
||||
"message": "Successfully added to crew",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// GetPresignedURL generates a presigned URL for GET, HEAD, or PUT operations
|
||||
@@ -1618,9 +1490,5 @@ func (h *XRPCHandler) HandleGetQuota(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(stats); err != nil {
|
||||
slog.Error("failed to encode json to http response", "error", err, "path", r.URL.Path)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
render.JSON(w, r, stats)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user