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.

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)