diff --git a/.vscode/launch.json b/.vscode/launch.json index 893fb84..cfe0a01 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,7 +19,7 @@ "mode": "auto", "program": "${workspaceFolder}/cmd/cli/main.go", "args": [ - "auth" + "auth", "--handle", "evan.jarrett.net" ], } ] diff --git a/.vscode/settings.json b/.vscode/settings.json index ba7ed3c..5da9a74 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { - "go.coverOnSave": true, "go.coverageDecorator": { "type": "gutter", "coveredHighlightColor": "rgba(64,128,128,0.5)", diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 7a1ccd5..8d1a9d2 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "net/http" - "net/url" "os" "path/filepath" "strings" @@ -16,34 +15,33 @@ import ( "github.com/bluesky-social/indigo/xrpc" oauth "github.com/haileyok/atproto-oauth-golang" "github.com/spf13/cobra" + "golang.design/x/clipboard" "tangled.sh/evan.jarrett.net/blup/internal/auth" "tangled.sh/evan.jarrett.net/blup/internal/config" "tangled.sh/evan.jarrett.net/blup/internal/util" ) +const ( + Name = "blup" + CDN = "https://imgs.blue" +) + var ( - cfg *config.Config - expiresIn time.Duration - contentType string + authHandle string ) func main() { var rootCmd = &cobra.Command{ - Use: "atimg", + Use: Name, Short: "AT Protocol image hosting CLI", } - var configureCmd = &cobra.Command{ - Use: "configure", - Short: "Configure OAuth client credentials", - RunE: runConfigure, - } - var authCmd = &cobra.Command{ Use: "auth", Short: "Authenticate with AT Protocol", RunE: runAuth, } + authCmd.Flags().StringVar(&authHandle, "handle", "", "Bluesky (ATProto) handle") var statusCmd = &cobra.Command{ Use: "status", @@ -64,10 +62,7 @@ func main() { RunE: runUpload, } - uploadCmd.Flags().DurationVarP(&expiresIn, "expires", "e", 24*time.Hour, "Expiration duration") - uploadCmd.Flags().StringVarP(&contentType, "type", "t", "", "Content type (auto-detected if not specified)") - - rootCmd.AddCommand(configureCmd, authCmd, statusCmd, logoutCmd, uploadCmd) + rootCmd.AddCommand(authCmd, statusCmd, logoutCmd, uploadCmd) var debugTokensCmd = &cobra.Command{ Use: "debug-tokens", @@ -90,40 +85,28 @@ func main() { rootCmd.AddCommand(debugTokensCmd) - // Load config - var err error - cfg, err = config.LoadConfig() - if err != nil { - fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) - os.Exit(1) - } - if err := rootCmd.Execute(); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } } -func runConfigure(cmd *cobra.Command, args []string) error { - fmt.Print("Enter your Bluesky handle: ") +func runAuth(cmd *cobra.Command, args []string) error { var handle string - fmt.Scanln(&handle) - if handle != "" { - cfg.Handle = handle + if authHandle == "" { + fmt.Print("Enter your Bluesky handle: ") + fmt.Scanln(&handle) + } else { + fmt.Printf("Using %s as bluesky handle\n", authHandle) + handle = authHandle } - return config.SaveConfig(cfg) -} - -func runAuth(cmd *cobra.Command, args []string) error { - if cfg.ClientID == "" { - return fmt.Errorf("please run 'atimg configure' first") - } - if cfg.ClientID == "http://localhost" { - params := url.Values{} - params.Set("redirect_uri", cfg.RedirectURI) - params.Set("scope", cfg.Scope) - cfg.ClientID = fmt.Sprintf("%s?%s", cfg.ClientID, params.Encode()) + cfg, err := config.SaveConfig(&config.Config{ + AuthserverIss: "https://bsky.social", + Handle: handle, + }) + if err != nil { + return err } if _, err := auth.RefreshTokens(cfg); err != nil { @@ -159,6 +142,13 @@ func runLogout(cmd *cobra.Command, args []string) error { } func runUpload(cmd *cobra.Command, args []string) error { + // Load config + cfg, err := config.LoadConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading config: %v\nTry running '%s auth'\n", err, Name) + os.Exit(1) + } + ctx := context.Background() imagepath := args[0] @@ -235,7 +225,7 @@ func runUpload(cmd *cobra.Command, args []string) error { } record := map[string]interface{}{ - "$type": "blue.imgs.blup.image", + "$type": fmt.Sprintf("blue.imgs.%s.image", Name), "blob": out.Blob, // Use the blob reference from upload "createdAt": time.Now().Format(time.RFC3339), "expiresAt": time.Now().Add(24 * time.Hour).Format(time.RFC3339), @@ -243,14 +233,14 @@ func runUpload(cmd *cobra.Command, args []string) error { "contentType": contentType, "size": fileSize, "metadata": map[string]interface{}{ - "uploadedFrom": "CLI tool", + "uploadedFrom": Name, "version": "1.0", }, } reqBody := map[string]interface{}{ "repo": tokens.Sub, - "collection": "blue.imgs.blup.image", + "collection": record["$type"], "record": record, } @@ -266,12 +256,23 @@ func runUpload(cmd *cobra.Command, args []string) error { if err != nil { return err } - fmt.Printf("https://imgs.blue/%s/%s\n", authArgs.Did, blobCID) - fmt.Printf("https://imgs.blue/%s/%s\n", cfg.Handle, converted) + compressedUrl := fmt.Sprintf("%s/%s/%s\n", CDN, cfg.Handle, converted) + + fmt.Printf("%s/%s/%s\n", CDN, authArgs.Did, blobCID) + fmt.Println(compressedUrl) + + // Initialize clipboard + err = clipboard.Init() + if err != nil { + fmt.Printf("Warning: Could not initialize clipboard: %v\n", err) + } else { + clipboard.Write(clipboard.FmtText, []byte(compressedUrl)) + fmt.Println("URL copied to clipboard") + } return nil } func userAgent() *string { - s := fmt.Sprintf("blup/1.0") + s := fmt.Sprintf("%s/1.0", Name) return &s } diff --git a/cmd/server/main.go b/cmd/server/main.go deleted file mode 100644 index c9ebe45..0000000 --- a/cmd/server/main.go +++ /dev/null @@ -1,79 +0,0 @@ -package main - -import ( - "context" - "log" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "tangled.sh/evan.jarrett.net/blup/internal/server" -) - -type ServerConfig struct { - ListenAddr string -} - -func loadServerConfig() (*ServerConfig, error) { - cfg := &ServerConfig{ - ListenAddr: getEnvOrDefault("LISTEN_ADDR", ":8080"), - } - - return cfg, nil -} - -func getEnvOrDefault(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - return defaultValue -} - -func main() { - // Load configuration - cfg, err := loadServerConfig() - if err != nil { - log.Fatalf("Configuration error: %v", err) - } - - log.Printf("Starting AT Protocol Image Server") - log.Printf("Listen Address: %s", cfg.ListenAddr) - - // Setup server - srv := server.NewServer() - - httpServer := &http.Server{ - Addr: cfg.ListenAddr, - Handler: srv.Router(), - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - IdleTimeout: 120 * time.Second, - } - - // Start server - go func() { - log.Printf("Server listening on http://localhost%s", cfg.ListenAddr) - if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("Server failed: %v", err) - } - }() - - // Wait for interrupt signal - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - <-sigChan - - log.Println("Shutting down server...") - - // Graceful shutdown - shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - if err := httpServer.Shutdown(shutdownCtx); err != nil { - log.Printf("Server shutdown error: %v", err) - } - - log.Println("Server stopped") -} diff --git a/go.mod b/go.mod index 8905fba..b4cdad4 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,15 @@ go 1.24.4 require ( github.com/bluesky-social/indigo v0.0.0-20250621010046-488d1b91889b + github.com/gen2brain/beeep v0.11.1 github.com/golang-jwt/jwt/v5 v5.2.2 - github.com/gorilla/mux v1.8.1 github.com/haileyok/atproto-oauth-golang v0.0.3-0.20250622200753-e07caa5274a7 github.com/ipfs/go-cid v0.5.0 github.com/lestrrat-go/jwx/v2 v2.1.6 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/spf13/cobra v1.9.1 github.com/zalando/go-keyring v0.2.6 + golang.design/x/clipboard v0.7.1 ) require ( @@ -70,6 +71,9 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.39.0 // indirect + golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 // indirect + golang.org/x/image v0.28.0 // indirect + golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect lukechampine.com/blake3 v1.4.1 // indirect diff --git a/go.sum b/go.sum index 1abd990..6498cb6 100644 --- a/go.sum +++ b/go.sum @@ -175,8 +175,6 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/haileyok/atproto-oauth-golang v0.0.3-0.20250622200753-e07caa5274a7 h1:P3oEaumqbWobYKGRj1Ku4jdgNxMogNlrLZJ097GkKmc= github.com/haileyok/atproto-oauth-golang v0.0.3-0.20250622200753-e07caa5274a7/go.mod h1:vVRo6BPEmWOZnYk9LtXLzBPzfkY63fUaBahA+o4h55Q= @@ -421,6 +419,8 @@ go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c= +golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -442,8 +442,12 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 h1:Wdx0vgH5Wgsw+lF//LJKmWOJBLWX6nprsMqnf99rYDE= +golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE= +golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -458,6 +462,8 @@ golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPI golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f h1:/n+PL2HlfqeSiDCuhdBbRNlGS/g2fM4OHufalHaTVG8= +golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f/go.mod h1:ESkJ836Z6LpG6mTVAhA48LpfW/8fNR0ifStlH2axyfg= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= diff --git a/internal/auth/oauth.go b/internal/auth/oauth.go index f6177f6..9c7db6e 100644 --- a/internal/auth/oauth.go +++ b/internal/auth/oauth.go @@ -1,6 +1,7 @@ package auth import ( + "bufio" "context" "encoding/json" "fmt" @@ -25,7 +26,7 @@ type OAuthFlow struct { cfg *config.Config jwk jwk.Key server *http.Server - authSuccess chan bool + authSuccess chan *StoredTokens authError chan error savedState string issuer string @@ -50,11 +51,13 @@ func NewOAuthFlow(cfg *config.Config) (*OAuthFlow, error) { } } + clientMetadata := GetClientMetadata() + // Create OAuth client clientConfig := oauth.ClientArgs{ ClientJwk: key, - ClientId: cfg.ClientID, - RedirectUri: cfg.RedirectURI, + ClientId: clientMetadata.ClientID, + RedirectUri: clientMetadata.RedirectURIs[0], } client, err := oauth.NewClient(clientConfig) @@ -66,21 +69,15 @@ func NewOAuthFlow(cfg *config.Config) (*OAuthFlow, error) { client: client, cfg: cfg, jwk: key, - authSuccess: make(chan bool, 1), + authSuccess: make(chan *StoredTokens, 1), authError: make(chan error, 1), }, nil } -func (f *OAuthFlow) Authenticate() error { +func (f *OAuthFlow) Authenticate() (*StoredTokens, error) { ctx := context.Background() - // Start local server for callback - err := f.startCallbackServer() - if err != nil { - return fmt.Errorf("failed to start callback server: %w", err) - } - defer f.stopCallbackServer() - var authServer, did, pdsHost string + var err error // Initialize authorization cfg := f.cfg @@ -88,15 +85,15 @@ func (f *OAuthFlow) Authenticate() error { if cfg.Handle != "" { did, err = ResolveHandle(ctx, cfg.Handle) if err != nil { - return err + return nil, err } pdsHost, err = ResolveService(ctx, did) if err != nil { - return err + return nil, err } authServer, err = f.client.ResolvePdsAuthServer(ctx, pdsHost) if err != nil { - return err + return nil, err } cfg.PDSHost = pdsHost cfg.AuthserverIss = authServer @@ -108,12 +105,14 @@ func (f *OAuthFlow) Authenticate() error { meta, err := f.client.FetchAuthServerMetadata(ctx, authServer) if err != nil { - return err + return nil, err } - parResp, err := f.client.SendParAuthRequest(ctx, authServer, meta, "", cfg.Scope, f.jwk) + clientMetadata := GetClientMetadata() + + parResp, err := f.client.SendParAuthRequest(ctx, authServer, meta, cfg.Handle, clientMetadata.Scope, f.jwk) if err != nil { - return err + return nil, err } f.savedState = parResp.State @@ -122,7 +121,9 @@ func (f *OAuthFlow) Authenticate() error { f.nonce = parResp.DpopAuthserverNonce authUrl, _ := url.Parse(meta.AuthorizationEndpoint) - authUrl.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(cfg.ClientID), parResp.RequestUri) + authUrl.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(clientMetadata.ClientID), parResp.RequestUri) + + go f.waitForAuthSSE() // Open browser to authorization URL fmt.Printf("Opening browser for authentication...\n") @@ -135,55 +136,62 @@ func (f *OAuthFlow) Authenticate() error { fmt.Println("Waiting for authentication...") select { - case <-f.authSuccess: - + case tokens := <-f.authSuccess: fmt.Println("Authentication successful!") - return nil - + return tokens, nil case err := <-f.authError: - return fmt.Errorf("authentication failed: %w", err) - + return nil, fmt.Errorf("authentication failed: %w", err) case <-time.After(5 * time.Minute): - return fmt.Errorf("authentication timeout") + return nil, fmt.Errorf("authentication timeout") } } -func (f *OAuthFlow) startCallbackServer() error { - listener, err := net.Listen("tcp", ":8080") - if err != nil { - return err - } - - mux := http.NewServeMux() - mux.HandleFunc("/callback", f.handleCallback) - - f.server = &http.Server{ - Handler: mux, - } - - go func() { - if err := f.server.Serve(listener); err != http.ErrServerClosed { - f.authError <- err - } - }() - - return nil -} - -func (f *OAuthFlow) stopCallbackServer() { - if f.server != nil { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - f.server.Shutdown(ctx) - } +func (f *OAuthFlow) waitForAuthSSE() (string, error) { + clientMetadata := GetClientMetadata() + req, err := http.NewRequest("GET", + fmt.Sprintf("%s/oauth/events?session=%s", clientMetadata.ClientURI, f.savedState), + nil) + if err != nil { + f.authError <- err + } + + req.Header.Set("Accept", "text/event-stream") + req.Header.Set("Cache-Control", "no-cache") + + client := &http.Client{Timeout: 5 * time.Minute} + resp, err := client.Do(req) + if err != nil { + f.authError <- err + } + defer resp.Body.Close() + + reader := bufio.NewReader(resp.Body) + + for { + line, err := reader.ReadString('\n') + if err != nil { + f.authError <- err + } + + if strings.HasPrefix(line, "event: auth-complete") { + // Read data line + dataLine, _ := reader.ReadString('\n') + if strings.HasPrefix(dataLine, "data: ") { + data := strings.TrimPrefix(dataLine, "data: ") + var authData struct { + Code string `json:"code"` + Iss string `json:"iss"` + State string `json:"state"` + } + json.Unmarshal([]byte(data), &authData) + f.handleCallback(req.Context(), authData.Code, authData.Iss, authData.State) + } + } + } } -func (f *OAuthFlow) handleCallback(w http.ResponseWriter, r *http.Request) { +func (f *OAuthFlow) handleCallback(ctx context.Context, code, iss, state string) { // Get authorization code from query params - code := r.URL.Query().Get("code") - iss := r.URL.Query().Get("iss") - state := r.URL.Query().Get("state") - if state == "" || iss == "" || code == "" { f.authError <- fmt.Errorf("request missing needed parameters") } @@ -194,43 +202,25 @@ func (f *OAuthFlow) handleCallback(w http.ResponseWriter, r *http.Request) { if iss != f.issuer { f.authError <- fmt.Errorf("incoming iss did not match authserver iss") - } - initialTokenResp, err := f.client.InitialTokenRequest(r.Context(), code, iss, f.verifier, f.nonce, f.jwk) + initialTokenResp, err := f.client.InitialTokenRequest(ctx, code, iss, f.verifier, f.nonce, f.jwk) if err != nil { f.authError <- err } - if initialTokenResp.Scope != f.cfg.Scope { + if initialTokenResp.Scope != GetClientMetadata().Scope { f.authError <- fmt.Errorf("did not receive correct scopes from token request") } // Store tokens - if _, err := StoreTokensFromLibrary(initialTokenResp); err != nil { + tokens, err := StoreTokensFromLibrary(initialTokenResp) + if err != nil { f.authError <- fmt.Errorf("failed to store tokens: %w", err) } // Send code through channel - f.authSuccess <- true - - // Send success response - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, ` - - -
-You can close this window and return to the application.
- -`) + f.authSuccess <- tokens } // RefreshTokens refreshes the access token using the stored refresh token @@ -245,7 +235,7 @@ func RefreshTokens(cfg *config.Config) (*StoredTokens, error) { var newTokens *oauth.TokenResponse if tokens == nil { - return nil, flow.Authenticate() + return flow.Authenticate() } if tokens.NeedsRefresh() { ctx := context.Background() @@ -279,8 +269,8 @@ func GetPDSFromToken(accessToken string) (string, error) { } // Convert did:web:... to https://... - if strings.HasPrefix(aud, "did:web:") { - pdsHost := strings.TrimPrefix(aud, "did:web:") + if after, ok0 :=strings.CutPrefix(aud, "did:web:"); ok0 { + pdsHost := after return "https://" + pdsHost, nil } diff --git a/internal/config/config.go b/internal/config/config.go index 78ffc56..6b16d84 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,43 +2,27 @@ package config import ( "encoding/json" - "fmt" - "net/url" "os" "path/filepath" "strings" ) type Config struct { - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` - RedirectURI string `json:"redirect_uri"` - Scope string `json:"scope"` AuthserverIss string `json:"auth_server"` PDSHost string `json:"pds_host"` Handle string `json:"handle"` } func LoadConfig() (*Config, error) { - configPath := filepath.Join(getConfigDir(), "config.json") + configPath := getConfigFile() // Default configuration cfg := &Config{ - RedirectURI: "http://127.0.0.1:8080/callback", - Scope: "atproto transition:generic", AuthserverIss: "https://bsky.social", } - params := url.Values{} - params.Set("redirect_uri", cfg.RedirectURI) - params.Set("scope", cfg.Scope) - cfg.ClientID = fmt.Sprintf("http://localhost?%s", params.Encode()) - data, err := os.ReadFile(configPath) if err != nil { - if os.IsNotExist(err) { - return cfg, nil - } return nil, err } @@ -49,26 +33,21 @@ func LoadConfig() (*Config, error) { return cfg, nil } -func SaveConfig(cfg *Config) error { - configDir := getConfigDir() - if err := os.MkdirAll(configDir, 0700); err != nil { - return err - } - +func SaveConfig(cfg *Config) (*Config, error) { if cfg.Handle != "" { cfg.Handle = strings.TrimPrefix(cfg.Handle, "@") } - configPath := filepath.Join(configDir, "config.json") + configPath := getConfigFile() data, err := json.MarshalIndent(cfg, "", " ") if err != nil { - return err + return cfg, err } - return os.WriteFile(configPath, data, 0600) + return cfg, os.WriteFile(configPath, data, 0600) } -func getConfigDir() string { +func getConfigFile() string { home, _ := os.UserHomeDir() - return filepath.Join(home, ".atproto-image-host") + return filepath.Join(home, ".blup.conf") } diff --git a/internal/server/server.go b/internal/server/server.go deleted file mode 100644 index 3364993..0000000 --- a/internal/server/server.go +++ /dev/null @@ -1,193 +0,0 @@ -package server - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "log" - - "net/http" - "strings" - "time" - - indigo_util "github.com/bluesky-social/indigo/util" - "github.com/gorilla/mux" - "tangled.sh/evan.jarrett.net/blup/internal/auth" - "tangled.sh/evan.jarrett.net/blup/internal/util" -) - -type Server struct { - router *mux.Router -} - -func NewServer() *Server { - s := &Server{} - - s.setupRoutes() - return s -} - -func (s *Server) setupRoutes() { - r := mux.NewRouter() - - // Health check - r.HandleFunc("/health", s.handleHealth).Methods("GET") - - // Image serving - r.HandleFunc("/i/{did}/{cid}", s.handleServeImage).Methods("GET") - - // Simple home page - r.HandleFunc("/", s.handleHome).Methods("GET") - - s.router = r -} - -func (s *Server) Router() http.Handler { - return s.router -} - -func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{ - "status": "ok", - "timestamp": time.Now().Format(time.RFC3339), - }) -} - -func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) { - html := ` - - -This server renders images uploaded to AT Protocol.
-Usage: /i/{blob-cid}
Upload images using: atimg upload image.png