mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-30 04:37:06 +00:00
big scary refactor. sync enable_bluesky_posts with captain record. implement oauth logout handler. implement crew assignment to hold. this caused a lot of circular dependencies and needed to move functions around in order to fix
This commit is contained in:
+146
-18
@@ -9,15 +9,19 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/bluesky-social/indigo/atproto/syntax"
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
"github.com/distribution/distribution/v3/registry"
|
||||
"github.com/distribution/distribution/v3/registry/handlers"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"atcr.io/pkg/appview/middleware"
|
||||
"atcr.io/pkg/appview/storage"
|
||||
"atcr.io/pkg/atproto"
|
||||
"atcr.io/pkg/auth"
|
||||
"atcr.io/pkg/auth/oauth"
|
||||
"atcr.io/pkg/auth/token"
|
||||
@@ -206,7 +210,7 @@ func serveRegistry(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println("README cache initialized for manifest push refresh")
|
||||
|
||||
// Initialize UI routes with OAuth app, refresher, device store, health checker, and readme cache
|
||||
uiTemplates, uiRouter := initializeUIRoutes(uiDatabase, uiReadOnlyDB, uiSessionStore, oauthApp, refresher, baseURL, deviceStore, defaultHoldDID, healthChecker, readmeCache)
|
||||
uiTemplates, uiRouter := initializeUIRoutes(uiDatabase, uiReadOnlyDB, uiSessionStore, oauthApp, oauthStore, refresher, baseURL, deviceStore, defaultHoldDID, healthChecker, readmeCache)
|
||||
|
||||
// Create OAuth server
|
||||
oauthServer := oauth.NewServer(oauthApp)
|
||||
@@ -216,15 +220,120 @@ func serveRegistry(cmd *cobra.Command, args []string) error {
|
||||
if uiSessionStore != nil {
|
||||
oauthServer.SetUISessionStore(uiSessionStore)
|
||||
}
|
||||
// Connect database for user avatar management
|
||||
oauthServer.SetDatabase(uiDatabase)
|
||||
|
||||
// Set default hold DID on OAuth server (extracted earlier)
|
||||
// This is used to create sailor profiles on first login
|
||||
if defaultHoldDID != "" {
|
||||
oauthServer.SetDefaultHoldDID(defaultHoldDID)
|
||||
fmt.Printf("OAuth server will create profiles with default hold: %s\n", defaultHoldDID)
|
||||
}
|
||||
// Register OAuth post-auth callback for AppView business logic
|
||||
// This decouples the OAuth package from AppView-specific dependencies
|
||||
oauthServer.SetPostAuthCallback(func(ctx context.Context, did, handle, pdsEndpoint, sessionID string) error {
|
||||
fmt.Printf("DEBUG [appview/callback]: OAuth post-auth callback for DID=%s\n", did)
|
||||
|
||||
// Parse DID for session resume
|
||||
didParsed, err := syntax.ParseDID(did)
|
||||
if err != nil {
|
||||
fmt.Printf("WARNING [appview/callback]: Failed to parse DID %s: %v\n", did, err)
|
||||
return nil // Non-fatal
|
||||
}
|
||||
|
||||
// Resume OAuth session to get authenticated client
|
||||
session, err := oauthApp.ResumeSession(ctx, didParsed, sessionID)
|
||||
if err != nil {
|
||||
fmt.Printf("WARNING [appview/callback]: Failed to resume session for DID=%s: %v\n", did, err)
|
||||
// Fallback: update user without avatar
|
||||
_ = db.UpsertUser(uiDatabase, &db.User{
|
||||
DID: did,
|
||||
Handle: handle,
|
||||
PDSEndpoint: pdsEndpoint,
|
||||
Avatar: "",
|
||||
LastSeen: time.Now(),
|
||||
})
|
||||
return nil // Non-fatal
|
||||
}
|
||||
|
||||
// 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 if configured)
|
||||
fmt.Printf("DEBUG [appview/callback]: Ensuring profile exists for %s (defaultHold=%s)\n", did, defaultHoldDID)
|
||||
if err := storage.EnsureProfile(ctx, client, defaultHoldDID); err != nil {
|
||||
fmt.Printf("WARNING [appview/callback]: Failed to ensure profile for %s: %v\n", did, err)
|
||||
// Continue anyway - profile creation is not critical for avatar fetch
|
||||
} else {
|
||||
fmt.Printf("DEBUG [appview/callback]: Profile ensured for %s\n", did)
|
||||
}
|
||||
|
||||
// Fetch user's profile record from PDS (contains blob references)
|
||||
profileRecord, err := client.GetProfileRecord(ctx, did)
|
||||
if err != nil {
|
||||
fmt.Printf("WARNING [appview/callback]: Failed to fetch profile record for DID=%s: %v\n", did, err)
|
||||
// Still update user without avatar
|
||||
_ = db.UpsertUser(uiDatabase, &db.User{
|
||||
DID: did,
|
||||
Handle: handle,
|
||||
PDSEndpoint: pdsEndpoint,
|
||||
Avatar: "",
|
||||
LastSeen: time.Now(),
|
||||
})
|
||||
return nil // Non-fatal
|
||||
}
|
||||
|
||||
// Construct avatar URL from blob CID using imgs.blue CDN
|
||||
var avatarURL string
|
||||
if profileRecord.Avatar != nil && profileRecord.Avatar.Ref.Link != "" {
|
||||
avatarURL = atproto.BlobCDNURL(did, profileRecord.Avatar.Ref.Link)
|
||||
fmt.Printf("DEBUG [appview/callback]: Constructed avatar URL: %s\n", avatarURL)
|
||||
}
|
||||
|
||||
// Store user with avatar in database
|
||||
err = db.UpsertUser(uiDatabase, &db.User{
|
||||
DID: did,
|
||||
Handle: handle,
|
||||
PDSEndpoint: pdsEndpoint,
|
||||
Avatar: avatarURL,
|
||||
LastSeen: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("WARNING [appview/callback]: Failed to store user in database: %v\n", err)
|
||||
return nil // Non-fatal
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG [appview/callback]: Stored user with avatar for DID=%s\n", did)
|
||||
|
||||
// Migrate profile URL→DID if needed
|
||||
profile, err := storage.GetProfile(ctx, client)
|
||||
if err != nil {
|
||||
fmt.Printf("WARNING [appview/callback]: Failed to get profile for %s: %v\n", did, err)
|
||||
return nil // Non-fatal
|
||||
}
|
||||
|
||||
var holdDID string
|
||||
if profile != nil && profile.DefaultHold != "" {
|
||||
// Check if defaultHold is a URL (needs migration)
|
||||
if strings.HasPrefix(profile.DefaultHold, "http://") || strings.HasPrefix(profile.DefaultHold, "https://") {
|
||||
fmt.Printf("DEBUG [appview/callback]: Migrating hold URL to DID for %s: %s\n", did, profile.DefaultHold)
|
||||
|
||||
// Resolve URL to DID
|
||||
holdDID := atproto.ResolveHoldDIDFromURL(profile.DefaultHold)
|
||||
|
||||
// Update profile with DID
|
||||
profile.DefaultHold = holdDID
|
||||
if err := storage.UpdateProfile(ctx, client, profile); err != nil {
|
||||
fmt.Printf("WARNING [appview/callback]: Failed to update profile with hold DID for %s: %v\n", did, err)
|
||||
} else {
|
||||
fmt.Printf("DEBUG [appview/callback]: Updated profile with hold DID: %s\n", holdDID)
|
||||
}
|
||||
fmt.Printf("DEBUG [oauth/server]: Attempting crew registration for %s at hold %s\n", did, holdDID)
|
||||
storage.EnsureCrewMembership(ctx, client, refresher, holdDID)
|
||||
} else {
|
||||
// Already a DID - use it
|
||||
holdDID = profile.DefaultHold
|
||||
}
|
||||
// Register crew regardless of migration (outside the migration block)
|
||||
fmt.Printf("DEBUG [appview/callback]: Attempting crew registration for %s at hold %s\n", did, holdDID)
|
||||
storage.EnsureCrewMembership(ctx, client, refresher, holdDID)
|
||||
|
||||
}
|
||||
|
||||
return nil // All errors are non-fatal, logged for debugging
|
||||
})
|
||||
|
||||
// Initialize auth keys and create token issuer
|
||||
var issuer *token.Issuer
|
||||
@@ -284,8 +393,27 @@ func serveRegistry(cmd *cobra.Command, args []string) error {
|
||||
// Mount auth endpoints if enabled
|
||||
if issuer != nil {
|
||||
// Basic Auth token endpoint (supports device secrets and app passwords)
|
||||
// Reuse defaultHoldDID extracted earlier
|
||||
tokenHandler := token.NewHandler(issuer, deviceStore, defaultHoldDID)
|
||||
tokenHandler := token.NewHandler(issuer, deviceStore)
|
||||
|
||||
// Register token post-auth callback for profile management
|
||||
// This decouples the token package from AppView-specific dependencies
|
||||
tokenHandler.SetPostAuthCallback(func(ctx context.Context, did, handle, pdsEndpoint, accessToken string) error {
|
||||
fmt.Printf("DEBUG [appview/callback]: Token post-auth callback for DID=%s\n", did)
|
||||
|
||||
// Create ATProto client with validated token
|
||||
atprotoClient := atproto.NewClient(pdsEndpoint, did, accessToken)
|
||||
|
||||
// Ensure profile exists (will create with default hold if not exists and default is configured)
|
||||
if err := storage.EnsureProfile(ctx, atprotoClient, defaultHoldDID); err != nil {
|
||||
// Log error but don't fail auth - profile management is not critical
|
||||
fmt.Printf("WARNING [appview/callback]: Failed to ensure profile for %s: %v\n", did, err)
|
||||
} else {
|
||||
fmt.Printf("DEBUG [appview/callback]: Profile ensured for %s with default hold %s\n", did, defaultHoldDID)
|
||||
}
|
||||
|
||||
return nil // All errors are non-fatal
|
||||
})
|
||||
|
||||
tokenHandler.RegisterRoutes(mux)
|
||||
|
||||
// Device authorization endpoints (public)
|
||||
@@ -401,7 +529,7 @@ func createTokenIssuer(config *configuration.Configuration) (*token.Issuer, erro
|
||||
// readOnlyDB: read-only connection for public queries (search, user pages, etc.)
|
||||
// defaultHoldDID: DID of the default hold service (e.g., "did:web:hold01.atcr.io")
|
||||
// healthChecker: hold endpoint health checker
|
||||
func initializeUIRoutes(database *sql.DB, readOnlyDB *sql.DB, sessionStore *db.SessionStore, oauthApp *oauth.App, refresher *oauth.Refresher, baseURL string, deviceStore *db.DeviceStore, defaultHoldDID string, healthChecker *holdhealth.Checker, readmeCache *readme.Cache) (*template.Template, *mux.Router) {
|
||||
func initializeUIRoutes(database *sql.DB, readOnlyDB *sql.DB, sessionStore *db.SessionStore, oauthApp *oauth.App, oauthStore *db.OAuthStore, refresher *oauth.Refresher, baseURL string, deviceStore *db.DeviceStore, defaultHoldDID string, healthChecker *holdhealth.Checker, readmeCache *readme.Cache) (*template.Template, *mux.Router) {
|
||||
// Check if UI is enabled
|
||||
uiEnabled := os.Getenv("ATCR_UI_ENABLED")
|
||||
if uiEnabled == "false" {
|
||||
@@ -582,12 +710,12 @@ func initializeUIRoutes(database *sql.DB, readOnlyDB *sql.DB, sessionStore *db.S
|
||||
}).Methods("DELETE")
|
||||
|
||||
// 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)
|
||||
// Properly revokes OAuth tokens on PDS side before clearing local session
|
||||
router.Handle("/auth/logout", &uihandlers.LogoutHandler{
|
||||
OAuthApp: oauthApp,
|
||||
Refresher: refresher,
|
||||
SessionStore: sessionStore,
|
||||
OAuthStore: oauthStore,
|
||||
}).Methods("GET", "POST")
|
||||
|
||||
// Start Jetstream worker
|
||||
|
||||
Reference in New Issue
Block a user