From 2422515259db10ba1cc3fd80fa9e21a81ac23017 Mon Sep 17 00:00:00 2001 From: Matt Evans Date: Fri, 14 Jun 2019 17:20:47 +1000 Subject: [PATCH 1/6] Added new provider: shopify --- examples/main.go | 3 + providers/shopify/scopes.go | 51 +++++++++ providers/shopify/session.go | 102 +++++++++++++++++ providers/shopify/session_test.go | 48 ++++++++ providers/shopify/shopify.go | 184 ++++++++++++++++++++++++++++++ providers/shopify/shopify_test.go | 56 +++++++++ 6 files changed, 444 insertions(+) create mode 100644 providers/shopify/scopes.go create mode 100755 providers/shopify/session.go create mode 100755 providers/shopify/session_test.go create mode 100755 providers/shopify/shopify.go create mode 100755 providers/shopify/shopify_test.go diff --git a/examples/main.go b/examples/main.go index 6162d83e7..11f87d0a5 100644 --- a/examples/main.go +++ b/examples/main.go @@ -46,6 +46,7 @@ import ( "github.com/markbates/goth/providers/openidConnect" "github.com/markbates/goth/providers/paypal" "github.com/markbates/goth/providers/salesforce" + "github.com/markbates/goth/providers/shopify" "github.com/markbates/goth/providers/slack" "github.com/markbates/goth/providers/soundcloud" "github.com/markbates/goth/providers/spotify" @@ -122,6 +123,7 @@ func main() { yandex.New(os.Getenv("YANDEX_KEY"), os.Getenv("YANDEX_SECRET"), "http://localhost:3000/auth/yandex/callback"), nextcloud.NewCustomisedDNS(os.Getenv("NEXTCLOUD_KEY"), os.Getenv("NEXTCLOUD_SECRET"), "http://localhost:3000/auth/nextcloud/callback", os.Getenv("NEXTCLOUD_URL")), gitea.New(os.Getenv("GITEA_KEY"), os.Getenv("GITEA_SECRET"), "http://localhost:3000/auth/gitea/callback"), + shopify.New(os.Getenv("SHOPIFY_KEY"), os.Getenv("SHOPIFY_SECRET"), os.Getenv("SHOPIFY_STORE_NAME"), "http://localhost:3000/auth/shopify/callback", shopify.ScopeReadCustomers, shopify.ScopeReadOrders), ) // OpenID Connect is based on OpenID Connect Auto Discovery URL (https://openid.net/specs/openid-connect-discovery-1_0-17.html) @@ -149,6 +151,7 @@ func main() { m["gitlab"] = "Gitlab" m["google"] = "Google" m["gplus"] = "Google Plus" + m["shopify"] = "Shopify" m["soundcloud"] = "SoundCloud" m["spotify"] = "Spotify" m["steam"] = "Steam" diff --git a/providers/shopify/scopes.go b/providers/shopify/scopes.go new file mode 100644 index 000000000..d29997f9b --- /dev/null +++ b/providers/shopify/scopes.go @@ -0,0 +1,51 @@ +// Package shopify implements the OAuth2 protocol for authenticating users through Shopify. +// This package can be used as a reference implementation of an OAuth2 provider for Goth. +package shopify + +// Define scopes supported by Shopify. +// See: https://help.shopify.com/en/api/getting-started/authentication/oauth/scopes#authenticated-access-scopes +const ( + ScopeReadContent = "read_content" + ScopeWriteContent = "write_content" + ScopeReadThemes = "read_themes" + ScopeWriteThemes = "write_themes" + ScopeReadProducts = "read_products" + ScopeWriteProducts = "write_products" + ScopeReadProductListings = "read_product_listings" + ScopeReadCustomers = "read_customers" + ScopeWriteCustomers = "write_customers" + ScopeReadOrders = "read_orders" + ScopeWriteOrders = "write_orders" + ScopeReadDrafOrders = "read_draft_orders" + ScopeWriteDrafOrders = "write_draft_orders" + ScopeReadInventory = "read_inventory" + ScopeWriteInventory = "write_inventory" + ScopeReadLocations = "read_locations" + ScopeReadScriptTags = "read_script_tags" + ScopeWriteScriptTags = "write_script_tags" + ScopeReadFulfillments = "read_fulfillments" + ScopeWriteFulfillments = "write_fulfillments" + ScopeReadShipping = "read_shipping" + ScopeWriteShipping = "write_shipping" + ScopeReadAnalytics = "read_analytics" + ScopeReadUsers = "read_users" + ScopeWriteUsers = "write_users" + ScopeReadCheckouts = "read_checkouts" + ScopeWriteCheckouts = "write_checkouts" + ScopeReadReports = "read_reports" + ScopeWriteReports = "write_reports" + ScopeReadPriceRules = "read_price_rules" + ScopeWritePriceRules = "write_price_rules" + ScopeMarketingEvents = "read_marketing_events" + ScopeWriteMarketingEvents = "write_marketing_events" + ScopeReadResourceFeedbacks = "read_resource_feedbacks" + ScopeWriteResourceFeedbacks = "write_resource_feedbacks" + ScopeReadShopifyPaymentsPayouts = "read_shopify_payments_payouts" + ScopeReadShopifyPaymentsDisputes = "read_shopify_payments_disputes" + + // Special: + // Grants access to all orders rather than the default window of 60 days worth of orders. + // This OAuth scope is used in conjunction with read_orders, or write_orders. You need to request + // this scope from your Partner Dashboard before adding it to your app. + ScopeReadAllOrders = "read_all_orders" +) diff --git a/providers/shopify/session.go b/providers/shopify/session.go new file mode 100755 index 000000000..8a4b0c6ee --- /dev/null +++ b/providers/shopify/session.go @@ -0,0 +1,102 @@ +package shopify + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "regexp" + "strings" + "time" + + "github.com/markbates/goth" +) + +const ( + shopifyHostnameRegex = `^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$` +) + +// Session stores data during the auth process with Shopify. +type Session struct { + AuthURL string + AccessToken string + Hostname string + HMAC string + ExpiresAt time.Time +} + +var _ goth.Session = &Session{} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Shopify provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(goth.NoAuthUrlErrorMessage) + } + return s.AuthURL, nil +} + +// Authorize the session with Shopify and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + // Validate the incoming HMAC is valid. + // See: https://help.shopify.com/en/api/getting-started/authentication/oauth#verification + digest := fmt.Sprintf( + "code=%s&shop=%s&state=%s×tamp=%s", + params.Get("code"), + params.Get("shop"), + params.Get("state"), + params.Get("timestamp"), + ) + h := hmac.New(sha256.New, []byte(os.Getenv("SHOPIFY_SECRET"))) + h.Write([]byte(digest)) + sha := hex.EncodeToString(h.Sum(nil)) + + // Ensure our HMAC hash's match. + if sha != params.Get("hmac") { + return "", errors.New("Invalid HMAC received") + } + + // Validate the hostname matches what we're expecting. + // See: https://help.shopify.com/en/api/getting-started/authentication/oauth#step-3-confirm-installation + re := regexp.MustCompile(shopifyHostnameRegex) + if !re.MatchString(params.Get("shop")) { + return "", errors.New("Invalid hostname received") + } + + // Make the exchange for an access token. + p := provider.(*Provider) + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) + if err != nil { + return "", err + } + + // Ensure it's valid. + if !token.Valid() { + return "", errors.New("Invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.Hostname = params.Get("hostname") + s.HMAC = params.Get("hmac") + + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession wil unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + s := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(s) + return s, err +} diff --git a/providers/shopify/session_test.go b/providers/shopify/session_test.go new file mode 100755 index 000000000..85ea9adc0 --- /dev/null +++ b/providers/shopify/session_test.go @@ -0,0 +1,48 @@ +package shopify_test + +import ( + "testing" + + "github.com/markbates/goth" + "github.com/markbates/goth/providers/shopify" + "github.com/stretchr/testify/assert" +) + +func Test_Implements_Session(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &shopify.Session{} + + a.Implements((*goth.Session)(nil), s) +} + +func Test_GetAuthURL(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &shopify.Session{} + + _, err := s.GetAuthURL() + a.Error(err) + + s.AuthURL = "/foo" + + url, _ := s.GetAuthURL() + a.Equal(url, "/foo") +} + +func Test_ToJSON(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &shopify.Session{} + + data := s.Marshal() + a.Equal(data, `{"AuthURL":"","AccessToken":"","Hostname":"","HMAC":"","ExpiresAt":"0001-01-01T00:00:00Z"}`) +} + +func Test_String(t *testing.T) { + t.Parallel() + a := assert.New(t) + s := &shopify.Session{} + + a.Equal(s.String(), s.Marshal()) +} diff --git a/providers/shopify/shopify.go b/providers/shopify/shopify.go new file mode 100755 index 000000000..5797d0acd --- /dev/null +++ b/providers/shopify/shopify.go @@ -0,0 +1,184 @@ +// Package shopify implements the OAuth2 protocol for authenticating users through Shopify. +// This package can be used as a reference implementation of an OAuth2 provider for Goth. +package shopify + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "strconv" + + "fmt" + + "github.com/markbates/goth" + "golang.org/x/oauth2" +) + +const ( + providerName = "shopify" + + // URL protocol and subdomain will be populated by newConfig(). + authURL = "myshopify.com/admin/oauth/authorize" + tokenURL = "myshopify.com/admin/oauth/access_token" + endpointProfile = "myshopify.com/admin/api/2019-04/shop.json" +) + +// Provider is the implementation of `goth.Provider` for accessing Shopify. +type Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + providerName string + shopName string +} + +// New creates a new Shopify provider and sets up important connection details. +// You should always call `shopify.New` to get a new provider. Never try to +// create one manually. +func New(clientKey, secret, shopName, callbackURL string, scopes ...string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: providerName, + shopName: shopName, + } + p.config = newConfig(p, scopes) + return p +} + +// Client is HTTP client to be used in all fetch operations. +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +// Debug is a no-op for the Shopify package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks Shopify for an authentication end-point. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + return &Session{ + AuthURL: p.config.AuthCodeURL(state), + }, nil +} + +// RefreshTokenAvailable refresh token is provided by auth provider or not +func (p *Provider) RefreshTokenAvailable() bool { + return false +} + +// RefreshToken get new access token based on the refresh token +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + return nil, errors.New("Refresh token is not provided by Shopify") +} + +// FetchUser will go to Uber and access basic information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + s := session.(*Session) + shop := goth.User{ + AccessToken: s.AccessToken, + Provider: p.Name(), + } + + if shop.AccessToken == "" { + // Data is not yet retrieved since accessToken is still empty. + return shop, fmt.Errorf("%s cannot get shop information without accessToken", p.providerName) + } + + // Build the request. + req, err := http.NewRequest("GET", fmt.Sprintf("https://%s.%s", p.shopName, endpointProfile), nil) + if err != nil { + return shop, err + } + req.Header.Set("X-Shopify-Access-Token", s.AccessToken) + + // Execute the request. + resp, err := p.Client().Do(req) + if err != nil { + if resp != nil { + resp.Body.Close() + } + return shop, err + } + defer resp.Body.Close() + + // Check our response status. + if resp.StatusCode != http.StatusOK { + return shop, fmt.Errorf("%s responded with a %d trying to fetch shop information", p.providerName, resp.StatusCode) + } + + // Parse response. + return shop, shopFromReader(resp.Body, &shop) +} + +func shopFromReader(r io.Reader, shop *goth.User) error { + rsp := struct { + Shop struct { + ID int64 `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + City string `json:"city"` + Country string `json:"country"` + ShopOwner string `json:"shop_owner"` + MyShopifyDomain string `json:"myshopify_domain"` + PlanDisplayName string `json:"plan_display_name"` + } `json:"shop"` + }{} + + err := json.NewDecoder(r).Decode(&rsp) + if err != nil { + return err + } + + shop.UserID = strconv.Itoa(int(rsp.Shop.ID)) + shop.Name = rsp.Shop.Name + shop.Email = rsp.Shop.Email + shop.Description = fmt.Sprintf("%s (%s)", rsp.Shop.MyShopifyDomain, rsp.Shop.PlanDisplayName) + shop.Location = fmt.Sprintf("%s, %s", rsp.Shop.City, rsp.Shop.Country) + shop.AvatarURL = "Not provided by the Shopify API" + shop.NickName = "Not provided by the Shopify API" + + return nil +} + +func newConfig(p *Provider, scopes []string) *oauth2.Config { + c := &oauth2.Config{ + ClientID: p.ClientKey, + ClientSecret: p.Secret, + RedirectURL: p.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("https://%s.%s", p.shopName, authURL), + TokenURL: fmt.Sprintf("https://%s.%s", p.shopName, tokenURL), + }, + Scopes: []string{}, + } + + if len(scopes) > 0 { + for i, scope := range scopes { + // Shopify require comma separated scopes. + s := fmt.Sprintf("%s,", scope) + if i == len(scopes)+1 { + s = scope + } + c.Scopes = append(c.Scopes, s) + } + } else { + // Default to a read customers scope. + c.Scopes = append(c.Scopes, ScopeReadCustomers) + } + + return c +} diff --git a/providers/shopify/shopify_test.go b/providers/shopify/shopify_test.go new file mode 100755 index 000000000..48e7825c4 --- /dev/null +++ b/providers/shopify/shopify_test.go @@ -0,0 +1,56 @@ +package shopify_test + +import ( + "fmt" + "os" + "testing" + + "github.com/markbates/goth" + "github.com/markbates/goth/providers/shopify" + "github.com/stretchr/testify/assert" +) + +func Test_New(t *testing.T) { + t.Parallel() + a := assert.New(t) + p := provider() + + a.Equal(p.ClientKey, os.Getenv("SHOPIFY_KEY")) + a.Equal(p.Secret, os.Getenv("SHOPIFY_SECRET")) + a.Equal(p.CallbackURL, "/foo") +} + +func Test_Implements_Provider(t *testing.T) { + t.Parallel() + a := assert.New(t) + a.Implements((*goth.Provider)(nil), provider()) +} + +func Test_BeginAuth(t *testing.T) { + t.Parallel() + a := assert.New(t) + p := provider() + session, err := p.BeginAuth("test_state") + s := session.(*shopify.Session) + a.NoError(err) + a.Contains(s.AuthURL, fmt.Sprintf("https://%s.myshopify.com/admin/oauth/authorize", os.Getenv("SHOPIFY_STORE_NAME"))) +} + +func Test_SessionFromJSON(t *testing.T) { + aurl := fmt.Sprintf("https://%s.myshopify.com/admin/oauth/authorize", os.Getenv("SHOPIFY_STORE_NAME")) + + t.Parallel() + a := assert.New(t) + + p := provider() + session, err := p.UnmarshalSession(fmt.Sprintf(`{"AuthURL":"%s","AccessToken":"1234567890"}"`, aurl)) + a.NoError(err) + + s := session.(*shopify.Session) + a.Equal(s.AuthURL, aurl) + a.Equal(s.AccessToken, "1234567890") +} + +func provider() *shopify.Provider { + return shopify.New(os.Getenv("SHOPIFY_KEY"), os.Getenv("SHOPIFY_SECRET"), os.Getenv("SHOPIFY_STORE_NAME"), "/foo") +} From e7d5aec3aae341d4fcad9cf8ccfd7b6c600b0414 Mon Sep 17 00:00:00 2001 From: Matt Evans Date: Fri, 14 Jun 2019 17:22:47 +1000 Subject: [PATCH 2/6] Updated comments --- providers/shopify/shopify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/shopify/shopify.go b/providers/shopify/shopify.go index 5797d0acd..ac2410584 100755 --- a/providers/shopify/shopify.go +++ b/providers/shopify/shopify.go @@ -85,7 +85,7 @@ func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { return nil, errors.New("Refresh token is not provided by Shopify") } -// FetchUser will go to Uber and access basic information about the user. +// FetchUser will go to Shopify and access basic information about the user. func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { s := session.(*Session) shop := goth.User{ From 25a18f8e4aa0e52af234df9ae7110d2dd3031b1b Mon Sep 17 00:00:00 2001 From: Matt Evans Date: Fri, 14 Jun 2019 17:23:24 +1000 Subject: [PATCH 3/6] Updated comments --- providers/shopify/scopes.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/providers/shopify/scopes.go b/providers/shopify/scopes.go index d29997f9b..52c8e52db 100644 --- a/providers/shopify/scopes.go +++ b/providers/shopify/scopes.go @@ -1,5 +1,3 @@ -// Package shopify implements the OAuth2 protocol for authenticating users through Shopify. -// This package can be used as a reference implementation of an OAuth2 provider for Goth. package shopify // Define scopes supported by Shopify. From ee4749c50bf4986d75146b4d440383619c0d4b51 Mon Sep 17 00:00:00 2001 From: Matt Evans Date: Fri, 14 Jun 2019 17:30:09 +1000 Subject: [PATCH 4/6] Updated readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d347d1532..a35746e4e 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ $ go get github.com/markbates/goth * OpenID Connect (auto discovery) * Paypal * SalesForce +* Shopify * Slack * Soundcloud * Spotify From 36d84fc3649a90c15a037d68c409e8a8a4e70ff5 Mon Sep 17 00:00:00 2001 From: Matt Evans Date: Fri, 14 Jun 2019 22:37:49 +1000 Subject: [PATCH 5/6] Added the ability to switch out the shop name, when working with multiple shops - updated tests to reflect --- examples/main.go | 2 +- providers/shopify/shopify.go | 11 +++++++++-- providers/shopify/shopify_test.go | 13 ++++++------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/examples/main.go b/examples/main.go index 11f87d0a5..7743ad0bf 100644 --- a/examples/main.go +++ b/examples/main.go @@ -123,7 +123,7 @@ func main() { yandex.New(os.Getenv("YANDEX_KEY"), os.Getenv("YANDEX_SECRET"), "http://localhost:3000/auth/yandex/callback"), nextcloud.NewCustomisedDNS(os.Getenv("NEXTCLOUD_KEY"), os.Getenv("NEXTCLOUD_SECRET"), "http://localhost:3000/auth/nextcloud/callback", os.Getenv("NEXTCLOUD_URL")), gitea.New(os.Getenv("GITEA_KEY"), os.Getenv("GITEA_SECRET"), "http://localhost:3000/auth/gitea/callback"), - shopify.New(os.Getenv("SHOPIFY_KEY"), os.Getenv("SHOPIFY_SECRET"), os.Getenv("SHOPIFY_STORE_NAME"), "http://localhost:3000/auth/shopify/callback", shopify.ScopeReadCustomers, shopify.ScopeReadOrders), + shopify.New(os.Getenv("SHOPIFY_KEY"), os.Getenv("SHOPIFY_SECRET"), "http://localhost:3000/auth/shopify/callback", shopify.ScopeReadCustomers, shopify.ScopeReadOrders), ) // OpenID Connect is based on OpenID Connect Auto Discovery URL (https://openid.net/specs/openid-connect-discovery-1_0-17.html) diff --git a/providers/shopify/shopify.go b/providers/shopify/shopify.go index ac2410584..dd6de05ff 100755 --- a/providers/shopify/shopify.go +++ b/providers/shopify/shopify.go @@ -38,13 +38,12 @@ type Provider struct { // New creates a new Shopify provider and sets up important connection details. // You should always call `shopify.New` to get a new provider. Never try to // create one manually. -func New(clientKey, secret, shopName, callbackURL string, scopes ...string) *Provider { +func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { p := &Provider{ ClientKey: clientKey, Secret: secret, CallbackURL: callbackURL, providerName: providerName, - shopName: shopName, } p.config = newConfig(p, scopes) return p @@ -65,6 +64,14 @@ func (p *Provider) SetName(name string) { p.providerName = name } +// SetShopName is to update the shopify shop name, needed when interfacing with different shops. +func (p *Provider) SetShopName(name string) { + p.shopName = name + + // Reparse config with the new shop name. + p.config = newConfig(p, p.config.Scopes) +} + // Debug is a no-op for the Shopify package. func (p *Provider) Debug(debug bool) {} diff --git a/providers/shopify/shopify_test.go b/providers/shopify/shopify_test.go index 48e7825c4..393a887c3 100755 --- a/providers/shopify/shopify_test.go +++ b/providers/shopify/shopify_test.go @@ -1,7 +1,6 @@ package shopify_test import ( - "fmt" "os" "testing" @@ -33,24 +32,24 @@ func Test_BeginAuth(t *testing.T) { session, err := p.BeginAuth("test_state") s := session.(*shopify.Session) a.NoError(err) - a.Contains(s.AuthURL, fmt.Sprintf("https://%s.myshopify.com/admin/oauth/authorize", os.Getenv("SHOPIFY_STORE_NAME"))) + a.Contains(s.AuthURL, "https://test-shop.myshopify.com/admin/oauth/authorize") } func Test_SessionFromJSON(t *testing.T) { - aurl := fmt.Sprintf("https://%s.myshopify.com/admin/oauth/authorize", os.Getenv("SHOPIFY_STORE_NAME")) - t.Parallel() a := assert.New(t) p := provider() - session, err := p.UnmarshalSession(fmt.Sprintf(`{"AuthURL":"%s","AccessToken":"1234567890"}"`, aurl)) + session, err := p.UnmarshalSession(`{"AuthURL":"https://test-shop.myshopify.com/admin/oauth/authorize","AccessToken":"1234567890"}"`) a.NoError(err) s := session.(*shopify.Session) - a.Equal(s.AuthURL, aurl) + a.Equal(s.AuthURL, "https://test-shop.myshopify.com/admin/oauth/authorize") a.Equal(s.AccessToken, "1234567890") } func provider() *shopify.Provider { - return shopify.New(os.Getenv("SHOPIFY_KEY"), os.Getenv("SHOPIFY_SECRET"), os.Getenv("SHOPIFY_STORE_NAME"), "/foo") + p := shopify.New(os.Getenv("SHOPIFY_KEY"), os.Getenv("SHOPIFY_SECRET"), "/foo") + p.SetShopName("test-shop") + return p } From 1824858aea45d13b4ac8ac49e47119cbe4cf814c Mon Sep 17 00:00:00 2001 From: Matt Evans Date: Fri, 14 Jun 2019 23:19:59 +1000 Subject: [PATCH 6/6] Stores scopes on provider so when config is reparsed when setting store they don't dup --- providers/shopify/shopify.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/providers/shopify/shopify.go b/providers/shopify/shopify.go index dd6de05ff..e3d2ae8fb 100755 --- a/providers/shopify/shopify.go +++ b/providers/shopify/shopify.go @@ -33,6 +33,7 @@ type Provider struct { config *oauth2.Config providerName string shopName string + scopes []string } // New creates a new Shopify provider and sets up important connection details. @@ -44,6 +45,7 @@ func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { Secret: secret, CallbackURL: callbackURL, providerName: providerName, + scopes: scopes, } p.config = newConfig(p, scopes) return p @@ -69,7 +71,7 @@ func (p *Provider) SetShopName(name string) { p.shopName = name // Reparse config with the new shop name. - p.config = newConfig(p, p.config.Scopes) + p.config = newConfig(p, p.scopes) } // Debug is a no-op for the Shopify package.