From 8715231d486552875a834a7bddf45ab762dbea94 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Thu, 13 Feb 2025 07:55:01 +0100 Subject: [PATCH 1/6] wip --- cmd/account/account_login.go | 136 +++++++++++++++++++++++------------ 1 file changed, 90 insertions(+), 46 deletions(-) diff --git a/cmd/account/account_login.go b/cmd/account/account_login.go index 90fc6ab0..206fb9d5 100644 --- a/cmd/account/account_login.go +++ b/cmd/account/account_login.go @@ -2,74 +2,118 @@ package account import ( "context" + "crypto/rand" + "encoding/hex" "fmt" + "net" + "net/http" + "strings" "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "golang.org/x/oauth2" accountApi "github.com/shopware/shopware-cli/internal/account-api" "github.com/shopware/shopware-cli/logging" ) +func generateRandomState() string { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + panic(err) + } + return hex.EncodeToString(b) +} + var loginCmd = &cobra.Command{ Use: "login", Short: "Login into your Shopware Account", Long: "", RunE: func(cmd *cobra.Command, _ []string) error { - email := services.Conf.GetAccountEmail() - password := services.Conf.GetAccountPassword() - newCredentials := false - - if len(email) == 0 || len(password) == 0 { - var err error - email, password, err = askUserForEmailAndPassword() - if err != nil { - return err - } - - newCredentials = true - - if err := services.Conf.SetAccountEmail(email); err != nil { - return err - } - if err := services.Conf.SetAccountPassword(password); err != nil { - return err - } - } else { - logging.FromContext(cmd.Context()).Infof("Using existing credentials. Use account:logout to logout") + client := &oauth2.Config{ + ClientID: "fd3c9ce4-259e-4f6a-9ab0-7d8bab4de907", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://laughing-wiles-0b5m1rau2n.projects.oryapis.com/oauth2/auth", + TokenURL: "https://laughing-wiles-0b5m1rau2n.projects.oryapis.com/oauth2/token", + AuthStyle: oauth2.AuthStyleInParams, + }, } - client, err := accountApi.NewApi(cmd.Context(), accountApi.LoginRequest{Email: email, Password: password}) - if err != nil { - return fmt.Errorf("login failed with error: %w", err) - } + var ( + state = generateRandomState() + pkceVerifier = oauth2.GenerateVerifier() + serverErr = make(chan error) + serverToken = make(chan *oauth2.Token) + ) - if companyId := services.Conf.GetAccountCompanyId(); companyId > 0 { - err = changeAPIMembership(cmd.Context(), client, companyId) - if err != nil { - return fmt.Errorf("cannot change company member ship: %w", err) - } + l, err := net.Listen("tcp", "localhost:61472") + if err != nil { + return fmt.Errorf("failed to allocate port for OAuth2 callback handler, try again later: %w", err) } - - if newCredentials { - err := services.Conf.Save() - if err != nil { - return fmt.Errorf("cannot save config: %w", err) - } + client.RedirectURL = strings.ReplaceAll(fmt.Sprintf("http://%s/callback", l.Addr().String()), "127.0.0.1", "localhost") + fmt.Printf("Redirect URL: %s\n", client.RedirectURL) + + srv := http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // for retries the user has to start from the beginning + defer close(serverErr) + defer close(serverToken) + + ctx := r.Context() + if err := r.ParseForm(); err != nil { + serverErr <- fmt.Errorf("failed to parse form: %w", err) + return + } + if s := r.Form.Get("state"); s != state { + serverErr <- fmt.Errorf("state mismatch: expected %q, got %q", state, s) + return + } + if r.Form.Has("error") { + e, d := r.Form.Get("error"), r.Form.Get("error_description") + serverErr <- fmt.Errorf("upsteam error: %s: %s", e, d) + return + } + code := r.Form.Get("code") + if code == "" { + serverErr <- fmt.Errorf("missing code") + return + } + t, err := client.Exchange( + ctx, + code, + oauth2.VerifierOption(pkceVerifier), + ) + if err != nil { + serverErr <- fmt.Errorf("failed OAuth2 token exchange: %w", err) + return + } + serverToken <- t + }), } + go func() { _ = srv.Serve(l) }() + defer srv.Close() + + u := client.AuthCodeURL(state, + oauth2.S256ChallengeOption(pkceVerifier), + oauth2.SetAuthURLParam("scope", "offline_access openid"), + oauth2.SetAuthURLParam("response_type", "code"), + oauth2.SetAuthURLParam("prompt", "login consent"), + ) - profile, err := client.GetMyProfile(cmd.Context()) - if err != nil { - return err + fmt.Println("Please open the following URL in your browser:") + fmt.Println(u) + + select { + case err := <-serverErr: + return fmt.Errorf("failed to handle OAuth2 callback: %w", err) + case t := <-serverToken: + fmt.Println("Successfully authenticated") + fmt.Println("Access Token:", t.AccessToken) + fmt.Println("Refresh Token:", t.RefreshToken) + fmt.Println("Expiry:", t.Expiry) } - logging.FromContext(cmd.Context()).Infof( - "Hey %s %s. You are now authenticated on company %s and can use all account commands", - profile.PersonalData.FirstName, - profile.PersonalData.LastName, - client.GetActiveMembership().Company.Name, - ) - return nil }, } From 18617cba450de3bc076e8dd23c4e6389d4af6149 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Thu, 13 Feb 2025 10:22:14 +0100 Subject: [PATCH 2/6] feat: implement OAuth2 interactive login and user info fetching --- cmd/account/account_login.go | 164 ++++--------------------------- cmd/account/account_logout.go | 2 - cmd/root.go | 2 +- go.mod | 3 +- go.sum | 2 - internal/account-api/client.go | 32 ++---- internal/account-api/login.go | 110 ++++++--------------- internal/account-api/merchant.go | 10 +- internal/account-api/oauth2.go | 103 +++++++++++++++++++ internal/account-api/oidc.go | 39 ++++++++ internal/account-api/producer.go | 6 +- internal/account-api/profile.go | 112 --------------------- internal/config/config.go | 82 ++++------------ 13 files changed, 223 insertions(+), 444 deletions(-) create mode 100644 internal/account-api/oauth2.go create mode 100644 internal/account-api/oidc.go delete mode 100644 internal/account-api/profile.go diff --git a/cmd/account/account_login.go b/cmd/account/account_login.go index 206fb9d5..eb161f4d 100644 --- a/cmd/account/account_login.go +++ b/cmd/account/account_login.go @@ -1,119 +1,24 @@ package account import ( - "context" - "crypto/rand" - "encoding/hex" "fmt" - "net" - "net/http" - "strings" - "github.com/manifoldco/promptui" "github.com/spf13/cobra" - "golang.org/x/oauth2" accountApi "github.com/shopware/shopware-cli/internal/account-api" - "github.com/shopware/shopware-cli/logging" ) -func generateRandomState() string { - b := make([]byte, 16) - _, err := rand.Read(b) - if err != nil { - panic(err) - } - return hex.EncodeToString(b) -} - var loginCmd = &cobra.Command{ Use: "login", Short: "Login into your Shopware Account", Long: "", RunE: func(cmd *cobra.Command, _ []string) error { - client := &oauth2.Config{ - ClientID: "fd3c9ce4-259e-4f6a-9ab0-7d8bab4de907", - Endpoint: oauth2.Endpoint{ - AuthURL: "https://laughing-wiles-0b5m1rau2n.projects.oryapis.com/oauth2/auth", - TokenURL: "https://laughing-wiles-0b5m1rau2n.projects.oryapis.com/oauth2/token", - AuthStyle: oauth2.AuthStyleInParams, - }, - } - - var ( - state = generateRandomState() - pkceVerifier = oauth2.GenerateVerifier() - serverErr = make(chan error) - serverToken = make(chan *oauth2.Token) - ) - - l, err := net.Listen("tcp", "localhost:61472") + client, err := accountApi.NewApi(cmd.Context(), nil) if err != nil { - return fmt.Errorf("failed to allocate port for OAuth2 callback handler, try again later: %w", err) - } - client.RedirectURL = strings.ReplaceAll(fmt.Sprintf("http://%s/callback", l.Addr().String()), "127.0.0.1", "localhost") - fmt.Printf("Redirect URL: %s\n", client.RedirectURL) - - srv := http.Server{ - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // for retries the user has to start from the beginning - defer close(serverErr) - defer close(serverToken) - - ctx := r.Context() - if err := r.ParseForm(); err != nil { - serverErr <- fmt.Errorf("failed to parse form: %w", err) - return - } - if s := r.Form.Get("state"); s != state { - serverErr <- fmt.Errorf("state mismatch: expected %q, got %q", state, s) - return - } - if r.Form.Has("error") { - e, d := r.Form.Get("error"), r.Form.Get("error_description") - serverErr <- fmt.Errorf("upsteam error: %s: %s", e, d) - return - } - code := r.Form.Get("code") - if code == "" { - serverErr <- fmt.Errorf("missing code") - return - } - t, err := client.Exchange( - ctx, - code, - oauth2.VerifierOption(pkceVerifier), - ) - if err != nil { - serverErr <- fmt.Errorf("failed OAuth2 token exchange: %w", err) - return - } - serverToken <- t - }), - } - go func() { _ = srv.Serve(l) }() - defer srv.Close() - - u := client.AuthCodeURL(state, - oauth2.S256ChallengeOption(pkceVerifier), - oauth2.SetAuthURLParam("scope", "offline_access openid"), - oauth2.SetAuthURLParam("response_type", "code"), - oauth2.SetAuthURLParam("prompt", "login consent"), - ) - - fmt.Println("Please open the following URL in your browser:") - fmt.Println(u) - - select { - case err := <-serverErr: - return fmt.Errorf("failed to handle OAuth2 callback: %w", err) - case t := <-serverToken: - fmt.Println("Successfully authenticated") - fmt.Println("Access Token:", t.AccessToken) - fmt.Println("Refresh Token:", t.RefreshToken) - fmt.Println("Expiry:", t.Expiry) + return err } + fmt.Println("Client: ", client) return nil }, } @@ -122,51 +27,18 @@ func init() { accountRootCmd.AddCommand(loginCmd) } -func askUserForEmailAndPassword() (string, string, error) { - emailPrompt := promptui.Prompt{ - Label: "Email", - Validate: emptyValidator, - } - - email, err := emailPrompt.Run() - if err != nil { - return "", "", fmt.Errorf("prompt failed %w", err) - } - - passwordPrompt := promptui.Prompt{ - Label: "Password", - Validate: emptyValidator, - Mask: '*', - } - - password, err := passwordPrompt.Run() - if err != nil { - return "", "", fmt.Errorf("prompt failed %w", err) - } - - return email, password, nil -} - -func emptyValidator(s string) error { - if len(s) == 0 { - return fmt.Errorf("this cannot be empty") - } - - return nil -} - -func changeAPIMembership(ctx context.Context, client *accountApi.Client, companyID int) error { - if companyID == 0 || client.GetActiveCompanyID() == companyID { - logging.FromContext(ctx).Debugf("Client is on correct membership skip") - return nil - } - - for _, membership := range client.GetMemberships() { - if membership.Company.Id == companyID { - logging.FromContext(ctx).Debugf("Changing member ship from %s (%d) to %s (%d)", client.ActiveMembership.Company.Name, client.ActiveMembership.Company.Id, membership.Company.Name, membership.Company.Id) - return client.ChangeActiveMembership(ctx, membership) - } - } - - return fmt.Errorf("could not find configured company with id %d", companyID) -} +// func changeAPIMembership(ctx context.Context, client *accountApi.Client, companyID int) error { +// if companyID == 0 || client.GetActiveCompanyID() == companyID { +// logging.FromContext(ctx).Debugf("Client is on correct membership skip") +// return nil +// } + +// for _, membership := range client.GetMemberships() { +// if membership.Company.Id == companyID { +// logging.FromContext(ctx).Debugf("Changing member ship from %s (%d) to %s (%d)", client.ActiveMembership.Company.Name, client.ActiveMembership.Company.Id, membership.Company.Name, membership.Company.Id) +// return client.ChangeActiveMembership(ctx, membership) +// } +// } + +// return fmt.Errorf("could not find configured company with id %d", companyID) +// } diff --git a/cmd/account/account_logout.go b/cmd/account/account_logout.go index 545490dc..d3c44890 100644 --- a/cmd/account/account_logout.go +++ b/cmd/account/account_logout.go @@ -20,8 +20,6 @@ var logoutCmd = &cobra.Command{ } _ = services.Conf.SetAccountCompanyId(0) - _ = services.Conf.SetAccountEmail("") - _ = services.Conf.SetAccountPassword("") if err := services.Conf.Save(); err != nil { return fmt.Errorf("cannot write config: %w", err) diff --git a/cmd/root.go b/cmd/root.go index 20ff44a6..17b5c8da 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -65,7 +65,7 @@ func init() { AccountClient: nil, }, nil } - client, err := accountApi.NewApi(rootCmd.Context(), conf) + client, err := accountApi.NewApi(rootCmd.Context(), nil) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 3f0a7221..8b6b58e5 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( dario.cat/mergo v1.0.1 github.com/NYTimes/gziphandler v1.1.1 github.com/bep/godartsass/v2 v2.3.2 - github.com/caarlos0/env/v9 v9.0.0 github.com/doutorfinancas/go-mad v0.0.0-20240205120830-463c1e9760f0 github.com/evanw/esbuild v0.25.0 github.com/friendsofshopware/go-shopware-admin-api-sdk v0.0.0-20231210091330-92f38f1ae77c @@ -60,7 +59,7 @@ require ( github.com/tidwall/sjson v1.2.5 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/net v0.35.0 - golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/oauth2 v0.24.0 golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect google.golang.org/protobuf v1.35.2 // indirect diff --git a/go.sum b/go.sum index bff9db2a..a5bbb9c1 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,6 @@ github.com/bep/godartsass/v2 v2.3.2 h1:meuc76J1C1soSCAnlnJRdGqJ5S4m6/GW+8hmOe9tO github.com/bep/godartsass/v2 v2.3.2/go.mod h1:Qe5WOS9nVJy7G0jHssXPd3c+Pqk/f7+Tm6k/vahbVgs= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc= -github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= diff --git a/internal/account-api/client.go b/internal/account-api/client.go index f28811f3..209abbae 100644 --- a/internal/account-api/client.go +++ b/internal/account-api/client.go @@ -11,6 +11,7 @@ import ( "time" "github.com/shopware/shopware-cli/logging" + "golang.org/x/oauth2" ) var httpUserAgent = "shopware-cli/0.0.0" @@ -20,9 +21,11 @@ func SetUserAgent(userAgent string) { } type Client struct { - Token token `json:"token"` - ActiveMembership Membership `json:"active_membership"` - Memberships []Membership `json:"memberships"` + Token *oauth2.Token `json:"token"` + ActiveMembership Membership `json:"active_membership"` + Memberships []Membership `json:"memberships"` + UserID int `json:"user_id"` + ComapnyID int `json:"company_id"` } func (c *Client) NewAuthenticatedRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { @@ -34,7 +37,7 @@ func (c *Client) NewAuthenticatedRequest(ctx context.Context, method, path strin r.Header.Set("content-type", "application/json") r.Header.Set("accept", "application/json") - r.Header.Set("x-shopware-token", c.Token.Token) + c.Token.SetAuthHeader(r) r.Header.Set("user-agent", httpUserAgent) return r, nil @@ -64,14 +67,6 @@ func (*Client) doRequest(request *http.Request) ([]byte, error) { return data, nil } -func (c *Client) GetActiveCompanyID() int { - return c.Token.UserID -} - -func (c *Client) GetUserID() int { - return c.Token.UserAccountID -} - func (c *Client) GetActiveMembership() Membership { return c.ActiveMembership } @@ -81,21 +76,14 @@ func (c *Client) GetMemberships() []Membership { } func (c *Client) isTokenValid() bool { - loc, err := time.LoadLocation(c.Token.Expire.Timezone) - if err != nil { - return false - } - - expire, err := time.ParseInLocation("2006-01-02 15:04:05.000000", c.Token.Expire.Date, loc) - if err != nil { + if c.Token == nil { return false } - // When it will be expire in the next minute. Respond with false - return expire.UTC().Sub(time.Now().UTC()).Seconds() > 60 + return time.Until(c.Token.Expiry) > 60 } -const CacheFileName = "shopware-api-client-token.json" +const CacheFileName = "shopware-api-oauth2-token.json" func getApiTokenCacheFilePath() (string, error) { cacheDir, err := os.UserCacheDir() diff --git a/internal/account-api/login.go b/internal/account-api/login.go index bde0afc4..14dcc39f 100644 --- a/internal/account-api/login.go +++ b/internal/account-api/login.go @@ -9,84 +9,57 @@ import ( "net/http" "github.com/shopware/shopware-cli/logging" + "golang.org/x/oauth2" ) const ApiUrl = "https://api.shopware.com" -type AccountConfig interface { - GetAccountEmail() string - GetAccountPassword() string -} - -func NewApi(ctx context.Context, config AccountConfig) (*Client, error) { +func NewApi(ctx context.Context, token *oauth2.Token) (*Client, error) { errorFormat := "login: %v" - request := LoginRequest{ - Email: config.GetAccountEmail(), - Password: config.GetAccountPassword(), - } client, err := createApiFromTokenCache(ctx) - if err == nil { - return client, nil + if client == nil { + client = &Client{} } - s, err := json.Marshal(request) - if err != nil { - return nil, fmt.Errorf(errorFormat, err) + if token != nil { + client.Token = token } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, ApiUrl+"/accesstokens", bytes.NewBuffer(s)) - if err != nil { - return nil, fmt.Errorf("create access token request: %w", err) - } - - req.Header.Set("Content-Type", "application/json") + if !client.isTokenValid() { + newToken, err := InteractiveLogin(ctx) + if err != nil { + return nil, fmt.Errorf(errorFormat, err) + } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, fmt.Errorf(errorFormat, err) + client.Token = newToken } - defer func() { - if err := resp.Body.Close(); err != nil { - logging.FromContext(ctx).Errorf("Cannot close response body: %v", err) - } - }() - - data, err := io.ReadAll(resp.Body) + userInfo, err := fetchUserInfo(ctx, client.Token) if err != nil { return nil, fmt.Errorf(errorFormat, err) } - if resp.StatusCode != 200 { - logging.FromContext(ctx).Debugf("Login failed with response: %s", string(data)) - return nil, fmt.Errorf("login failed. Check your credentials") - } + j, _ := json.Marshal(userInfo) - var token token - if err := json.Unmarshal(data, &token); err != nil { - return nil, fmt.Errorf(errorFormat, err) - } + fmt.Println(string(j)) - memberships, err := fetchMemberships(ctx, token) + memberships, err := fetchMemberships(ctx, client.Token) if err != nil { return nil, err } var activeMemberShip Membership - for _, membership := range memberships { - if membership.Company.Id == token.UserID { - activeMemberShip = membership - } - } + // for _, membership := range memberships { + // if membership.Company.Id == token.UserID { + // activeMemberShip = membership + // } + // } - client = &Client{ - Token: token, - Memberships: memberships, - ActiveMembership: activeMemberShip, - } + client.Memberships = memberships + client.ActiveMembership = activeMemberShip if err := saveApiTokenToTokenCache(client); err != nil { logging.FromContext(ctx).Errorf(fmt.Sprintf("Cannot token cache: %v", err)) @@ -95,9 +68,9 @@ func NewApi(ctx context.Context, config AccountConfig) (*Client, error) { return client, nil } -func fetchMemberships(ctx context.Context, token token) ([]Membership, error) { - r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/account/%d/memberships", ApiUrl, token.UserAccountID), http.NoBody) - r.Header.Set("x-shopware-token", token.Token) +func fetchMemberships(ctx context.Context, token *oauth2.Token) ([]Membership, error) { + r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/account/%d/memberships", ApiUrl, 0), http.NoBody) + token.SetAuthHeader(r) if err != nil { return nil, err @@ -127,33 +100,6 @@ func fetchMemberships(ctx context.Context, token token) ([]Membership, error) { return companies, nil } -type token struct { - Token string `json:"token"` - Expire tokenExpire `json:"expire"` - UserAccountID int `json:"userAccountId"` - UserID int `json:"userId"` - LegacyLogin bool `json:"legacyLogin"` -} - -type tokenExpire struct { - Date string `json:"date"` - TimezoneType int `json:"timezone_type"` - Timezone string `json:"timezone"` -} - -type LoginRequest struct { - Email string `json:"shopwareId"` - Password string `json:"password"` -} - -func (l LoginRequest) GetAccountEmail() string { - return l.Email -} - -func (l LoginRequest) GetAccountPassword() string { - return l.Password -} - type Membership struct { Id int `json:"id"` CreationDate string `json:"creationDate"` @@ -220,7 +166,7 @@ func (c *Client) ChangeActiveMembership(ctx context.Context, selected Membership return fmt.Errorf("ChangeActiveMembership: %v", err) } - r, err := c.NewAuthenticatedRequest(ctx, "POST", fmt.Sprintf("%s/account/%d/memberships/change", ApiUrl, c.GetUserID()), bytes.NewBuffer(s)) + r, err := c.NewAuthenticatedRequest(ctx, "POST", fmt.Sprintf("%s/account/%d/memberships/change", ApiUrl, c.UserID), bytes.NewBuffer(s)) if err != nil { return err } @@ -239,7 +185,7 @@ func (c *Client) ChangeActiveMembership(ctx context.Context, selected Membership if resp.StatusCode == 200 { c.ActiveMembership = selected - c.Token.UserID = selected.Company.Id + c.UserID = selected.Company.Id if err := saveApiTokenToTokenCache(c); err != nil { return err diff --git a/internal/account-api/merchant.go b/internal/account-api/merchant.go index 5f27b282..c5524efe 100644 --- a/internal/account-api/merchant.go +++ b/internal/account-api/merchant.go @@ -15,7 +15,7 @@ func (c *Client) Merchant() *MerchantEndpoint { } func (m MerchantEndpoint) Shops(ctx context.Context) (MerchantShopList, error) { - r, err := m.c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/shops?limit=100&userId=%d", ApiUrl, m.c.GetActiveCompanyID()), nil) + r, err := m.c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/shops?limit=100&userId=%d", ApiUrl, m.c.ComapnyID), nil) if err != nil { return nil, err } @@ -141,7 +141,7 @@ func (m MerchantShopList) GetByDomain(domain string) *MerchantShop { } func (m MerchantEndpoint) GetComposerToken(ctx context.Context, shopId int) (string, error) { - r, err := m.c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/companies/%d/shops/%d/packagestoken", ApiUrl, m.c.GetActiveCompanyID(), shopId), nil) + r, err := m.c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/companies/%d/shops/%d/packagestoken", ApiUrl, m.c.ComapnyID, shopId), nil) if err != nil { return "", err } @@ -159,7 +159,6 @@ func (m MerchantEndpoint) GetComposerToken(ctx context.Context, shopId int) (str var token composerToken err = json.Unmarshal(body, &token) - if err != nil { return "", err } @@ -168,7 +167,7 @@ func (m MerchantEndpoint) GetComposerToken(ctx context.Context, shopId int) (str } func (m MerchantEndpoint) GenerateComposerToken(ctx context.Context, shopId int) (string, error) { - r, err := m.c.NewAuthenticatedRequest(ctx, "POST", fmt.Sprintf("%s/companies/%d/shops/%d/packagestoken", ApiUrl, m.c.GetActiveCompanyID(), shopId), nil) + r, err := m.c.NewAuthenticatedRequest(ctx, "POST", fmt.Sprintf("%s/companies/%d/shops/%d/packagestoken", ApiUrl, m.c.ComapnyID, shopId), nil) if err != nil { return "", err } @@ -181,7 +180,6 @@ func (m MerchantEndpoint) GenerateComposerToken(ctx context.Context, shopId int) var token composerToken err = json.Unmarshal(body, &token) - if err != nil { return "", err } @@ -190,7 +188,7 @@ func (m MerchantEndpoint) GenerateComposerToken(ctx context.Context, shopId int) } func (m MerchantEndpoint) SaveComposerToken(ctx context.Context, shopId int, token string) error { - r, err := m.c.NewAuthenticatedRequest(ctx, "POST", fmt.Sprintf("%s/companies/%d/shops/%d/packagestoken/%s", ApiUrl, m.c.GetActiveCompanyID(), shopId, token), nil) + r, err := m.c.NewAuthenticatedRequest(ctx, "POST", fmt.Sprintf("%s/companies/%d/shops/%d/packagestoken/%s", ApiUrl, m.c.ComapnyID, shopId, token), nil) if err != nil { return err } diff --git a/internal/account-api/oauth2.go b/internal/account-api/oauth2.go new file mode 100644 index 00000000..51f29318 --- /dev/null +++ b/internal/account-api/oauth2.go @@ -0,0 +1,103 @@ +package account_api + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "net" + "net/http" + "strings" + + "golang.org/x/oauth2" +) + +func InteractiveLogin(ctx context.Context) (*oauth2.Token, error) { + client := &oauth2.Config{ + ClientID: OIDCClientID, + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf("%s/oauth2/auth", OIDCEndpoint), + TokenURL: fmt.Sprintf("%s/oauth2/token", OIDCEndpoint), + AuthStyle: oauth2.AuthStyleInParams, + }, + } + + var ( + state = generateRandomState() + pkceVerifier = oauth2.GenerateVerifier() + serverErr = make(chan error) + serverToken = make(chan *oauth2.Token) + ) + + l, err := net.Listen("tcp", "localhost:61472") + if err != nil { + return nil, fmt.Errorf("failed to allocate port for OAuth2 callback handler, try again later: %w", err) + } + + client.RedirectURL = strings.ReplaceAll(fmt.Sprintf("http://%s/callback", l.Addr().String()), "127.0.0.1", "localhost") + + srv := http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer close(serverErr) + defer close(serverToken) + + ctx := r.Context() + if err := r.ParseForm(); err != nil { + serverErr <- fmt.Errorf("failed to parse form: %w", err) + return + } + if s := r.Form.Get("state"); s != state { + serverErr <- fmt.Errorf("state mismatch: expected %q, got %q", state, s) + return + } + if r.Form.Has("error") { + e, d := r.Form.Get("error"), r.Form.Get("error_description") + serverErr <- fmt.Errorf("upsteam error: %s: %s", e, d) + return + } + code := r.Form.Get("code") + if code == "" { + serverErr <- fmt.Errorf("missing code") + return + } + t, err := client.Exchange( + ctx, + code, + oauth2.VerifierOption(pkceVerifier), + ) + if err != nil { + serverErr <- fmt.Errorf("failed OAuth2 token exchange: %w", err) + return + } + serverToken <- t + }), + } + go func() { _ = srv.Serve(l) }() + defer srv.Close() + + u := client.AuthCodeURL(state, + oauth2.S256ChallengeOption(pkceVerifier), + oauth2.SetAuthURLParam("scope", "offline_access openid"), + oauth2.SetAuthURLParam("response_type", "code"), + oauth2.SetAuthURLParam("prompt", "login consent"), + ) + + fmt.Println("Please open the following URL in your browser:") + fmt.Println(u) + + select { + case err := <-serverErr: + return nil, fmt.Errorf("failed to handle OAuth2 callback: %w", err) + case t := <-serverToken: + return t, nil + } +} + +func generateRandomState() string { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + panic(err) + } + return hex.EncodeToString(b) +} diff --git a/internal/account-api/oidc.go b/internal/account-api/oidc.go new file mode 100644 index 00000000..d0a8a9ba --- /dev/null +++ b/internal/account-api/oidc.go @@ -0,0 +1,39 @@ +package account_api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "golang.org/x/oauth2" +) + +const ( + OIDCEndpoint = "https://laughing-wiles-0b5m1rau2n.projects.oryapis.com" + OIDCClientID = "fd3c9ce4-259e-4f6a-9ab0-7d8bab4de907" +) + +func fetchUserInfo(ctx context.Context, token *oauth2.Token) (interface{}, error) { + r, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/userinfo", OIDCEndpoint), nil) + if err != nil { + return nil, err + } + + token.SetAuthHeader(r) + + resp, err := http.DefaultClient.Do(r) + if err != nil { + return nil, nil + } + + defer resp.Body.Close() + + var userInfo interface{} + + if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { + return nil, err + } + + return userInfo, nil +} diff --git a/internal/account-api/producer.go b/internal/account-api/producer.go index e44dfdbb..65966147 100644 --- a/internal/account-api/producer.go +++ b/internal/account-api/producer.go @@ -22,7 +22,7 @@ func (e ProducerEndpoint) GetId() int { } func (c *Client) Producer(ctx context.Context) (*ProducerEndpoint, error) { - r, err := c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/companies/%d/allocations", ApiUrl, c.GetActiveCompanyID()), nil) + r, err := c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/companies/%d/allocations", ApiUrl, c.ComapnyID), nil) if err != nil { return nil, err } @@ -54,7 +54,7 @@ type companyAllocation struct { } func (e ProducerEndpoint) Profile(ctx context.Context) (*Producer, error) { - r, err := e.c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/producers?companyId=%d", ApiUrl, e.c.GetActiveCompanyID()), nil) + r, err := e.c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/producers?companyId=%d", ApiUrl, e.c.ComapnyID), nil) if err != nil { return nil, err } @@ -363,7 +363,6 @@ func (e ProducerEndpoint) GetSoftwareVersions(ctx context.Context, generation st var versions SoftwareVersionList err = json.Unmarshal(body, &versions) - if err != nil { return nil, fmt.Errorf(errorFormat, err) } @@ -483,7 +482,6 @@ func (e ProducerEndpoint) GetExtensionGeneralInfo(ctx context.Context) (*Extensi var info *ExtensionGeneralInformation err = json.Unmarshal(body, &info) - if err != nil { return nil, fmt.Errorf("shopware_versions: %v", err) } diff --git a/internal/account-api/profile.go b/internal/account-api/profile.go deleted file mode 100644 index 2f43290f..00000000 --- a/internal/account-api/profile.go +++ /dev/null @@ -1,112 +0,0 @@ -package account_api - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/shopware/shopware-cli/logging" -) - -func (c *Client) GetMyProfile(ctx context.Context) (*MyProfile, error) { - errorFormat := "GetMyProfile: %v" - - request, err := c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/account/%d", ApiUrl, c.Token.UserAccountID), nil) - if err != nil { - return nil, fmt.Errorf(errorFormat, err) - } - - resp, err := http.DefaultClient.Do(request) - if err != nil { - return nil, fmt.Errorf(errorFormat, err) - } - - defer func() { - if err := resp.Body.Close(); err != nil { - logging.FromContext(ctx).Errorf("GetMyProfile: %v", err) - } - }() - - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf(errorFormat, err) - } - - if resp.StatusCode != 200 { - return nil, fmt.Errorf(errorFormat, err) - } - - var profile MyProfile - if err := json.Unmarshal(data, &profile); err != nil { - return nil, fmt.Errorf(errorFormat, err) - } - - return &profile, nil -} - -type MyProfile struct { - Id int `json:"id"` - Email string `json:"email"` - CreationDate string `json:"creationDate"` - Banned bool `json:"banned"` - Verified bool `json:"verified"` - PersonalData struct { - Id int `json:"id"` - Salutation struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - } `json:"salutation"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Locale struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - } `json:"locale"` - } `json:"personalData"` - PartnerMarketingOptIn bool `json:"partnerMarketingOptIn"` - SelectedMembership struct { - Id int `json:"id"` - CreationDate string `json:"creationDate"` - Active bool `json:"active"` - Member struct { - Id int `json:"id"` - Email string `json:"email"` - AvatarUrl interface{} `json:"avatarUrl"` - PersonalData struct { - Id int `json:"id"` - Salutation struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - } `json:"salutation"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Locale struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - } `json:"locale"` - } `json:"personalData"` - } `json:"member"` - Company struct { - Id int `json:"id"` - Name string `json:"name"` - CustomerNumber string `json:"customerNumber"` - } `json:"company"` - Roles []struct { - Id int `json:"id"` - Name string `json:"name"` - CreationDate string `json:"creationDate"` - Company interface{} `json:"company"` - Permissions []struct { - Id int `json:"id"` - Context string `json:"context"` - Name string `json:"name"` - } `json:"permissions"` - } `json:"roles"` - } `json:"selectedMembership"` -} diff --git a/internal/config/config.go b/internal/config/config.go index d72f685d..511152b3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,11 +2,11 @@ package config import ( "fmt" - "github.com/caarlos0/env/v9" - "gopkg.in/yaml.v3" "os" "strconv" "sync" + + "gopkg.in/yaml.v3" ) var ( @@ -25,24 +25,10 @@ type configState struct { type configData struct { Account struct { - Email string `env:"SHOPWARE_CLI_ACCOUNT_EMAIL" yaml:"email"` - Password string `env:"SHOPWARE_CLI_ACCOUNT_PASSWORD" yaml:"password"` - Company int `env:"SHOPWARE_CLI_ACCOUNT_COMPANY" yaml:"company"` + Company int `yaml:"company"` } `yaml:"account"` } -type ExtensionConfig struct { - Name string - Namespace string - ComposerPackage string - ShopwareVersion string - Description string - License string - Label string - ManufacturerLink string - SupportLink string -} - type Config struct{} func init() { @@ -55,8 +41,6 @@ func init() { func defaultConfig() *configData { config := &configData{} - config.Account.Email = "" - config.Account.Password = "" config.Account.Company = 0 return config } @@ -68,6 +52,20 @@ func InitConfig(configPath string) error { return nil } + companyId := os.Getenv("SHOPWARE_CLI_ACCOUNT_COMPANY") + + if len(companyId) > 0 { + state.loadedFromEnv = true + companyIdInt, err := strconv.Atoi(companyId) + if err != nil { + return err + } + state.inner.Account.Company = companyIdInt + state.isReady = true + + return nil + } + if len(configPath) > 0 { state.cfgPath = configPath } else { @@ -79,17 +77,6 @@ func InitConfig(configPath string) error { state.cfgPath = fmt.Sprintf("%s/.shopware-cli.yml", configDir) } - err := env.Parse(state.inner) - if err != nil { - return err - } - if len(state.inner.Account.Email) > 0 { - state.loadedFromEnv = true - - state.isReady = true - - return nil - } if _, err := os.Stat(state.cfgPath); os.IsNotExist(err) { if err := createNewConfig(state.cfgPath); err != nil { return err @@ -102,7 +89,6 @@ func InitConfig(configPath string) error { } err = yaml.Unmarshal(content, &state.inner) - if err != nil { return err } @@ -146,46 +132,12 @@ func createNewConfig(path string) error { return f.Close() } -func (Config) GetAccountEmail() string { - state.mu.RLock() - defer state.mu.RUnlock() - return state.inner.Account.Email -} - -func (Config) GetAccountPassword() string { - state.mu.RLock() - defer state.mu.RUnlock() - return state.inner.Account.Password -} - func (Config) GetAccountCompanyId() int { state.mu.RLock() defer state.mu.RUnlock() return state.inner.Account.Company } -func (Config) SetAccountEmail(email string) error { - state.mu.Lock() - defer state.mu.Unlock() - if state.loadedFromEnv { - return fmt.Errorf(environmentConfigErrorFormat, "account.email", email) - } - state.modified = true - state.inner.Account.Email = email - return nil -} - -func (Config) SetAccountPassword(password string) error { - state.mu.Lock() - defer state.mu.Unlock() - if state.loadedFromEnv { - return fmt.Errorf(environmentConfigErrorFormat, "account.password", "***") - } - state.modified = true - state.inner.Account.Password = password - return nil -} - func (Config) SetAccountCompanyId(id int) error { state.mu.Lock() defer state.mu.Unlock() From b062dbad539c08bc1f6e80ed07aa1cf070c9a654 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Thu, 13 Feb 2025 10:34:03 +0100 Subject: [PATCH 3/6] refactor: clean up login API and remove unused user info fetching --- internal/account-api/login.go | 11 +---------- internal/account-api/oauth2.go | 6 +++--- internal/account-api/oidc.go | 33 --------------------------------- internal/config/config_test.go | 22 ---------------------- 4 files changed, 4 insertions(+), 68 deletions(-) diff --git a/internal/account-api/login.go b/internal/account-api/login.go index 14dcc39f..a9ec5848 100644 --- a/internal/account-api/login.go +++ b/internal/account-api/login.go @@ -17,7 +17,7 @@ const ApiUrl = "https://api.shopware.com" func NewApi(ctx context.Context, token *oauth2.Token) (*Client, error) { errorFormat := "login: %v" - client, err := createApiFromTokenCache(ctx) + client, _ := createApiFromTokenCache(ctx) if client == nil { client = &Client{} @@ -36,15 +36,6 @@ func NewApi(ctx context.Context, token *oauth2.Token) (*Client, error) { client.Token = newToken } - userInfo, err := fetchUserInfo(ctx, client.Token) - if err != nil { - return nil, fmt.Errorf(errorFormat, err) - } - - j, _ := json.Marshal(userInfo) - - fmt.Println(string(j)) - memberships, err := fetchMemberships(ctx, client.Token) if err != nil { return nil, err diff --git a/internal/account-api/oauth2.go b/internal/account-api/oauth2.go index 51f29318..6c4a682b 100644 --- a/internal/account-api/oauth2.go +++ b/internal/account-api/oauth2.go @@ -9,6 +9,7 @@ import ( "net/http" "strings" + "github.com/shopware/shopware-cli/logging" "golang.org/x/oauth2" ) @@ -52,7 +53,7 @@ func InteractiveLogin(ctx context.Context) (*oauth2.Token, error) { } if r.Form.Has("error") { e, d := r.Form.Get("error"), r.Form.Get("error_description") - serverErr <- fmt.Errorf("upsteam error: %s: %s", e, d) + serverErr <- fmt.Errorf("upstream error: %s: %s", e, d) return } code := r.Form.Get("code") @@ -82,8 +83,7 @@ func InteractiveLogin(ctx context.Context) (*oauth2.Token, error) { oauth2.SetAuthURLParam("prompt", "login consent"), ) - fmt.Println("Please open the following URL in your browser:") - fmt.Println(u) + logging.FromContext(ctx).Infof("Please open the following URL in your browser: %s", u) select { case err := <-serverErr: diff --git a/internal/account-api/oidc.go b/internal/account-api/oidc.go index d0a8a9ba..1a72a55d 100644 --- a/internal/account-api/oidc.go +++ b/internal/account-api/oidc.go @@ -1,39 +1,6 @@ package account_api -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "golang.org/x/oauth2" -) - const ( OIDCEndpoint = "https://laughing-wiles-0b5m1rau2n.projects.oryapis.com" OIDCClientID = "fd3c9ce4-259e-4f6a-9ab0-7d8bab4de907" ) - -func fetchUserInfo(ctx context.Context, token *oauth2.Token) (interface{}, error) { - r, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/userinfo", OIDCEndpoint), nil) - if err != nil { - return nil, err - } - - token.SetAuthHeader(r) - - resp, err := http.DefaultClient.Do(r) - if err != nil { - return nil, nil - } - - defer resp.Body.Close() - - var userInfo interface{} - - if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil { - return nil, err - } - - return userInfo, nil -} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0ad11b15..3cc7615f 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -18,21 +18,15 @@ func TestParseEnvConfig(t *testing.T) { email, password string companyId int }{ - email: "test@test.com", - password: "test123", companyId: 456, } - t.Setenv("SHOPWARE_CLI_ACCOUNT_EMAIL", testData.email) - t.Setenv("SHOPWARE_CLI_ACCOUNT_PASSWORD", testData.password) t.Setenv("SHOPWARE_CLI_ACCOUNT_COMPANY", strconv.Itoa(testData.companyId)) assert.NoError(t, InitConfig("")) assert.True(t, state.loadedFromEnv) confService := Config{} - assert.Equal(t, testData.email, confService.GetAccountEmail()) - assert.Equal(t, testData.password, confService.GetAccountPassword()) assert.Equal(t, testData.companyId, confService.GetAccountCompanyId()) } @@ -43,8 +37,6 @@ func TestParseFileConfig(t *testing.T) { email, password string companyId int }{ - email: "test@test.com", - password: "test123", companyId: 456, } @@ -56,8 +48,6 @@ func TestParseFileConfig(t *testing.T) { assert.False(t, state.loadedFromEnv) confService := Config{} - assert.Equal(t, testData.email, confService.GetAccountEmail()) - assert.Equal(t, testData.password, confService.GetAccountPassword()) assert.Equal(t, testData.companyId, confService.GetAccountCompanyId()) assert.Equal(t, testConfig, state.cfgPath) } @@ -69,8 +59,6 @@ func TestSaveConfig(t *testing.T) { email, password string companyId int }{ - email: "test@new.com", - password: "test", companyId: 111, } @@ -87,10 +75,6 @@ func TestSaveConfig(t *testing.T) { configService := Config{} - assert.NoError(t, configService.SetAccountEmail(testData.email)) - - assert.NoError(t, configService.SetAccountPassword(testData.password)) - assert.NoError(t, configService.SetAccountCompanyId(testData.companyId)) assert.True(t, state.modified) @@ -105,8 +89,6 @@ func TestSaveConfig(t *testing.T) { var newConf configData assert.NoError(t, yaml.Unmarshal(newConfData, &newConf)) - assert.Equal(t, testData.email, newConf.Account.Email) - assert.Equal(t, testData.password, newConf.Account.Password) assert.Equal(t, testData.companyId, newConf.Account.Company) } @@ -122,16 +104,12 @@ func TestDontWriteEnvConfig(t *testing.T) { companyId: 456, } - t.Setenv("SHOPWARE_CLI_ACCOUNT_EMAIL", testData.email) - t.Setenv("SHOPWARE_CLI_ACCOUNT_PASSWORD", testData.password) t.Setenv("SHOPWARE_CLI_ACCOUNT_COMPANY", strconv.Itoa(testData.companyId)) assert.NoError(t, InitConfig("")) assert.True(t, state.loadedFromEnv) confService := Config{} - assert.Error(t, confService.SetAccountEmail("test@foo.com")) - assert.Error(t, confService.SetAccountPassword("S3CR3TF4RT3St")) assert.Error(t, confService.SetAccountCompanyId(111)) } From 7744bff2ab8e9e7077b59d7d24ae0d1b913bbae8 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Mon, 17 Feb 2025 15:07:50 +0100 Subject: [PATCH 4/6] feat: update OAuth2 scopes to include profile and email --- internal/account-api/oauth2.go | 2 +- internal/account-api/oidc.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/account-api/oauth2.go b/internal/account-api/oauth2.go index 6c4a682b..0e0202bb 100644 --- a/internal/account-api/oauth2.go +++ b/internal/account-api/oauth2.go @@ -78,7 +78,7 @@ func InteractiveLogin(ctx context.Context) (*oauth2.Token, error) { u := client.AuthCodeURL(state, oauth2.S256ChallengeOption(pkceVerifier), - oauth2.SetAuthURLParam("scope", "offline_access openid"), + oauth2.SetAuthURLParam("scope", "offline_access profile email"), oauth2.SetAuthURLParam("response_type", "code"), oauth2.SetAuthURLParam("prompt", "login consent"), ) diff --git a/internal/account-api/oidc.go b/internal/account-api/oidc.go index 1a72a55d..bcb96544 100644 --- a/internal/account-api/oidc.go +++ b/internal/account-api/oidc.go @@ -2,5 +2,5 @@ package account_api const ( OIDCEndpoint = "https://laughing-wiles-0b5m1rau2n.projects.oryapis.com" - OIDCClientID = "fd3c9ce4-259e-4f6a-9ab0-7d8bab4de907" + OIDCClientID = "737445e8-80e0-4263-a067-4da24a7e2174" ) From de2761f3872c08e7af013f218d68b7696eb5e9bb Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Tue, 16 Dec 2025 09:00:47 +0900 Subject: [PATCH 5/6] chore: cleanup --- cmd/account/account_login.go | 2 +- internal/account-api/client.go | 3 ++- internal/account-api/login.go | 14 ++++++++------ internal/account-api/oauth2.go | 6 +++--- internal/account-api/oidc.go | 7 +++++-- internal/account-api/updates.go | 2 +- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/cmd/account/account_login.go b/cmd/account/account_login.go index 5e127f46..26c29cf4 100644 --- a/cmd/account/account_login.go +++ b/cmd/account/account_login.go @@ -5,10 +5,10 @@ import ( "fmt" "github.com/charmbracelet/huh" - "github.com/shopware/shopware-cli/logging" "github.com/spf13/cobra" accountApi "github.com/shopware/shopware-cli/internal/account-api" + "github.com/shopware/shopware-cli/logging" ) var loginCmd = &cobra.Command{ diff --git a/internal/account-api/client.go b/internal/account-api/client.go index 6ff3689d..dc8d9caa 100644 --- a/internal/account-api/client.go +++ b/internal/account-api/client.go @@ -10,8 +10,9 @@ import ( "path/filepath" "time" - "github.com/shopware/shopware-cli/logging" "golang.org/x/oauth2" + + "github.com/shopware/shopware-cli/logging" ) var httpUserAgent = "shopware-cli/0.0.0" diff --git a/internal/account-api/login.go b/internal/account-api/login.go index 3fe573e7..2806792a 100644 --- a/internal/account-api/login.go +++ b/internal/account-api/login.go @@ -8,11 +8,10 @@ import ( "io" "net/http" - "github.com/shopware/shopware-cli/logging" "golang.org/x/oauth2" -) -const ApiUrl = "https://api.shopware.com" + "github.com/shopware/shopware-cli/logging" +) func NewApi(ctx context.Context, token *oauth2.Token) (*Client, error) { errorFormat := "login: %v" @@ -63,13 +62,16 @@ func fetchMemberships(ctx context.Context, token *oauth2.Token) ([]Membership, e r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/account/%d/memberships", ApiUrl, 0), http.NoBody) token.SetAuthHeader(r) + fmt.Println(r.URL) + fmt.Println(r.Header.Get("Authorization")) + if err != nil { - return nil, err + return nil, fmt.Errorf("fetchMemberships: %v", err) } resp, err := http.DefaultClient.Do(r) if err != nil { - return nil, err + return nil, fmt.Errorf("fetchMemberships: %v", err) } defer func() { @@ -84,7 +86,7 @@ func fetchMemberships(ctx context.Context, token *oauth2.Token) ([]Membership, e } if resp.StatusCode != 200 { - return nil, fmt.Errorf(string(data)+" but got status code %d", resp.StatusCode) + return nil, fmt.Errorf("fetchMemberships: %s but got status code %d", string(data), resp.StatusCode) } var companies []Membership diff --git a/internal/account-api/oauth2.go b/internal/account-api/oauth2.go index 0e0202bb..8acebacc 100644 --- a/internal/account-api/oauth2.go +++ b/internal/account-api/oauth2.go @@ -9,8 +9,9 @@ import ( "net/http" "strings" - "github.com/shopware/shopware-cli/logging" "golang.org/x/oauth2" + + "github.com/shopware/shopware-cli/logging" ) func InteractiveLogin(ctx context.Context) (*oauth2.Token, error) { @@ -78,9 +79,8 @@ func InteractiveLogin(ctx context.Context) (*oauth2.Token, error) { u := client.AuthCodeURL(state, oauth2.S256ChallengeOption(pkceVerifier), - oauth2.SetAuthURLParam("scope", "offline_access profile email"), + oauth2.SetAuthURLParam("scope", OIDCScopes), oauth2.SetAuthURLParam("response_type", "code"), - oauth2.SetAuthURLParam("prompt", "login consent"), ) logging.FromContext(ctx).Infof("Please open the following URL in your browser: %s", u) diff --git a/internal/account-api/oidc.go b/internal/account-api/oidc.go index bcb96544..ffade484 100644 --- a/internal/account-api/oidc.go +++ b/internal/account-api/oidc.go @@ -1,6 +1,9 @@ package account_api const ( - OIDCEndpoint = "https://laughing-wiles-0b5m1rau2n.projects.oryapis.com" - OIDCClientID = "737445e8-80e0-4263-a067-4da24a7e2174" + OIDCEndpoint = "https://auth-api.shopware.in" + OIDCClientID = "def413d7-4c4e-439f-8b51-74c352436b2f" + OIDCScopes = "openid offline_access email profile extension_management_read_write" + + ApiUrl = "https://next-api.shopware.com" ) diff --git a/internal/account-api/updates.go b/internal/account-api/updates.go index 093e1424..59b7fc12 100644 --- a/internal/account-api/updates.go +++ b/internal/account-api/updates.go @@ -30,7 +30,7 @@ type UpdateCheckExtensionCompatibilityStatus struct { } func GetFutureExtensionUpdates(ctx context.Context, currentVersion string, futureVersion string, extensions []UpdateCheckExtension) ([]UpdateCheckExtensionCompatibility, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.shopware.com/swplatform/autoupdate", nil) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, ApiUrl+"/swplatform/autoupdate", nil) if err != nil { return nil, err } From e07dc43efdafa527d240f1ee390adbcae041de7f Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Fri, 19 Dec 2025 12:07:34 +0100 Subject: [PATCH 6/6] refactor(account-api): simplify OAuth2 flow and remove membership management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove company/membership selection logic since the new OAuth2 API handles producer access through a dedicated endpoint that returns all available producers for the authenticated user. This eliminates the need for manual company switching and simplifies the authentication flow. - Remove account_company*.go commands (list, use, root) - Remove account_producer_info.go command - Remove Membership type and related methods from client - Update ProducerEndpoint to fetch all producers via new API endpoint - Add producerId parameter to extension binary methods - Display success message in OAuth2 callback page 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/account/account_company.go | 14 -- cmd/account/account_company_list.go | 37 ----- cmd/account/account_company_use.go | 50 ------ cmd/account/account_login.go | 56 +------ .../account_producer_extension_upload.go | 8 +- cmd/account/account_producer_info.go | 36 ---- internal/account-api/client.go | 19 +-- internal/account-api/login.go | 156 ------------------ internal/account-api/oauth2.go | 3 + internal/account-api/producer.go | 69 +++----- internal/account-api/producer_extension.go | 16 +- 11 files changed, 46 insertions(+), 418 deletions(-) delete mode 100644 cmd/account/account_company.go delete mode 100644 cmd/account/account_company_list.go delete mode 100644 cmd/account/account_company_use.go delete mode 100644 cmd/account/account_producer_info.go diff --git a/cmd/account/account_company.go b/cmd/account/account_company.go deleted file mode 100644 index a7f60d54..00000000 --- a/cmd/account/account_company.go +++ /dev/null @@ -1,14 +0,0 @@ -package account - -import ( - "github.com/spf13/cobra" -) - -var accountCompanyRootCmd = &cobra.Command{ - Use: "company", - Short: "Manage your Shopware company", -} - -func init() { - accountRootCmd.AddCommand(accountCompanyRootCmd) -} diff --git a/cmd/account/account_company_list.go b/cmd/account/account_company_list.go deleted file mode 100644 index cdb8211b..00000000 --- a/cmd/account/account_company_list.go +++ /dev/null @@ -1,37 +0,0 @@ -package account - -import ( - "os" - "strconv" - "strings" - - "github.com/spf13/cobra" - - "github.com/shopware/shopware-cli/internal/table" -) - -var accountCompanyListCmd = &cobra.Command{ - Use: "list", - Short: "Lists all available company for your Account", - Aliases: []string{"ls"}, - Long: ``, - Run: func(_ *cobra.Command, _ []string) { - table := table.NewWriter(os.Stdout) - table.Header([]string{"ID", "Name", "Customer ID", "Roles"}) - - for _, membership := range services.AccountClient.GetMemberships() { - _ = table.Append([]string{ - strconv.FormatInt(int64(membership.Company.Id), 10), - membership.Company.Name, - membership.Company.CustomerNumber, - strings.Join(membership.GetRoles(), ", "), - }) - } - - _ = table.Render() - }, -} - -func init() { - accountCompanyRootCmd.AddCommand(accountCompanyListCmd) -} diff --git a/cmd/account/account_company_use.go b/cmd/account/account_company_use.go deleted file mode 100644 index cb602be3..00000000 --- a/cmd/account/account_company_use.go +++ /dev/null @@ -1,50 +0,0 @@ -package account - -import ( - "fmt" - "strconv" - - "github.com/spf13/cobra" - - accountApi "github.com/shopware/shopware-cli/internal/account-api" - "github.com/shopware/shopware-cli/logging" -) - -var accountCompanyUseCmd = &cobra.Command{ - Use: "use [companyId]", - Short: "Use another company for your Account", - Args: cobra.MinimumNArgs(1), - Long: ``, - RunE: func(cmd *cobra.Command, args []string) error { - companyID, err := strconv.Atoi(args[0]) - if err != nil { - return err - } - - for _, membership := range services.AccountClient.GetMemberships() { - if membership.Company.Id == companyID { - if err := services.Conf.SetAccountCompanyId(companyID); err != nil { - return err - } - - if err := services.Conf.Save(); err != nil { - return err - } - - err = accountApi.InvalidateTokenCache() - if err != nil { - return fmt.Errorf("cannot invalidate token cache: %w", err) - } - - logging.FromContext(cmd.Context()).Infof("Successfully changed your company to %s (%s)", membership.Company.Name, membership.Company.CustomerNumber) - return nil - } - } - - return fmt.Errorf("company with ID \"%d\" not found", companyID) - }, -} - -func init() { - accountCompanyRootCmd.AddCommand(accountCompanyUseCmd) -} diff --git a/cmd/account/account_login.go b/cmd/account/account_login.go index 1a0790f8..6ba017ab 100644 --- a/cmd/account/account_login.go +++ b/cmd/account/account_login.go @@ -1,14 +1,11 @@ package account import ( - "context" "fmt" - "github.com/charmbracelet/huh" "github.com/spf13/cobra" accountApi "github.com/shopware/shopware-cli/internal/account-api" - "github.com/shopware/shopware-cli/internal/system" "github.com/shopware/shopware-cli/logging" ) @@ -22,7 +19,10 @@ var loginCmd = &cobra.Command{ return err } - fmt.Println("Client: ", client) + fmt.Println(client.Token) + + logging.FromContext(cmd.Context()).Infof("Loggedin as %s", client.Token.Extra("email")) + return nil }, } @@ -30,51 +30,3 @@ var loginCmd = &cobra.Command{ func init() { accountRootCmd.AddCommand(loginCmd) } - -func askUserForEmailAndPassword() (string, string, error) { - var email, password string - - form := huh.NewForm( - huh.NewGroup( - huh.NewInput(). - Title("Email"). - Validate(emptyValidator). - Value(&email), - huh.NewInput(). - Title("Password"). - EchoMode(huh.EchoModePassword). - Validate(emptyValidator). - Value(&password), - ), - ) - - if err := form.Run(); err != nil { - return "", "", fmt.Errorf("prompt failed %w", err) - } - - return email, password, nil -} - -func emptyValidator(s string) error { - if len(s) == 0 { - return fmt.Errorf("this cannot be empty") - } - - return nil -} - -func changeAPIMembership(ctx context.Context, client *accountApi.Client, companyID int) error { - if companyID == 0 || client.GetActiveCompanyID() == companyID { - logging.FromContext(ctx).Debugf("Client is on correct membership skip") - return nil - } - - for _, membership := range client.GetMemberships() { - if membership.Company.Id == companyID { - logging.FromContext(ctx).Debugf("Changing member ship from %s (%d) to %s (%d)", client.ActiveMembership.Company.Name, client.ActiveMembership.Company.Id, membership.Company.Name, membership.Company.Id) - return client.ChangeActiveMembership(ctx, membership) - } - } - - return fmt.Errorf("could not find configured company with id %d", companyID) -} diff --git a/cmd/account/account_producer_extension_upload.go b/cmd/account/account_producer_extension_upload.go index 12185c32..db1aa42a 100644 --- a/cmd/account/account_producer_extension_upload.go +++ b/cmd/account/account_producer_extension_upload.go @@ -53,7 +53,7 @@ var accountCompanyProducerExtensionUploadCmd = &cobra.Command{ logging.FromContext(cmd.Context()).Debugf("Found extension with ID: %d", ext.Id) - binaries, err := p.GetExtensionBinaries(cmd.Context(), ext.Id) + binaries, err := p.GetExtensionBinaries(cmd.Context(), ext.Producer.Id, ext.Id) if err != nil { logging.FromContext(cmd.Context()).Debugf("Failed to get extension binaries for extension ID %d: %v", ext.Id, err) return err @@ -110,7 +110,7 @@ var accountCompanyProducerExtensionUploadCmd = &cobra.Command{ }, } - foundBinary, err = p.CreateExtensionBinary(cmd.Context(), ext.Id, create) + foundBinary, err = p.CreateExtensionBinary(cmd.Context(), ext.Producer.Id, ext.Id, create) if err != nil { logging.FromContext(cmd.Context()).Debugf("Failed to create extension binary: %v", err) return fmt.Errorf("create extension binary: %w", err) @@ -134,7 +134,7 @@ var accountCompanyProducerExtensionUploadCmd = &cobra.Command{ logging.FromContext(cmd.Context()).Debugf("Updating extension binary info for extension ID %d, binary ID %d", ext.Id, foundBinary.Id) - err = p.UpdateExtensionBinaryInfo(cmd.Context(), ext.Id, update) + err = p.UpdateExtensionBinaryInfo(cmd.Context(), ext.Producer.Id, ext.Id, update) if err != nil { logging.FromContext(cmd.Context()).Debugf("Failed to update extension binary info: %v", err) return err @@ -143,7 +143,7 @@ var accountCompanyProducerExtensionUploadCmd = &cobra.Command{ logging.FromContext(cmd.Context()).Infof("Updated changelog. Uploading now the zip to remote") logging.FromContext(cmd.Context()).Debugf("Uploading zip file from path: %s", path) - err = p.UpdateExtensionBinaryFile(cmd.Context(), ext.Id, foundBinary.Id, path) + err = p.UpdateExtensionBinaryFile(cmd.Context(), ext.Producer.Id, ext.Id, foundBinary.Id, path) if err != nil { logging.FromContext(cmd.Context()).Debugf("UpdateExtensionBinaryFile returned error: %v", err) if strings.Contains(err.Error(), "BinariesException-40") { diff --git a/cmd/account/account_producer_info.go b/cmd/account/account_producer_info.go deleted file mode 100644 index e5a7b2d5..00000000 --- a/cmd/account/account_producer_info.go +++ /dev/null @@ -1,36 +0,0 @@ -package account - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/shopware/shopware-cli/logging" -) - -var accountProducerInfoCmd = &cobra.Command{ - Use: "info", - Short: "List information about your producer account", - Long: ``, - RunE: func(cmd *cobra.Command, _ []string) error { - p, err := services.AccountClient.Producer(cmd.Context()) - if err != nil { - return fmt.Errorf("cannot get producer endpoint: %w", err) - } - - profile, err := p.Profile(cmd.Context()) - if err != nil { - return fmt.Errorf("cannot get producer profile: %w", err) - } - - logging.FromContext(cmd.Context()).Infof("Name: %s", profile.Name) - logging.FromContext(cmd.Context()).Infof("Prefix: %s", profile.Prefix) - logging.FromContext(cmd.Context()).Infof("Website: %s", profile.Website) - - return nil - }, -} - -func init() { - accountCompanyProducerCmd.AddCommand(accountProducerInfoCmd) -} diff --git a/internal/account-api/client.go b/internal/account-api/client.go index dc8d9caa..493b46b1 100644 --- a/internal/account-api/client.go +++ b/internal/account-api/client.go @@ -22,11 +22,7 @@ func SetUserAgent(userAgent string) { } type Client struct { - Token *oauth2.Token `json:"token"` - ActiveMembership Membership `json:"active_membership"` - Memberships []Membership `json:"memberships"` - UserID int `json:"user_id"` - ComapnyID int `json:"company_id"` + Token *oauth2.Token `json:"token"` } func (c *Client) NewAuthenticatedRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { @@ -68,18 +64,6 @@ func (*Client) doRequest(request *http.Request) ([]byte, error) { return data, nil } -func (c *Client) GetActiveMembership() Membership { - return c.ActiveMembership -} - -func (c *Client) GetMemberships() []Membership { - return c.Memberships -} - -func (c *Client) GetActiveCompanyID() int { - return c.ActiveMembership.Company.Id -} - func (c *Client) isTokenValid() bool { if c.Token == nil { return false @@ -122,7 +106,6 @@ func createApiFromTokenCache(ctx context.Context) (*Client, error) { } logging.FromContext(ctx).Debugf("Using token cache from %s", tokenFilePath) - logging.FromContext(ctx).Debugf("Impersonating currently as %s (%d)", client.ActiveMembership.Company.Name, client.ActiveMembership.Company.Id) if !client.isTokenValid() { return nil, fmt.Errorf("token is expired") diff --git a/internal/account-api/login.go b/internal/account-api/login.go index 2806792a..20b2047d 100644 --- a/internal/account-api/login.go +++ b/internal/account-api/login.go @@ -1,12 +1,8 @@ package account_api import ( - "bytes" "context" - "encoding/json" "fmt" - "io" - "net/http" "golang.org/x/oauth2" @@ -35,161 +31,9 @@ func NewApi(ctx context.Context, token *oauth2.Token) (*Client, error) { client.Token = newToken } - memberships, err := fetchMemberships(ctx, client.Token) - if err != nil { - return nil, err - } - - var activeMemberShip Membership - - // for _, membership := range memberships { - // if membership.Company.Id == token.UserID { - // activeMemberShip = membership - // } - // } - - client.Memberships = memberships - client.ActiveMembership = activeMemberShip - if err := saveApiTokenToTokenCache(client); err != nil { logging.FromContext(ctx).Errorf(fmt.Sprintf("Cannot token cache: %v", err)) } return client, nil } - -func fetchMemberships(ctx context.Context, token *oauth2.Token) ([]Membership, error) { - r, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/account/%d/memberships", ApiUrl, 0), http.NoBody) - token.SetAuthHeader(r) - - fmt.Println(r.URL) - fmt.Println(r.Header.Get("Authorization")) - - if err != nil { - return nil, fmt.Errorf("fetchMemberships: %v", err) - } - - resp, err := http.DefaultClient.Do(r) - if err != nil { - return nil, fmt.Errorf("fetchMemberships: %v", err) - } - - defer func() { - if err := resp.Body.Close(); err != nil { - logging.FromContext(ctx).Errorf("Cannot close response body: %v", err) - } - }() - - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("fetchMemberships: %v", err) - } - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("fetchMemberships: %s but got status code %d", string(data), resp.StatusCode) - } - - var companies []Membership - if err := json.Unmarshal(data, &companies); err != nil { - return nil, fmt.Errorf("fetchMemberships: %v", err) - } - - return companies, nil -} - -type Membership struct { - Id int `json:"id"` - CreationDate string `json:"creationDate"` - Active bool `json:"active"` - Member struct { - Id int `json:"id"` - Email string `json:"email"` - AvatarUrl interface{} `json:"avatarUrl"` - PersonalData struct { - Id int `json:"id"` - Salutation struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - } `json:"salutation"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Locale struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - } `json:"locale"` - } `json:"personalData"` - } `json:"member"` - Company struct { - Id int `json:"id"` - Name string `json:"name"` - CustomerNumber string `json:"customerNumber"` - } `json:"company"` - Roles []struct { - Id int `json:"id"` - Name string `json:"name"` - CreationDate string `json:"creationDate"` - Company interface{} `json:"company"` - Permissions []struct { - Id int `json:"id"` - Context string `json:"context"` - Name string `json:"name"` - } `json:"permissions"` - } `json:"roles"` -} - -func (m Membership) GetRoles() []string { - roles := make([]string, 0) - - for _, role := range m.Roles { - roles = append(roles, role.Name) - } - - return roles -} - -type changeMembershipRequest struct { - SelectedMembership struct { - Id int `json:"id"` - } `json:"membership"` -} - -func (c *Client) ChangeActiveMembership(ctx context.Context, selected Membership) error { - s, err := json.Marshal(changeMembershipRequest{SelectedMembership: struct { - Id int `json:"id"` - }(struct{ Id int }{Id: selected.Id})}) - if err != nil { - return fmt.Errorf("ChangeActiveMembership: %v", err) - } - - r, err := c.NewAuthenticatedRequest(ctx, "POST", fmt.Sprintf("%s/account/%d/memberships/change", ApiUrl, c.UserID), bytes.NewBuffer(s)) - if err != nil { - return err - } - - resp, err := http.DefaultClient.Do(r) - if err != nil { - return err - } - - defer func() { - if err := resp.Body.Close(); err != nil { - logging.FromContext(ctx).Errorf("ChangeActiveMembership: %v", err) - } - }() - _, _ = io.Copy(io.Discard, resp.Body) - - if resp.StatusCode == 200 { - c.ActiveMembership = selected - c.UserID = selected.Company.Id - - if err := saveApiTokenToTokenCache(c); err != nil { - return err - } - - return nil - } - - return fmt.Errorf("could not change active membership due http error %d", resp.StatusCode) -} diff --git a/internal/account-api/oauth2.go b/internal/account-api/oauth2.go index 8acebacc..dbecba51 100644 --- a/internal/account-api/oauth2.go +++ b/internal/account-api/oauth2.go @@ -72,6 +72,9 @@ func InteractiveLogin(ctx context.Context) (*oauth2.Token, error) { return } serverToken <- t + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = w.Write([]byte(`

Login successful

You can close this window now.

`)) }), } go func() { _ = srv.Serve(l) }() diff --git a/internal/account-api/producer.go b/internal/account-api/producer.go index 373060e9..c48230c1 100644 --- a/internal/account-api/producer.go +++ b/internal/account-api/producer.go @@ -13,16 +13,12 @@ import ( ) type ProducerEndpoint struct { - c *Client - producerId int -} - -func (e ProducerEndpoint) GetId() int { - return e.producerId + c *Client + producerIds []int } func (c *Client) Producer(ctx context.Context) (*ProducerEndpoint, error) { - r, err := c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/companies/%d/allocations", ApiUrl, c.ComapnyID), nil) + r, err := c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/integrations/shopwarecli/producers", ApiUrl), nil) if err != nil { return nil, err } @@ -32,48 +28,21 @@ func (c *Client) Producer(ctx context.Context) (*ProducerEndpoint, error) { return nil, err } - var allocation companyAllocation - if err := json.Unmarshal(body, &allocation); err != nil { + var producers []Producer + if err := json.Unmarshal(body, &producers); err != nil { return nil, fmt.Errorf("producer.profile: %v", err) } - if !allocation.IsProducer { - return nil, fmt.Errorf("this company is not unlocked as producer") + if len(producers) == 0 { + return nil, fmt.Errorf("producer.profile: no producer found for current user") } - return &ProducerEndpoint{producerId: allocation.ProducerID, c: c}, nil -} - -type companyAllocation struct { - HasShops bool `json:"hasShops"` - HasCommercialShop bool `json:"hasCommercialShop"` - IsEducationMember bool `json:"isEducationMember"` - IsPartner bool `json:"isPartner"` - IsProducer bool `json:"isProducer"` - ProducerID int `json:"producerId"` -} - -func (e ProducerEndpoint) Profile(ctx context.Context) (*Producer, error) { - r, err := e.c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/producers?companyId=%d", ApiUrl, e.c.ComapnyID), nil) - if err != nil { - return nil, err + var producerIds []int + for _, p := range producers { + producerIds = append(producerIds, p.Id) } - body, err := e.c.doRequest(r) - if err != nil { - return nil, err - } - - var producers []Producer - if err := json.Unmarshal(body, &producers); err != nil { - return nil, fmt.Errorf("my_profile: %v", err) - } - - for _, profile := range producers { - return &profile, nil - } - - return nil, fmt.Errorf("cannot find a profile") + return &ProducerEndpoint{producerIds: producerIds, c: c}, nil } type Producer struct { @@ -124,9 +93,23 @@ type ListExtensionCriteria struct { } func (e ProducerEndpoint) Extensions(ctx context.Context, criteria *ListExtensionCriteria) ([]Extension, error) { + var allExtensions []Extension + + for _, producerId := range e.producerIds { + extensions, err := e.singleExtensionsByProducer(ctx, criteria, producerId) + if err != nil { + return nil, err + } + allExtensions = append(allExtensions, extensions...) + } + + return allExtensions, nil +} + +func (e ProducerEndpoint) singleExtensionsByProducer(ctx context.Context, criteria *ListExtensionCriteria, producerId int) ([]Extension, error) { encoder := schema.NewEncoder() form := url.Values{} - form.Set("producerId", strconv.FormatInt(int64(e.GetId()), 10)) + form.Set("producerId", strconv.FormatInt(int64(producerId), 10)) err := encoder.Encode(criteria, form) if err != nil { return nil, fmt.Errorf("list_extensions: %v", err) diff --git a/internal/account-api/producer_extension.go b/internal/account-api/producer_extension.go index ae3bf16e..2366bea8 100644 --- a/internal/account-api/producer_extension.go +++ b/internal/account-api/producer_extension.go @@ -67,10 +67,10 @@ type ExtensionCreate struct { Version string `json:"version"` } -func (e ProducerEndpoint) GetExtensionBinaries(ctx context.Context, extensionId int) ([]*ExtensionBinary, error) { +func (e ProducerEndpoint) GetExtensionBinaries(ctx context.Context, producerId int, extensionId int) ([]*ExtensionBinary, error) { errorFormat := "GetExtensionBinaries: %v" - r, err := e.c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/producers/%d/plugins/%d/binaries", ApiUrl, e.producerId, extensionId), nil) + r, err := e.c.NewAuthenticatedRequest(ctx, "GET", fmt.Sprintf("%s/producers/%d/plugins/%d/binaries", ApiUrl, producerId, extensionId), nil) if err != nil { return nil, fmt.Errorf(errorFormat, err) } @@ -88,7 +88,7 @@ func (e ProducerEndpoint) GetExtensionBinaries(ctx context.Context, extensionId return binaries, nil } -func (e ProducerEndpoint) UpdateExtensionBinaryInfo(ctx context.Context, extensionId int, update ExtensionUpdate) error { +func (e ProducerEndpoint) UpdateExtensionBinaryInfo(ctx context.Context, producerId, extensionId int, update ExtensionUpdate) error { errorFormat := "UpdateExtensionBinaryInfo: %v" content, err := json.Marshal(update) @@ -96,7 +96,7 @@ func (e ProducerEndpoint) UpdateExtensionBinaryInfo(ctx context.Context, extensi return fmt.Errorf(errorFormat, err) } - r, err := e.c.NewAuthenticatedRequest(ctx, "PUT", fmt.Sprintf("%s/producers/%d/plugins/%d/binaries/%d", ApiUrl, e.producerId, extensionId, update.Id), bytes.NewReader(content)) + r, err := e.c.NewAuthenticatedRequest(ctx, "PUT", fmt.Sprintf("%s/producers/%d/plugins/%d/binaries/%d", ApiUrl, producerId, extensionId, update.Id), bytes.NewReader(content)) if err != nil { return fmt.Errorf(errorFormat, err) } @@ -106,7 +106,7 @@ func (e ProducerEndpoint) UpdateExtensionBinaryInfo(ctx context.Context, extensi return err } -func (e ProducerEndpoint) CreateExtensionBinary(ctx context.Context, extensionId int, create ExtensionCreate) (*ExtensionBinary, error) { +func (e ProducerEndpoint) CreateExtensionBinary(ctx context.Context, producerId, extensionId int, create ExtensionCreate) (*ExtensionBinary, error) { errorFormat := "CreateExtensionBinary: %v" createPayload, err := json.Marshal(create) @@ -114,7 +114,7 @@ func (e ProducerEndpoint) CreateExtensionBinary(ctx context.Context, extensionId return nil, fmt.Errorf(errorFormat, err) } - r, err := e.c.NewAuthenticatedRequest(ctx, "POST", fmt.Sprintf("%s/producers/%d/plugins/%d/binaries", ApiUrl, e.producerId, extensionId), bytes.NewReader(createPayload)) + r, err := e.c.NewAuthenticatedRequest(ctx, "POST", fmt.Sprintf("%s/producers/%d/plugins/%d/binaries", ApiUrl, producerId, extensionId), bytes.NewReader(createPayload)) if err != nil { return nil, fmt.Errorf(errorFormat, err) } @@ -132,7 +132,7 @@ func (e ProducerEndpoint) CreateExtensionBinary(ctx context.Context, extensionId return binary, nil } -func (e ProducerEndpoint) UpdateExtensionBinaryFile(ctx context.Context, extensionId, binaryId int, zipPath string) error { +func (e ProducerEndpoint) UpdateExtensionBinaryFile(ctx context.Context, producerId, extensionId, binaryId int, zipPath string) error { errorFormat := "UpdateExtensionBinaryFile: %v" var b bytes.Buffer @@ -157,7 +157,7 @@ func (e ProducerEndpoint) UpdateExtensionBinaryFile(ctx context.Context, extensi return fmt.Errorf(errorFormat, err) } - r, err := e.c.NewAuthenticatedRequest(ctx, "POST", fmt.Sprintf("%s/producers/%d/plugins/%d/binaries/%d/file", ApiUrl, e.producerId, extensionId, binaryId), &b) + r, err := e.c.NewAuthenticatedRequest(ctx, "POST", fmt.Sprintf("%s/producers/%d/plugins/%d/binaries/%d/file", ApiUrl, producerId, extensionId, binaryId), &b) if err != nil { return fmt.Errorf(errorFormat, err) }