Deduplicate connector driver request helpers
The Crisp and Railway connectors each repeated their HTTP plumbing across the driver, the name resolver, and (for Crisp) the subscription settings fetcher: the same JoinPath/headers/Do for Crisp GETs and the same marshal/POST/headers for Railway GraphQL. The Crisp and Scaleway drivers also carried byte-identical owner/member role mapping and admin checks. Extract crispGet and railwayPost as package-private request helpers so each call site owns only status handling, and lift the owner/member role mapping into shared ownerMemberRoles/isOwnerRole helpers beside activeFromStatus in driver.go. Name the Crisp base URL and tier header as consts in probe.go's const block rather than inlining the literals, matching the file's existing convention. Behavior is unchanged. Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
@@ -96,22 +96,9 @@ func NewCrispDriver(httpClient *http.Client, websiteID string) *CrispDriver {
|
||||
}
|
||||
|
||||
func (d *CrispDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) {
|
||||
endpoint, err := url.JoinPath(crispAPIBaseURL, "website", url.PathEscape(d.websiteID), "operators", "list")
|
||||
httpResp, err := crispGet(ctx, d.httpClient, "operators", "website", url.PathEscape(d.websiteID), "operators", "list")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build crisp operators URL: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create crisp operators request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set(crispTierHeader, crispTierValue)
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute crisp operators request: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
@@ -138,9 +125,9 @@ func (d *CrispDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
|
||||
records = append(records, AccountRecord{
|
||||
Email: email,
|
||||
FullName: crispFullName(details, email),
|
||||
Roles: crispRoles(details.Role),
|
||||
Roles: ownerMemberRoles(details.Role),
|
||||
JobTitle: strings.TrimSpace(details.Title),
|
||||
IsAdmin: crispIsAdmin(details.Role),
|
||||
IsAdmin: isOwnerRole(details.Role),
|
||||
MFAStatus: coredata.MFAStatusUnknown,
|
||||
AuthMethod: coredata.AccessReviewEntryAuthMethodUnknown,
|
||||
AccountType: coredata.AccessReviewEntryAccountTypeUser,
|
||||
@@ -164,28 +151,17 @@ func GetCrispSubscriptionSettings(
|
||||
websiteID string,
|
||||
pluginID string,
|
||||
) (*CrispSubscriptionSettings, error) {
|
||||
endpoint, err := url.JoinPath(
|
||||
crispAPIBaseURL,
|
||||
httpResp, err := crispGet(
|
||||
ctx,
|
||||
httpClient,
|
||||
"subscription settings",
|
||||
"plugins", "subscription",
|
||||
url.PathEscape(websiteID),
|
||||
url.PathEscape(pluginID),
|
||||
"settings",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build crisp subscription settings URL: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create crisp subscription settings request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set(crispTierHeader, crispTierValue)
|
||||
|
||||
httpResp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute crisp subscription settings request: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
@@ -210,6 +186,33 @@ func GetCrispSubscriptionSettings(
|
||||
return &resp.Data.Settings, nil
|
||||
}
|
||||
|
||||
// crispGet issues an authenticated GET against the Crisp API for the given path
|
||||
// segments (joined onto crispAPIBaseURL), setting the Accept and X-Crisp-Tier
|
||||
// headers every Crisp request needs; the Basic plugin credential is attached by
|
||||
// the connection transport. The caller owns status-code handling and must close
|
||||
// the returned response body. label names the request in wrapped errors.
|
||||
func crispGet(ctx context.Context, httpClient *http.Client, label string, path ...string) (*http.Response, error) {
|
||||
endpoint, err := url.JoinPath(crispAPIBaseURL, path...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot build crisp %s URL: %w", label, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create crisp %s request: %w", label, err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set(crispTierHeader, crispTierValue)
|
||||
|
||||
httpResp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute crisp %s request: %w", label, err)
|
||||
}
|
||||
|
||||
return httpResp, nil
|
||||
}
|
||||
|
||||
func crispFullName(details crispOperatorDetails, fallback string) string {
|
||||
if name := strings.TrimSpace(details.FirstName + " " + details.LastName); name != "" {
|
||||
return name
|
||||
@@ -217,27 +220,3 @@ func crispFullName(details crispOperatorDetails, fallback string) string {
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
// crispRoles maps a Crisp operator role to a display label. Documented roles
|
||||
// are owner/member; an unknown future value is passed through verbatim and no
|
||||
// role yields an empty slice.
|
||||
func crispRoles(role string) []string {
|
||||
switch strings.ToLower(strings.TrimSpace(role)) {
|
||||
case "owner":
|
||||
return []string{"Owner"}
|
||||
case "member":
|
||||
return []string{"Member"}
|
||||
default:
|
||||
if r := strings.TrimSpace(role); r != "" {
|
||||
return []string{r}
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// crispIsAdmin reports whether a Crisp operator role grants administrative
|
||||
// access. Only the website owner is an administrator; members are not.
|
||||
func crispIsAdmin(role string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(role), "owner")
|
||||
}
|
||||
|
||||
@@ -106,3 +106,28 @@ func activeFromStatus(status string) *bool {
|
||||
return &inactive
|
||||
}
|
||||
}
|
||||
|
||||
// ownerMemberRoles maps a provider role to a display label for providers whose
|
||||
// role model is exactly owner/member (e.g. Crisp operators, Scaleway org user
|
||||
// types): "owner" → Owner, "member" → Member. An unknown future value is passed
|
||||
// through verbatim and no role yields an empty slice.
|
||||
func ownerMemberRoles(role string) []string {
|
||||
switch strings.ToLower(strings.TrimSpace(role)) {
|
||||
case "owner":
|
||||
return []string{"Owner"}
|
||||
case "member":
|
||||
return []string{"Member"}
|
||||
default:
|
||||
if r := strings.TrimSpace(role); r != "" {
|
||||
return []string{r}
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// isOwnerRole reports whether a provider role is the owner, the only role in the
|
||||
// owner/member model that grants administrative access.
|
||||
func isOwnerRole(role string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(role), "owner")
|
||||
}
|
||||
|
||||
@@ -1546,28 +1546,9 @@ func NewRailwayNameResolver(httpClient *http.Client) NameResolver {
|
||||
}
|
||||
|
||||
func (r *railwayNameResolver) ResolveInstanceName(ctx context.Context) (string, error) {
|
||||
body := struct {
|
||||
Query string `json:"query"`
|
||||
}{
|
||||
Query: `query { me { name workspaces { id name } } }`,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(body)
|
||||
httpResp, err := railwayPost(ctx, r.httpClient, "account", `query { me { name workspaces { id name } } }`)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot marshal railway account query: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, railwayGraphQLEndpoint, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create railway account request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute railway account request: %w", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
@@ -1628,22 +1609,9 @@ func (r *crispNameResolver) ResolveInstanceName(ctx context.Context) (string, er
|
||||
return "", nil
|
||||
}
|
||||
|
||||
endpoint, err := url.JoinPath(crispAPIBaseURL, "website", url.PathEscape(r.websiteID))
|
||||
httpResp, err := crispGet(ctx, r.httpClient, "website", "website", url.PathEscape(r.websiteID))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot build crisp website URL: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create crisp website request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set(crispTierHeader, crispTierValue)
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute crisp website request: %w", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
@@ -189,28 +189,9 @@ func railwayRecords(me *railwayMe) []AccountRecord {
|
||||
}
|
||||
|
||||
func (d *RailwayDriver) queryMe(ctx context.Context) (*railwayMe, error) {
|
||||
body := struct {
|
||||
Query string `json:"query"`
|
||||
}{
|
||||
Query: railwayMembersQuery,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(body)
|
||||
httpResp, err := railwayPost(ctx, d.httpClient, "members", railwayMembersQuery)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot marshal railway members query: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, railwayGraphQLEndpoint, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create railway members request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute railway members request: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
@@ -238,6 +219,38 @@ func (d *RailwayDriver) queryMe(ctx context.Context) (*railwayMe, error) {
|
||||
return resp.Data.Me, nil
|
||||
}
|
||||
|
||||
// railwayPost issues a GraphQL POST carrying query to Railway's endpoint,
|
||||
// setting the Content-Type and Accept headers; the Bearer credential is attached
|
||||
// by the connection transport. The caller owns status handling and must close
|
||||
// the returned response body. label names the request in wrapped errors.
|
||||
func railwayPost(ctx context.Context, httpClient *http.Client, label, query string) (*http.Response, error) {
|
||||
body := struct {
|
||||
Query string `json:"query"`
|
||||
}{
|
||||
Query: query,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot marshal railway %s query: %w", label, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, railwayGraphQLEndpoint, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create railway %s request: %w", label, err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute railway %s request: %w", label, err)
|
||||
}
|
||||
|
||||
return httpResp, nil
|
||||
}
|
||||
|
||||
func railwayFullName(m railwayMember, fallback string) string {
|
||||
if name := strings.TrimSpace(m.Name); name != "" {
|
||||
return name
|
||||
|
||||
@@ -96,9 +96,9 @@ func (d *ScalewayDriver) ListAccounts(ctx context.Context) ([]AccountRecord, err
|
||||
record := AccountRecord{
|
||||
Email: email,
|
||||
FullName: scalewayFullName(u, email),
|
||||
Roles: scalewayRoles(u.Type),
|
||||
Roles: ownerMemberRoles(u.Type),
|
||||
Active: scalewayActive(u.Status, u.Locked),
|
||||
IsAdmin: scalewayIsAdmin(u.Type),
|
||||
IsAdmin: isOwnerRole(u.Type),
|
||||
MFAStatus: scalewayMFAStatus(u),
|
||||
AuthMethod: coredata.AccessReviewEntryAuthMethodUnknown,
|
||||
AccountType: coredata.AccessReviewEntryAccountTypeUser,
|
||||
@@ -176,30 +176,6 @@ func scalewayFullName(u scalewayUser, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// scalewayRoles maps the Scaleway org-level user type to a display label. The
|
||||
// users endpoint exposes only owner/member; an unknown future value is passed
|
||||
// through verbatim and no type yields an empty slice.
|
||||
func scalewayRoles(userType string) []string {
|
||||
switch strings.ToLower(strings.TrimSpace(userType)) {
|
||||
case "owner":
|
||||
return []string{"Owner"}
|
||||
case "member":
|
||||
return []string{"Member"}
|
||||
default:
|
||||
if t := strings.TrimSpace(userType); t != "" {
|
||||
return []string{t}
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// scalewayIsAdmin reports whether a Scaleway user type grants administrative
|
||||
// access. Only the organization owner is an administrator; members are not.
|
||||
func scalewayIsAdmin(userType string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(userType), "owner")
|
||||
}
|
||||
|
||||
// scalewayActive maps the Scaleway user status to the three-valued Active
|
||||
// signal. A locked account is always inactive; otherwise only the documented
|
||||
// "activated"/"invitation_pending" values are an explicit signal and any other
|
||||
|
||||
@@ -40,6 +40,9 @@ const (
|
||||
posthogOrganizationPath = "/api/organizations/@current/"
|
||||
posthogUSBaseURL = "https://us.posthog.com"
|
||||
posthogEUBaseURL = "https://eu.posthog.com"
|
||||
crispAPIBaseURL = "https://api.crisp.chat/v1"
|
||||
crispTierHeader = "X-Crisp-Tier"
|
||||
crispTierValue = "plugin"
|
||||
)
|
||||
|
||||
// ProbeConnection verifies that the connector credential is accepted by the
|
||||
@@ -505,7 +508,7 @@ func probeCrisp(
|
||||
return fmt.Errorf("missing crisp website_id")
|
||||
}
|
||||
|
||||
endpoint, err := url.JoinPath("https://api.crisp.chat/v1", "website", url.PathEscape(s.WebsiteID), "operators", "list")
|
||||
endpoint, err := url.JoinPath(crispAPIBaseURL, "website", url.PathEscape(s.WebsiteID), "operators", "list")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot build crisp probe URL: %w", err)
|
||||
}
|
||||
@@ -516,7 +519,7 @@ func probeCrisp(
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("X-Crisp-Tier", "plugin")
|
||||
req.Header.Set(crispTierHeader, crispTierValue)
|
||||
|
||||
return doProbeRequest(httpClient, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user