an updated and hopefully faster version of the ST Toolbox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

481 lines
12 KiB

package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
)
// CreateUserPayload represents the payload required to create a ServiceTrade user.
type CreateUserPayload struct {
Username string `json:"username"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Password string `json:"password"`
Email string `json:"email"`
Phone string `json:"phone,omitempty"`
CompanyID int `json:"companyId"`
LocationID int `json:"locationId"`
Details string `json:"details,omitempty"`
Status string `json:"status,omitempty"`
IsSales *bool `json:"isSales,omitempty"`
Timezone string `json:"timezone,omitempty"`
ServiceLineIDs []int `json:"serviceLineIds,omitempty"`
ManagerID *int `json:"managerId,omitempty"`
MFARequired *bool `json:"mfaRequired,omitempty"`
}
// UserRecord represents a simplified ServiceTrade user returned from the API.
type UserRecord struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Status string `json:"status"`
Roles []Role `json:"roles,omitempty"`
Raw []byte `json:"-"`
}
// Role represents a ServiceTrade role.
type Role struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Activities []string `json:"activities,omitempty"`
Global bool `json:"global"`
}
// UpdateUserPayload represents the fields that can be updated on an existing ServiceTrade user.
type UpdateUserPayload struct {
Username *string `json:"username,omitempty"`
FirstName *string `json:"firstName,omitempty"`
LastName *string `json:"lastName,omitempty"`
Password *string `json:"password,omitempty"`
Email *string `json:"email,omitempty"`
Phone *string `json:"phone,omitempty"`
CompanyID *int `json:"companyId,omitempty"`
LocationID *int `json:"locationId,omitempty"`
Details *string `json:"details,omitempty"`
Status *string `json:"status,omitempty"`
IsSales *bool `json:"isSales,omitempty"`
Timezone *string `json:"timezone,omitempty"`
ServiceLineIDs *[]int `json:"serviceLineIds,omitempty"`
ManagerID *int `json:"managerId,omitempty"`
MFARequired *bool `json:"mfaRequired,omitempty"`
}
// CreateUser creates a new ServiceTrade user and returns the created user record.
func (s *Session) CreateUser(payload CreateUserPayload) (*UserRecord, error) {
bodyBytes, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("error marshalling user payload: %w", err)
}
resp, err := s.DoRequest(http.MethodPost, "/user", bytes.NewBuffer(bodyBytes))
if err != nil {
return nil, fmt.Errorf("error sending create user request: %w", err)
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading create user response: %w", err)
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("create user failed: %s - %s", resp.Status, string(responseBody))
}
user, err := parseUserFromResponse(responseBody)
if err != nil {
return nil, fmt.Errorf("unable to parse create user response: %w", err)
}
user.Raw = responseBody
return user, nil
}
// AssignRoleToUser assigns a role to a ServiceTrade user.
func (s *Session) AssignRoleToUser(userID, roleID int) error {
endpoint := fmt.Sprintf("/user/%d/role/%d", userID, roleID)
resp, err := s.DoRequest(http.MethodPost, endpoint, nil)
if err != nil {
return fmt.Errorf("error assigning role %d to user %d: %w", roleID, userID, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("assign role failed: %s - %s", resp.Status, string(body))
}
return nil
}
// ListRoles retrieves all ServiceTrade roles visible to the authenticated account.
func (s *Session) ListRoles() ([]Role, error) {
resp, err := s.DoRequest(http.MethodGet, "/role", nil)
if err != nil {
return nil, fmt.Errorf("error requesting roles: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading roles response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("list roles failed: %s - %s", resp.Status, string(body))
}
roles, err := parseRolesFromResponse(body)
if err != nil {
return nil, err
}
return roles, nil
}
// UpdateUser updates an existing ServiceTrade user with the provided fields.
func (s *Session) UpdateUser(userID int, payload UpdateUserPayload) (*UserRecord, error) {
bodyBytes, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("error marshalling user update payload: %w", err)
}
endpoint := fmt.Sprintf("/user/%d", userID)
resp, err := s.DoRequest(http.MethodPut, endpoint, bytes.NewBuffer(bodyBytes))
if err != nil {
return nil, fmt.Errorf("error sending update user request: %w", err)
}
defer resp.Body.Close()
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading update user response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("update user failed: %s - %s", resp.Status, string(responseBody))
}
user, err := parseUserFromResponse(responseBody)
if err != nil {
return nil, fmt.Errorf("unable to parse update user response: %w", err)
}
user.Raw = responseBody
return user, nil
}
// FindUserByUsername searches for a user by username. Returns an error if not exactly one match is found.
func (s *Session) FindUserByUsername(username string) (*UserRecord, error) {
if strings.TrimSpace(username) == "" {
return nil, fmt.Errorf("username is required")
}
query := url.Values{}
query.Set("search", username)
query.Set("searchFields", "username")
query.Set("status", "active,inactive,pending")
endpoint := fmt.Sprintf("/user?%s", query.Encode())
resp, err := s.DoRequest(http.MethodGet, endpoint, nil)
if err != nil {
return nil, fmt.Errorf("error searching for user %q: %w", username, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading user search response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("user search failed: %s - %s", resp.Status, string(body))
}
users, err := parseUsersFromResponse(body)
if err != nil {
return nil, fmt.Errorf("error parsing user search response: %w", err)
}
var matches []*UserRecord
for i := range users {
if strings.EqualFold(users[i].Username, username) {
matches = append(matches, &users[i])
}
}
if len(matches) == 0 {
return nil, fmt.Errorf("no user found with username %q", username)
}
if len(matches) > 1 {
return nil, fmt.Errorf("multiple users found with username %q", username)
}
return matches[0], nil
}
func parseUserFromResponse(body []byte) (*UserRecord, error) {
var root map[string]interface{}
if err := json.Unmarshal(body, &root); err != nil {
return nil, fmt.Errorf("error decoding user response: %w", err)
}
candidate := extractMap(root, "data", "user")
if candidate == nil {
candidate = extractFirstMapFromArray(root, "data", "users")
}
if candidate == nil {
if userVal, ok := root["user"].(map[string]interface{}); ok {
candidate = userVal
}
}
if candidate == nil {
if dataVal, ok := root["data"].(map[string]interface{}); ok {
candidate = dataVal
}
}
if candidate == nil {
candidate = root
}
return buildUserRecord(candidate)
}
func parseUsersFromResponse(body []byte) ([]UserRecord, error) {
var root map[string]interface{}
if err := json.Unmarshal(body, &root); err != nil {
return nil, fmt.Errorf("error decoding users response: %w", err)
}
var records []interface{}
if dataMap, ok := root["data"].(map[string]interface{}); ok {
if list, ok := dataMap["users"].([]interface{}); ok {
records = list
} else if list, ok := dataMap["records"].([]interface{}); ok {
records = list
}
}
if records == nil {
if list, ok := root["users"].([]interface{}); ok {
records = list
}
}
if records == nil {
return []UserRecord{}, nil
}
results := make([]UserRecord, 0, len(records))
for _, item := range records {
userMap, ok := item.(map[string]interface{})
if !ok {
continue
}
user, err := buildUserRecord(userMap)
if err != nil {
return nil, err
}
results = append(results, *user)
}
return results, nil
}
func parseRolesFromResponse(body []byte) ([]Role, error) {
var root map[string]interface{}
if err := json.Unmarshal(body, &root); err != nil {
return nil, fmt.Errorf("error decoding roles response: %w", err)
}
var rolesList []interface{}
if dataMap, ok := root["data"].(map[string]interface{}); ok {
if list, ok := dataMap["roles"].([]interface{}); ok {
rolesList = list
} else if list, ok := dataMap["records"].([]interface{}); ok {
rolesList = list
}
}
if rolesList == nil {
if list, ok := root["roles"].([]interface{}); ok {
rolesList = list
}
}
if rolesList == nil {
return nil, fmt.Errorf("roles not found in response")
}
return convertRoles(rolesList), nil
}
func extractMap(root map[string]interface{}, keys ...string) map[string]interface{} {
current := root
for _, key := range keys {
val, ok := current[key]
if !ok {
return nil
}
if nextMap, ok := val.(map[string]interface{}); ok {
current = nextMap
} else {
return nil
}
}
return current
}
func extractFirstMapFromArray(root map[string]interface{}, keys ...string) map[string]interface{} {
if len(keys) == 0 {
return nil
}
lastKey := keys[len(keys)-1]
parent := extractMap(root, keys[:len(keys)-1]...)
if parent == nil {
return nil
}
val, ok := parent[lastKey]
if !ok {
return nil
}
arr, ok := val.([]interface{})
if !ok || len(arr) == 0 {
return nil
}
first, ok := arr[0].(map[string]interface{})
if !ok {
return nil
}
return first
}
func convertRoles(items []interface{}) []Role {
var roles []Role
for _, item := range items {
roleMap, ok := item.(map[string]interface{})
if !ok {
continue
}
role := Role{
ID: toInt(roleMap["id"]),
Name: toString(roleMap["name"]),
Description: toString(roleMap["description"]),
Global: toBool(roleMap["global"]),
Activities: toStringSlice(roleMap["activities"]),
}
roles = append(roles, role)
}
return roles
}
func toInt(value interface{}) int {
switch v := value.(type) {
case int:
return v
case int64:
return int(v)
case float64:
return int(v)
case json.Number:
i, _ := v.Int64()
return int(i)
case string:
if v == "" {
return 0
}
i, err := strconv.Atoi(strings.TrimSpace(v))
if err != nil {
return 0
}
return i
default:
return 0
}
}
func toString(value interface{}) string {
switch v := value.(type) {
case string:
return v
case json.Number:
return v.String()
case fmt.Stringer:
return v.String()
case int:
return strconv.Itoa(v)
case int64:
return strconv.FormatInt(v, 10)
case float64:
return strconv.FormatFloat(v, 'f', -1, 64)
default:
return ""
}
}
func toBool(value interface{}) bool {
switch v := value.(type) {
case bool:
return v
case string:
switch strings.ToLower(strings.TrimSpace(v)) {
case "true", "1", "yes", "y":
return true
case "false", "0", "no", "n":
return false
}
case float64:
return v != 0
case int:
return v != 0
case json.Number:
i, err := v.Int64()
if err == nil {
return i != 0
}
}
return false
}
func toStringSlice(value interface{}) []string {
switch v := value.(type) {
case []string:
return v
case []interface{}:
result := make([]string, 0, len(v))
for _, item := range v {
result = append(result, toString(item))
}
return result
default:
return nil
}
}
func buildUserRecord(candidate map[string]interface{}) (*UserRecord, error) {
if candidate == nil {
return nil, fmt.Errorf("user data missing in response")
}
user := &UserRecord{
ID: toInt(candidate["id"]),
Username: toString(candidate["username"]),
Email: toString(candidate["email"]),
FirstName: toString(candidate["firstName"]),
LastName: toString(candidate["lastName"]),
Status: toString(candidate["status"]),
}
if user.ID == 0 {
return nil, fmt.Errorf("user ID missing in response")
}
if rolesVal, ok := candidate["roles"].([]interface{}); ok {
user.Roles = convertRoles(rolesVal)
}
return user, nil
}