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

This commit is contained in:
Evan Jarrett
2025-10-12 10:55:27 -05:00
parent 9e34903ea9
commit 4ab3cea3d1
4 changed files with 54 additions and 20 deletions
+13 -8
View File
@@ -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")
+13 -5
View File
@@ -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 {
+3 -3
View File
@@ -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
+25 -4
View File
@@ -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 {