mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-10 04:06:06 +00:00
fix app-passwords remove dead code
This commit is contained in:
@@ -1,122 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"atcr.io/pkg/atproto"
|
||||
atprotoAuth "atcr.io/pkg/auth/atproto"
|
||||
)
|
||||
|
||||
// DockerConfig represents ~/.docker/config.json
|
||||
type DockerConfig struct {
|
||||
Auths map[string]AuthEntry `json:"auths"`
|
||||
}
|
||||
|
||||
type AuthEntry struct {
|
||||
Auth string `json:"auth"` // base64(username:password)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var defaultHold string
|
||||
var registryURL string
|
||||
|
||||
flag.StringVar(&defaultHold, "default-hold", "", "Default hold endpoint URL (e.g., http://172.28.0.3:8080)")
|
||||
flag.StringVar(®istryURL, "registry", "127.0.0.1:5000", "Registry URL to read auth from Docker config")
|
||||
flag.Parse()
|
||||
|
||||
// Read Docker config
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get home directory: %v", err)
|
||||
}
|
||||
dockerConfigPath := filepath.Join(home, ".docker", "config.json")
|
||||
|
||||
configData, err := os.ReadFile(dockerConfigPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read Docker config: %v\n\nMake sure you've logged in with: docker login %s", err, registryURL)
|
||||
}
|
||||
|
||||
var dockerConfig DockerConfig
|
||||
if err := json.Unmarshal(configData, &dockerConfig); err != nil {
|
||||
log.Fatalf("Failed to parse Docker config: %v", err)
|
||||
}
|
||||
|
||||
// Get auth for registry
|
||||
authEntry, ok := dockerConfig.Auths[registryURL]
|
||||
if !ok {
|
||||
log.Fatalf("No auth found for registry %s in Docker config", registryURL)
|
||||
}
|
||||
|
||||
// Decode base64 auth (format: "username:password")
|
||||
authBytes, err := base64.StdEncoding.DecodeString(authEntry.Auth)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to decode auth: %v", err)
|
||||
}
|
||||
|
||||
parts := strings.SplitN(string(authBytes), ":", 2)
|
||||
if len(parts) != 2 {
|
||||
log.Fatalf("Invalid auth format")
|
||||
}
|
||||
|
||||
handle := parts[0]
|
||||
password := parts[1] // This should be an app password
|
||||
|
||||
fmt.Printf("Handle: %s\n", handle)
|
||||
|
||||
// Create session validator and get access token
|
||||
validator := atprotoAuth.NewSessionValidator()
|
||||
ctx := context.Background()
|
||||
|
||||
did, pdsEndpoint, accessToken, err := validator.CreateSessionAndGetToken(ctx, handle, password)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to authenticate: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("DID: %s\n", did)
|
||||
fmt.Printf("PDS: %s\n\n", pdsEndpoint)
|
||||
|
||||
// Create client with the access token from createSession
|
||||
client := atproto.NewClient(pdsEndpoint, did, accessToken)
|
||||
|
||||
// Get current profile
|
||||
profile, err := atproto.GetProfile(ctx, client)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get current profile: %v", err)
|
||||
}
|
||||
|
||||
if profile == nil {
|
||||
if defaultHold == "" {
|
||||
fmt.Println("No existing profile found.")
|
||||
fmt.Println("\nTo create profile with default hold, use: -default-hold <url>")
|
||||
return
|
||||
}
|
||||
fmt.Println("No existing profile found. Creating new profile...")
|
||||
profile = atproto.NewSailorProfileRecord(defaultHold)
|
||||
} else {
|
||||
fmt.Printf("Current defaultHold: %s\n", profile.DefaultHold)
|
||||
if defaultHold == "" {
|
||||
// Just show current profile
|
||||
fmt.Println("\nTo update, use: -default-hold <url>")
|
||||
return
|
||||
}
|
||||
profile.DefaultHold = defaultHold
|
||||
}
|
||||
|
||||
// Update profile
|
||||
if defaultHold != "" {
|
||||
err = atproto.UpdateProfile(ctx, client, profile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to update profile: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n✓ Updated defaultHold to: %s\n", defaultHold)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
// CachedSession represents a cached session
|
||||
type CachedSession struct {
|
||||
DID string
|
||||
Handle string
|
||||
PDS string
|
||||
AccessToken string
|
||||
ExpiresAt time.Time
|
||||
@@ -83,49 +84,13 @@ type SessionResponse struct {
|
||||
AccessToken string `json:"access_token,omitempty"` // Alternative field name
|
||||
}
|
||||
|
||||
// ValidateCredentials validates username and password against ATProto
|
||||
// Returns the user's DID and PDS endpoint if valid
|
||||
func (v *SessionValidator) ValidateCredentials(ctx context.Context, identifier, password string) (did, pdsEndpoint string, err error) {
|
||||
// Resolve identifier (handle or DID) to PDS endpoint
|
||||
atID, err := syntax.ParseAtIdentifier(identifier)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("invalid identifier %q: %w", identifier, err)
|
||||
}
|
||||
|
||||
ident, err := v.directory.Lookup(ctx, *atID)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to resolve identity %q: %w", identifier, err)
|
||||
}
|
||||
|
||||
resolvedDID := ident.DID.String()
|
||||
pds := ident.PDSEndpoint()
|
||||
if pds == "" {
|
||||
return "", "", fmt.Errorf("no PDS endpoint found for %q", identifier)
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG: Resolved %s to DID=%s, PDS=%s\n", identifier, resolvedDID, pds)
|
||||
|
||||
// Create session with the PDS
|
||||
fmt.Printf("DEBUG [atproto/session]: Creating session for %s at PDS %s\n", identifier, pds)
|
||||
sessionResp, err := v.createSession(ctx, pds, identifier, password)
|
||||
if err != nil {
|
||||
fmt.Printf("DEBUG [atproto/session]: Session creation failed: %v\n", err)
|
||||
return "", "", fmt.Errorf("authentication failed for %s at PDS %s: %w", identifier, pds, err)
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG [atproto/session]: Session created successfully, DID=%s, Handle=%s, AccessJWT length=%d\n",
|
||||
sessionResp.DID, sessionResp.Handle, len(sessionResp.AccessJWT))
|
||||
|
||||
return sessionResp.DID, pds, nil
|
||||
}
|
||||
|
||||
// CreateSessionAndGetToken creates a session and returns the DID, PDS endpoint, and access token
|
||||
func (v *SessionValidator) CreateSessionAndGetToken(ctx context.Context, identifier, password string) (did, pdsEndpoint, accessToken string, err error) {
|
||||
// CreateSessionAndGetToken creates a session and returns the DID, handle, and access token
|
||||
func (v *SessionValidator) CreateSessionAndGetToken(ctx context.Context, identifier, password string) (did, handle, accessToken string, err error) {
|
||||
// Check cache first
|
||||
cacheKey := getCacheKey(identifier, password)
|
||||
if cached, ok := v.getCachedSession(cacheKey); ok {
|
||||
fmt.Printf("DEBUG [atproto/session]: Using cached session for %s (DID=%s)\n", identifier, cached.DID)
|
||||
return cached.DID, cached.PDS, cached.AccessToken, nil
|
||||
return cached.DID, cached.Handle, cached.AccessToken, nil
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG [atproto/session]: No cached session for %s, creating new session\n", identifier)
|
||||
@@ -156,13 +121,14 @@ func (v *SessionValidator) CreateSessionAndGetToken(ctx context.Context, identif
|
||||
// Cache the session (ATProto sessions typically last 2 hours)
|
||||
v.setCachedSession(cacheKey, &CachedSession{
|
||||
DID: sessionResp.DID,
|
||||
Handle: sessionResp.Handle,
|
||||
PDS: pds,
|
||||
AccessToken: sessionResp.AccessJWT,
|
||||
ExpiresAt: time.Now().Add(2 * time.Hour),
|
||||
})
|
||||
fmt.Printf("DEBUG [atproto/session]: Cached session for %s (expires in 2 hours)\n", identifier)
|
||||
|
||||
return sessionResp.DID, pds, sessionResp.AccessJWT, nil
|
||||
return sessionResp.DID, sessionResp.Handle, sessionResp.AccessJWT, nil
|
||||
}
|
||||
|
||||
// createSession calls com.atproto.server.createSession
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
package atproto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/bluesky-social/indigo/atproto/identity"
|
||||
"github.com/bluesky-social/indigo/atproto/syntax"
|
||||
)
|
||||
|
||||
// TokenValidator validates ATProto OAuth access tokens
|
||||
type TokenValidator struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewTokenValidator creates a new token validator
|
||||
func NewTokenValidator() *TokenValidator {
|
||||
return &TokenValidator{
|
||||
httpClient: &http.Client{},
|
||||
}
|
||||
}
|
||||
|
||||
// SessionInfo represents the response from com.atproto.server.getSession
|
||||
type SessionInfo struct {
|
||||
DID string `json:"did"`
|
||||
Handle string `json:"handle"`
|
||||
Email string `json:"email,omitempty"`
|
||||
EmailConfirmed bool `json:"emailConfirmed,omitempty"`
|
||||
Active bool `json:"active,omitempty"`
|
||||
}
|
||||
|
||||
// ValidateToken validates an ATProto OAuth access token by calling getSession
|
||||
// Returns the user's DID and handle if the token is valid
|
||||
// dpopProof is optional - if provided, uses DPoP auth; otherwise uses Bearer
|
||||
func (v *TokenValidator) ValidateToken(ctx context.Context, pdsEndpoint, accessToken, dpopProof string) (*SessionInfo, error) {
|
||||
// Call com.atproto.server.getSession with the access token
|
||||
url := fmt.Sprintf("%s/xrpc/com.atproto.server.getSession", pdsEndpoint)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Always use Bearer auth for getSession validation
|
||||
// The DPoP proof from the client is bound to their request to us (POST /auth/exchange),
|
||||
// not to our request to the PDS (GET /getSession)
|
||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||
|
||||
fmt.Printf("DEBUG [validator]: calling %s with Bearer auth, token_prefix=%s...\n",
|
||||
url, accessToken[:min(20, len(accessToken))])
|
||||
|
||||
resp, err := v.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get session: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read body once for both logging and error handling
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
fmt.Printf("DEBUG [validator]: getSession returned 401: %s\n", string(bodyBytes))
|
||||
return nil, fmt.Errorf("invalid or expired token")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Printf("DEBUG [validator]: getSession failed with status %d: %s\n", resp.StatusCode, string(bodyBytes))
|
||||
return nil, fmt.Errorf("getSession failed with status %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var session SessionInfo
|
||||
if err := json.Unmarshal(bodyBytes, &session); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode session: %w", err)
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if session.DID == "" {
|
||||
return nil, fmt.Errorf("session response missing DID")
|
||||
}
|
||||
if session.Handle == "" {
|
||||
return nil, fmt.Errorf("session response missing handle")
|
||||
}
|
||||
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
// ValidateTokenWithResolver validates a token and automatically resolves the PDS endpoint
|
||||
// dpopProof is optional - if provided, uses DPoP auth; otherwise uses Bearer
|
||||
func (v *TokenValidator) ValidateTokenWithResolver(ctx context.Context, handle, accessToken, dpopProof string) (*SessionInfo, error) {
|
||||
// Resolve handle to PDS endpoint
|
||||
directory := identity.DefaultDirectory()
|
||||
atID, err := syntax.ParseAtIdentifier(handle)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid identifier %q: %w", handle, err)
|
||||
}
|
||||
|
||||
ident, err := directory.Lookup(ctx, *atID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve PDS endpoint: %w", err)
|
||||
}
|
||||
|
||||
pdsEndpoint := ident.PDSEndpoint()
|
||||
if pdsEndpoint == "" {
|
||||
return nil, fmt.Errorf("no PDS endpoint found for %q", handle)
|
||||
}
|
||||
|
||||
// Validate token against the PDS
|
||||
return v.ValidateToken(ctx, pdsEndpoint, accessToken, dpopProof)
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bluesky-social/indigo/atproto/auth/oauth"
|
||||
@@ -18,91 +16,6 @@ type InteractiveResult struct {
|
||||
App *App
|
||||
}
|
||||
|
||||
// RunInteractiveFlow runs an interactive OAuth flow for CLI tools
|
||||
// This is a simplified wrapper around indigo's OAuth flow
|
||||
func RunInteractiveFlow(
|
||||
ctx context.Context,
|
||||
baseURL string,
|
||||
handle string,
|
||||
scopes []string,
|
||||
onAuthURL func(string) error,
|
||||
) (*InteractiveResult, error) {
|
||||
// Create temporary file store for this flow
|
||||
store, err := NewFileStore("/tmp/atcr-oauth-temp.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create OAuth store: %w", err)
|
||||
}
|
||||
|
||||
// Create OAuth app
|
||||
app, err := NewApp(baseURL, store)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create OAuth app: %w", err)
|
||||
}
|
||||
|
||||
// Set custom scopes if provided
|
||||
if len(scopes) > 0 {
|
||||
// Note: indigo's ClientApp doesn't expose SetScopes, so we need to use default scopes
|
||||
// This is a limitation of the current implementation
|
||||
// TODO: Enhance if custom scopes are needed
|
||||
}
|
||||
|
||||
// Start auth flow
|
||||
authURL, err := app.StartAuthFlow(ctx, handle)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start auth flow: %w", err)
|
||||
}
|
||||
|
||||
// Call the callback to display the auth URL
|
||||
if err := onAuthURL(authURL); err != nil {
|
||||
return nil, fmt.Errorf("auth URL callback failed: %w", err)
|
||||
}
|
||||
|
||||
// Wait for OAuth callback
|
||||
// The callback will be handled by the http.HandleFunc registered by the caller
|
||||
// We need to wait for ProcessCallback to be called
|
||||
// This is a bit awkward, but matches the old pattern
|
||||
|
||||
// Setup a channel to receive callback params
|
||||
callbackChan := make(chan url.Values, 1)
|
||||
var setupOnce sync.Once
|
||||
|
||||
// Return a function that the caller can use to process the callback
|
||||
// This is called from the HTTP handler
|
||||
processCallback := func(params url.Values) (*oauth.ClientSessionData, error) {
|
||||
setupOnce.Do(func() {
|
||||
callbackChan <- params
|
||||
})
|
||||
sessionData, err := app.ProcessCallback(ctx, params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to process callback: %w", err)
|
||||
}
|
||||
return sessionData, nil
|
||||
}
|
||||
|
||||
// Wait for callback with timeout
|
||||
select {
|
||||
case params := <-callbackChan:
|
||||
sessionData, err := processCallback(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Resume session to get ClientSession
|
||||
session, err := app.ResumeSession(ctx, sessionData.AccountDID, sessionData.SessionID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resume session: %w", err)
|
||||
}
|
||||
|
||||
return &InteractiveResult{
|
||||
SessionData: sessionData,
|
||||
Session: session,
|
||||
App: app,
|
||||
}, nil
|
||||
case <-time.After(5 * time.Minute):
|
||||
return nil, fmt.Errorf("OAuth flow timed out after 5 minutes")
|
||||
}
|
||||
}
|
||||
|
||||
// InteractiveFlowWithCallback runs an interactive OAuth flow with explicit callback handling
|
||||
// This version allows the caller to register the callback handler before starting the flow
|
||||
func InteractiveFlowWithCallback(
|
||||
|
||||
@@ -3,7 +3,6 @@ package oauth
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/bluesky-social/indigo/atproto/auth/oauth"
|
||||
@@ -74,33 +73,6 @@ func (r *Refresher) GetSession(ctx context.Context, did string) (*oauth.ClientSe
|
||||
return r.resumeSession(ctx, did)
|
||||
}
|
||||
|
||||
// GetAccessToken gets a fresh access token for a DID
|
||||
// This is a convenience method that extracts the access token from the session
|
||||
func (r *Refresher) GetAccessToken(ctx context.Context, did string) (string, error) {
|
||||
session, err := r.GetSession(ctx, did)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get access token and DPoP nonce from session
|
||||
accessToken, _ := session.GetHostAccessData()
|
||||
return accessToken, nil
|
||||
}
|
||||
|
||||
// GetHTTPClient returns an HTTP client with DPoP authentication for a DID
|
||||
// The client automatically adds DPoP headers and refreshes tokens as needed
|
||||
func (r *Refresher) GetHTTPClient(ctx context.Context, did string) (*http.Client, error) {
|
||||
session, err := r.GetSession(ctx, did)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get API client from session
|
||||
// This client automatically handles DPoP and token refresh
|
||||
apiClient := session.APIClient()
|
||||
return apiClient.Client, nil
|
||||
}
|
||||
|
||||
// resumeSession loads a session from storage and caches it
|
||||
func (r *Refresher) resumeSession(ctx context.Context, did string) (*oauth.ClientSession, error) {
|
||||
// Parse DID
|
||||
@@ -153,72 +125,3 @@ func (r *Refresher) InvalidateSession(did string) {
|
||||
delete(r.sessions, did)
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
// RevokeSession removes a session from both cache and storage
|
||||
func (r *Refresher) RevokeSession(ctx context.Context, did string) error {
|
||||
// Remove from cache
|
||||
r.mu.Lock()
|
||||
cached, ok := r.sessions[did]
|
||||
delete(r.sessions, did)
|
||||
r.mu.Unlock()
|
||||
|
||||
if !ok {
|
||||
// Not cached, still try to delete from storage
|
||||
accountDID, err := syntax.ParseDID(did)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse DID: %w", err)
|
||||
}
|
||||
|
||||
// Find session ID from store
|
||||
fileStore, ok := r.app.clientApp.Store.(*FileStore)
|
||||
if !ok {
|
||||
return fmt.Errorf("store is not a FileStore")
|
||||
}
|
||||
|
||||
sessions := fileStore.ListSessions()
|
||||
for _, sessionData := range sessions {
|
||||
if sessionData.AccountDID.String() == did {
|
||||
return r.app.clientApp.Store.DeleteSession(ctx, accountDID, sessionData.SessionID)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("no session found for DID: %s", did)
|
||||
}
|
||||
|
||||
// Revoke the session via OAuth
|
||||
if err := cached.Session.RevokeSession(ctx); err != nil {
|
||||
fmt.Printf("WARNING: failed to revoke session for %s: %v\n", did, err)
|
||||
// Continue anyway to delete from storage
|
||||
}
|
||||
|
||||
// Delete from storage
|
||||
accountDID, err := syntax.ParseDID(did)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse DID: %w", err)
|
||||
}
|
||||
|
||||
return r.app.clientApp.Store.DeleteSession(ctx, accountDID, cached.SessionID)
|
||||
}
|
||||
|
||||
// CleanupExpiredSessions removes expired sessions from cache
|
||||
// Note: indigo handles token expiry automatically, but we clean up orphaned cache entries
|
||||
func (r *Refresher) CleanupExpiredSessions(ctx context.Context) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// For each cached session, verify it still exists in storage
|
||||
for did, cached := range r.sessions {
|
||||
accountDID, err := syntax.ParseDID(did)
|
||||
if err != nil {
|
||||
delete(r.sessions, did)
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to get session from store
|
||||
_, err = r.app.clientApp.Store.GetSession(ctx, accountDID, cached.SessionID)
|
||||
if err != nil {
|
||||
// Session no longer exists, remove from cache
|
||||
delete(r.sessions, did)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/bluesky-social/indigo/atproto/syntax"
|
||||
)
|
||||
|
||||
// UISessionStore is the interface for UI session management
|
||||
@@ -102,9 +99,9 @@ func (s *Server) ServeCallback(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Printf("DEBUG [oauth/server]: Invalidated cached session for DID=%s after creating new session\n", did)
|
||||
}
|
||||
|
||||
// We need to get the handle for UI sessions and settings redirect
|
||||
// Resolve DID to handle using our resolver
|
||||
handle, err := s.resolveHandle(r.Context(), did)
|
||||
// Look up identity
|
||||
ident, err := s.app.directory.LookupDID(r.Context(), sessionData.AccountDID)
|
||||
handle := ident.Handle.String()
|
||||
if err != nil {
|
||||
fmt.Printf("WARNING [oauth/server]: Failed to resolve DID to handle: %v, using DID as handle\n", err)
|
||||
handle = did // Fallback to DID if resolution fails
|
||||
@@ -152,25 +149,6 @@ func (s *Server) ServeCallback(w http.ResponseWriter, r *http.Request) {
|
||||
s.renderRedirectToSettings(w, handle)
|
||||
}
|
||||
|
||||
// resolveHandle attempts to resolve a DID to a handle
|
||||
// This is a best-effort helper - we use the directory to look up the handle
|
||||
func (s *Server) resolveHandle(ctx context.Context, didStr string) (string, error) {
|
||||
// Parse DID
|
||||
did, err := syntax.ParseDID(didStr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid DID: %w", err)
|
||||
}
|
||||
|
||||
// Look up identity
|
||||
ident, err := s.app.directory.LookupDID(ctx, did)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to lookup DID: %w", err)
|
||||
}
|
||||
|
||||
// Return handle (may be handle.invalid if verification failed)
|
||||
return ident.Handle.String(), nil
|
||||
}
|
||||
|
||||
// renderRedirectToSettings redirects to the settings page to generate an API key
|
||||
func (s *Server) renderRedirectToSettings(w http.ResponseWriter, handle string) {
|
||||
tmpl := template.Must(template.New("redirect").Parse(redirectToSettingsTemplate))
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/bluesky-social/indigo/atproto/identity"
|
||||
)
|
||||
|
||||
// ATProtoHandler wraps an HTTP handler to provide name resolution
|
||||
// This is an optional layer if middleware doesn't provide enough control
|
||||
type ATProtoHandler struct {
|
||||
handler http.Handler
|
||||
directory identity.Directory
|
||||
}
|
||||
|
||||
// NewATProtoHandler creates a new HTTP handler wrapper
|
||||
func NewATProtoHandler(handler http.Handler) *ATProtoHandler {
|
||||
return &ATProtoHandler{
|
||||
handler: handler,
|
||||
directory: identity.DefaultDirectory(),
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP handles HTTP requests with name resolution
|
||||
func (h *ATProtoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse the request path to extract user/image
|
||||
// OCI Distribution API paths look like:
|
||||
// /v2/<name>/manifests/<reference>
|
||||
// /v2/<name>/blobs/<digest>
|
||||
|
||||
path := r.URL.Path
|
||||
|
||||
// Check if this is a v2 API request
|
||||
if strings.HasPrefix(path, "/v2/") {
|
||||
// Extract the repository name
|
||||
parts := strings.Split(strings.TrimPrefix(path, "/v2/"), "/")
|
||||
if len(parts) >= 2 {
|
||||
// parts[0] might be username/DID
|
||||
// We could do early resolution here if needed
|
||||
// For now, we'll let the middleware handle it
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate to the underlying handler
|
||||
// The registry middleware will handle the actual resolution
|
||||
h.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Note: In the current architecture, most of the name resolution
|
||||
// is handled by the registry middleware. This HTTP handler wrapper
|
||||
// is here for cases where you need to intercept requests before
|
||||
// they reach the distribution handlers, such as for:
|
||||
// - Custom authentication based on DIDs
|
||||
// - Request rewriting
|
||||
// - Early validation
|
||||
// - Custom API endpoints beyond OCI spec
|
||||
Reference in New Issue
Block a user