fix: use installedLocations from bulk token response instead of /locations/search
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
GHL includes installedLocations[] in the company-level token response for bulk installs. Use those IDs directly to avoid calling /locations/search, which requires locations.readonly scope that GHL doesn't grant. Falls back to /locations/search only when the list is absent. Also adds raw_body and installed_locations fields to token response debug logging. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
3d1e80cd86
commit
3863e8f0cd
@ -227,31 +227,48 @@ func (h *OAuthHandler) postToken(ctx context.Context, data url.Values) (*TokenRe
|
|||||||
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse token response: %w", err)
|
return nil, fmt.Errorf("failed to parse token response: %w", err)
|
||||||
}
|
}
|
||||||
slog.Info("ghl token response fields", "location_id", tokenResp.LocationID, "company_id", tokenResp.CompanyID, "user_type", tokenResp.UserType)
|
slog.Info("ghl token response fields",
|
||||||
|
"location_id", tokenResp.LocationID,
|
||||||
|
"company_id", tokenResp.CompanyID,
|
||||||
|
"user_type", tokenResp.UserType,
|
||||||
|
"installed_locations", tokenResp.InstalledLocations,
|
||||||
|
"raw_body", string(body),
|
||||||
|
)
|
||||||
return &tokenResp, nil
|
return &tokenResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// installAllLocations fetches all locations for the company and exchanges the company token
|
// installAllLocations exchanges the company token for per-location tokens and stores them.
|
||||||
// for a per-location token for each, storing them all. Returns the count installed.
|
// It first tries to use the InstalledLocations list from the token response (provided by GHL
|
||||||
|
// for bulk installs). Falls back to GET /locations/search if that list is empty.
|
||||||
func (h *OAuthHandler) installAllLocations(ctx context.Context, companyToken *TokenResponse) (int, error) {
|
func (h *OAuthHandler) installAllLocations(ctx context.Context, companyToken *TokenResponse) (int, error) {
|
||||||
locations, err := h.getCompanyLocations(ctx, companyToken.AccessToken, companyToken.CompanyID)
|
// Build location ID list from token response when GHL provides it (bulk install path).
|
||||||
if err != nil {
|
locationIDs := companyToken.InstalledLocations
|
||||||
return 0, fmt.Errorf("list locations: %w", err)
|
if len(locationIDs) == 0 {
|
||||||
}
|
slog.Info("ghl bulk install: no installedLocations in token, falling back to locations search", "company_id", companyToken.CompanyID)
|
||||||
if len(locations) == 0 {
|
locations, err := h.getCompanyLocations(ctx, companyToken.AccessToken, companyToken.CompanyID)
|
||||||
return 0, fmt.Errorf("no locations found for company %s", companyToken.CompanyID)
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("list locations: %w", err)
|
||||||
|
}
|
||||||
|
if len(locations) == 0 {
|
||||||
|
return 0, fmt.Errorf("no locations found for company %s", companyToken.CompanyID)
|
||||||
|
}
|
||||||
|
for _, loc := range locations {
|
||||||
|
locationIDs = append(locationIDs, loc.ID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slog.Info("ghl bulk install: using installedLocations from token", "company_id", companyToken.CompanyID, "count", len(locationIDs))
|
||||||
}
|
}
|
||||||
|
|
||||||
installed := 0
|
installed := 0
|
||||||
for _, loc := range locations {
|
for _, locID := range locationIDs {
|
||||||
locToken, err := h.exchangeForLocationToken(ctx, companyToken.AccessToken, companyToken.CompanyID, loc.ID)
|
locToken, err := h.exchangeForLocationToken(ctx, companyToken.AccessToken, companyToken.CompanyID, locID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("ghl location token exchange failed", "location_id", loc.ID, "location_name", loc.Name, "err", err)
|
slog.Warn("ghl location token exchange failed", "location_id", locID, "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
expiresAt := time.Now().Add(time.Duration(locToken.ExpiresIn) * time.Second)
|
expiresAt := time.Now().Add(time.Duration(locToken.ExpiresIn) * time.Second)
|
||||||
record := &store.TokenRecord{
|
record := &store.TokenRecord{
|
||||||
LocationID: loc.ID,
|
LocationID: locID,
|
||||||
CompanyID: companyToken.CompanyID,
|
CompanyID: companyToken.CompanyID,
|
||||||
AccessToken: locToken.AccessToken,
|
AccessToken: locToken.AccessToken,
|
||||||
RefreshToken: locToken.RefreshToken,
|
RefreshToken: locToken.RefreshToken,
|
||||||
@ -260,10 +277,10 @@ func (h *OAuthHandler) installAllLocations(ctx context.Context, companyToken *To
|
|||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
if err := h.store.SaveToken(ctx, record); err != nil {
|
if err := h.store.SaveToken(ctx, record); err != nil {
|
||||||
slog.Warn("ghl location token save failed", "location_id", loc.ID, "err", err)
|
slog.Warn("ghl location token save failed", "location_id", locID, "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
slog.Info("ghl location installed", "location_id", loc.ID, "location_name", loc.Name)
|
slog.Info("ghl location installed", "location_id", locID)
|
||||||
installed++
|
installed++
|
||||||
}
|
}
|
||||||
return installed, nil
|
return installed, nil
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
package ghl
|
package ghl
|
||||||
|
|
||||||
type TokenResponse struct {
|
type TokenResponse struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
ExpiresIn int `json:"expires_in"`
|
ExpiresIn int `json:"expires_in"`
|
||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
LocationID string `json:"locationId"`
|
LocationID string `json:"locationId"`
|
||||||
CompanyID string `json:"companyId"`
|
CompanyID string `json:"companyId"`
|
||||||
UserType string `json:"userType"`
|
UserType string `json:"userType"`
|
||||||
|
InstalledLocations []string `json:"installedLocations"` // populated on bulk company installs
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocationInfo struct {
|
type LocationInfo struct {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user