Skip to content

Commit 0320e72

Browse files
chore: oauth facility
1 parent fdc6798 commit 0320e72

8 files changed

Lines changed: 182 additions & 86 deletions

File tree

README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,13 @@ createos --help
157157
| `createos open` | Open a project's live URL in your browser |
158158
| `createos scale` | Adjust replicas and resources for an environment |
159159

160-
### OAuth
160+
### OAuth Clients
161161

162-
| Command | Description |
163-
| ------------------------------- | ------------------------ |
164-
| `createos oauth clients list` | List OAuth clients |
165-
| `createos oauth clients create` | Create a new OAuth client |
162+
| Command | Description |
163+
| -------------------------------------- | ------------------------------------- |
164+
| `createos oauth-clients list` | List your OAuth clients |
165+
| `createos oauth-clients create` | Create a new OAuth client |
166+
| `createos oauth-clients instructions` | Show setup instructions for a client |
166167

167168
### Users
168169

@@ -184,6 +185,16 @@ createos --help
184185
All commands that would normally show an interactive prompt accept flags instead:
185186

186187
```bash
188+
# OAuth clients
189+
createos oauth-clients instructions --client <client-id>
190+
createos oauth-clients create \
191+
--name "My App" \
192+
--redirect-uri https://myapp.com/callback \
193+
--app-url https://myapp.com \
194+
--policy-url https://myapp.com/privacy \
195+
--tos-url https://myapp.com/tos \
196+
--logo-url https://myapp.com/logo.png
197+
187198
# Projects
188199
createos projects get --project <id>
189200

cmd/oauth/clients.go

Lines changed: 0 additions & 16 deletions
This file was deleted.

cmd/oauth/create.go

Lines changed: 106 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,39 @@
1+
// Package oauth provides OAuth client management commands.
12
package oauth
23

34
import (
45
"fmt"
6+
"strings"
57

68
"github.com/pterm/pterm"
79
"github.com/urfave/cli/v2"
810

911
"github.com/NodeOps-app/createos-cli/internal/api"
12+
"github.com/NodeOps-app/createos-cli/internal/terminal"
1013
)
1114

1215
func newCreateCommand() *cli.Command {
1316
return &cli.Command{
1417
Name: "create",
1518
Usage: "Create an OAuth client",
16-
Description: "Starts an interactive flow to create an OAuth client.\n\n" +
17-
" You'll be asked for the client name, redirect URIs, visibility, and policy URLs.",
19+
Description: "Creates an OAuth client interactively, or non-interactively via flags.\n\n" +
20+
" Non-interactive example:\n" +
21+
" createos oauth-clients create \\\n" +
22+
" --name \"My App\" \\\n" +
23+
" --redirect-uri https://myapp.com/callback \\\n" +
24+
" --app-url https://myapp.com \\\n" +
25+
" --policy-url https://myapp.com/privacy \\\n" +
26+
" --tos-url https://myapp.com/tos \\\n" +
27+
" --logo-url https://myapp.com/logo.png",
28+
Flags: []cli.Flag{
29+
&cli.StringFlag{Name: "name", Usage: "Client name"},
30+
&cli.StringSliceFlag{Name: "redirect-uri", Usage: "Redirect URI (repeatable)"},
31+
&cli.BoolFlag{Name: "public", Usage: "Create a public client (no client secret)"},
32+
&cli.StringFlag{Name: "app-url", Usage: "Application URL"},
33+
&cli.StringFlag{Name: "policy-url", Usage: "Privacy policy URL"},
34+
&cli.StringFlag{Name: "tos-url", Usage: "Terms of service URL"},
35+
&cli.StringFlag{Name: "logo-url", Usage: "Logo URL"},
36+
},
1837
Action: func(c *cli.Context) error {
1938
client, err := getClient(c)
2039
if err != nil {
@@ -29,51 +48,95 @@ func newCreateCommand() *cli.Command {
2948
return fmt.Errorf("you already have the maximum number of OAuth clients (4)\n\n To continue, delete one through the dashboard or reuse an existing client")
3049
}
3150

32-
name, err := promptRequiredText("Client name", validateClientName)
33-
if err != nil {
34-
return err
35-
}
51+
isInteractive := terminal.IsInteractive()
52+
hasFlags := c.IsSet("name") || c.IsSet("redirect-uri")
3653

37-
redirectURIs, err := promptRedirectURIs()
38-
if err != nil {
39-
return err
40-
}
54+
var name string
55+
var redirectURIs []string
56+
var public bool
57+
var clientURI, policyURI, tosURI, logoURI string
4158

42-
public, err := pterm.DefaultInteractiveConfirm.
43-
WithDefaultText("Should this be a public client? Public clients do not use a client secret").
44-
WithDefaultValue(false).
45-
Show()
46-
if err != nil {
47-
return fmt.Errorf("could not read client type: %w", err)
48-
}
59+
if isInteractive && !hasFlags {
60+
name, err = promptRequiredText("Client name", validateClientName)
61+
if err != nil {
62+
return err
63+
}
4964

50-
clientURI, err := promptRequiredText("Application URL", validateURI)
51-
if err != nil {
52-
return err
53-
}
54-
policyURI, err := promptRequiredText("Privacy policy URL", validateURI)
55-
if err != nil {
56-
return err
57-
}
58-
tosURI, err := promptRequiredText("Terms of service URL", validateURI)
59-
if err != nil {
60-
return err
61-
}
62-
logoURI, err := promptRequiredText("Logo URL", validateURI)
63-
if err != nil {
64-
return err
65-
}
65+
redirectURIs, err = promptRedirectURIs()
66+
if err != nil {
67+
return err
68+
}
6669

67-
confirm, err := pterm.DefaultInteractiveConfirm.
68-
WithDefaultText("Create this OAuth client now?").
69-
WithDefaultValue(true).
70-
Show()
71-
if err != nil {
72-
return fmt.Errorf("could not read confirmation: %w", err)
73-
}
74-
if !confirm {
75-
fmt.Println("Cancelled. Your OAuth client was not created.")
76-
return nil
70+
public, err = pterm.DefaultInteractiveConfirm.
71+
WithDefaultText("Should this be a public client? Public clients do not use a client secret").
72+
WithDefaultValue(false).
73+
Show()
74+
if err != nil {
75+
return fmt.Errorf("could not read client type: %w", err)
76+
}
77+
78+
clientURI, err = promptRequiredText("Application URL", validateURI)
79+
if err != nil {
80+
return err
81+
}
82+
policyURI, err = promptRequiredText("Privacy policy URL", validateURI)
83+
if err != nil {
84+
return err
85+
}
86+
tosURI, err = promptRequiredText("Terms of service URL", validateURI)
87+
if err != nil {
88+
return err
89+
}
90+
logoURI, err = promptRequiredText("Logo URL", validateURI)
91+
if err != nil {
92+
return err
93+
}
94+
95+
confirm, err := pterm.DefaultInteractiveConfirm.
96+
WithDefaultText("Create this OAuth client now?").
97+
WithDefaultValue(true).
98+
Show()
99+
if err != nil {
100+
return fmt.Errorf("could not read confirmation: %w", err)
101+
}
102+
if !confirm {
103+
fmt.Println("Cancelled. Your OAuth client was not created.")
104+
return nil
105+
}
106+
} else {
107+
name = strings.TrimSpace(c.String("name"))
108+
if err := validateClientName(name); err != nil {
109+
return fmt.Errorf("--name: %w", err)
110+
}
111+
112+
redirectURIs = c.StringSlice("redirect-uri")
113+
if len(redirectURIs) == 0 {
114+
return fmt.Errorf("at least one --redirect-uri is required")
115+
}
116+
for _, u := range redirectURIs {
117+
if err := validateURI(u); err != nil {
118+
return fmt.Errorf("--redirect-uri %q: %w", u, err)
119+
}
120+
}
121+
122+
public = c.Bool("public")
123+
124+
clientURI = c.String("app-url")
125+
if err := validateURI(clientURI); err != nil {
126+
return fmt.Errorf("--app-url: %w", err)
127+
}
128+
policyURI = c.String("policy-url")
129+
if err := validateURI(policyURI); err != nil {
130+
return fmt.Errorf("--policy-url: %w", err)
131+
}
132+
tosURI = c.String("tos-url")
133+
if err := validateURI(tosURI); err != nil {
134+
return fmt.Errorf("--tos-url: %w", err)
135+
}
136+
logoURI = c.String("logo-url")
137+
if err := validateURI(logoURI); err != nil {
138+
return fmt.Errorf("--logo-url: %w", err)
139+
}
77140
}
78141

79142
clientID, err := client.CreateOAuthClient(api.CreateOAuthClientInput{
@@ -94,10 +157,6 @@ func newCreateCommand() *cli.Command {
94157
detail, err := client.GetOAuthClient(clientID)
95158
if err == nil && detail != nil {
96159
printInstructions(c.String("api-url"), detail)
97-
} else {
98-
fmt.Println()
99-
pterm.Println(pterm.Gray(" Hint: To see setup instructions for this client, run:"))
100-
pterm.Println(pterm.Gray(" createos oauth clients instructions " + clientID))
101160
}
102161

103162
return nil

cmd/oauth/helpers.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/urfave/cli/v2"
1010

1111
"github.com/NodeOps-app/createos-cli/internal/api"
12+
"github.com/NodeOps-app/createos-cli/internal/terminal"
1213
)
1314

1415
func getClient(c *cli.Context) (*api.APIClient, error) {
@@ -19,6 +20,50 @@ func getClient(c *cli.Context) (*api.APIClient, error) {
1920
return client, nil
2021
}
2122

23+
// resolveOAuthClientID resolves a client ID from flag, arg, or interactive select.
24+
func resolveOAuthClientID(c *cli.Context, apiClient *api.APIClient) (string, error) {
25+
if id := c.String("client"); id != "" {
26+
return id, nil
27+
}
28+
if c.NArg() > 0 {
29+
return c.Args().First(), nil
30+
}
31+
if !terminal.IsInteractive() {
32+
return "", fmt.Errorf("please provide a client ID\n\n Example:\n createos oauth-clients %s --client <client-id>", c.Command.Name)
33+
}
34+
return pickOAuthClient(apiClient)
35+
}
36+
37+
func pickOAuthClient(apiClient *api.APIClient) (string, error) {
38+
clients, err := apiClient.ListOAuthClients()
39+
if err != nil {
40+
return "", err
41+
}
42+
if len(clients) == 0 {
43+
return "", fmt.Errorf("you don't have any OAuth clients yet — run 'createos oauth-clients create' to create one")
44+
}
45+
if len(clients) == 1 {
46+
return clients[0].ID, nil
47+
}
48+
options := make([]string, len(clients))
49+
for i, cl := range clients {
50+
options[i] = cl.Name
51+
}
52+
selected, err := pterm.DefaultInteractiveSelect.
53+
WithOptions(options).
54+
WithDefaultText("Select an OAuth client").
55+
Show()
56+
if err != nil {
57+
return "", fmt.Errorf("could not read selection: %w", err)
58+
}
59+
for i, opt := range options {
60+
if opt == selected {
61+
return clients[i].ID, nil
62+
}
63+
}
64+
return "", fmt.Errorf("no client selected")
65+
}
66+
2267
func promptRequiredText(prompt string, validate func(string) error) (string, error) {
2368
for {
2469
value, err := pterm.DefaultInteractiveTextInput.Show(prompt)
@@ -171,6 +216,4 @@ func printInstructions(apiURL string, client *api.OAuthClientDetail) {
171216
fmt.Println("This CLI can fetch your client details, but the CreateOS API does not currently return the auth/token endpoints directly.")
172217
fmt.Println("The URLs above match the tested oauth-client-test setup. If your deployment uses a different identity host, replace them there.")
173218
fmt.Println()
174-
pterm.Println(pterm.Gray(" Hint: To review app consents and revoke them, run:"))
175-
pterm.Println(pterm.Gray(" createos users oauth-consents list"))
176219
}

cmd/oauth/instructions.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,31 @@
11
package oauth
22

33
import (
4-
"fmt"
5-
64
"github.com/urfave/cli/v2"
75
)
86

97
func newInstructionsCommand() *cli.Command {
108
return &cli.Command{
119
Name: "instructions",
1210
Usage: "Show setup instructions for an OAuth client",
13-
ArgsUsage: "<client-id>",
11+
ArgsUsage: "[client-id]",
1412
Description: "Shows the client details you need after registration, including redirect URIs,\n" +
1513
"whether the client is public, and the user info endpoint.",
14+
Flags: []cli.Flag{
15+
&cli.StringFlag{Name: "client", Usage: "OAuth client ID"},
16+
},
1617
Action: func(c *cli.Context) error {
17-
if c.NArg() == 0 {
18-
return fmt.Errorf("please provide a client ID\n\n To see your client IDs, run:\n createos oauth clients list")
18+
apiClient, err := getClient(c)
19+
if err != nil {
20+
return err
1921
}
2022

21-
client, err := getClient(c)
23+
clientID, err := resolveOAuthClientID(c, apiClient)
2224
if err != nil {
2325
return err
2426
}
2527

26-
detail, err := client.GetOAuthClient(c.Args().First())
28+
detail, err := apiClient.GetOAuthClient(clientID)
2729
if err != nil {
2830
return err
2931
}

cmd/oauth/list.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ func newListCommand() *cli.Command {
2424

2525
if len(clients) == 0 {
2626
fmt.Println("You don't have any OAuth clients yet.")
27-
fmt.Println()
28-
pterm.Println(pterm.Gray(" Hint: To create one, run:"))
29-
pterm.Println(pterm.Gray(" createos oauth clients create"))
3027
return nil
3128
}
3229

@@ -45,8 +42,6 @@ func newListCommand() *cli.Command {
4542
return err
4643
}
4744
fmt.Println()
48-
pterm.Println(pterm.Gray(" Hint: To see setup instructions for a client, run:"))
49-
pterm.Println(pterm.Gray(" createos oauth clients instructions <client-id>"))
5045
return nil
5146
},
5247
}

cmd/oauth/oauth.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package oauth
22

33
import "github.com/urfave/cli/v2"
44

5-
// NewOAuthCommand creates the oauth command with subcommands.
5+
// NewOAuthCommand creates the oauth-clients command with subcommands.
66
func NewOAuthCommand() *cli.Command {
77
return &cli.Command{
8-
Name: "oauth",
8+
Name: "oauth-clients",
99
Usage: "Manage OAuth clients",
1010
Subcommands: []*cli.Command{
11-
newClientsCommand(),
11+
newListCommand(),
12+
newCreateCommand(),
13+
newInstructionsCommand(),
1214
},
1315
}
1416
}

cmd/root/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func NewApp() *cli.App {
127127
fmt.Println(" environments Manage project environments")
128128
fmt.Println(" init Link this directory to a CreateOS project")
129129
fmt.Println(" logout Sign out from CreateOS")
130-
fmt.Println(" oauth Manage OAuth clients")
130+
fmt.Println(" oauth-clients Manage OAuth clients")
131131
fmt.Println(" open Open project URL or dashboard in browser")
132132
fmt.Println(" projects Manage projects")
133133
fmt.Println(" scale Adjust replicas and resources")

0 commit comments

Comments
 (0)