Skip to content

Commit d7b61c7

Browse files
authored
fix: unify REST API under /api/apps/:provider (#15)
1 parent eabe16a commit d7b61c7

File tree

3 files changed

+96
-60
lines changed

3 files changed

+96
-60
lines changed

cmd/onecli/apps.go

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import (
1111

1212
// AppsCmd is the `onecli apps` command group.
1313
type AppsCmd struct {
14-
List AppsListCmd `cmd:"" help:"List all app connections."`
15-
Connect AppsConnectCmd `cmd:"" help:"Connect an OAuth app (e.g. Google)."`
16-
Disconnect AppsDisconnectCmd `cmd:"" help:"Disconnect an app."`
14+
List AppsListCmd `cmd:"" help:"List all apps with config and connection status."`
15+
Configure AppsConfigureCmd `cmd:"" help:"Save OAuth credentials (BYOC) for a provider."`
16+
Remove AppsRemoveCmd `cmd:"" help:"Remove OAuth credentials for a provider."`
17+
Disconnect AppsDisconnectCmd `cmd:"" help:"Disconnect an app connection."`
1718
}
1819

1920
// AppsListCmd is `onecli apps list`.
@@ -41,87 +42,97 @@ func (c *AppsListCmd) Run(out *output.Writer) error {
4142
return out.WriteFiltered(apps, c.Fields)
4243
}
4344

44-
// AppsConnectCmd is `onecli apps connect`.
45-
type AppsConnectCmd struct {
46-
Provider string `required:"" help:"Provider name (e.g. 'google')."`
45+
// AppsConfigureCmd is `onecli apps configure`.
46+
type AppsConfigureCmd struct {
47+
Provider string `required:"" help:"Provider name (e.g. 'github', 'gmail')."`
4748
ClientID string `required:"" name:"client-id" help:"OAuth client ID."`
4849
ClientSecret string `required:"" name:"client-secret" help:"OAuth client secret."`
4950
Json string `optional:"" help:"Raw JSON payload. Overrides individual flags."`
5051
DryRun bool `optional:"" name:"dry-run" help:"Validate the request without executing it."`
5152
}
5253

53-
const docsBaseURL = "https://onecli.sh/docs/guides/credential-stubs"
54-
55-
// connectResult wraps the API response with onboarding guidance as structured fields.
56-
type connectResult struct {
57-
api.App
58-
NextSteps string `json:"next_steps"`
59-
DocsURL string `json:"docs_url"`
60-
}
61-
62-
func (c *AppsConnectCmd) Run(out *output.Writer) error {
63-
var input api.ConnectAppInput
54+
func (c *AppsConfigureCmd) Run(out *output.Writer) error {
55+
var input api.ConfigAppInput
6456
if c.Json != "" {
6557
if err := json.Unmarshal([]byte(c.Json), &input); err != nil {
6658
return fmt.Errorf("invalid JSON payload: %w", err)
6759
}
6860
} else {
69-
input = api.ConnectAppInput{
70-
Provider: c.Provider,
61+
input = api.ConfigAppInput{
7162
ClientID: c.ClientID,
7263
ClientSecret: c.ClientSecret,
7364
}
7465
}
7566

76-
if err := validate.ResourceID(input.Provider); err != nil {
67+
if err := validate.ResourceID(c.Provider); err != nil {
7768
return fmt.Errorf("invalid provider: %w", err)
7869
}
7970

8071
if c.DryRun {
8172
preview := map[string]string{
82-
"provider": input.Provider,
73+
"provider": c.Provider,
8374
"clientId": input.ClientID,
8475
"clientSecret": "***",
8576
}
86-
return out.WriteDryRun("Would connect app", preview)
77+
return out.WriteDryRun("Would configure app", preview)
8778
}
8879

8980
client, err := newClient()
9081
if err != nil {
9182
return err
9283
}
93-
app, err := client.ConnectApp(newContext(), input)
94-
if err != nil {
84+
if err := client.ConfigureApp(newContext(), c.Provider, input); err != nil {
9585
return err
9686
}
9787

98-
result := connectResult{
99-
App: *app,
100-
NextSteps: "Create local credential stub files using 'onecli-managed' as placeholder for all secrets. The OneCLI gateway handles real OAuth token exchange at request time.",
101-
DocsURL: docsBaseURL + "/" + input.Provider + ".md",
88+
return out.Write(map[string]string{
89+
"status": "configured",
90+
"provider": c.Provider,
91+
})
92+
}
93+
94+
// AppsRemoveCmd is `onecli apps remove`.
95+
type AppsRemoveCmd struct {
96+
Provider string `required:"" help:"Provider name (e.g. 'github', 'gmail')."`
97+
DryRun bool `optional:"" name:"dry-run" help:"Validate the request without executing it."`
98+
}
99+
100+
func (c *AppsRemoveCmd) Run(out *output.Writer) error {
101+
if err := validate.ResourceID(c.Provider); err != nil {
102+
return fmt.Errorf("invalid provider: %w", err)
102103
}
103-
return out.Write(result)
104+
if c.DryRun {
105+
return out.WriteDryRun("Would remove app config", map[string]string{"provider": c.Provider})
106+
}
107+
client, err := newClient()
108+
if err != nil {
109+
return err
110+
}
111+
if err := client.UnconfigureApp(newContext(), c.Provider); err != nil {
112+
return err
113+
}
114+
return out.Write(map[string]string{"status": "removed", "provider": c.Provider})
104115
}
105116

106117
// AppsDisconnectCmd is `onecli apps disconnect`.
107118
type AppsDisconnectCmd struct {
108-
ID string `required:"" help:"ID of the app connection to disconnect."`
109-
DryRun bool `optional:"" name:"dry-run" help:"Validate the request without executing it."`
119+
Provider string `required:"" help:"Provider name (e.g. 'github', 'gmail')."`
120+
DryRun bool `optional:"" name:"dry-run" help:"Validate the request without executing it."`
110121
}
111122

112123
func (c *AppsDisconnectCmd) Run(out *output.Writer) error {
113-
if err := validate.ResourceID(c.ID); err != nil {
114-
return fmt.Errorf("invalid app ID: %w", err)
124+
if err := validate.ResourceID(c.Provider); err != nil {
125+
return fmt.Errorf("invalid provider: %w", err)
115126
}
116127
if c.DryRun {
117-
return out.WriteDryRun("Would disconnect app", map[string]string{"id": c.ID})
128+
return out.WriteDryRun("Would disconnect app", map[string]string{"provider": c.Provider})
118129
}
119130
client, err := newClient()
120131
if err != nil {
121132
return err
122133
}
123-
if err := client.DisconnectApp(newContext(), c.ID); err != nil {
134+
if err := client.DisconnectApp(newContext(), c.Provider); err != nil {
124135
return err
125136
}
126-
return out.Write(map[string]string{"status": "disconnected", "id": c.ID})
137+
return out.Write(map[string]string{"status": "disconnected", "provider": c.Provider})
127138
}

cmd/onecli/help.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,17 @@ func (cmd *HelpCmd) Run(out *output.Writer) error {
7979
{Name: "secrets delete", Description: "Delete a secret.", Args: []ArgInfo{
8080
{Name: "--id", Required: true, Description: "ID of the secret to delete."},
8181
}},
82-
{Name: "apps list", Description: "List all app connections."},
83-
{Name: "apps connect", Description: "Connect an OAuth app.", Args: []ArgInfo{
84-
{Name: "--provider", Required: true, Description: "Provider name (e.g. 'google')."},
82+
{Name: "apps list", Description: "List all apps with config and connection status."},
83+
{Name: "apps configure", Description: "Save OAuth credentials (BYOC) for a provider.", Args: []ArgInfo{
84+
{Name: "--provider", Required: true, Description: "Provider name (e.g. 'github', 'gmail')."},
8585
{Name: "--client-id", Required: true, Description: "OAuth client ID."},
8686
{Name: "--client-secret", Required: true, Description: "OAuth client secret."},
8787
}},
88-
{Name: "apps disconnect", Description: "Disconnect an app.", Args: []ArgInfo{
89-
{Name: "--id", Required: true, Description: "ID of the app connection to disconnect."},
88+
{Name: "apps remove", Description: "Remove OAuth credentials for a provider.", Args: []ArgInfo{
89+
{Name: "--provider", Required: true, Description: "Provider name (e.g. 'github', 'gmail')."},
90+
}},
91+
{Name: "apps disconnect", Description: "Disconnect an app connection.", Args: []ArgInfo{
92+
{Name: "--provider", Required: true, Description: "Provider name (e.g. 'github', 'gmail')."},
9093
}},
9194
{Name: "rules list", Description: "List all policy rules."},
9295
{Name: "rules create", Description: "Create a new policy rule.", Args: []ArgInfo{

internal/api/apps.go

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,37 @@ import (
66
"net/http"
77
)
88

9-
// App represents an app connection returned by the API.
9+
// App represents an app from the unified /api/apps listing.
1010
type App struct {
11-
ID string `json:"id"`
12-
Provider string `json:"provider"`
13-
Status string `json:"status"`
14-
Docs string `json:"docs,omitempty"`
15-
CreatedAt string `json:"createdAt"`
11+
ID string `json:"id"`
12+
Name string `json:"name"`
13+
Available bool `json:"available"`
14+
ConnectionType string `json:"connectionType"`
15+
Configurable bool `json:"configurable"`
16+
Config *AppConfig `json:"config"`
17+
Connection *AppConnection `json:"connection"`
1618
}
1719

18-
// ConnectAppInput is the request body for connecting an app.
19-
type ConnectAppInput struct {
20-
Provider string `json:"provider"`
20+
// AppConfig is the BYOC credential configuration status.
21+
type AppConfig struct {
22+
HasCredentials bool `json:"hasCredentials"`
23+
Enabled bool `json:"enabled"`
24+
}
25+
26+
// AppConnection is the OAuth connection status.
27+
type AppConnection struct {
28+
Status string `json:"status"`
29+
Scopes []string `json:"scopes"`
30+
ConnectedAt string `json:"connectedAt"`
31+
}
32+
33+
// ConfigAppInput is the request body for saving BYOC credentials.
34+
type ConfigAppInput struct {
2135
ClientID string `json:"clientId"`
2236
ClientSecret string `json:"clientSecret"`
2337
}
2438

25-
// ListApps returns all app connections for the authenticated user.
39+
// ListApps returns all apps with their config and connection status.
2640
func (c *Client) ListApps(ctx context.Context) ([]App, error) {
2741
var apps []App
2842
if err := c.do(ctx, http.MethodGet, "/api/apps", nil, &apps); err != nil {
@@ -31,18 +45,26 @@ func (c *Client) ListApps(ctx context.Context) ([]App, error) {
3145
return apps, nil
3246
}
3347

34-
// ConnectApp creates a new app connection.
35-
func (c *Client) ConnectApp(ctx context.Context, input ConnectAppInput) (*App, error) {
36-
var app App
37-
if err := c.do(ctx, http.MethodPost, "/api/apps", input, &app); err != nil {
38-
return nil, fmt.Errorf("connecting app: %w", err)
48+
// ConfigureApp saves BYOC credentials for a provider.
49+
func (c *Client) ConfigureApp(ctx context.Context, provider string, input ConfigAppInput) error {
50+
var resp SuccessResponse
51+
if err := c.do(ctx, http.MethodPost, "/api/apps/"+provider+"/config", input, &resp); err != nil {
52+
return fmt.Errorf("configuring app: %w", err)
3953
}
40-
return &app, nil
54+
return nil
55+
}
56+
57+
// UnconfigureApp removes BYOC credentials for a provider.
58+
func (c *Client) UnconfigureApp(ctx context.Context, provider string) error {
59+
if err := c.do(ctx, http.MethodDelete, "/api/apps/"+provider+"/config", nil, nil); err != nil {
60+
return fmt.Errorf("unconfiguring app: %w", err)
61+
}
62+
return nil
4163
}
4264

43-
// DisconnectApp removes an app connection by ID.
44-
func (c *Client) DisconnectApp(ctx context.Context, id string) error {
45-
if err := c.do(ctx, http.MethodDelete, "/api/apps/"+id, nil, nil); err != nil {
65+
// DisconnectApp removes the OAuth connection for a provider.
66+
func (c *Client) DisconnectApp(ctx context.Context, provider string) error {
67+
if err := c.do(ctx, http.MethodDelete, "/api/apps/"+provider+"/connection", nil, nil); err != nil {
4668
return fmt.Errorf("disconnecting app: %w", err)
4769
}
4870
return nil

0 commit comments

Comments
 (0)