diff --git a/cmd/ninja/cmd/login.go b/cmd/ninja/cmd/login.go index 35d22d1..d456e53 100644 --- a/cmd/ninja/cmd/login.go +++ b/cmd/ninja/cmd/login.go @@ -57,9 +57,13 @@ func newLoginCmd() *cobra.Command { 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 + if applyPolledAccount(&hc, poll.Msg.AccountId, poll.Msg.AccountSlug) { + fmt.Printf("Active account: %s\n", hc.ActiveAccountSlug) + } else { + 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 { @@ -103,6 +107,18 @@ func newWhoamiCmd() *cobra.Command { } } +// applyPolledAccount copies the account chosen on the device-approval page +// into hc. Returns false when the server sent no account (older orchestrator, +// or an approval without a selection) — the stdin picker should run instead. +func applyPolledAccount(hc *creds.HostCreds, accountID, accountSlug string) bool { + if accountID == "" { + return false + } + hc.ActiveAccountID = accountID + hc.ActiveAccountSlug = accountSlug + return true +} + // 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 diff --git a/cmd/ninja/cmd/login_test.go b/cmd/ninja/cmd/login_test.go new file mode 100644 index 0000000..3037b23 --- /dev/null +++ b/cmd/ninja/cmd/login_test.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "testing" + + "git.dev.alexdunmow.com/block/cli/internal/creds" +) + +func TestApplyPolledAccount_Set(t *testing.T) { + hc := creds.HostCreds{Token: "tok"} + if !applyPolledAccount(&hc, "acct-id-1", "blockninja") { + t.Fatal("want true when server supplies an account") + } + if hc.ActiveAccountID != "acct-id-1" || hc.ActiveAccountSlug != "blockninja" { + t.Fatalf("hc = %+v, want active account set", hc) + } +} + +func TestApplyPolledAccount_EmptyFallsThrough(t *testing.T) { + hc := creds.HostCreds{Token: "tok"} + if applyPolledAccount(&hc, "", "") { + t.Fatal("want false when server sent no account (older orchestrator)") + } + if hc.ActiveAccountID != "" || hc.ActiveAccountSlug != "" { + t.Fatalf("hc = %+v, want untouched", hc) + } +} diff --git a/internal/api/orchestrator/v1/plugin_registry.pb.go b/internal/api/orchestrator/v1/plugin_registry.pb.go index 3f60b33..a99eb68 100644 --- a/internal/api/orchestrator/v1/plugin_registry.pb.go +++ b/internal/api/orchestrator/v1/plugin_registry.pb.go @@ -5326,6 +5326,8 @@ type PollDeviceResponse struct { state protoimpl.MessageState `protogen:"open.v1"` AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + AccountId string `protobuf:"bytes,3,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` + AccountSlug string `protobuf:"bytes,4,opt,name=account_slug,json=accountSlug,proto3" json:"account_slug,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5374,9 +5376,24 @@ func (x *PollDeviceResponse) GetStatus() string { return "" } +func (x *PollDeviceResponse) GetAccountId() string { + if x != nil { + return x.AccountId + } + return "" +} + +func (x *PollDeviceResponse) GetAccountSlug() string { + if x != nil { + return x.AccountSlug + } + return "" +} + type ApproveDeviceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` UserCode string `protobuf:"bytes,1,opt,name=user_code,json=userCode,proto3" json:"user_code,omitempty"` + AccountId string `protobuf:"bytes,2,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5418,6 +5435,13 @@ func (x *ApproveDeviceRequest) GetUserCode() string { return "" } +func (x *ApproveDeviceRequest) GetAccountId() string { + if x != nil { + return x.AccountId + } + return "" +} + type ApproveDeviceResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -6290,12 +6314,17 @@ const file_orchestrator_v1_plugin_registry_proto_rawDesc = "" + "\x12expires_in_seconds\x18\x05 \x01(\x05R\x10expiresInSeconds\"4\n" + "\x11PollDeviceRequest\x12\x1f\n" + "\vdevice_code\x18\x01 \x01(\tR\n" + - "deviceCode\"O\n" + + "deviceCode\"\x91\x01\n" + "\x12PollDeviceResponse\x12!\n" + "\faccess_token\x18\x01 \x01(\tR\vaccessToken\x12\x16\n" + - "\x06status\x18\x02 \x01(\tR\x06status\"3\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12\x1d\n" + + "\n" + + "account_id\x18\x03 \x01(\tR\taccountId\x12!\n" + + "\faccount_slug\x18\x04 \x01(\tR\vaccountSlug\"R\n" + "\x14ApproveDeviceRequest\x12\x1b\n" + - "\tuser_code\x18\x01 \x01(\tR\buserCode\"\x17\n" + + "\tuser_code\x18\x01 \x01(\tR\buserCode\x12\x1d\n" + + "\n" + + "account_id\x18\x02 \x01(\tR\taccountId\"\x17\n" + "\x15ApproveDeviceResponse\"0\n" + "\x11DenyDeviceRequest\x12\x1b\n" + "\tuser_code\x18\x01 \x01(\tR\buserCode\"\x14\n" + diff --git a/proto/orchestrator/v1/plugin_registry.proto b/proto/orchestrator/v1/plugin_registry.proto index 42a5efb..ba70b5c 100644 --- a/proto/orchestrator/v1/plugin_registry.proto +++ b/proto/orchestrator/v1/plugin_registry.proto @@ -650,9 +650,14 @@ message PollDeviceRequest { string device_code = 1; } message PollDeviceResponse { string access_token = 1; string status = 2; + string account_id = 3; + string account_slug = 4; } -message ApproveDeviceRequest { string user_code = 1; } +message ApproveDeviceRequest { + string user_code = 1; + string account_id = 2; +} message ApproveDeviceResponse {} message DenyDeviceRequest { string user_code = 1; }