Tracks the shared proto rename that resolves the message-name collision between PluginAuthService and AccountService. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
151 lines
4.3 KiB
Go
151 lines
4.3 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"connectrpc.com/connect"
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.dev.alexdunmow.com/block/core/cmd/ninja/internal/creds"
|
|
"git.dev.alexdunmow.com/block/core/cmd/ninja/internal/orchclient"
|
|
v1 "git.dev.alexdunmow.com/block/core/internal/api/orchestrator/v1"
|
|
)
|
|
|
|
func newLoginCmd() *cobra.Command {
|
|
var host string
|
|
cmd := &cobra.Command{
|
|
Use: "login",
|
|
Short: "Authenticate against the orchestrator using device flow",
|
|
RunE: func(c *cobra.Command, _ []string) error {
|
|
if host == "" {
|
|
host, _ = c.Flags().GetString("host")
|
|
}
|
|
if host == "" {
|
|
host = "https://my.blockninjacms.com"
|
|
}
|
|
cli := orchclient.New(host, "")
|
|
ctx := context.Background()
|
|
start, err := cli.Auth.StartDevice(ctx, connect.NewRequest(&v1.StartDeviceRequest{
|
|
Scopes: []string{"plugin:read", "plugin:publish", "scope:admin"},
|
|
}))
|
|
if err != nil {
|
|
return fmt.Errorf("start device: %w", err)
|
|
}
|
|
fmt.Printf("Visit %s?user_code=%s to authorize.\n", start.Msg.VerificationUri, start.Msg.UserCode)
|
|
interval := time.Duration(start.Msg.IntervalSeconds) * time.Second
|
|
deadline := time.Now().Add(time.Duration(start.Msg.ExpiresInSeconds) * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
time.Sleep(interval)
|
|
poll, err := cli.Auth.PollDevice(ctx, connect.NewRequest(&v1.PollDeviceRequest{DeviceCode: start.Msg.DeviceCode}))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch poll.Msg.Status {
|
|
case "pending":
|
|
continue
|
|
case "approved":
|
|
cr, err := creds.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cr.DefaultHost = host
|
|
if cr.Hosts == nil {
|
|
cr.Hosts = map[string]creds.HostCreds{}
|
|
}
|
|
hc := creds.HostCreds{Token: poll.Msg.AccessToken}
|
|
authed := orchclient.New(host, hc.Token)
|
|
if err := selectActiveAccount(ctx, authed, &hc); err != nil {
|
|
return err
|
|
}
|
|
cr.Hosts[host] = hc
|
|
if err := cr.Save(); err != nil {
|
|
return err
|
|
}
|
|
fmt.Println("Logged in.")
|
|
return nil
|
|
case "expired":
|
|
return fmt.Errorf("device code expired; try again")
|
|
}
|
|
}
|
|
return fmt.Errorf("login timed out")
|
|
},
|
|
}
|
|
cmd.Flags().StringVar(&host, "host", "", "Orchestrator base URL")
|
|
return cmd
|
|
}
|
|
|
|
func newWhoamiCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "whoami",
|
|
Short: "Show the currently logged-in user",
|
|
RunE: func(c *cobra.Command, _ []string) error {
|
|
host, _ := c.Flags().GetString("host")
|
|
cr, err := creds.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resolvedHost, hc, err := cr.Resolve(host)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cli := orchclient.New(resolvedHost, hc.Token)
|
|
r, err := cli.Auth.Whoami(context.Background(), connect.NewRequest(&v1.WhoamiRequest{}))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s <%s> at %s\n", r.Msg.DisplayName, r.Msg.Email, resolvedHost)
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// selectActiveAccount fetches the user's accounts and writes the active one
|
|
// into hc. With 0 accounts it errors (the server contract guarantees every
|
|
// user has at least one). With 1 it auto-selects silently. With ≥2 it
|
|
// prompts interactively on stdin.
|
|
func selectActiveAccount(ctx context.Context, cli *orchclient.Client, hc *creds.HostCreds) error {
|
|
resp, err := cli.Auth.ListMyAccountsForCLI(ctx, connect.NewRequest(&v1.ListMyAccountsForCLIRequest{}))
|
|
if err != nil {
|
|
return fmt.Errorf("list accounts: %w", err)
|
|
}
|
|
accts := resp.Msg.Accounts
|
|
switch len(accts) {
|
|
case 0:
|
|
return fmt.Errorf("no accounts found for this user; contact support")
|
|
case 1:
|
|
hc.ActiveAccountID = accts[0].Id
|
|
hc.ActiveAccountSlug = accts[0].Slug
|
|
fmt.Printf("Active account: %s (%s)\n", accts[0].Slug, accts[0].Name)
|
|
return nil
|
|
}
|
|
chosen, err := pickAccountInteractive(bufio.NewScanner(os.Stdin), accts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hc.ActiveAccountID = chosen.Id
|
|
hc.ActiveAccountSlug = chosen.Slug
|
|
fmt.Printf("Active account: %s (%s)\n", chosen.Slug, chosen.Name)
|
|
return nil
|
|
}
|
|
|
|
func newLogoutCmd() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "logout",
|
|
Short: "Remove stored credentials",
|
|
RunE: func(c *cobra.Command, _ []string) error {
|
|
host, _ := c.Flags().GetString("host")
|
|
cr, err := creds.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resolvedHost, _, _ := cr.Resolve(host)
|
|
delete(cr.Hosts, resolvedHost)
|
|
return cr.Save()
|
|
},
|
|
}
|
|
}
|