A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at codeberg-source 303 lines 10 kB view raw
1package handlers 2 3import ( 4 "database/sql" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "log/slog" 9 "net/http" 10 "strings" 11 12 "atcr.io/pkg/appview/db" 13 "atcr.io/pkg/appview/middleware" 14 "atcr.io/pkg/atproto" 15 "atcr.io/pkg/auth/oauth" 16 "github.com/bluesky-social/indigo/atproto/identity" 17 "github.com/go-chi/chi/v5" 18) 19 20// StarRepositoryHandler handles starring a repository 21type StarRepositoryHandler struct { 22 DB *sql.DB 23 Directory identity.Directory 24 Refresher *oauth.Refresher 25} 26 27func (h *StarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 28 // Get authenticated user from middleware 29 user := middleware.GetUser(r) 30 if user == nil { 31 http.Error(w, "Unauthorized", http.StatusUnauthorized) 32 return 33 } 34 35 // Extract parameters 36 handle := chi.URLParam(r, "handle") 37 repository := chi.URLParam(r, "repository") 38 39 // Resolve owner's handle to DID 40 ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) 41 if err != nil { 42 slog.Warn("Failed to resolve handle for star", "handle", handle, "error", err) 43 http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest) 44 return 45 } 46 47 // Create ATProto client with session provider (uses DoWithSession for DPoP nonce safety) 48 slog.Debug("Creating PDS client for star", "user_did", user.DID) 49 pdsClient := atproto.NewClientWithSessionProvider(user.PDSEndpoint, user.DID, h.Refresher) 50 51 // Create star record 52 starRecord := atproto.NewStarRecord(ownerDID, repository) 53 rkey := atproto.StarRecordKey(ownerDID, repository) 54 55 // Write star record to user's PDS 56 _, err = pdsClient.PutRecord(r.Context(), atproto.StarCollection, rkey, starRecord) 57 if err != nil { 58 // Check if OAuth error - if so, invalidate sessions and return 401 59 if handleOAuthError(r.Context(), h.Refresher, user.DID, err) { 60 http.Error(w, "Authentication failed, please log in again", http.StatusUnauthorized) 61 return 62 } 63 slog.Error("Failed to create star record", "error", err) 64 http.Error(w, fmt.Sprintf("Failed to create star: %v", err), http.StatusInternalServerError) 65 return 66 } 67 68 // Return success 69 w.Header().Set("Content-Type", "application/json") 70 w.WriteHeader(http.StatusCreated) 71 json.NewEncoder(w).Encode(map[string]bool{"starred": true}) 72} 73 74// UnstarRepositoryHandler handles unstarring a repository 75type UnstarRepositoryHandler struct { 76 DB *sql.DB 77 Directory identity.Directory 78 Refresher *oauth.Refresher 79} 80 81func (h *UnstarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 82 // Get authenticated user from middleware 83 user := middleware.GetUser(r) 84 if user == nil { 85 http.Error(w, "Unauthorized", http.StatusUnauthorized) 86 return 87 } 88 89 // Extract parameters 90 handle := chi.URLParam(r, "handle") 91 repository := chi.URLParam(r, "repository") 92 93 // Resolve owner's handle to DID 94 ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) 95 if err != nil { 96 slog.Warn("Failed to resolve handle for unstar", "handle", handle, "error", err) 97 http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest) 98 return 99 } 100 101 // Create ATProto client with session provider (uses DoWithSession for DPoP nonce safety) 102 slog.Debug("Creating PDS client for unstar", "user_did", user.DID) 103 pdsClient := atproto.NewClientWithSessionProvider(user.PDSEndpoint, user.DID, h.Refresher) 104 105 // Delete star record from user's PDS 106 rkey := atproto.StarRecordKey(ownerDID, repository) 107 slog.Debug("Deleting star record", "handle", handle, "repository", repository, "rkey", rkey) 108 err = pdsClient.DeleteRecord(r.Context(), atproto.StarCollection, rkey) 109 if err != nil { 110 // If record doesn't exist, still return success (idempotent) 111 if !errors.Is(err, atproto.ErrRecordNotFound) { 112 // Check if OAuth error - if so, invalidate sessions and return 401 113 if handleOAuthError(r.Context(), h.Refresher, user.DID, err) { 114 http.Error(w, "Authentication failed, please log in again", http.StatusUnauthorized) 115 return 116 } 117 slog.Error("Failed to delete star record", "error", err) 118 http.Error(w, fmt.Sprintf("Failed to delete star: %v", err), http.StatusInternalServerError) 119 return 120 } 121 slog.Debug("Star record not found, already unstarred") 122 } 123 124 // Return success 125 w.Header().Set("Content-Type", "application/json") 126 json.NewEncoder(w).Encode(map[string]bool{"starred": false}) 127} 128 129// CheckStarHandler checks if current user has starred a repository 130type CheckStarHandler struct { 131 DB *sql.DB 132 Directory identity.Directory 133 Refresher *oauth.Refresher 134} 135 136func (h *CheckStarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 137 // Get authenticated user from middleware 138 user := middleware.GetUser(r) 139 if user == nil { 140 // Not authenticated - return not starred 141 w.Header().Set("Content-Type", "application/json") 142 json.NewEncoder(w).Encode(map[string]bool{"starred": false}) 143 return 144 } 145 146 // Extract parameters 147 handle := chi.URLParam(r, "handle") 148 repository := chi.URLParam(r, "repository") 149 150 // Resolve owner's handle to DID 151 ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) 152 if err != nil { 153 slog.Warn("Failed to resolve handle for check star", "handle", handle, "error", err) 154 http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest) 155 return 156 } 157 158 // Create ATProto client with session provider (uses DoWithSession for DPoP nonce safety) 159 // Note: Error handling moves to the PDS call - if session doesn't exist, GetRecord will fail 160 pdsClient := atproto.NewClientWithSessionProvider(user.PDSEndpoint, user.DID, h.Refresher) 161 162 // Check if star record exists 163 rkey := atproto.StarRecordKey(ownerDID, repository) 164 _, err = pdsClient.GetRecord(r.Context(), atproto.StarCollection, rkey) 165 166 // Check if OAuth error - if so, invalidate sessions 167 if err != nil && handleOAuthError(r.Context(), h.Refresher, user.DID, err) { 168 // For a read operation, just return not starred instead of error 169 w.Header().Set("Content-Type", "application/json") 170 json.NewEncoder(w).Encode(map[string]bool{"starred": false}) 171 return 172 } 173 174 starred := err == nil 175 176 // Return result 177 w.Header().Set("Content-Type", "application/json") 178 json.NewEncoder(w).Encode(map[string]bool{"starred": starred}) 179} 180 181// GetStatsHandler returns repository statistics 182type GetStatsHandler struct { 183 DB *sql.DB 184 Directory identity.Directory 185} 186 187func (h *GetStatsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 188 // Extract parameters 189 handle := chi.URLParam(r, "handle") 190 repository := chi.URLParam(r, "repository") 191 192 // Resolve owner's handle to DID 193 ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) 194 if err != nil { 195 http.Error(w, "Failed to resolve handle", http.StatusBadRequest) 196 return 197 } 198 199 // Get repository stats from database 200 stats, err := db.GetRepositoryStats(h.DB, ownerDID, repository) 201 if err != nil { 202 http.Error(w, "Failed to fetch stats", http.StatusInternalServerError) 203 return 204 } 205 206 // Return stats as JSON 207 w.Header().Set("Content-Type", "application/json") 208 json.NewEncoder(w).Encode(stats) 209} 210 211// ManifestDetailHandler returns detailed manifest information including platforms 212type ManifestDetailHandler struct { 213 DB *sql.DB 214 Directory identity.Directory 215} 216 217func (h *ManifestDetailHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 218 // Extract parameters 219 handle := chi.URLParam(r, "handle") 220 repository := chi.URLParam(r, "repository") 221 digest := chi.URLParam(r, "digest") 222 223 // Resolve owner's handle to DID 224 ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) 225 if err != nil { 226 http.Error(w, "Failed to resolve handle", http.StatusBadRequest) 227 return 228 } 229 230 // Get manifest detail from database 231 manifest, err := db.GetManifestDetail(h.DB, ownerDID, repository, digest) 232 if err != nil { 233 if err.Error() == "manifest not found" { 234 http.Error(w, "Manifest not found", http.StatusNotFound) 235 return 236 } 237 slog.Error("Failed to get manifest detail", "error", err) 238 http.Error(w, "Failed to fetch manifest", http.StatusInternalServerError) 239 return 240 } 241 242 // Return manifest as JSON 243 w.Header().Set("Content-Type", "application/json") 244 json.NewEncoder(w).Encode(manifest) 245} 246 247// CredentialHelperVersionResponse is the response for the credential helper version API 248type CredentialHelperVersionResponse struct { 249 Latest string `json:"latest"` 250 DownloadURLs map[string]string `json:"download_urls"` 251 Checksums map[string]string `json:"checksums"` 252 ReleaseNotes string `json:"release_notes,omitempty"` 253} 254 255// CredentialHelperVersionHandler returns the latest credential helper version info 256type CredentialHelperVersionHandler struct { 257 Version string 258 TangledRepo string 259 Checksums map[string]string 260} 261 262// Supported platforms for download URLs 263var credentialHelperPlatforms = []struct { 264 key string // API key (e.g., "linux_amd64") 265 os string // OS name in archive (e.g., "Linux") 266 arch string // Arch name in archive (e.g., "x86_64") 267 ext string // Archive extension (e.g., "tar.gz" or "zip") 268}{ 269 {"linux_amd64", "Linux", "x86_64", "tar.gz"}, 270 {"linux_arm64", "Linux", "arm64", "tar.gz"}, 271 {"darwin_amd64", "Darwin", "x86_64", "tar.gz"}, 272 {"darwin_arm64", "Darwin", "arm64", "tar.gz"}, 273 {"windows_amd64", "Windows", "x86_64", "zip"}, 274 {"windows_arm64", "Windows", "arm64", "zip"}, 275} 276 277func (h *CredentialHelperVersionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 278 // Check if version is configured 279 if h.Version == "" { 280 http.Error(w, "Credential helper version not configured", http.StatusServiceUnavailable) 281 return 282 } 283 284 // Build download URLs for all platforms 285 // URL format: {TangledRepo}/tags/{version}/download/docker-credential-atcr_{version_without_v}_{OS}_{Arch}.{ext} 286 downloadURLs := make(map[string]string) 287 versionWithoutV := strings.TrimPrefix(h.Version, "v") 288 289 for _, p := range credentialHelperPlatforms { 290 filename := fmt.Sprintf("docker-credential-atcr_%s_%s_%s.%s", versionWithoutV, p.os, p.arch, p.ext) 291 downloadURLs[p.key] = fmt.Sprintf("%s/tags/%s/download/%s", h.TangledRepo, h.Version, filename) 292 } 293 294 response := CredentialHelperVersionResponse{ 295 Latest: h.Version, 296 DownloadURLs: downloadURLs, 297 Checksums: h.Checksums, 298 } 299 300 w.Header().Set("Content-Type", "application/json") 301 w.Header().Set("Cache-Control", "public, max-age=300") // Cache for 5 minutes 302 json.NewEncoder(w).Encode(response) 303}