diff --git a/internal/cli/apis.go b/internal/cli/apis.go index 6203ed4b7..a732995d4 100644 --- a/internal/cli/apis.go +++ b/internal/cli/apis.go @@ -3,7 +3,6 @@ package cli import ( "errors" "fmt" - "strings" "github.com/auth0/auth0-cli/internal/ansi" "github.com/auth0/auth0-cli/internal/prompt" @@ -12,26 +11,30 @@ import ( ) const ( - apiID = "id" - apiScopes = "scopes" + apiID = "id" ) var ( apiName = Flag{ - Name: "Name", - LongForm: "name", - ShortForm: "n", - DefaultValue: "", - Help: "Name of the API.", - IsRequired: true, + Name: "Name", + LongForm: "name", + ShortForm: "n", + Help: "Name of the API.", + IsRequired: true, } apiIdentifier = Flag{ - Name: "Identifier", - LongForm: "identifier", - ShortForm: "i", - DefaultValue: "", - Help: "Identifier of the API. Cannot be changed once set.", - IsRequired: true, + Name: "Identifier", + LongForm: "identifier", + ShortForm: "i", + Help: "Identifier of the API. Cannot be changed once set.", + IsRequired: true, + } + apiScopes = Flag{ + Name: "Scopes", + LongForm: "scopes", + ShortForm: "s", + Help: "Comma-separated list of scopes.", + IsRequired: true, } ) @@ -135,7 +138,7 @@ auth0 apis show }) if err != nil { - return fmt.Errorf("Unable to get an API with Id %s: %w", inputs.ID, err) + return fmt.Errorf("Unable to get an API with Id '%s': %w", inputs.ID, err) } cli.renderer.ApiShow(api) @@ -147,10 +150,11 @@ auth0 apis show } func createApiCmd(cli *cli) *cobra.Command { - var flags struct { - Name string - Identifier string - Scopes string + var inputs struct { + Name string + Identifier string + Scopes []string + ScopesString string } cmd := &cobra.Command{ @@ -164,29 +168,27 @@ auth0 apis create --name myapi --identifier http://my-api prepareInteractivity(cmd) }, RunE: func(cmd *cobra.Command, args []string) error { - if err := apiName.Ask(cmd, &flags.Name); err != nil { + if err := apiName.Ask(cmd, &inputs.Name); err != nil { return err } - if err := apiIdentifier.Ask(cmd, &flags.Identifier); err != nil { + if err := apiIdentifier.Ask(cmd, &inputs.Identifier); err != nil { return err } - if shouldPrompt(cmd, apiScopes) { - input := prompt.TextInput(apiScopes, "Scopes:", "Space-separated list of scopes.", false) - - if err := prompt.AskOne(input, &flags); err != nil { - return fmt.Errorf("An unexpected error occurred: %w", err) - } + if err := apiScopes.Ask(cmd, &inputs.ScopesString); err != nil { + return err } api := &management.ResourceServer{ - Name: &flags.Name, - Identifier: &flags.Identifier, + Name: &inputs.Name, + Identifier: &inputs.Identifier, } - if flags.Scopes != "" { - api.Scopes = apiScopesFor(flags.Scopes) + if len(inputs.ScopesString) > 0 { + api.Scopes = apiScopesFor(commaSeparatedStringToSlice(inputs.ScopesString)) + } else if len(inputs.Scopes) > 0 { + api.Scopes = apiScopesFor(inputs.Scopes) } err := ansi.Spinner("Creating API", func() error { @@ -194,7 +196,7 @@ auth0 apis create --name myapi --identifier http://my-api }) if err != nil { - return fmt.Errorf("An unexpected error occurred while attempting to create an API with name %s and identifier %s : %w", flags.Name, flags.Identifier, err) + return fmt.Errorf("An unexpected error occurred while attempting to create an API with name '%s' and identifier '%s': %w", inputs.Name, inputs.Identifier, err) } cli.renderer.ApiCreate(api) @@ -202,9 +204,9 @@ auth0 apis create --name myapi --identifier http://my-api }, } - apiName.RegisterString(cmd, &flags.Name) - apiIdentifier.RegisterString(cmd, &flags.Identifier) - cmd.Flags().StringVarP(&flags.Scopes, apiScopes, "s", "", "Space-separated list of scopes.") + apiName.RegisterString(cmd, &inputs.Name, "") + apiIdentifier.RegisterString(cmd, &inputs.Identifier, "") + apiScopes.RegisterStringSlice(cmd, &inputs.Scopes, nil) return cmd } @@ -213,7 +215,8 @@ func updateApiCmd(cli *cli) *cobra.Command { var inputs struct { ID string Name string - Scopes string + Scopes []string + ScopesString string } cmd := &cobra.Command{ @@ -246,12 +249,8 @@ auth0 apis update --name myapi return err } - if shouldPromptWhenFlagless(cmd, apiScopes) { - input := prompt.TextInput(apiScopes, "Scopes:", "Space-separated list of scopes.", false) - - if err := prompt.AskOne(input, &inputs); err != nil { - return fmt.Errorf("An unexpected error occurred: %w", err) - } + if err := apiScopes.AskU(cmd, &inputs.ScopesString); err != nil { + return err } api := &management.ResourceServer{} @@ -270,7 +269,11 @@ auth0 apis update --name myapi } if len(inputs.Scopes) == 0 { - api.Scopes = current.Scopes + if len(inputs.ScopesString) == 0 { + api.Scopes = current.Scopes + } else { + api.Scopes = apiScopesFor(commaSeparatedStringToSlice(inputs.ScopesString)) + } } else { api.Scopes = apiScopesFor(inputs.Scopes) } @@ -279,7 +282,7 @@ auth0 apis update --name myapi }) if err != nil { - return fmt.Errorf("An unexpected error occurred while trying to update an API with Id %s: %w", inputs.ID, err) + return fmt.Errorf("An unexpected error occurred while trying to update an API with Id '%s': %w", inputs.ID, err) } cli.renderer.ApiUpdate(api) @@ -287,8 +290,8 @@ auth0 apis update --name myapi }, } - apiName.RegisterStringU(cmd, &inputs.Name) - cmd.Flags().StringVarP(&inputs.Scopes, apiScopes, "s", "", "Space-separated list of scopes.") + apiName.RegisterStringU(cmd, &inputs.Name, "") + apiScopes.RegisterStringSliceU(cmd, &inputs.Scopes, nil) return cmd } @@ -333,7 +336,7 @@ auth0 apis delete return ansi.Spinner("Deleting API", func() error { err := cli.api.ResourceServer.Delete(inputs.ID) if err != nil { - return fmt.Errorf("An unexpected error occurred while attempting to delete an API with Id %s: %w", inputs.ID, err) + return fmt.Errorf("An unexpected error occurred while attempting to delete an API with Id '%s': %w", inputs.ID, err) } return nil }) @@ -383,7 +386,7 @@ auth0 apis scopes list }) if err != nil { - return fmt.Errorf("An unexpected error occurred while getting scopes for an API with Id %s: %w", inputs.ID, err) + return fmt.Errorf("An unexpected error occurred while getting scopes for an API with Id '%s': %w", inputs.ID, err) } cli.renderer.ScopesList(api.GetName(), api.Scopes) @@ -394,11 +397,10 @@ auth0 apis scopes list return cmd } -func apiScopesFor(scopes string) []*management.ResourceServerScope { - list := strings.Fields(scopes) +func apiScopesFor(scopes []string) []*management.ResourceServerScope { models := []*management.ResourceServerScope{} - for _, scope := range list { + for _, scope := range scopes { value := scope models = append(models, &management.ResourceServerScope{Value: &value}) } diff --git a/internal/cli/apps.go b/internal/cli/apps.go index 1a3137dde..224561e35 100644 --- a/internal/cli/apps.go +++ b/internal/cli/apps.go @@ -13,34 +13,82 @@ import ( ) const ( - appID = "id" - appType = "type" + appID = "id" ) var ( appName = Flag{ - Name: "Name", - LongForm: "name", - ShortForm: "n", - DefaultValue: "", - Help: "Name of the application.", - IsRequired: true, + Name: "Name", + LongForm: "name", + ShortForm: "n", + Help: "Name of the application.", + IsRequired: true, + } + appType = Flag{ + Name: "Type", + LongForm: "type", + ShortForm: "t", + Help: "Type of application:\n" + + "- native: mobile, desktop, CLI and smart device apps running natively.\n" + + "- spa (single page application): a JavaScript front-end app that uses an API.\n" + + "- regular: Traditional web app using redirects.\n" + + "- m2m (machine to machine): CLIs, daemons or services running on your backend.", + IsRequired: true, + } + appTypeOptions = []string{ + "Native", + "Single Page Web Application", + "Regular Web Application", + "Machine to Machine", } appDescription = Flag{ - Name: "Description", - LongForm: "description", - ShortForm: "d", - DefaultValue: "", - Help: "Description of the application. Max character count is 140.", - IsRequired: false, + Name: "Description", + LongForm: "description", + ShortForm: "d", + Help: "Description of the application. Max character count is 140.", + IsRequired: false, + } + appCallbacks = Flag{ + Name: "Callback URLs", + LongForm: "callbacks", + ShortForm: "c", + Help: "After the user authenticates we will only call back to any of these URLs. You can specify multiple valid URLs by comma-separating them (typically to handle different environments like QA or testing). Make sure to specify the protocol (https://) otherwise the callback may fail in some cases. With the exception of custom URI schemes for native apps, all callbacks should use protocol https://.", + IsRequired: false, + } + appOrigins = Flag{ + Name: "Allowed Origin URLs", + LongForm: "origins", + ShortForm: "o", + Help: "Comma-separated list of URLs allowed to make requests from JavaScript to Auth0 API (typically used with CORS). By default, all your callback URLs will be allowed. This field allows you to enter other origins if necessary. You can also use wildcards at the subdomain level (e.g., https://*.contoso.com). Query strings and hash information are not taken into account when validating these URLs.", + IsRequired: false, + } + appWebOrigins = Flag{ + Name: "Allowed Web Origin URLs", + LongForm: "web-origins", + ShortForm: "w", + Help: "Comma-separated list of allowed origins for use with Cross-Origin Authentication, Device Flow, and web message response mode.", + IsRequired: false, + } + appLogoutURLs = Flag{ + Name: "Allowed Logout URLs", + LongForm: "logout-urls", + ShortForm: "l", + Help: "Comma-separated list of URLs that are valid to redirect to after logout from Auth0. Wildcards are allowed for subdomains.", + IsRequired: false, } appAuthMethod = Flag{ - Name: "Auth Method", - LongForm: "auth-method", - ShortForm: "a", - DefaultValue: "", - Help: "Defines the requested authentication method for the token endpoint. Possible values are 'None' (public application without a client secret), 'Post' (application uses HTTP POST parameters) or 'Basic' (application uses HTTP Basic).", - IsRequired: false, + Name: "Auth Method", + LongForm: "auth-method", + ShortForm: "a", + Help: "Defines the requested authentication method for the token endpoint. Possible values are 'None' (public application without a client secret), 'Post' (application uses HTTP POST parameters) or 'Basic' (application uses HTTP Basic).", + IsRequired: false, + } + appGrants = Flag{ + Name: "Grants", + LongForm: "grants", + ShortForm: "g", + Help: "List of grant types supported for this application. Can include code, implicit, refresh-token, credentials, password, password-realm, mfa-oob, mfa-otp, mfa-recovery-code, and device-code.", + IsRequired: false, } ) @@ -218,20 +266,8 @@ auth0 apps create --name myapp --type [native|spa|regular|m2m] return err } - if shouldPrompt(cmd, appType) { - input := prompt.SelectInput( - appType, - "Type:", - "\n- Native: Mobile, desktop, CLI and smart device apps running natively."+ - "\n- Single Page Web Application: A JavaScript front-end app that uses an API."+ - "\n- Regular Web Application: Traditional web app using redirects."+ - "\n- Machine To Machine: CLIs, daemons or services running on your backend.", - []string{"Native", "Single Page Web Application", "Regular Web Application", "Machine to Machine"}, - true) - - if err := prompt.AskOne(input, &flags); err != nil { - return fmt.Errorf("An unexpected error occurred: %w", err) - } + if err := appType.Select(cmd, &flags.Type, appTypeOptions); err != nil { + return err } if err := appDescription.Ask(cmd, &flags.Description); err != nil { @@ -273,20 +309,15 @@ auth0 apps create --name myapp --type [native|spa|regular|m2m] }, } - appName.RegisterString(cmd, &flags.Name) - cmd.Flags().StringVarP(&flags.Type, "type", "t", "", "Type of application:\n"+ - "- native: mobile, desktop, CLI and smart device apps running natively.\n"+ - "- spa (single page application): a JavaScript front-end app that uses an API.\n"+ - "- regular: Traditional web app using redirects.\n"+ - "- m2m (machine to machine): CLIs, daemons or services running on your backend.") - appDescription.RegisterString(cmd, &flags.Description) - cmd.Flags().StringSliceVarP(&flags.Callbacks, "callbacks", "c", nil, "After the user authenticates we will only call back to any of these URLs. You can specify multiple valid URLs by comma-separating them (typically to handle different environments like QA or testing). Make sure to specify the protocol (https://) otherwise the callback may fail in some cases. With the exception of custom URI schemes for native apps, all callbacks should use protocol https://.") - cmd.Flags().StringSliceVarP(&flags.AllowedOrigins, "origins", "o", nil, "Comma-separated list of URLs allowed to make requests from JavaScript to Auth0 API (typically used with CORS). By default, all your callback URLs will be allowed. This field allows you to enter other origins if necessary. You can also use wildcards at the subdomain level (e.g., https://*.contoso.com). Query strings and hash information are not taken into account when validating these URLs.") - cmd.Flags().StringSliceVarP(&flags.AllowedWebOrigins, "web-origins", "w", nil, "Comma-separated list of allowed origins for use with Cross-Origin Authentication, Device Flow, and web message response mode.") - cmd.Flags().StringSliceVarP(&flags.AllowedLogoutURLs, "logout-urls", "l", nil, "Comma-separated list of URLs that are valid to redirect to after logout from Auth0. Wildcards are allowed for subdomains.") - appAuthMethod.RegisterString(cmd, &flags.AuthMethod) - cmd.Flags().StringSliceVarP(&flags.Grants, "grants", "g", nil, "List of grant types supported for this application. Can include code, implicit, refresh-token, credentials, password, password-realm, mfa-oob, mfa-otp, mfa-recovery-code, and device-code.") - mustRequireFlags(cmd, appType) + appName.RegisterString(cmd, &flags.Name, "") + appType.RegisterString(cmd, &flags.Type, "") + appDescription.RegisterString(cmd, &flags.Description, "") + appCallbacks.RegisterStringSlice(cmd, &flags.Callbacks, nil) + appOrigins.RegisterStringSlice(cmd, &flags.AllowedOrigins, nil) + appWebOrigins.RegisterStringSlice(cmd, &flags.AllowedWebOrigins, nil) + appLogoutURLs.RegisterStringSlice(cmd, &flags.AllowedLogoutURLs, nil) + appAuthMethod.RegisterString(cmd, &flags.AuthMethod, "") + appGrants.RegisterStringSlice(cmd, &flags.Grants, nil) return cmd } @@ -336,32 +367,16 @@ auth0 apps update --name myapp --type [native|spa|regular|m2m] return err } - if shouldPromptWhenFlagless(cmd, appType) { - input := prompt.SelectInput( - appType, - "Type:", - "\n- Native: Mobile, desktop, CLI and smart device apps running natively."+ - "\n- Single Page Web Application: A JavaScript front-end app that uses an API."+ - "\n- Regular Web Application: Traditional web app using redirects."+ - "\n- Machine To Machine: CLIs, daemons or services running on your backend.", - []string{"Native", "Single Page Web Application", "Regular Web Application", "Machine to Machine"}, - true) - - if err := prompt.AskOne(input, &inputs); err != nil { - return fmt.Errorf("An unexpected error occurred: %w", err) - } + if err := appType.SelectU(cmd, &inputs.Type, appTypeOptions); err != nil { + return err } if err := appDescription.AskU(cmd, &inputs.Description); err != nil { return err } - if shouldPromptWhenFlagless(cmd, "CallbacksString") { - input := prompt.TextInput("CallbacksString", "Callback URLs:", "Callback URLs of the application, comma-separated.", false) - - if err := prompt.AskOne(input, &inputs); err != nil { - return fmt.Errorf("An unexpected error occurred: %w", err) - } + if err := appCallbacks.AskU(cmd, &inputs.CallbacksString); err != nil { + return err } a := &management.Client{} @@ -445,19 +460,15 @@ auth0 apps update --name myapp --type [native|spa|regular|m2m] }, } - appName.RegisterStringU(cmd, &inputs.Name) - cmd.Flags().StringVarP(&inputs.Type, "type", "t", "", "Type of application:\n"+ - "- native: mobile, desktop, CLI and smart device apps running natively.\n"+ - "- spa (single page application): a JavaScript front-end app that uses an API.\n"+ - "- regular: Traditional web app using redirects.\n"+ - "- m2m (machine to machine): CLIs, daemons or services running on your backend.") - appDescription.RegisterStringU(cmd, &inputs.Description) - cmd.Flags().StringSliceVarP(&inputs.Callbacks, "callbacks", "c", nil, "After the user authenticates we will only call back to any of these URLs. You can specify multiple valid URLs by comma-separating them (typically to handle different environments like QA or testing). Make sure to specify the protocol (https://) otherwise the callback may fail in some cases. With the exception of custom URI schemes for native apps, all callbacks should use protocol https://.") - cmd.Flags().StringSliceVarP(&inputs.AllowedOrigins, "origins", "o", nil, "Comma-separated list of URLs allowed to make requests from JavaScript to Auth0 API (typically used with CORS). By default, all your callback URLs will be allowed. This field allows you to enter other origins if necessary. You can also use wildcards at the subdomain level (e.g., https://*.contoso.com). Query strings and hash information are not taken into account when validating these URLs.") - cmd.Flags().StringSliceVarP(&inputs.AllowedWebOrigins, "web-origins", "w", nil, "Comma-separated list of allowed origins for use with Cross-Origin Authentication, Device Flow, and web message response mode.") - cmd.Flags().StringSliceVarP(&inputs.AllowedLogoutURLs, "logout-urls", "l", nil, "Comma-separated list of URLs that are valid to redirect to after logout from Auth0. Wildcards are allowed for subdomains.") - appAuthMethod.RegisterStringU(cmd, &inputs.AuthMethod) - cmd.Flags().StringSliceVarP(&inputs.Grants, "grants", "g", nil, "List of grant types supported for this application. Can include code, implicit, refresh-token, credentials, password, password-realm, mfa-oob, mfa-otp, mfa-recovery-code, and device-code.") + appName.RegisterStringU(cmd, &inputs.Name, "") + appType.RegisterStringU(cmd, &inputs.Type, "") + appDescription.RegisterStringU(cmd, &inputs.Description, "") + appCallbacks.RegisterStringSliceU(cmd, &inputs.Callbacks, nil) + appOrigins.RegisterStringSliceU(cmd, &inputs.AllowedOrigins, nil) + appWebOrigins.RegisterStringSliceU(cmd, &inputs.AllowedWebOrigins, nil) + appLogoutURLs.RegisterStringSliceU(cmd, &inputs.AllowedLogoutURLs, nil) + appAuthMethod.RegisterStringU(cmd, &inputs.AuthMethod, "") + appGrants.RegisterStringSliceU(cmd, &inputs.Grants, nil) return cmd } diff --git a/internal/cli/flags.go b/internal/cli/flags.go index 42e98af8f..815cec836 100644 --- a/internal/cli/flags.go +++ b/internal/cli/flags.go @@ -8,58 +8,97 @@ import ( ) type Flag struct { - Name string - LongForm string - ShortForm string - DefaultValue string - Help string - IsRequired bool + Name string + LongForm string + ShortForm string + Help string + IsRequired bool } func (f *Flag) Ask(cmd *cobra.Command, value interface{}) error { - return ask(cmd, f, value, false) + return askInput(cmd, f, value, false) } func (f *Flag) AskU(cmd *cobra.Command, value interface{}) error { - return ask(cmd, f, value, true) + return askInput(cmd, f, value, true) } -func (f *Flag) RegisterString(cmd *cobra.Command, value *string) { - registerString(cmd, f, value, false) +func (f *Flag) Select(cmd *cobra.Command, value interface{}, options []string) error { + return selectInput(cmd, f, value, options, false) } -func (f *Flag) RegisterStringU(cmd *cobra.Command, value *string) { - registerString(cmd, f, value, true) +func (f *Flag) SelectU(cmd *cobra.Command, value interface{}, options []string) error { + return selectInput(cmd, f, value, options, true) } -func ask(cmd *cobra.Command, f *Flag, value interface{}, isUpdate bool) error { - var shouldAsk bool +func (f *Flag) RegisterString(cmd *cobra.Command, value *string, defaultValue string) { + registerString(cmd, f, value, defaultValue, false) +} - if isUpdate { - shouldAsk = shouldPromptWhenFlagless(cmd, f.LongForm) - } else { - shouldAsk = shouldPrompt(cmd, f.LongForm) +func (f *Flag) RegisterStringU(cmd *cobra.Command, value *string, defaultValue string) { + registerString(cmd, f, value, defaultValue, true) +} + +func (f *Flag) RegisterStringSlice(cmd *cobra.Command, value *[]string, defaultValue []string) { + registerStringSlice(cmd, f, value, defaultValue, false) +} + +func (f *Flag) RegisterStringSliceU(cmd *cobra.Command, value *[]string, defaultValue []string) { + registerStringSlice(cmd, f, value, defaultValue, true) +} + +func (f *Flag) label() string { + return fmt.Sprintf("%s:", f.Name) +} + +func askInput(cmd *cobra.Command, f *Flag, value interface{}, isUpdate bool) error { + if shouldAsk(cmd, f, isUpdate) { + input := prompt.TextInput("", f.label(), f.Help, f.IsRequired) + + if err := prompt.AskOne(input, value); err != nil { + return unexpectedError(err) + } } - if shouldAsk { - input := prompt.TextInput("", fmt.Sprintf("%s:", f.Name), f.Help, f.IsRequired) + return nil +} + +func selectInput(cmd *cobra.Command, f *Flag, value interface{}, options []string, isUpdate bool) error { + if shouldAsk(cmd, f, isUpdate) { + input := prompt.SelectInput("", f.label(), f.Help, options, f.IsRequired) if err := prompt.AskOne(input, value); err != nil { - return fmt.Errorf("An unexpected error occurred: %w", err) + return unexpectedError(err) } } return nil } -func registerString(cmd *cobra.Command, f *Flag, value *string, isUpdate bool) { - cmd.Flags().StringVarP(value, f.LongForm, f.ShortForm, f.DefaultValue, f.Help) +func registerString(cmd *cobra.Command, f *Flag, value *string, defaultValue string, isUpdate bool) { + cmd.Flags().StringVarP(value, f.LongForm, f.ShortForm, defaultValue, f.Help) + + if err := markFlagRequired(cmd, f, isUpdate); err != nil { + panic(unexpectedError(err)) // TODO: Handle + } +} + +func registerStringSlice(cmd *cobra.Command, f *Flag, value *[]string, defaultValue []string, isUpdate bool) { + cmd.Flags().StringSliceVarP(value, f.LongForm, f.ShortForm, defaultValue, f.Help) if err := markFlagRequired(cmd, f, isUpdate); err != nil { - panic(fmt.Errorf("An unexpected error occurred: %w", err)) // TODO: Handle + panic(unexpectedError(err)) // TODO: Handle } } +func shouldAsk(cmd *cobra.Command, f *Flag, isUpdate bool) bool { + if isUpdate { + return shouldPromptWhenFlagless(cmd, f.LongForm) + } + + return shouldPrompt(cmd, f.LongForm) +} + func markFlagRequired(cmd *cobra.Command, f *Flag, isUpdate bool) error { if f.IsRequired && !isUpdate { return cmd.MarkFlagRequired(f.LongForm) @@ -67,3 +106,7 @@ func markFlagRequired(cmd *cobra.Command, f *Flag, isUpdate bool) error { return nil } + +func unexpectedError(err error) error { + return fmt.Errorf("An unexpected error occurred: %w", err) +}