diff --git a/internal/cli/apis.go b/internal/cli/apis.go index 871ef54da..a2abe1cc8 100644 --- a/internal/cli/apis.go +++ b/internal/cli/apis.go @@ -144,7 +144,7 @@ auth0 apis create --name myapi --identifier http://my-api if shouldPrompt(cmd, apiName) { input := prompt.TextInput( apiName, "Name:", - "Name of the API. You can change the API name later in the API settings.", + "Name of the API. You can change the name later in the API settings.", true) if err := prompt.AskOne(input, &flags); err != nil { @@ -265,7 +265,7 @@ auth0 apis update --id id --name myapi cmd.Flags().StringVarP(&flags.ID, apiID, "i", "", "ID of the API.") cmd.Flags().StringVarP(&flags.Name, apiName, "n", "", "Name of the API.") cmd.Flags().StringVarP(&flags.Scopes, apiScopes, "s", "", "Space-separated list of scopes.") - mustRequireFlags(cmd, apiID, apiName) + mustRequireFlags(cmd, apiID) return cmd } @@ -300,15 +300,9 @@ auth0 apis delete --id id } } - err := ansi.Spinner("Deleting API", func() error { + return ansi.Spinner("Deleting API", func() error { return cli.api.ResourceServer.Delete(flags.ID) }) - - if err != nil { - return err - } - - return nil }, } diff --git a/internal/cli/apps.go b/internal/cli/apps.go new file mode 100644 index 000000000..a69fe214a --- /dev/null +++ b/internal/cli/apps.go @@ -0,0 +1,344 @@ +package cli + +import ( + "fmt" + "strings" + + "github.com/auth0/auth0-cli/internal/ansi" + "github.com/auth0/auth0-cli/internal/auth0" + "github.com/auth0/auth0-cli/internal/prompt" + "github.com/spf13/cobra" + "gopkg.in/auth0.v5/management" +) + +const ( + appID = "id" + appName = "name" + appType = "type" + appDescription = "description" +) + +func appsCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "apps", + Short: "Manage resources for applications", + Aliases: []string{"clients"}, + } + + cmd.SetUsageTemplate(resourceUsageTemplate()) + cmd.AddCommand(listAppsCmd(cli)) + cmd.AddCommand(createAppCmd(cli)) + cmd.AddCommand(updateAppCmd(cli)) + cmd.AddCommand(deleteAppCmd(cli)) + + return cmd +} + +func listAppsCmd(cli *cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List your applications", + Long: `auth0 apps list +Lists your existing applications. To create one try: + + auth0 apps create +`, + RunE: func(cmd *cobra.Command, args []string) error { + var list *management.ClientList + err := ansi.Spinner("Loading applications", func() error { + var err error + list, err = cli.api.Client.List() + return err + }) + + if err != nil { + return err + } + + cli.renderer.ApplicationList(list.Clients) + return nil + }, + } + + return cmd +} + +func deleteAppCmd(cli *cli) *cobra.Command { + var flags struct { + ID string + } + + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete an application", + Long: `Delete an application: + +auth0 apps delete --id id +`, + PreRun: func(cmd *cobra.Command, args []string) { + prepareInteractivity(cmd) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if shouldPrompt(cmd, appID) { + input := prompt.TextInput(appID, "Id:", "Id of the application.", true) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + if !cli.force && canPrompt(cmd) { + if confirmed := prompt.Confirm("Are you sure you want to proceed?"); !confirmed { + return nil + } + } + + return ansi.Spinner("Deleting application", func() error { + return cli.api.Client.Delete(flags.ID) + }) + }, + } + + cmd.Flags().StringVarP(&flags.ID, appID, "i", "", "ID of the application.") + mustRequireFlags(cmd, appID) + + return cmd +} + +func createAppCmd(cli *cli) *cobra.Command { + var flags struct { + Name string + Type string + Description string + Callbacks []string + AuthMethod string + } + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new application", + Long: `Create a new application: + +auth0 apps create --name myapp --type [native|spa|regular|m2m] +`, + PreRun: func(cmd *cobra.Command, args []string) { + prepareInteractivity(cmd) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if shouldPrompt(cmd, appName) { + input := prompt.TextInput( + appName, "Name:", + "Name of the application. You can change the name later in the application settings.", + true) + + if err := prompt.AskOne(input, &flags); err != nil { + 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 err + } + } + + if shouldPrompt(cmd, appDescription) { + input := prompt.TextInput(appDescription, "Description:", "Description of the application.", false) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + a := &management.Client{ + Name: &flags.Name, + Description: &flags.Description, + AppType: auth0.String(apiTypeFor(flags.Type)), + Callbacks: apiCallbacksFor(flags.Callbacks), + TokenEndpointAuthMethod: apiAuthMethodFor(flags.AuthMethod), + } + + err := ansi.Spinner("Creating application", func() error { + return cli.api.Client.Create(a) + }) + + if err != nil { + return err + } + + // note: c is populated with the rest of the client fields by the API during creation. + revealClientSecret := auth0.StringValue(a.AppType) != "native" && auth0.StringValue(a.AppType) != "spa" + cli.renderer.ApplicationCreate(a, revealClientSecret) + + return nil + }, + } + + cmd.Flags().StringVarP(&flags.Name, "name", "n", "", "Name of the application.") + 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.") + cmd.Flags().StringVarP(&flags.Description, "description", "d", "", "Description of the application. Max character count is 140.") + 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().StringVar(&flags.AuthMethod, "auth-method", "", "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).") + mustRequireFlags(cmd, appName, appType) + + return cmd +} + +func updateAppCmd(cli *cli) *cobra.Command { + var flags struct { + ID string + Name string + Type string + Description string + Callbacks []string + AuthMethod string + } + + cmd := &cobra.Command{ + Use: "update", + Short: "Update a new application", + Long: `Update a new application: + +auth0 apps update --id id --name myapp --type [native|spa|regular|m2m] +`, + PreRun: func(cmd *cobra.Command, args []string) { + prepareInteractivity(cmd) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if shouldPrompt(cmd, appID) { + input := prompt.TextInput(appID, "Id:", "Id of the application.", true) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + if shouldPrompt(cmd, appName) { + input := prompt.TextInput(appName, "Name:", "Name of the application", true) + + if err := prompt.AskOne(input, &flags); err != nil { + 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 err + } + } + + if shouldPrompt(cmd, appDescription) { + input := prompt.TextInput(appDescription, "Description:", "Description of the application.", false) + + if err := prompt.AskOne(input, &flags); err != nil { + return err + } + } + + a := &management.Client{ + Name: &flags.Name, + Description: &flags.Description, + AppType: auth0.String(apiTypeFor(flags.Type)), + Callbacks: apiCallbacksFor(flags.Callbacks), + TokenEndpointAuthMethod: apiAuthMethodFor(flags.AuthMethod), + } + + err := ansi.Spinner("Updating application", func() error { + return cli.api.Client.Update(flags.ID, a) + }) + + if err != nil { + return err + } + + // note: c is populated with the rest of the client fields by the API during creation. + revealClientSecret := auth0.StringValue(a.AppType) != "native" && auth0.StringValue(a.AppType) != "spa" + cli.renderer.ApplicationUpdate(a, revealClientSecret) + + return nil + }, + } + + cmd.Flags().StringVarP(&flags.ID, appID, "i", "", "ID of the application.") + cmd.Flags().StringVarP(&flags.Name, "name", "n", "", "Name of the application.") + 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.") + cmd.Flags().StringVarP(&flags.Description, "description", "d", "", "Description of the application. Max character count is 140.") + 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().StringVar(&flags.AuthMethod, "auth-method", "", "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).") + mustRequireFlags(cmd, appID) + + return cmd +} + +func apiTypeFor(v string) string { + switch strings.ToLower(v) { + case "native": + return "native" + case "spa", "single page web application": + return "spa" + case "regular", "regular web application": + return "regular_web" + case "m2m", "machine to machine": + return "non_interactive" + + default: + return v + } +} + +func apiCallbacksFor(s []string) []interface{} { + res := make([]interface{}, len(s)) + for i, v := range s { + res[i] = v + } + return res +} + +func apiAuthMethodFor(v string) *string { + switch strings.ToLower(v) { + case "none": + return auth0.String("none") + case "post": + return auth0.String("client_secret_post") + case "basic": + return auth0.String("client_secret_basic") + default: + return nil + } +} + +func callbacksFor(s []interface{}) []string { + res := make([]string, len(s)) + for i, v := range s { + res[i] = fmt.Sprintf("%s", v) + } + return res +} diff --git a/internal/cli/clients_test.go b/internal/cli/apps_test.go similarity index 98% rename from internal/cli/clients_test.go rename to internal/cli/apps_test.go index 9157c12f6..c0624d33e 100644 --- a/internal/cli/clients_test.go +++ b/internal/cli/apps_test.go @@ -42,7 +42,7 @@ func TestClientsListCmd(t *testing.T) { api: &auth0.API{Client: clientAPI}, } - cmd := appsListCmd(cli) + cmd := listAppsCmd(cli) if err := cmd.Execute(); err != nil { t.Fatal(err) diff --git a/internal/cli/clients.go b/internal/cli/clients.go deleted file mode 100644 index 237e15c97..000000000 --- a/internal/cli/clients.go +++ /dev/null @@ -1,256 +0,0 @@ -package cli - -import ( - "fmt" - "strings" - - "github.com/AlecAivazis/survey/v2" - "github.com/auth0/auth0-cli/internal/ansi" - "github.com/auth0/auth0-cli/internal/auth0" - "github.com/spf13/cobra" - "gopkg.in/auth0.v5/management" -) - -func appsCmd(cli *cli) *cobra.Command { - cmd := &cobra.Command{ - Use: "apps", - Short: "Manage resources for apps", - } - - cmd.SetUsageTemplate(resourceUsageTemplate()) - cmd.AddCommand(appsListCmd(cli)) - cmd.AddCommand(appsCreateCmd(cli)) - cmd.AddCommand(appsDeleteCmd(cli)) - - return cmd -} - -func appsListCmd(cli *cli) *cobra.Command { - cmd := &cobra.Command{ - Use: "list", - Short: "List your existing apps", - Long: `auth0 apps list -Lists your existing apps. To create one try: - - auth0 apps create -`, - RunE: func(cmd *cobra.Command, args []string) error { - var list *management.ClientList - err := ansi.Spinner("Loading apps", func() error { - var err error - list, err = cli.api.Client.List() - return err - }) - - if err != nil { - return err - } - - cli.renderer.ClientList(list.Clients) - return nil - }, - } - - return cmd -} - -func appsDeleteCmd(cli *cli) *cobra.Command { - var flags struct { - AppID string - } - cmd := &cobra.Command{ - Use: "delete", - Short: "Delete an existing app", - Long: `auth0 apps delete --name appName - -auth0 apps delete --app-id myapp - -`, - RunE: func(cmd *cobra.Command, args []string) error { - - if !cmd.Flags().Changed("app-id") { - qs := []*survey.Question{ - { - Name: "AppID", - Prompt: &survey.Input{ - Message: "AppID:", - Default: "My App", - Help: "ID of the application to delete.", - }, - }, - } - - err := survey.Ask(qs, &flags) - if err != nil { - return err - } - } - c := &management.Client{ - ClientID: &flags.AppID, - } - - return ansi.Spinner("Deleting application", func() error { - return cli.api.Client.Delete(*c.ClientID) - }) - }, - } - - cmd.Flags().StringVarP(&flags.AppID, "app-id", "i", "", "app-id of the app.") - - return cmd -} - -func appsCreateCmd(cli *cli) *cobra.Command { - var flags struct { - Name string - AppType string - Description string - Callbacks []string - TokenEndpointAuthMethod string - } - cmd := &cobra.Command{ - Use: "create", - Short: "Create a new application", - Long: `Create a new application: - -auth0 apps create --name myapp --type [native|spa|regular|m2m] - -- supported application type: - `, - RunE: func(cmd *cobra.Command, args []string) error { - // todo(jfatta) on non-interactive the cmd should fail on missing mandatory args (name, type) - if !cmd.Flags().Changed("name") { - qs := []*survey.Question{ - { - Name: "Name", - Prompt: &survey.Input{ - Message: "Name:", - Default: "My App", - Help: "Name of the client (also known as application). You can change the application name later in the application settings.", - }, - }, - } - - err := survey.Ask(qs, &flags) - if err != nil { - return err - } - } - - if !cmd.Flags().Changed("type") { - qs := []*survey.Question{ - { - Name: "AppType", - Prompt: &survey.Select{ - Message: "Type:", - Help: "\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.", - Options: []string{"Native", "Single Page Web Application", "Regular Web Application", "Machine to Machine"}, - }, - }, - } - err := survey.Ask(qs, &flags) - if err != nil { - return err - } - } - - if !cmd.Flags().Changed("description") { - qs := []*survey.Question{ - { - Name: "Description", - Prompt: &survey.Input{ - Message: "Description:", - Help: "A free text description of the application.", - }, - }, - } - err := survey.Ask(qs, &flags) - if err != nil { - return err - } - } - - c := &management.Client{ - Name: &flags.Name, - Description: &flags.Description, - AppType: auth0.String(apiAppTypeFor(flags.AppType)), - Callbacks: apiCallbacksFor(flags.Callbacks), - TokenEndpointAuthMethod: apiTokenEndpointAuthMethodFor(flags.TokenEndpointAuthMethod), - } - - err := ansi.Spinner("Creating client", func() error { - return cli.api.Client.Create(c) - }) - - if err != nil { - return err - } - - // note: c is populated with the rest of the client fields by the API during creation. - revealClientSecret := auth0.StringValue(c.AppType) != "native" && auth0.StringValue(c.AppType) != "spa" - cli.renderer.ClientCreate(c, revealClientSecret) - return nil - }, - } - cmd.Flags().StringVarP(&flags.Name, "name", "n", "", "Name of the client.") - cmd.Flags().StringVarP(&flags.AppType, "type", "t", "", "Type of the client:\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.") - cmd.Flags().StringVarP(&flags.Description, "description", "d", "", "A free text description of the application. Max character count is 140.") - 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().StringVar(&flags.TokenEndpointAuthMethod, "auth-method", "", "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).") - - return cmd -} - -func apiAppTypeFor(v string) string { - switch strings.ToLower(v) { - case "native": - return "native" - case "spa", "single page web application": - return "spa" - case "regular", "regular web application": - return "regular_web" - case "m2m", "machine to machine": - return "non_interactive" - - default: - return v - } -} - -func apiCallbacksFor(s []string) []interface{} { - res := make([]interface{}, len(s)) - for i, v := range s { - res[i] = v - } - return res - -} - -func apiTokenEndpointAuthMethodFor(v string) *string { - switch strings.ToLower(v) { - case "none": - return auth0.String("none") - case "post": - return auth0.String("client_secret_post") - case "basic": - return auth0.String("client_secret_basic") - default: - return nil - } -} - -func callbacksFor(s []interface{}) []string { - res := make([]string, len(s)) - for i, v := range s { - res[i] = fmt.Sprintf("%s", v) - } - return res -} diff --git a/internal/display/clients.go b/internal/display/clients.go index 25391fcb1..ee20b36e3 100644 --- a/internal/display/clients.go +++ b/internal/display/clients.go @@ -53,8 +53,8 @@ func (v *clientView) AsTableRow() []string { } -func (r *Renderer) ClientList(clients []*management.Client) { - r.Heading(ansi.Bold(r.Tenant), "clients\n") +func (r *Renderer) ApplicationList(clients []*management.Client) { + r.Heading(ansi.Bold(r.Tenant), "applications\n") var res []View for _, c := range clients { if auth0.StringValue(c.Name) == deprecatedAppName { @@ -72,8 +72,8 @@ func (r *Renderer) ClientList(clients []*management.Client) { r.Results(res) } -func (r *Renderer) ClientCreate(client *management.Client, revealSecrets bool) { - r.Heading(ansi.Bold(r.Tenant), "client created\n") +func (r *Renderer) ApplicationCreate(client *management.Client, revealSecrets bool) { + r.Heading(ansi.Bold(r.Tenant), "application created\n") // note(jfatta): list and create uses the same view for now, // eventually we might want to show different columns for each command: @@ -91,6 +91,21 @@ func (r *Renderer) ClientCreate(client *management.Client, revealSecrets bool) { r.Infof("\nQuickstarts: %s", quickstartsURIFor(client.AppType)) } +func (r *Renderer) ApplicationUpdate(client *management.Client, revealSecrets bool) { + r.Heading(ansi.Bold(r.Tenant), "application updated\n") + + v := &clientView{ + revealSecret: revealSecrets, + Name: auth0.StringValue(client.Name), + Type: appTypeFor(client.AppType), + ClientID: auth0.StringValue(client.ClientID), + ClientSecret: auth0.StringValue(client.ClientSecret), + Callbacks: callbacksFor(client.Callbacks), + } + + r.Results([]View{v}) +} + // TODO(cyx): determine if there's a better way to filter this out. const deprecatedAppName = "All Applications"