From 4ab3cea3d1b3a133ffaf07e33c2892caf3662908 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Sun, 12 Oct 2025 10:55:27 -0500 Subject: [PATCH] fix error messages for outdated self hosted PDS. fix profile not being creatd on login --- cmd/appview/serve.go | 21 +++++++++++------- pkg/appview/handlers/settings.go | 18 ++++++++++----- pkg/appview/storage/proxy_blob_store.go | 6 ++--- pkg/auth/oauth/server.go | 29 +++++++++++++++++++++---- 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/cmd/appview/serve.go b/cmd/appview/serve.go index 26ff636..9a95ee2 100644 --- a/cmd/appview/serve.go +++ b/cmd/appview/serve.go @@ -135,6 +135,7 @@ func serveRegistry(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("failed to create OAuth app: %w", err) } + fmt.Println("Using full OAuth scopes (including blob: scope)") // 5. Create refresher refresher := oauth.NewRefresher(oauthApp) @@ -160,7 +161,15 @@ func serveRegistry(cmd *cobra.Command, args []string) error { // Connect database for user avatar management oauthServer.SetDatabase(uiDatabase) - // 8. Initialize auth keys and create token issuer + // 8.5. Extract default hold endpoint and set it on OAuth server + // This is used to create sailor profiles on first login + defaultHoldEndpoint := extractDefaultHoldEndpoint(config) + if defaultHoldEndpoint != "" { + oauthServer.SetDefaultHoldEndpoint(defaultHoldEndpoint) + fmt.Printf("OAuth server will create profiles with default hold: %s\n", defaultHoldEndpoint) + } + + // 9. Initialize auth keys and create token issuer var issuer *token.Issuer if config.Auth["token"] != nil { if err := initializeAuthKeys(config); err != nil { @@ -203,11 +212,9 @@ func serveRegistry(cmd *cobra.Command, args []string) error { // OAuth client metadata endpoint mux.HandleFunc("/client-metadata.json", func(w http.ResponseWriter, r *http.Request) { - // Get the client config from the OAuth app config := oauth.NewClientConfig(baseURL) metadata := config.ClientMetadata() - // Serve as JSON w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") if err := json.NewEncoder(w).Encode(metadata); err != nil { @@ -219,10 +226,8 @@ func serveRegistry(cmd *cobra.Command, args []string) error { // Mount auth endpoints if enabled if issuer != nil { - // Extract default hold endpoint from middleware config - defaultHoldEndpoint := extractDefaultHoldEndpoint(config) - // Basic Auth token endpoint (supports device secrets and app passwords) + // Reuse defaultHoldEndpoint extracted earlier tokenHandler := token.NewHandler(issuer, deviceStore, defaultHoldEndpoint) tokenHandler.RegisterRoutes(mux) @@ -600,14 +605,14 @@ func initializeUIRoutes(database *sql.DB, readOnlyDB *sql.DB, sessionStore *db.S SessionStore: sessionStore, }).Methods("DELETE") - // Logout endpoint + // Logout endpoint (supports both GET and POST) router.HandleFunc("/auth/logout", func(w http.ResponseWriter, r *http.Request) { if sessionID, ok := db.GetSessionID(r); ok { sessionStore.Delete(sessionID) } db.ClearCookie(w) http.Redirect(w, r, "/", http.StatusFound) - }).Methods("POST") + }).Methods("GET", "POST") // Start Jetstream worker jetstreamURL := os.Getenv("JETSTREAM_URL") diff --git a/pkg/appview/handlers/settings.go b/pkg/appview/handlers/settings.go index dc87a73..67f1498 100644 --- a/pkg/appview/handlers/settings.go +++ b/pkg/appview/handlers/settings.go @@ -43,13 +43,21 @@ func (h *SettingsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Fetch sailor profile profile, err := atproto.GetProfile(r.Context(), client) if err != nil { - // Log error but don't fail - profile might not exist yet - fmt.Printf("WARNING [settings]: Failed to fetch profile for %s: %v\n", user.DID, err) - profile = &atproto.SailorProfileRecord{} - } else { - fmt.Printf("DEBUG [settings]: Fetched profile for %s: defaultHold=%s\n", user.DID, profile.DefaultHold) + // Error fetching profile - log out user + fmt.Printf("WARNING [settings]: Failed to fetch profile for %s: %v - logging out\n", user.DID, err) + http.Redirect(w, r, "/auth/logout", http.StatusFound) + return } + if profile == nil { + // Profile doesn't exist yet (404) - user needs to log out and back in to create it + fmt.Printf("WARNING [settings]: Profile doesn't exist for %s - logging out\n", user.DID) + http.Redirect(w, r, "/auth/logout", http.StatusFound) + return + } + + fmt.Printf("DEBUG [settings]: Fetched profile for %s: defaultHold=%s\n", user.DID, profile.DefaultHold) + data := struct { PageData Profile struct { diff --git a/pkg/appview/storage/proxy_blob_store.go b/pkg/appview/storage/proxy_blob_store.go index beb390b..86ae347 100644 --- a/pkg/appview/storage/proxy_blob_store.go +++ b/pkg/appview/storage/proxy_blob_store.go @@ -17,7 +17,7 @@ import ( const ( // maxChunkSize is the maximum buffer size before flushing to hold service // Matches S3's minimum multipart upload size - maxChunkSize = 5 * 1024 * 1024 // 5MB + maxChunkSize = 10 * 1024 * 1024 // 10MB ) // Global upload tracking (shared across all ProxyBlobStore instances) @@ -242,7 +242,7 @@ func (p *ProxyBlobStore) Create(ctx context.Context, options ...distribution.Blo uploadID: uploadID, parts: make([]CompletedPart, 0), partNumber: 1, - buffer: bytes.NewBuffer(make([]byte, 0, maxChunkSize)), // 5MB buffer + buffer: bytes.NewBuffer(make([]byte, 0, maxChunkSize)), id: writerID, startedAt: time.Now(), } @@ -527,7 +527,7 @@ func (w *ProxyBlobWriter) Write(p []byte) (int, error) { n, err := w.buffer.Write(p) w.size += int64(n) - // Flush if buffer reaches 5MB (S3 minimum part size) + // Flush if buffer reaches limit (S3 part size) if w.buffer.Len() >= maxChunkSize { if err := w.flushPart(); err != nil { return n, err diff --git a/pkg/auth/oauth/server.go b/pkg/auth/oauth/server.go index 68aedc8..b07ec4c 100644 --- a/pkg/auth/oauth/server.go +++ b/pkg/auth/oauth/server.go @@ -6,6 +6,7 @@ import ( "fmt" "html/template" "net/http" + "strings" "time" "atcr.io/pkg/appview/db" @@ -25,10 +26,11 @@ type UserStore interface { // Server handles OAuth authorization for the AppView type Server struct { - app *App - refresher *Refresher - uiSessionStore UISessionStore - db *sql.DB + app *App + refresher *Refresher + uiSessionStore UISessionStore + db *sql.DB + defaultHoldEndpoint string } // NewServer creates a new OAuth server @@ -38,6 +40,11 @@ func NewServer(app *App) *Server { } } +// SetDefaultHoldEndpoint sets the default hold endpoint for profile creation +func (s *Server) SetDefaultHoldEndpoint(endpoint string) { + s.defaultHoldEndpoint = endpoint +} + // SetRefresher sets the refresher for invalidating session cache func (s *Server) SetRefresher(refresher *Refresher) { s.refresher = refresher @@ -73,6 +80,14 @@ func (s *Server) ServeAuthorize(w http.ResponseWriter, r *http.Request) { authURL, err := s.app.StartAuthFlow(r.Context(), handle) if err != nil { fmt.Printf("ERROR [oauth/server]: Failed to start auth flow: %v\n", err) + + // Check if error is about invalid_client_metadata (usually means PDS doesn't support required scopes) + errMsg := err.Error() + if strings.Contains(errMsg, "invalid_client_metadata") { + 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.") + return + } + http.Error(w, fmt.Sprintf("failed to start auth flow: %v", err), http.StatusInternalServerError) return } @@ -252,6 +267,12 @@ func (s *Server) fetchAndStoreAvatar(ctx context.Context, did, sessionID, handle // Create authenticated atproto client using the indigo session's API client client := atproto.NewClientWithIndigoClient(pdsEndpoint, did, session.APIClient()) + // Ensure sailor profile exists (creates with default hold on first login) + if err := atproto.EnsureProfile(ctx, client, s.defaultHoldEndpoint); err != nil { + fmt.Printf("WARNING [oauth/server]: Failed to ensure profile for %s: %v\n", did, err) + // Continue anyway - profile creation is not critical + } + // Fetch user's profile record from PDS (contains blob references) profileRecord, err := client.GetProfileRecord(ctx, did) if err != nil {