mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-25 03:34:14 +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:
+20
-145
@@ -2,17 +2,11 @@ package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"atcr.io/pkg/appview/db"
|
||||
"atcr.io/pkg/atproto"
|
||||
indigooauth "github.com/bluesky-social/indigo/atproto/auth/oauth"
|
||||
"github.com/bluesky-social/indigo/atproto/syntax"
|
||||
)
|
||||
|
||||
// UISessionStore is the interface for UI session management
|
||||
@@ -23,13 +17,18 @@ type UserStore interface {
|
||||
UpsertUser(did, handle, pdsEndpoint, avatar string) error
|
||||
}
|
||||
|
||||
// PostAuthCallback is called after successful OAuth authentication.
|
||||
// Parameters: ctx, did, handle, pdsEndpoint, sessionID
|
||||
// This allows AppView to perform business logic (profile creation, avatar fetch, etc.)
|
||||
// without coupling the OAuth package to AppView-specific dependencies.
|
||||
type PostAuthCallback func(ctx context.Context, did, handle, pdsEndpoint, sessionID string) error
|
||||
|
||||
// Server handles OAuth authorization for the AppView
|
||||
type Server struct {
|
||||
app *App
|
||||
refresher *Refresher
|
||||
uiSessionStore UISessionStore
|
||||
db *sql.DB
|
||||
defaultHoldDID string // Default hold DID (e.g., "did:web:hold01.atcr.io")
|
||||
app *App
|
||||
refresher *Refresher
|
||||
uiSessionStore UISessionStore
|
||||
postAuthCallback PostAuthCallback
|
||||
}
|
||||
|
||||
// NewServer creates a new OAuth server
|
||||
@@ -39,13 +38,6 @@ func NewServer(app *App) *Server {
|
||||
}
|
||||
}
|
||||
|
||||
// SetDefaultHoldDID sets the default hold DID for profile creation
|
||||
// Expected format: "did:web:hold01.atcr.io"
|
||||
// To find a hold's DID, visit: https://hold-url/.well-known/did.json
|
||||
func (s *Server) SetDefaultHoldDID(did string) {
|
||||
s.defaultHoldDID = did
|
||||
}
|
||||
|
||||
// SetRefresher sets the refresher for invalidating session cache
|
||||
func (s *Server) SetRefresher(refresher *Refresher) {
|
||||
s.refresher = refresher
|
||||
@@ -56,9 +48,10 @@ func (s *Server) SetUISessionStore(store UISessionStore) {
|
||||
s.uiSessionStore = store
|
||||
}
|
||||
|
||||
// SetDatabase sets the database for user management
|
||||
func (s *Server) SetDatabase(db *sql.DB) {
|
||||
s.db = db
|
||||
// SetPostAuthCallback sets the callback to be invoked after successful OAuth authentication
|
||||
// This allows AppView to inject business logic without coupling the OAuth package
|
||||
func (s *Server) SetPostAuthCallback(callback PostAuthCallback) {
|
||||
s.postAuthCallback = callback
|
||||
}
|
||||
|
||||
// ServeAuthorize handles GET /auth/oauth/authorize
|
||||
@@ -140,9 +133,12 @@ func (s *Server) ServeCallback(w http.ResponseWriter, r *http.Request) {
|
||||
handle = did // Fallback to DID if resolution fails
|
||||
}
|
||||
|
||||
// Fetch user's Bluesky profile (including avatar) and store in database
|
||||
if s.db != nil {
|
||||
s.fetchAndStoreAvatar(r.Context(), did, sessionID, handle, sessionData.HostURL)
|
||||
// Call post-auth callback for AppView business logic (profile, avatar, etc.)
|
||||
if s.postAuthCallback != nil {
|
||||
if err := s.postAuthCallback(r.Context(), did, handle, sessionData.HostURL, sessionID); err != nil {
|
||||
// Log error but don't fail OAuth flow - business logic is non-critical
|
||||
fmt.Printf("WARNING [oauth/server]: Post-auth callback failed for DID=%s: %v\n", did, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a UI login (has oauth_return_to cookie)
|
||||
@@ -241,127 +237,6 @@ func (s *Server) renderError(w http.ResponseWriter, message string) {
|
||||
}
|
||||
}
|
||||
|
||||
// fetchAndStoreAvatar fetches the user's Bluesky profile and stores avatar in database
|
||||
func (s *Server) fetchAndStoreAvatar(ctx context.Context, did, sessionID, handle, pdsEndpoint string) {
|
||||
fmt.Printf("DEBUG [oauth/server]: Fetching avatar for DID=%s from PDS=%s\n", did, pdsEndpoint)
|
||||
|
||||
// Parse DID for session resume
|
||||
didParsed, err := syntax.ParseDID(did)
|
||||
if err != nil {
|
||||
fmt.Printf("WARNING [oauth/server]: Failed to parse DID %s: %v\n", did, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Resume OAuth session to get authenticated client
|
||||
session, err := s.app.ResumeSession(ctx, didParsed, sessionID)
|
||||
if err != nil {
|
||||
fmt.Printf("WARNING [oauth/server]: Failed to resume session for DID=%s: %v\n", did, err)
|
||||
// Fallback: update user without avatar
|
||||
_ = db.UpsertUser(s.db, &db.User{
|
||||
DID: did,
|
||||
Handle: handle,
|
||||
PDSEndpoint: pdsEndpoint,
|
||||
Avatar: "",
|
||||
LastSeen: time.Now(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 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, or empty profile if not)
|
||||
fmt.Printf("DEBUG [oauth/server]: Ensuring profile exists for %s (defaultHold=%s)\n", did, s.defaultHoldDID)
|
||||
if err := atproto.EnsureProfile(ctx, client, s.defaultHoldDID); err != nil {
|
||||
fmt.Printf("WARNING [oauth/server]: Failed to ensure profile for %s: %v\n", did, err)
|
||||
// Continue anyway - profile creation is not critical for avatar fetch
|
||||
} else {
|
||||
fmt.Printf("DEBUG [oauth/server]: 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 [oauth/server]: Failed to fetch profile record for DID=%s: %v\n", did, err)
|
||||
// Still update user without avatar
|
||||
_ = db.UpsertUser(s.db, &db.User{
|
||||
DID: did,
|
||||
Handle: handle,
|
||||
PDSEndpoint: pdsEndpoint,
|
||||
Avatar: "",
|
||||
LastSeen: time.Now(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 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 [oauth/server]: Constructed avatar URL: %s\n", avatarURL)
|
||||
}
|
||||
|
||||
// Store user with avatar in database
|
||||
err = db.UpsertUser(s.db, &db.User{
|
||||
DID: did,
|
||||
Handle: handle,
|
||||
PDSEndpoint: pdsEndpoint,
|
||||
Avatar: avatarURL,
|
||||
LastSeen: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("WARNING [oauth/server]: Failed to store user in database: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG [oauth/server]: Stored user with avatar for DID=%s\n", did)
|
||||
|
||||
// Handle profile migration and crew registration
|
||||
s.migrateProfileAndRegisterCrew(ctx, client, did, session)
|
||||
}
|
||||
|
||||
// migrateProfileAndRegisterCrew handles URL→DID migration and crew registration
|
||||
func (s *Server) migrateProfileAndRegisterCrew(ctx context.Context, client *atproto.Client, did string, session *indigooauth.ClientSession) {
|
||||
// Get user's sailor profile
|
||||
profile, err := atproto.GetProfile(ctx, client)
|
||||
if err != nil {
|
||||
fmt.Printf("WARNING [oauth/server]: Failed to get profile for %s: %v\n", did, err)
|
||||
return
|
||||
}
|
||||
|
||||
if profile == nil || profile.DefaultHold == "" {
|
||||
// No profile or no default hold configured
|
||||
return
|
||||
}
|
||||
|
||||
// Check if defaultHold is a URL (needs migration)
|
||||
var holdDID string
|
||||
if strings.HasPrefix(profile.DefaultHold, "http://") || strings.HasPrefix(profile.DefaultHold, "https://") {
|
||||
fmt.Printf("DEBUG [oauth/server]: 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 := atproto.UpdateProfile(ctx, client, profile); err != nil {
|
||||
fmt.Printf("WARNING [oauth/server]: Failed to update profile with hold DID for %s: %v\n", did, err)
|
||||
// Continue anyway - crew registration might still work
|
||||
} else {
|
||||
fmt.Printf("DEBUG [oauth/server]: Updated profile with hold DID: %s\n", holdDID)
|
||||
}
|
||||
} else {
|
||||
// Already a DID
|
||||
holdDID = profile.DefaultHold
|
||||
}
|
||||
|
||||
// TODO: Request crew membership at the hold
|
||||
// This requires understanding how to make authenticated HTTP requests with indigo's ClientSession
|
||||
// For now, crew registration will happen on first push when appview validates access
|
||||
fmt.Printf("DEBUG [oauth/server]: Skipping crew registration for now - will happen on first push. Hold DID: %s\n", holdDID)
|
||||
_ = session // TODO: use session for crew registration
|
||||
}
|
||||
|
||||
// HTML templates
|
||||
|
||||
const redirectToSettingsTemplate = `
|
||||
|
||||
Reference in New Issue
Block a user