Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
24 changes: 13 additions & 11 deletions api/dashboard/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,39 +539,41 @@ var WriteACL = []string{
"settings", "editSettings", "recommendation",
}

// CreateAPIKey creates a new API key with the given ACL for the specified application.
// CreateAPIKey creates a new API key with the given ACL for the specified
// application. It returns the API key value and its resource ID (UUID), the
// latter so callers can persist it for later rotation/revocation.
func (c *Client) CreateAPIKey(
accessToken, appID string,
acl []string,
description string,
) (string, error) {
) (key, uuid string, err error) {
payload := CreateAPIKeyRequest{ACL: acl, Description: description}
body, err := json.Marshal(payload)
if err != nil {
return "", err
return "", "", err
}

endpoint := fmt.Sprintf("%s/1/applications/%s/api-keys", c.APIURL, url.PathEscape(appID))
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return "", err
return "", "", err
}
c.setAPIHeaders(req, accessToken)
req.Header.Set("Content-Type", "application/json")

resp, err := c.client.Do(req)
if err != nil {
return "", fmt.Errorf("create API key request failed: %w", err)
return "", "", fmt.Errorf("create API key request failed: %w", err)
}
defer resp.Body.Close()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read API key response: %w", err)
return "", "", fmt.Errorf("failed to read API key response: %w", err)
}

if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf(
return "", "", fmt.Errorf(
"create API key failed with status %d: %s",
resp.StatusCode,
string(respBody),
Expand All @@ -580,22 +582,22 @@ func (c *Client) CreateAPIKey(

var keyResp CreateAPIKeyResponse
if err := json.Unmarshal(respBody, &keyResp); err != nil {
return "", fmt.Errorf(
return "", "", fmt.Errorf(
"failed to parse API key response: %w (body: %s)",
err,
string(respBody),
)
}

key := keyResp.Data.Attributes.Value
key = keyResp.Data.Attributes.Value
if key == "" {
return "", fmt.Errorf(
return "", "", fmt.Errorf(
"API key creation succeeded but no key was returned in the response: %s",
string(respBody),
)
}

return key, nil
return key, keyResp.Data.ID, nil
}

// GetCrawlerUser gets the crawler API user data for the current authenticated user
Expand Down
9 changes: 5 additions & 4 deletions api/dashboard/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ type ApplicationPlan struct {

// Application is a flattened view of an Algolia application for CLI consumption.
type Application struct {
ID string `json:"id"`
Name string `json:"name"`
APIKey string `json:"api_key,omitempty"`
PlanLabel string `json:"plan_label,omitempty"` // current plan label, e.g. "Grow Plus"
ID string `json:"id"`
Name string `json:"name"`
APIKey string `json:"api_key,omitempty"`
APIKeyUUID string `json:"-"` // resource ID of APIKey when freshly created; persisted as api_key_uuid
PlanLabel string `json:"plan_label,omitempty"` // current plan label, e.g. "Grow Plus"
}

// PaginationMeta contains page-based pagination metadata.
Expand Down
12 changes: 6 additions & 6 deletions pkg/cmd/application/selectapp/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/algolia/cli/pkg/cmd/shared/apputil"
"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/config"
"github.com/algolia/cli/pkg/config/state"
"github.com/algolia/cli/pkg/iostreams"
"github.com/algolia/cli/pkg/prompt"
"github.com/algolia/cli/pkg/validators"
Expand Down Expand Up @@ -122,12 +123,9 @@ func runSelectCmd(opts *SelectOptions) (*dashboard.Application, error) {
return nil, err
}

// If a profile already exists for this app, switch the default
// and ensure it has an API key.
// If a profile already exists for this app, switch the current
// application and ensure it has an API key.
if exists, profileName := opts.Config.ApplicationIDExists(chosen.ID); exists {
// Read the profile BEFORE SetDefaultProfile, because viper.Set() calls
// inside SetDefaultProfile pollute the override map and cause
// UnmarshalKey to return empty fields (known viper issue).
var existingProfile *config.Profile
for _, p := range opts.Config.ConfiguredProfiles() {
if p.Name == profileName {
Expand All @@ -142,13 +140,15 @@ func runSelectCmd(opts *SelectOptions) (*dashboard.Application, error) {
fmt.Fprintf(opts.IO.Out, "%s Switched to profile %q (application %s).\n",
cs.SuccessIcon(), profileName, cs.Bold(chosen.ID))

if existingProfile != nil && existingProfile.APIKey == "" {
storedKey, _ := state.GetSecret(chosen.ID, state.SecretAPIKey)
if existingProfile != nil && storedKey == "" {
app := &dashboard.Application{ID: chosen.ID, Name: chosen.Name}
if err := apputil.EnsureAPIKey(opts.IO, client, accessToken, app); err != nil {
return nil, err
}
existingProfile.ApplicationID = chosen.ID
existingProfile.APIKey = app.APIKey
existingProfile.APIKeyUUID = app.APIKeyUUID
if err := existingProfile.Add(); err != nil {
return nil, err
}
Expand Down
14 changes: 12 additions & 2 deletions pkg/cmd/auth/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/algolia/cli/pkg/cmd/shared/apputil"
"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/config"
"github.com/algolia/cli/pkg/config/state"
"github.com/algolia/cli/pkg/iostreams"
"github.com/algolia/cli/pkg/prompt"
"github.com/algolia/cli/pkg/telemetry"
Expand Down Expand Up @@ -169,13 +170,22 @@ func applyStoredIdentity(ctx context.Context) bool {
}

// reuseExistingAPIKey checks if a local profile already has an API key for
// the given application. If so, it sets app.APIKey and returns true.
// the given application. If so, it sets app.APIKey and returns true. The key
// itself lives in the OS keychain; the profile only carries metadata.
func reuseExistingAPIKey(cfg config.IConfig, app *dashboard.Application) bool {
for _, p := range cfg.ConfiguredProfiles() {
if p.ApplicationID == app.ID && p.APIKey != "" {
if p.ApplicationID != app.ID {
continue
}
// In-memory profiles (tests/legacy) may already hold the key.
if p.APIKey != "" {
app.APIKey = p.APIKey
return true
}
if key, err := state.GetSecret(app.ID, state.SecretAPIKey); err == nil && key != "" {
app.APIKey = key
return true
}
}
return false
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/profile/add/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func NewAddCmd(f *cmdutil.Factory, runF func(*AddOptions) error) *cobra.Command
cmd := &cobra.Command{
Use: "add",
Args: validators.NoArgs(),
Short: "Add a new profile configuration to the CLI",
Short: "(deprecated) Add a new profile configuration to the CLI",
Example: heredoc.Doc(`
# Add a new profile (interactive)
$ algolia profile add
Expand Down
17 changes: 16 additions & 1 deletion pkg/cmd/profile/application.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package profile

import (
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"

"github.com/algolia/cli/pkg/auth"
Expand All @@ -16,7 +17,21 @@ func NewProfileCmd(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "profile",
Aliases: []string{"profiles"},
Short: "Manage your Algolia CLI profiles",
Short: "(deprecated) Manage your Algolia CLI profiles",
Long: heredoc.Doc(`
Manage your Algolia CLI profiles.

These commands are deprecated. Credentials now live in state.toml
(non-secrets) and the OS keychain (secrets), managed by:

- algolia auth login sign in and configure an application
- algolia application list list applications, marking configured ones
- algolia application select switch the active application

Existing profiles keep working and remain resolvable as aliases via
the deprecated --profile flag until the next major version.
`),
Deprecated: "use `algolia auth login`, `algolia application list`, and `algolia application select` instead. Profiles still resolve as aliases until the next major version.",
}

auth.DisableAuthCheck(cmd)
Expand Down
6 changes: 5 additions & 1 deletion pkg/cmd/profile/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/algolia/cli/pkg/cmd/factory"
"github.com/algolia/cli/pkg/cmdutil"
"github.com/algolia/cli/pkg/config"
"github.com/algolia/cli/pkg/config/state"
"github.com/algolia/cli/pkg/iostreams"
"github.com/algolia/cli/pkg/printers"
"github.com/algolia/cli/pkg/validators"
Expand All @@ -32,7 +33,7 @@ func NewListCmd(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
Use: "list",
Aliases: []string{"l"},
Args: validators.NoArgs(),
Short: "List the configured profile(s)",
Short: "(deprecated) List the configured profile(s)",
Example: heredoc.Doc(`
# List the configured profiles
$ algolia profile list
Expand Down Expand Up @@ -75,6 +76,9 @@ func runListCmd(opts *ListOptions) error {
table.AddField(profile.ApplicationID, nil, nil)

apiKey := profile.APIKey
if apiKey == "" {
apiKey, _ = state.GetSecret(profile.ApplicationID, state.SecretAPIKey)
}
if apiKey == "" {
apiKey = profile.AdminAPIKey // Legacy
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/profile/remove/remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func NewRemoveCmd(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Co
Use: "remove <profile>",
Args: validators.ExactArgs(1),
ValidArgsFunction: cmdutil.ConfiguredProfilesCompletionFunc(f),
Short: "Remove the specified profile",
Short: "(deprecated) Remove the specified profile",
Long: `Remove the specified profile from the configuration.`,
Example: heredoc.Doc(`
# Remove the profile named "my-app" from the configuration
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/profile/setdefault/setdefault.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func NewSetDefaultCmd(f *cmdutil.Factory, runF func(*SetDefaultOptions) error) *
Use: "setdefault <profile>",
Args: validators.ExactArgs(1),
ValidArgsFunction: cmdutil.ConfiguredProfilesCompletionFunc(f),
Short: "Set the default profile",
Short: "(deprecated) Set the default profile",
Example: heredoc.Doc(`
# Set the default profile to "my-app"
$ algolia profile setdefault my-app
Expand Down
4 changes: 3 additions & 1 deletion pkg/cmd/shared/apputil/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,14 @@ func EnsureAPIKey(
) error {
cs := io.ColorScheme()
io.StartProgressIndicatorWithLabel("Generating API key")
apiKey, err := client.CreateAPIKey(accessToken, app.ID, dashboard.WriteACL, "Algolia CLI")
apiKey, apiKeyUUID, err := client.CreateAPIKey(accessToken, app.ID, dashboard.WriteACL, "Algolia CLI")
io.StopProgressIndicator()
if err != nil {
return fmt.Errorf("failed to generate API key: %w", err)
}

app.APIKey = apiKey
app.APIKeyUUID = apiKeyUUID
fmt.Fprintf(io.Out, "%s API key generated for application %s\n",
cs.SuccessIcon(), cs.Bold(app.ID))
return nil
Expand Down Expand Up @@ -168,6 +169,7 @@ func ConfigureProfile(
Name: profileName,
ApplicationID: appDetails.ID,
APIKey: appDetails.APIKey,
APIKeyUUID: appDetails.APIKeyUUID,
Default: setDefault,
}

Expand Down
Loading
Loading