Add Google Analytics, Dotfile, Segment and Square access-review connectors
Two OAuth2 and two API-key connectors:
- Google Analytics (GA4): OAuth2 with both analytics.readonly and
analytics.manage.users.readonly (readonly alone 403s on the accounts
list); v1alpha accessBindings enumerated at account and property level
and merged by email; manual account picker (Pattern 1) with a
per-connection probe and name resolver; distinct from Google Workspace.
- Dotfile: API key in the X-DOTFILE-API-KEY header (Pattern 3); GET
/v1/users (owner/admin, suspended_at) with a static probe.
- Segment (Twilio): Public API token as Bearer with a required Region
setting (US or EU) mapped to the regional host; GET /users plus per-user
GET /users/{id} for roles and /invites for pending members; per-connection
BuildProbeURL.
- Square: OAuth2 (EMPLOYEES_READ) or a personal access token (Pattern 3);
POST /v2/team-members/search returns email/status/is_owner directly, so no
role resolution; custom probe and name resolver.
Google Analytics and Square are confidential OAuth clients, wired into the
bootstrap OAuth provider list and .env.example. Segment carries a required
extra setting, so the console add-source dialog maps region onto its
segmentRegion API-key input; without that mapping the value is silently
dropped and the create is rejected.
Cassette-backed driver tests plus unit tests for the Segment probe URL and
the bootstrap OAuth provider list.
Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
@@ -1661,3 +1661,102 @@ func (r *crispNameResolver) ResolveInstanceName(ctx context.Context) (string, er
|
||||
|
||||
return resp.Data.Name, nil
|
||||
}
|
||||
|
||||
// squareNameResolver resolves the Square merchant's business name via
|
||||
// GET /v2/merchants/me. A Square token — OAuth or PAT — is scoped to a single
|
||||
// merchant, so "me" resolves it for both connection kinds.
|
||||
type squareNameResolver struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
func NewSquareNameResolver(httpClient *http.Client) NameResolver {
|
||||
return &squareNameResolver{httpClient: httpClient}
|
||||
}
|
||||
|
||||
func (r *squareNameResolver) ResolveInstanceName(ctx context.Context) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://connect.squareup.com/v2/merchants/me", nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create square merchant request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Square-Version", squareAPIVersion)
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute square merchant request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
|
||||
// A non-2xx (revoked token, missing scope) is terminal: keep the generic
|
||||
// source name rather than make the source-name worker retry forever.
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Merchant struct {
|
||||
BusinessName string `json:"business_name"`
|
||||
} `json:"merchant"`
|
||||
}
|
||||
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
|
||||
return "", fmt.Errorf("cannot decode square merchant response: %w", err)
|
||||
}
|
||||
|
||||
return resp.Merchant.BusinessName, nil
|
||||
}
|
||||
|
||||
// googleAnalyticsNameResolver resolves a GA4 account's display name.
|
||||
type googleAnalyticsNameResolver struct {
|
||||
httpClient *http.Client
|
||||
accountID string
|
||||
}
|
||||
|
||||
func NewGoogleAnalyticsNameResolver(httpClient *http.Client, accountID string) NameResolver {
|
||||
return &googleAnalyticsNameResolver{httpClient: httpClient, accountID: accountID}
|
||||
}
|
||||
|
||||
func (r *googleAnalyticsNameResolver) ResolveInstanceName(ctx context.Context) (string, error) {
|
||||
if r.accountID == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
endpoint, err := url.JoinPath("https://"+googleAnalyticsAPIHost, "v1alpha", "accounts", url.PathEscape(r.accountID))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot build google analytics account URL: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create google analytics account request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute google analytics account request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
|
||||
// A non-2xx (revoked token, renamed/deleted account) is terminal: keep the
|
||||
// generic source name rather than retry forever.
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
|
||||
return "", fmt.Errorf("cannot decode google analytics account response: %w", err)
|
||||
}
|
||||
|
||||
return resp.DisplayName, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user