A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
80
fork

Configure Feed

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

fix error messages for outdated self hosted PDS. fix profile not being creatd on login

+54 -20
+13 -8
cmd/appview/serve.go
··· 135 135 if err != nil { 136 136 return fmt.Errorf("failed to create OAuth app: %w", err) 137 137 } 138 + fmt.Println("Using full OAuth scopes (including blob: scope)") 138 139 139 140 // 5. Create refresher 140 141 refresher := oauth.NewRefresher(oauthApp) ··· 160 161 // Connect database for user avatar management 161 162 oauthServer.SetDatabase(uiDatabase) 162 163 163 - // 8. Initialize auth keys and create token issuer 164 + // 8.5. Extract default hold endpoint and set it on OAuth server 165 + // This is used to create sailor profiles on first login 166 + defaultHoldEndpoint := extractDefaultHoldEndpoint(config) 167 + if defaultHoldEndpoint != "" { 168 + oauthServer.SetDefaultHoldEndpoint(defaultHoldEndpoint) 169 + fmt.Printf("OAuth server will create profiles with default hold: %s\n", defaultHoldEndpoint) 170 + } 171 + 172 + // 9. Initialize auth keys and create token issuer 164 173 var issuer *token.Issuer 165 174 if config.Auth["token"] != nil { 166 175 if err := initializeAuthKeys(config); err != nil { ··· 203 212 204 213 // OAuth client metadata endpoint 205 214 mux.HandleFunc("/client-metadata.json", func(w http.ResponseWriter, r *http.Request) { 206 - // Get the client config from the OAuth app 207 215 config := oauth.NewClientConfig(baseURL) 208 216 metadata := config.ClientMetadata() 209 217 210 - // Serve as JSON 211 218 w.Header().Set("Content-Type", "application/json") 212 219 w.Header().Set("Access-Control-Allow-Origin", "*") 213 220 if err := json.NewEncoder(w).Encode(metadata); err != nil { ··· 219 226 220 227 // Mount auth endpoints if enabled 221 228 if issuer != nil { 222 - // Extract default hold endpoint from middleware config 223 - defaultHoldEndpoint := extractDefaultHoldEndpoint(config) 224 - 225 229 // Basic Auth token endpoint (supports device secrets and app passwords) 230 + // Reuse defaultHoldEndpoint extracted earlier 226 231 tokenHandler := token.NewHandler(issuer, deviceStore, defaultHoldEndpoint) 227 232 tokenHandler.RegisterRoutes(mux) 228 233 ··· 600 605 SessionStore: sessionStore, 601 606 }).Methods("DELETE") 602 607 603 - // Logout endpoint 608 + // Logout endpoint (supports both GET and POST) 604 609 router.HandleFunc("/auth/logout", func(w http.ResponseWriter, r *http.Request) { 605 610 if sessionID, ok := db.GetSessionID(r); ok { 606 611 sessionStore.Delete(sessionID) 607 612 } 608 613 db.ClearCookie(w) 609 614 http.Redirect(w, r, "/", http.StatusFound) 610 - }).Methods("POST") 615 + }).Methods("GET", "POST") 611 616 612 617 // Start Jetstream worker 613 618 jetstreamURL := os.Getenv("JETSTREAM_URL")
+13 -5
pkg/appview/handlers/settings.go
··· 43 43 // Fetch sailor profile 44 44 profile, err := atproto.GetProfile(r.Context(), client) 45 45 if err != nil { 46 - // Log error but don't fail - profile might not exist yet 47 - fmt.Printf("WARNING [settings]: Failed to fetch profile for %s: %v\n", user.DID, err) 48 - profile = &atproto.SailorProfileRecord{} 49 - } else { 50 - fmt.Printf("DEBUG [settings]: Fetched profile for %s: defaultHold=%s\n", user.DID, profile.DefaultHold) 46 + // Error fetching profile - log out user 47 + fmt.Printf("WARNING [settings]: Failed to fetch profile for %s: %v - logging out\n", user.DID, err) 48 + http.Redirect(w, r, "/auth/logout", http.StatusFound) 49 + return 51 50 } 51 + 52 + if profile == nil { 53 + // Profile doesn't exist yet (404) - user needs to log out and back in to create it 54 + fmt.Printf("WARNING [settings]: Profile doesn't exist for %s - logging out\n", user.DID) 55 + http.Redirect(w, r, "/auth/logout", http.StatusFound) 56 + return 57 + } 58 + 59 + fmt.Printf("DEBUG [settings]: Fetched profile for %s: defaultHold=%s\n", user.DID, profile.DefaultHold) 52 60 53 61 data := struct { 54 62 PageData
+3 -3
pkg/appview/storage/proxy_blob_store.go
··· 17 17 const ( 18 18 // maxChunkSize is the maximum buffer size before flushing to hold service 19 19 // Matches S3's minimum multipart upload size 20 - maxChunkSize = 5 * 1024 * 1024 // 5MB 20 + maxChunkSize = 10 * 1024 * 1024 // 10MB 21 21 ) 22 22 23 23 // Global upload tracking (shared across all ProxyBlobStore instances) ··· 242 242 uploadID: uploadID, 243 243 parts: make([]CompletedPart, 0), 244 244 partNumber: 1, 245 - buffer: bytes.NewBuffer(make([]byte, 0, maxChunkSize)), // 5MB buffer 245 + buffer: bytes.NewBuffer(make([]byte, 0, maxChunkSize)), 246 246 id: writerID, 247 247 startedAt: time.Now(), 248 248 } ··· 527 527 n, err := w.buffer.Write(p) 528 528 w.size += int64(n) 529 529 530 - // Flush if buffer reaches 5MB (S3 minimum part size) 530 + // Flush if buffer reaches limit (S3 part size) 531 531 if w.buffer.Len() >= maxChunkSize { 532 532 if err := w.flushPart(); err != nil { 533 533 return n, err
+25 -4
pkg/auth/oauth/server.go
··· 6 6 "fmt" 7 7 "html/template" 8 8 "net/http" 9 + "strings" 9 10 "time" 10 11 11 12 "atcr.io/pkg/appview/db" ··· 25 26 26 27 // Server handles OAuth authorization for the AppView 27 28 type Server struct { 28 - app *App 29 - refresher *Refresher 30 - uiSessionStore UISessionStore 31 - db *sql.DB 29 + app *App 30 + refresher *Refresher 31 + uiSessionStore UISessionStore 32 + db *sql.DB 33 + defaultHoldEndpoint string 32 34 } 33 35 34 36 // NewServer creates a new OAuth server ··· 38 40 } 39 41 } 40 42 43 + // SetDefaultHoldEndpoint sets the default hold endpoint for profile creation 44 + func (s *Server) SetDefaultHoldEndpoint(endpoint string) { 45 + s.defaultHoldEndpoint = endpoint 46 + } 47 + 41 48 // SetRefresher sets the refresher for invalidating session cache 42 49 func (s *Server) SetRefresher(refresher *Refresher) { 43 50 s.refresher = refresher ··· 73 80 authURL, err := s.app.StartAuthFlow(r.Context(), handle) 74 81 if err != nil { 75 82 fmt.Printf("ERROR [oauth/server]: Failed to start auth flow: %v\n", err) 83 + 84 + // Check if error is about invalid_client_metadata (usually means PDS doesn't support required scopes) 85 + errMsg := err.Error() 86 + if strings.Contains(errMsg, "invalid_client_metadata") { 87 + s.renderError(w, "OAuth authorization failed: Your PDS does not support one or more required OAuth scopes (likely the 'blob:' scope). Please update your PDS to the latest version and try again.") 88 + return 89 + } 90 + 76 91 http.Error(w, fmt.Sprintf("failed to start auth flow: %v", err), http.StatusInternalServerError) 77 92 return 78 93 } ··· 251 266 252 267 // Create authenticated atproto client using the indigo session's API client 253 268 client := atproto.NewClientWithIndigoClient(pdsEndpoint, did, session.APIClient()) 269 + 270 + // Ensure sailor profile exists (creates with default hold on first login) 271 + if err := atproto.EnsureProfile(ctx, client, s.defaultHoldEndpoint); err != nil { 272 + fmt.Printf("WARNING [oauth/server]: Failed to ensure profile for %s: %v\n", did, err) 273 + // Continue anyway - profile creation is not critical 274 + } 254 275 255 276 // Fetch user's profile record from PDS (contains blob references) 256 277 profileRecord, err := client.GetProfileRecord(ctx, did)