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.
 
 
 
 

872 lines
25 KiB

package web
import (
"bytes"
"encoding/csv"
"fmt"
"html/template"
"log"
"net/http"
"strconv"
"strings"
"time"
root "marmic/servicetrade-toolbox"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/middleware"
"github.com/gorilla/csrf"
)
// RoleAssignmentResult captures the outcome of assigning a role to a user.
type RoleAssignmentResult struct {
Token string
RoleID int
Role string
Success bool
Message string
}
// UserImportResult represents the processing result for a single CSV row.
type UserImportResult struct {
Row int
Username string
Email string
UserID int
Created bool
Error string
RoleAssignments []RoleAssignmentResult
ProcessingTime time.Duration
ServiceLineIDs []int
AdditionalFields map[string]string
}
// UserImportSummary aggregates statistics about a CSV import run.
type UserImportSummary struct {
TotalRows int
UsersCreated int
RowsFailed int
RoleAssignments int
RoleAssignmentErrors int
ProcessedFilename string
ProcessedAt time.Time
}
// UserUpdateResult represents the processing result for a single update row.
type UserUpdateResult struct {
Row int
Username string
Email string
UserID int
Updated bool
Error string
UpdatedFields []string
LookupMethod string
RoleAssignments []RoleAssignmentResult
ProcessingTime time.Duration
}
// UserUpdateSummary aggregates statistics about a CSV update run.
type UserUpdateSummary struct {
TotalRows int
UsersUpdated int
RowsFailed int
RoleAssignments int
RoleAssignmentErrors int
LookupsByUsername int
LookupsByID int
ProcessedFilename string
ProcessedAt time.Time
}
// UsersHandler renders the user management page.
func UsersHandler(w http.ResponseWriter, r *http.Request) {
session, ok := r.Context().Value(middleware.SessionKey).(*api.Session)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
data := baseUsersPageData(r)
data["Title"] = "Users"
data["Session"] = session
if roles, err := session.ListRoles(); err != nil {
log.Printf("UsersHandler: unable to load roles: %v", err)
data["RolesError"] = err.Error()
} else {
data["Roles"] = roles
}
renderUsersPage(w, r, data)
}
// UsersUploadHandler processes uploaded CSV files to create users and assign roles.
func UsersUploadHandler(w http.ResponseWriter, r *http.Request) {
start := time.Now()
session, ok := r.Context().Value(middleware.SessionKey).(*api.Session)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if err := r.ParseMultipartForm(10 << 20); err != nil {
http.Error(w, fmt.Sprintf("Unable to parse upload: %v", err), http.StatusBadRequest)
return
}
file, header, err := r.FormFile("csvFile")
if err != nil {
http.Error(w, fmt.Sprintf("Unable to read file: %v", err), http.StatusBadRequest)
return
}
defer file.Close()
reader := csv.NewReader(file)
reader.FieldsPerRecord = -1
reader.TrimLeadingSpace = true
rows, err := reader.ReadAll()
if err != nil {
http.Error(w, fmt.Sprintf("Unable to read CSV: %v", err), http.StatusBadRequest)
return
}
data := baseUsersPageData(r)
data["Title"] = "Users"
data["Session"] = session
if len(rows) < 2 {
data["FlashError"] = "CSV must include a header row and at least one data row."
renderUsersPage(w, r, data)
return
}
headerMap := buildHeaderIndex(rows[0])
required := []string{"username", "firstname", "lastname", "email", "password", "companyid", "locationid"}
if missing := missingHeaders(headerMap, required); len(missing) > 0 {
data["FlashError"] = fmt.Sprintf("Missing required column headers: %s", strings.Join(missing, ", "))
renderUsersPage(w, r, data)
return
}
roles, rolesErr := session.ListRoles()
if rolesErr != nil {
log.Printf("UsersUploadHandler: unable to load roles: %v", rolesErr)
data["RolesError"] = fmt.Sprintf("Unable to load roles: %v. Role names in the CSV will not resolve.", rolesErr)
} else {
data["Roles"] = roles
}
roleIndexByName := buildRoleNameIndex(roles)
results := make([]UserImportResult, 0, len(rows)-1)
summary := UserImportSummary{
TotalRows: len(rows) - 1,
ProcessedAt: time.Now(),
ProcessedFilename: header.Filename,
}
for rowIdx, row := range rows[1:] {
rowStart := time.Now()
result := UserImportResult{
Row: rowIdx + 2,
Username: getValue(row, headerMap, "username"),
Email: getValue(row, headerMap, "email"),
AdditionalFields: map[string]string{},
}
if rowIsEmpty(row) {
continue
}
if err := validateRequiredRowFields(row, headerMap, required); err != nil {
result.Error = err.Error()
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
companyID, err := strconv.Atoi(getValue(row, headerMap, "companyid"))
if err != nil {
result.Error = fmt.Sprintf("invalid companyId: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
locationID, err := strconv.Atoi(getValue(row, headerMap, "locationid"))
if err != nil {
result.Error = fmt.Sprintf("invalid locationId: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
isSales, err := parseOptionalBool(getValue(row, headerMap, "issales"))
if err != nil {
result.Error = fmt.Sprintf("invalid isSales value: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
mfaRequired, err := parseOptionalBool(getValue(row, headerMap, "mfarequired"))
if err != nil {
result.Error = fmt.Sprintf("invalid mfaRequired value: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
managerID, err := parseOptionalInt(getValue(row, headerMap, "managerid"))
if err != nil {
result.Error = fmt.Sprintf("invalid managerId value: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
serviceLineIDs, err := parseIntList(getValue(row, headerMap, "servicelineids"))
if err != nil {
result.Error = fmt.Sprintf("invalid serviceLineIds value: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
result.ServiceLineIDs = serviceLineIDs
payload := api.CreateUserPayload{
Username: result.Username,
FirstName: getValue(row, headerMap, "firstname"),
LastName: getValue(row, headerMap, "lastname"),
Password: getValue(row, headerMap, "password"),
Email: result.Email,
Phone: getValue(row, headerMap, "phone"),
CompanyID: companyID,
LocationID: locationID,
Details: getValue(row, headerMap, "details"),
Status: getValue(row, headerMap, "status"),
Timezone: getValue(row, headerMap, "timezone"),
ServiceLineIDs: serviceLineIDs,
}
if isSales != nil {
payload.IsSales = isSales
}
if mfaRequired != nil {
payload.MFARequired = mfaRequired
}
if managerID != nil {
payload.ManagerID = managerID
}
userRecord, err := session.CreateUser(payload)
if err != nil {
result.Error = fmt.Sprintf("failed to create user: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
result.Created = true
result.UserID = userRecord.ID
summary.UsersCreated++
roleTokens := parseRoleTokens(getValue(row, headerMap, "roles"))
if len(roleTokens) > 0 {
assignments := make([]RoleAssignmentResult, 0, len(roleTokens))
for _, token := range roleTokens {
assignResult := RoleAssignmentResult{Token: token}
role, resolved, message := resolveRoleToken(token, roleIndexByName)
if !resolved {
assignResult.Message = message
assignments = append(assignments, assignResult)
summary.RoleAssignmentErrors++
continue
}
assignResult.RoleID = role.ID
assignResult.Role = role.Name
if err := session.AssignRoleToUser(userRecord.ID, role.ID); err != nil {
assignResult.Message = fmt.Sprintf("failed to assign role: %v", err)
summary.RoleAssignmentErrors++
} else {
assignResult.Success = true
assignResult.Message = "assigned"
summary.RoleAssignments++
}
assignments = append(assignments, assignResult)
}
result.RoleAssignments = assignments
}
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
}
data["ImportResults"] = results
data["ImportSummary"] = summary
data["FlashSuccess"] = fmt.Sprintf("Processed %d row(s) in %s.", summary.TotalRows, time.Since(start).Round(time.Millisecond))
renderUsersPage(w, r, data)
}
// UsersUpdateHandler renders the user update page.
func UsersUpdateHandler(w http.ResponseWriter, r *http.Request) {
session, ok := r.Context().Value(middleware.SessionKey).(*api.Session)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
data := baseUsersPageData(r)
data["Title"] = "User Updates"
data["Session"] = session
if roles, err := session.ListRoles(); err != nil {
log.Printf("UsersUpdateHandler: unable to load roles: %v", err)
data["RolesError"] = err.Error()
} else {
data["Roles"] = roles
}
renderUsersUpdatePage(w, r, data)
}
// UsersUpdateUploadHandler processes CSV uploads to update existing users.
func UsersUpdateUploadHandler(w http.ResponseWriter, r *http.Request) {
start := time.Now()
session, ok := r.Context().Value(middleware.SessionKey).(*api.Session)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if err := r.ParseMultipartForm(10 << 20); err != nil {
http.Error(w, fmt.Sprintf("Unable to parse upload: %v", err), http.StatusBadRequest)
return
}
file, header, err := r.FormFile("csvFile")
if err != nil {
http.Error(w, fmt.Sprintf("Unable to read file: %v", err), http.StatusBadRequest)
return
}
defer file.Close()
reader := csv.NewReader(file)
reader.FieldsPerRecord = -1
reader.TrimLeadingSpace = true
rows, err := reader.ReadAll()
if err != nil {
http.Error(w, fmt.Sprintf("Unable to read CSV: %v", err), http.StatusBadRequest)
return
}
data := baseUsersPageData(r)
data["Title"] = "User Updates"
data["Session"] = session
if len(rows) < 2 {
data["FlashError"] = "CSV must include a header row and at least one data row."
renderUsersUpdatePage(w, r, data)
return
}
headerMap := buildHeaderIndex(rows[0])
if _, ok := headerMap["username"]; !ok {
if _, ok := headerMap["userid"]; !ok {
data["FlashError"] = "CSV must include either a username or userId column to locate users."
renderUsersUpdatePage(w, r, data)
return
}
}
roles, rolesErr := session.ListRoles()
if rolesErr != nil {
log.Printf("UsersUpdateUploadHandler: unable to load roles: %v", rolesErr)
data["RolesError"] = fmt.Sprintf("Unable to load roles: %v. Role names in the CSV will not resolve.", rolesErr)
} else {
data["Roles"] = roles
}
roleIndexByName := buildRoleNameIndex(roles)
results := make([]UserUpdateResult, 0, len(rows)-1)
summary := UserUpdateSummary{
TotalRows: len(rows) - 1,
ProcessedAt: time.Now(),
ProcessedFilename: header.Filename,
}
for rowIdx, row := range rows[1:] {
rowStart := time.Now()
if rowIsEmpty(row) {
continue
}
result := UserUpdateResult{
Row: rowIdx + 2,
Username: getValue(row, headerMap, "username"),
Email: getValue(row, headerMap, "email"),
}
userIDValue := getValue(row, headerMap, "userid")
var userID int
if userIDValue != "" {
parsedID, err := strconv.Atoi(userIDValue)
if err != nil || parsedID <= 0 {
result.Error = fmt.Sprintf("invalid userId: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
userID = parsedID
result.UserID = userID
result.LookupMethod = "userId"
summary.LookupsByID++
} else if result.Username != "" {
userRecord, err := session.FindUserByUsername(result.Username)
if err != nil {
result.Error = fmt.Sprintf("failed to locate user by username: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
userID = userRecord.ID
result.UserID = userID
result.LookupMethod = "username"
summary.LookupsByUsername++
} else {
result.Error = "row missing username and userId"
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
payload := api.UpdateUserPayload{}
var updatedFields []string
if v := getValue(row, headerMap, "firstname"); v != "" {
payload.FirstName = stringPtr(v)
updatedFields = append(updatedFields, "firstName")
}
if v := getValue(row, headerMap, "new_username"); v != "" {
payload.Username = stringPtr(v)
updatedFields = append(updatedFields, "username")
}
if v := getValue(row, headerMap, "lastname"); v != "" {
payload.LastName = stringPtr(v)
updatedFields = append(updatedFields, "lastName")
}
if v := getValue(row, headerMap, "email"); v != "" {
payload.Email = stringPtr(v)
updatedFields = append(updatedFields, "email")
}
if v := getValue(row, headerMap, "password"); v != "" {
payload.Password = stringPtr(v)
updatedFields = append(updatedFields, "password")
}
if v := getValue(row, headerMap, "phone"); v != "" {
payload.Phone = stringPtr(v)
updatedFields = append(updatedFields, "phone")
}
if v := getValue(row, headerMap, "details"); v != "" {
payload.Details = stringPtr(v)
updatedFields = append(updatedFields, "details")
}
if v := getValue(row, headerMap, "status"); v != "" {
payload.Status = stringPtr(v)
updatedFields = append(updatedFields, "status")
}
if v := getValue(row, headerMap, "timezone"); v != "" {
payload.Timezone = stringPtr(v)
updatedFields = append(updatedFields, "timezone")
}
if companyVal := getValue(row, headerMap, "companyid"); companyVal != "" {
companyID, err := strconv.Atoi(companyVal)
if err != nil {
result.Error = fmt.Sprintf("invalid companyId: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
payload.CompanyID = intPtr(companyID)
updatedFields = append(updatedFields, "companyId")
}
if locationVal := getValue(row, headerMap, "locationid"); locationVal != "" {
locationID, err := strconv.Atoi(locationVal)
if err != nil {
result.Error = fmt.Sprintf("invalid locationId: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
payload.LocationID = intPtr(locationID)
updatedFields = append(updatedFields, "locationId")
}
if managerVal := getValue(row, headerMap, "managerid"); managerVal != "" {
managerID, err := strconv.Atoi(managerVal)
if err != nil {
result.Error = fmt.Sprintf("invalid managerId: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
payload.ManagerID = intPtr(managerID)
updatedFields = append(updatedFields, "managerId")
}
if isSales, err := parseOptionalBool(getValue(row, headerMap, "issales")); err != nil {
result.Error = fmt.Sprintf("invalid isSales value: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
} else if isSales != nil {
payload.IsSales = isSales
updatedFields = append(updatedFields, "isSales")
}
if mfaRequired, err := parseOptionalBool(getValue(row, headerMap, "mfarequired")); err != nil {
result.Error = fmt.Sprintf("invalid mfaRequired value: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
} else if mfaRequired != nil {
payload.MFARequired = mfaRequired
updatedFields = append(updatedFields, "mfaRequired")
}
if serviceLineIDs, err := parseIntList(getValue(row, headerMap, "servicelineids")); err != nil {
result.Error = fmt.Sprintf("invalid serviceLineIds value: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
} else if serviceLineIDs != nil {
payload.ServiceLineIDs = &serviceLineIDs
updatedFields = append(updatedFields, "serviceLineIds")
}
if len(updatedFields) == 0 {
result.Error = "no updatable fields provided in row"
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
if _, err := session.UpdateUser(userID, payload); err != nil {
result.Error = fmt.Sprintf("failed to update user: %v", err)
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
summary.RowsFailed++
continue
}
result.Updated = true
result.UpdatedFields = updatedFields
summary.UsersUpdated++
roleTokens := parseRoleTokens(getValue(row, headerMap, "roles"))
if len(roleTokens) > 0 {
assignments := make([]RoleAssignmentResult, 0, len(roleTokens))
for _, token := range roleTokens {
assignResult := RoleAssignmentResult{Token: token}
role, resolved, message := resolveRoleToken(token, roleIndexByName)
if !resolved {
assignResult.Message = message
assignments = append(assignments, assignResult)
summary.RoleAssignmentErrors++
continue
}
assignResult.RoleID = role.ID
assignResult.Role = role.Name
if err := session.AssignRoleToUser(userID, role.ID); err != nil {
assignResult.Message = fmt.Sprintf("failed to assign role: %v", err)
summary.RoleAssignmentErrors++
} else {
assignResult.Success = true
assignResult.Message = "assigned"
summary.RoleAssignments++
}
assignments = append(assignments, assignResult)
}
result.RoleAssignments = assignments
}
result.ProcessingTime = time.Since(rowStart)
results = append(results, result)
}
data["UpdateResults"] = results
data["UpdateSummary"] = summary
if summary.UsersUpdated > 0 {
data["FlashSuccess"] = fmt.Sprintf("Updated %d user(s) (processed %d rows in %s).", summary.UsersUpdated, summary.TotalRows, time.Since(start).Round(time.Millisecond))
} else {
data["FlashError"] = "No users were updated. Review the row errors below."
}
renderUsersUpdatePage(w, r, data)
}
func renderUsersPage(w http.ResponseWriter, r *http.Request, data map[string]interface{}) {
renderUsersTemplatePage(w, r, data, "users_content")
}
func renderUsersUpdatePage(w http.ResponseWriter, r *http.Request, data map[string]interface{}) {
renderUsersTemplatePage(w, r, data, "users_update_content")
}
func renderUsersTemplatePage(w http.ResponseWriter, r *http.Request, data map[string]interface{}, templateName string) {
tmpl := root.WebTemplates
if r.Header.Get("HX-Request") == "true" {
if err := tmpl.ExecuteTemplate(w, templateName, data); err != nil {
log.Printf("UsersHandler: template error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
return
}
var contentBuf bytes.Buffer
if err := tmpl.ExecuteTemplate(&contentBuf, templateName, data); err != nil {
log.Printf("UsersHandler: template error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
data["BodyContent"] = template.HTML(contentBuf.String())
if err := tmpl.ExecuteTemplate(w, "layout.html", data); err != nil {
log.Printf("UsersHandler: layout template error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
func baseUsersPageData(r *http.Request) map[string]interface{} {
data := map[string]interface{}{
"CSRFField": csrf.TemplateField(r),
"CSRFToken": csrf.Token(r),
}
if c, err := r.Cookie("XSRF-TOKEN"); err == nil {
data["CSRFCookie"] = c.Value
} else if c, err := r.Cookie("XSRF-TOKEN-VALUE"); err == nil {
data["CSRFCookie"] = c.Value
}
return data
}
func buildHeaderIndex(headers []string) map[string]int {
index := make(map[string]int, len(headers))
for idx, header := range headers {
if header == "" {
continue
}
normalized := normalizeHeader(header)
if canonical, ok := headerCanonicalMap[normalized]; ok {
index[canonical] = idx
} else {
index[normalized] = idx
}
}
return index
}
func missingHeaders(headerIndex map[string]int, required []string) []string {
var missing []string
for _, key := range required {
if _, ok := headerIndex[key]; !ok {
missing = append(missing, key)
}
}
return missing
}
func rowIsEmpty(row []string) bool {
for _, value := range row {
if strings.TrimSpace(value) != "" {
return false
}
}
return true
}
func validateRequiredRowFields(row []string, index map[string]int, required []string) error {
var missing []string
for _, key := range required {
if strings.TrimSpace(getValue(row, index, key)) == "" {
missing = append(missing, key)
}
}
if len(missing) > 0 {
return fmt.Errorf("missing required value(s): %s", strings.Join(missing, ", "))
}
return nil
}
func getValue(row []string, index map[string]int, key string) string {
if idx, ok := index[key]; ok && idx < len(row) {
return strings.TrimSpace(row[idx])
}
return ""
}
func parseOptionalBool(value string) (*bool, error) {
if value == "" {
return nil, nil
}
switch strings.ToLower(strings.TrimSpace(value)) {
case "true", "1", "yes", "y":
result := true
return &result, nil
case "false", "0", "no", "n":
result := false
return &result, nil
default:
return nil, fmt.Errorf("expected boolean value, got %q", value)
}
}
func parseOptionalInt(value string) (*int, error) {
if strings.TrimSpace(value) == "" {
return nil, nil
}
intVal, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil {
return nil, err
}
return &intVal, nil
}
func parseIntList(raw string) ([]int, error) {
if strings.TrimSpace(raw) == "" {
return nil, nil
}
parts := splitTokens(raw)
ints := make([]int, 0, len(parts))
for _, part := range parts {
id, err := strconv.Atoi(part)
if err != nil {
return nil, fmt.Errorf("invalid integer %q", part)
}
ints = append(ints, id)
}
return ints, nil
}
func parseRoleTokens(raw string) []string {
if strings.TrimSpace(raw) == "" {
return nil
}
return splitTokens(raw)
}
func splitTokens(raw string) []string {
cleaned := strings.NewReplacer(";", ",", "|", ",").Replace(raw)
parts := strings.Split(cleaned, ",")
result := make([]string, 0, len(parts))
for _, part := range parts {
token := strings.TrimSpace(part)
if token != "" {
result = append(result, token)
}
}
return result
}
func resolveRoleToken(token string, nameIndex map[string]api.Role) (api.Role, bool, string) {
clean := strings.TrimSpace(token)
if clean == "" {
return api.Role{}, false, "empty role token"
}
normalized := normalizeRoleName(clean)
if role, ok := nameIndex[normalized]; ok {
return role, true, ""
}
return api.Role{}, false, fmt.Sprintf("role %q not found; use the role name exactly as listed", token)
}
func buildRoleNameIndex(roles []api.Role) map[string]api.Role {
nameIndex := make(map[string]api.Role, len(roles))
for _, role := range roles {
nameKey := normalizeRoleName(role.Name)
if nameKey != "" {
nameIndex[nameKey] = role
}
}
return nameIndex
}
func normalizeHeader(value string) string {
v := strings.TrimSpace(strings.ToLower(value))
v = strings.ReplaceAll(v, " ", "")
v = strings.ReplaceAll(v, "_", "")
v = strings.ReplaceAll(v, "-", "")
return v
}
func normalizeRoleName(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
var headerCanonicalMap = map[string]string{
"username": "username",
"user": "username",
"firstname": "firstname",
"first": "firstname",
"lastname": "lastname",
"last": "lastname",
"email": "email",
"mail": "email",
"password": "password",
"pass": "password",
"companyid": "companyid",
"company": "companyid",
"locationid": "locationid",
"location": "locationid",
"userid": "userid",
"id": "userid",
"newusername": "new_username",
"username_new": "new_username",
"usernameupdate": "new_username",
"phone": "phone",
"phonenumber": "phone",
"roles": "roles",
"roleids": "roles",
"rolenames": "roles",
"status": "status",
"timezone": "timezone",
"details": "details",
"issales": "issales",
"sales": "issales",
"managerid": "managerid",
"manager": "managerid",
"mfarequired": "mfarequired",
"mfa": "mfarequired",
"servicelineids": "servicelineids",
}
func stringPtr(value string) *string {
v := value
return &v
}
func intPtr(value int) *int {
v := value
return &v
}