diff --git a/cmd/account/link_key/link_key.go b/cmd/account/link_key/link_key.go index d9899047..47d32b02 100644 --- a/cmd/account/link_key/link_key.go +++ b/cmd/account/link_key/link_key.go @@ -175,7 +175,23 @@ func (h *handler) Execute(ctx context.Context, in Inputs) error { ) return fmt.Errorf("missing required flags for --non-interactive mode") } - label, err := ui.Input("Provide a label for your owner address") + defaultLabel := h.defaultOwnerLabel() + + title := "Provide a label for your owner address" + if defaultLabel != "" { + title = fmt.Sprintf("%s [%s]", title, defaultLabel) + } + + label, err := ui.Input( + title, + ui.WithDefaultValue(defaultLabel), + ui.WithValidate(func(s string) error { + if strings.TrimSpace(s) == "" { + return fmt.Errorf("a label is required") + } + return nil + }), + ) if err != nil { return err } @@ -408,6 +424,20 @@ func (h *handler) checkIfAlreadyLinked() (bool, error) { return false, nil } +// defaultOwnerLabel derives a suggested label from the authenticated user's +// email address (the portion before the "@"). Returns "" if no email is available. +func (h *handler) defaultOwnerLabel() string { + email, err := h.credentials.GetEmail() + if err != nil || email == "" { + return "" + } + + if idx := strings.Index(email, "@"); idx > 0 { + return email[:idx] + } + return email +} + func (h *handler) displayDetails() { ui.Line() ui.Title("Linking web3 key to your CRE organization") diff --git a/internal/credentials/credentials.go b/internal/credentials/credentials.go index 0c81d6dc..a253281d 100644 --- a/internal/credentials/credentials.go +++ b/internal/credentials/credentials.go @@ -184,6 +184,31 @@ func (c *Credentials) GetOrgID() (string, error) { return orgID, nil } +// GetEmail returns the authenticated user's email address from the access token. +// It is not available for API key authentication. +func (c *Credentials) GetEmail() (string, error) { + if c.AuthType == AuthTypeApiKey { + return "", fmt.Errorf("email is not available for API key authentication") + } + + claims, err := c.decodeJWTClaims() + if err != nil { + return "", err + } + + // The email claim is namespaced (e.g. "https://api.cre.chain.link/email"), + // so find it by looking for any key ending with "email". + for key, value := range claims { + if strings.HasSuffix(key, "email") { + if email, ok := value.(string); ok && email != "" { + return email, nil + } + } + } + + return "", fmt.Errorf("email claim not found in access token") +} + // GetDeploymentAccessStatus returns the deployment access status for the organization. // This can be used to check and display whether the user has deployment access. func (c *Credentials) GetDeploymentAccessStatus() (*DeploymentAccess, error) { diff --git a/internal/ui/prompts.go b/internal/ui/prompts.go index c288e835..58413c29 100644 --- a/internal/ui/prompts.go +++ b/internal/ui/prompts.go @@ -80,8 +80,10 @@ func Confirm(title string, opts ...ConfirmOption) (bool, error) { type InputOption func(*inputConfig) type inputConfig struct { - description string - placeholder string + description string + placeholder string + defaultValue string + validate func(string) error } // WithInputDescription sets the description for an Input prompt. @@ -98,6 +100,22 @@ func WithPlaceholder(placeholder string) InputOption { } } +// WithDefaultValue pre-fills the Input prompt with the given value, so pressing +// Enter without editing submits the default. +func WithDefaultValue(value string) InputOption { + return func(c *inputConfig) { + c.defaultValue = value + } +} + +// WithValidate sets a validation function for an Input prompt. The form +// will not submit until the function returns nil. +func WithValidate(validate func(string) error) InputOption { + return func(c *inputConfig) { + c.validate = validate + } +} + // Input displays a single text input prompt and returns the entered value. func Input(title string, opts ...InputOption) (string, error) { cfg := inputConfig{} @@ -105,7 +123,7 @@ func Input(title string, opts ...InputOption) (string, error) { o(&cfg) } - var result string + result := cfg.defaultValue input := huh.NewInput(). Title(title). Value(&result) @@ -116,6 +134,9 @@ func Input(title string, opts ...InputOption) (string, error) { if cfg.placeholder != "" { input = input.Placeholder(cfg.placeholder) } + if cfg.validate != nil { + input = input.Validate(cfg.validate) + } form := huh.NewForm( huh.NewGroup(input),