mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-04-20 16:40:29 +00:00
109 lines
3.5 KiB
Go
109 lines
3.5 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"atcr.io/pkg/atproto"
|
|
"atcr.io/pkg/auth"
|
|
"atcr.io/pkg/auth/oauth"
|
|
)
|
|
|
|
// EnsureCrewMembership attempts to register the user as a crew member on their default hold.
|
|
// The hold's requestCrew endpoint handles all authorization logic (checking allowAllCrew, existing membership, etc).
|
|
// On success, clears any cached denial to ensure immediate access.
|
|
// This is best-effort and does not fail on errors.
|
|
func EnsureCrewMembership(ctx context.Context, client *atproto.Client, refresher *oauth.Refresher, defaultHoldDID string, authorizer auth.HoldAuthorizer) {
|
|
if defaultHoldDID == "" {
|
|
return
|
|
}
|
|
|
|
// Normalize URL to DID if needed
|
|
holdDID, err := atproto.ResolveHoldDID(ctx, defaultHoldDID)
|
|
if err != nil {
|
|
slog.Warn("failed to resolve hold DID", "defaultHold", defaultHoldDID, "error", err)
|
|
return
|
|
}
|
|
|
|
// Resolve hold DID to HTTP endpoint
|
|
holdEndpoint, err := atproto.ResolveHoldURL(ctx, holdDID)
|
|
if err != nil {
|
|
slog.Warn("failed to resolve hold URL", "holdDID", holdDID, "error", err)
|
|
return
|
|
}
|
|
|
|
// Get service token for the hold
|
|
// Only works with OAuth (refresher required) - app passwords can't get service tokens
|
|
if refresher == nil {
|
|
slog.Debug("skipping crew registration - no OAuth refresher (app password flow)", "holdDID", holdDID)
|
|
return
|
|
}
|
|
|
|
// Wrap the refresher to match OAuthSessionRefresher interface
|
|
serviceToken, err := auth.GetOrFetchServiceToken(ctx, refresher, client.DID(), holdDID, client.PDSEndpoint())
|
|
if err != nil {
|
|
slog.Warn("failed to get service token", "holdDID", holdDID, "error", err)
|
|
return
|
|
}
|
|
|
|
// Call requestCrew endpoint - it handles all the logic:
|
|
// - Checks allowAllCrew flag
|
|
// - Checks if already a crew member (returns success if so)
|
|
// - Creates crew record if authorized
|
|
if err := requestCrewMembership(ctx, holdEndpoint, serviceToken); err != nil {
|
|
slog.Warn("failed to request crew membership", "holdDID", holdDID, "error", err)
|
|
return
|
|
}
|
|
|
|
slog.Info("successfully registered as crew member", "holdDID", holdDID, "userDID", client.DID())
|
|
|
|
// Clear any cached denial to ensure immediate access
|
|
if authorizer != nil {
|
|
if err := authorizer.ClearCrewDenial(ctx, holdDID, client.DID()); err != nil {
|
|
slog.Warn("failed to clear denial cache after crew registration",
|
|
"holdDID", holdDID,
|
|
"userDID", client.DID(),
|
|
"error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// requestCrewMembership calls the hold's requestCrew endpoint
|
|
// The endpoint handles all authorization and duplicate checking internally
|
|
func requestCrewMembership(ctx context.Context, holdEndpoint, serviceToken string) error {
|
|
// Add 5 second timeout to prevent hanging on offline holds
|
|
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
defer cancel()
|
|
|
|
url := fmt.Sprintf("%s%s", holdEndpoint, atproto.HoldRequestCrew)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+serviceToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
|
// Read response body to capture actual error message from hold
|
|
body, readErr := io.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return fmt.Errorf("requestCrew failed with status %d (failed to read error body: %w)", resp.StatusCode, readErr)
|
|
}
|
|
return fmt.Errorf("requestCrew failed with status %d: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return nil
|
|
}
|