Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,4 @@ chocolateys:
api_key: '{{ .Env.CHOCOLATEY_API_KEY }}'
source_repo: "https://push.chocolatey.org/"
skip_publish: false
goamd64: v1
goamd64: v1
52 changes: 52 additions & 0 deletions api/dashboard/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,58 @@ func (c *Client) CreateAPIKey(accessToken, appID string, acl []string, descripti
return key, nil
}

func (c *Client) GetCrawlerMe(accessToken string) (*CrawlerUserData, error) {
Comment thread
8bittitan marked this conversation as resolved.
Outdated
req, err := http.NewRequest(http.MethodGet, c.APIURL+"/1/crawler/me", nil)
if err != nil {
return nil, err
}

c.setAPIHeaders(req, accessToken)

resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("crawler me failed with status: %d", resp.StatusCode)
Comment thread
8bittitan marked this conversation as resolved.
Outdated
}

var meResp CrawlerMeResponse
if err := json.NewDecoder(resp.Body).Decode(&meResp); err != nil {
return nil, fmt.Errorf("failed to parse crawler response: %w", err)
}

return &meResp.Data, nil
}

func (c *Client) GetCrawlerAPIKey(accessToken string) (string, error) {
req, err := http.NewRequest(http.MethodGet, c.APIURL+"/1/crawler/api_key", nil)
if err != nil {
return "", err
}

c.setAPIHeaders(req, accessToken)

resp, err := c.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("crawler api key failed with status: %d", resp.StatusCode)
}

var apiKeyResp CrawlerAPIKeyResponse
if err := json.NewDecoder(resp.Body).Decode(&apiKeyResp); err != nil {
return "", fmt.Errorf("failed to parse crawler response: %w", err)
}

return apiKeyResp.Data.APIKey, nil
}

func (c *Client) setAPIHeaders(req *http.Request, accessToken string) {
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Accept", "application/vnd.api+json")
Expand Down
18 changes: 18 additions & 0 deletions api/dashboard/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ type CreateAPIKeyResponse struct {
Data APIKeyResource `json:"data"`
}

type CrawlerUserData struct {
ID string `json:"id"`
}

type CrawlerMeResponse struct {
Success bool `json:"success"`
Data CrawlerUserData `json:"data"`
}

type CrawlerAPIKeyData struct {
APIKey string `json:"apiKey"`
}

type CrawlerAPIKeyResponse struct {
Success bool `json:"success"`
Data CrawlerAPIKeyData `json:"data"`
}

// toApplication flattens a JSON:API resource into a simple Application.
func (r *ApplicationResource) toApplication() Application {
return Application{
Expand Down
2 changes: 1 addition & 1 deletion pkg/auth/oauth_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func OAuthClientID() string {

// RunOAuth runs the OAuth PKCE flow with a local callback server and returns
// a valid access token. A local HTTP server is started on a random port to
// receive the authorization code via redirect no copy-paste required.
// receive the authorization code via redirect - no copy-paste required.
//
// When openBrowser is true the authorize URL is opened automatically;
// otherwise only the URL is printed (useful when the browser can't be
Expand Down
2 changes: 2 additions & 0 deletions pkg/cmd/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/spf13/cobra"

"github.com/algolia/cli/pkg/auth"
"github.com/algolia/cli/pkg/cmd/auth/crawler"
"github.com/algolia/cli/pkg/cmd/auth/login"
"github.com/algolia/cli/pkg/cmd/auth/logout"
"github.com/algolia/cli/pkg/cmd/auth/signup"
Expand All @@ -22,6 +23,7 @@ func NewAuthCmd(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(login.NewLoginCmd(f))
cmd.AddCommand(logout.NewLogoutCmd(f))
cmd.AddCommand(signup.NewSignupCmd(f))
cmd.AddCommand(crawler.NewCrawlerCmd(f))

return cmd
}
89 changes: 89 additions & 0 deletions pkg/cmd/auth/crawler/crawler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package crawler

import (
"fmt"

"github.com/algolia/cli/api/dashboard"
"github.com/algolia/cli/pkg/auth"
"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/config"
"github.com/algolia/cli/pkg/iostreams"
"github.com/algolia/cli/pkg/validators"
"github.com/spf13/cobra"
)

type CrawlerOptions struct {
IO *iostreams.IOStreams
config config.IConfig
OAuthClientID func() string
NewDashboardClient func(clientID string) *dashboard.Client
GetValidToken func(client *dashboard.Client) (string, error)
}

func NewCrawlerCmd(f *cmdutil.Factory) *cobra.Command {
opts := &CrawlerOptions{
IO: f.IOStreams,
config: f.Config,
OAuthClientID: auth.OAuthClientID,
NewDashboardClient: func(clientID string) *dashboard.Client {
return dashboard.NewClient(clientID)
},
GetValidToken: auth.GetValidToken,
}

cmd := &cobra.Command{
Use: "crawler",
Short: "Load crawler auth details for the current profile",
Args: validators.NoArgs(),
RunE: func(cmd *cobra.Command, args []string) error {
return runCrawlerCmd(opts)
},
}

return cmd
}

func runCrawlerCmd(opts *CrawlerOptions) error {
cs := opts.IO.ColorScheme()
dashboardClient := opts.NewDashboardClient(opts.OAuthClientID())

accessToken, err := opts.GetValidToken(dashboardClient)
if err != nil {
return err
}

opts.IO.StartProgressIndicatorWithLabel("Fetching crawler information")
crawlerUserData, err := dashboardClient.GetCrawlerMe(accessToken)
if err != nil {
opts.IO.StopProgressIndicator()
return err
}

crawlerAPIKey, err := dashboardClient.GetCrawlerAPIKey(accessToken)
opts.IO.StopProgressIndicator()
if err != nil {
return err
}

currentProfileName := opts.config.Profile().Name
if currentProfileName == "" {
defaultProfile := opts.config.Default()
if defaultProfile != nil {
currentProfileName = defaultProfile.Name
opts.config.Profile().Name = currentProfileName
}
}
if currentProfileName == "" {
return fmt.Errorf("no profile selected and no default profile configured")
}

if err = opts.config.SetCrawlerAuth(currentProfileName, crawlerUserData.ID, crawlerAPIKey); err != nil {
return err
}

if opts.IO.IsStdoutTTY() {
fmt.Fprintf(opts.IO.Out, "%s Crawler API auth credentials configured for profile: %s\n", cs.SuccessIcon(), currentProfileName)
}

return nil
}
102 changes: 102 additions & 0 deletions pkg/cmd/auth/crawler/crawler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package crawler

import (
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/algolia/cli/api/dashboard"
"github.com/algolia/cli/pkg/config"
"github.com/algolia/cli/pkg/iostreams"
"github.com/algolia/cli/test"
)

func Test_runCrawlerCmd_UsesDefaultProfile(t *testing.T) {
io, _, stdout, _ := iostreams.Test()
io.SetStdoutTTY(true)

cfg := test.NewConfigStubWithProfiles([]*config.Profile{
{Name: "default", Default: true},
{Name: "other"},
})
cfg.CurrentProfile.Name = ""

server := newCrawlerTestServer(t, "token-1", "crawler-user", "crawler-key")
t.Cleanup(server.Close)

err := runCrawlerCmd(&CrawlerOptions{
IO: io,
config: cfg,
OAuthClientID: func() string { return "test-client-id" },
NewDashboardClient: newDashboardTestClient(server),
GetValidToken: func(client *dashboard.Client) (string, error) {
return "token-1", nil
},
})
require.NoError(t, err)

assert.Equal(t, "default", cfg.CurrentProfile.Name)
assert.Equal(t, test.CrawlerAuth{UserID: "crawler-user", APIKey: "crawler-key"}, cfg.CrawlerAuth["default"])
assert.Contains(t, stdout.String(), "configured for profile: default")
}

func Test_runCrawlerCmd_UsesExplicitProfile(t *testing.T) {
io, _, stdout, _ := iostreams.Test()
io.SetStdoutTTY(true)

cfg := test.NewConfigStubWithProfiles([]*config.Profile{
{Name: "target"},
{Name: "default", Default: true},
})
cfg.CurrentProfile.Name = "target"

server := newCrawlerTestServer(t, "token-2", "crawler-user-2", "crawler-key-2")
t.Cleanup(server.Close)

err := runCrawlerCmd(&CrawlerOptions{
IO: io,
config: cfg,
OAuthClientID: func() string { return "test-client-id" },
NewDashboardClient: newDashboardTestClient(server),
GetValidToken: func(client *dashboard.Client) (string, error) {
return "token-2", nil
},
})
require.NoError(t, err)

assert.Equal(t, test.CrawlerAuth{UserID: "crawler-user-2", APIKey: "crawler-key-2"}, cfg.CrawlerAuth["target"])
_, hasDefault := cfg.CrawlerAuth["default"]
assert.False(t, hasDefault)
assert.Contains(t, stdout.String(), "configured for profile: target")
}

func newCrawlerTestServer(t *testing.T, token, userID, apiKey string) *httptest.Server {
t.Helper()

return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "Bearer "+token, r.Header.Get("Authorization"))

switch r.URL.Path {
case "/1/crawler/me":
_, err := fmt.Fprintf(w, `{"success":true,"data":{"id":%q}}`, userID)
require.NoError(t, err)
case "/1/crawler/api_key":
_, err := fmt.Fprintf(w, `{"success":true,"data":{"apiKey":%q}}`, apiKey)
require.NoError(t, err)
default:
t.Fatalf("unexpected path: %s", r.URL.Path)
}
}))
}

func newDashboardTestClient(server *httptest.Server) func(string) *dashboard.Client {
return func(clientID string) *dashboard.Client {
client := dashboard.NewClientWithHTTPClient(clientID, server.Client())
client.APIURL = server.URL
return client
}
}
4 changes: 2 additions & 2 deletions pkg/cmd/auth/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func NewLoginCmd(f *cmdutil.Factory) *cobra.Command {
the authorization code for API tokens using OAuth 2.0 with PKCE.

A local HTTP server is started to receive the OAuth redirect
automatically no code copy-paste required.
automatically - no code copy-paste required.

Use --no-browser if the browser cannot be opened automatically
(e.g. SSH sessions, containers). The URL will be printed for you
Expand Down Expand Up @@ -156,7 +156,7 @@ func selectApplication(opts *LoginOptions, apps []dashboard.Application, interac
fmt.Fprintf(opts.IO.Out, " %d. %s (%s)\n", i+1, app.Name, app.ID)
}
fmt.Fprintf(opts.IO.Out, "Use --app-name to select one.\n")
return nil, fmt.Errorf("multiple applications found use --app-name to select one")
return nil, fmt.Errorf("multiple applications found - use --app-name to select one")
}

appNames := make([]string, len(apps))
Expand Down
Loading
Loading