package controllers import ( "errors" "hr_receiver/config" "hr_receiver/models" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type UserAdminController struct { DB *gorm.DB } type userAdminPayload struct { Email *string `json:"email"` FlavorType models.UserFlavorType `json:"flavorType"` IsActive *bool `json:"isActive"` Password string `json:"password"` Phone *string `json:"phone"` RegionIDs []uint32 `json:"regionIds"` RevokeTokens *bool `json:"revokeTokens"` Role models.UserRole `json:"role"` Username string `json:"username"` } type userAdminListItem struct { CreatedAt int64 `json:"created_at"` Email *string `json:"email"` FlavorType models.UserFlavorType `json:"flavorType"` ID uint `json:"id"` IsActive bool `json:"isActive"` KindergartenNames []string `json:"kindergartenNames"` Phone *string `json:"phone"` RegionIDs []uint32 `json:"regionIds"` Regions []models.UserRegionBinding `json:"regions"` Role models.UserRole `json:"role"` TokenInvalidBefore int64 `json:"tokenInvalidBefore"` UpdatedAt int64 `json:"updated_at"` Username string `json:"username"` } func NewUserAdminController() *UserAdminController { return &UserAdminController{DB: config.DB} } func (uc *UserAdminController) List(c *gin.Context) { var users []models.User query := uc.DB.Preload("Regions").Order("id ASC") if keyword := strings.TrimSpace(c.Query("keyword")); keyword != "" { likeValue := "%" + keyword + "%" query = query.Where("username LIKE ? OR email LIKE ? OR phone LIKE ?", likeValue, likeValue, likeValue) } if role := strings.TrimSpace(c.Query("role")); role != "" { query = query.Where("role = ?", role) } if flavorType := strings.TrimSpace(c.Query("flavorType")); flavorType != "" { query = query.Where("flavor_type = ?", flavorType) } if err := query.Find(&users).Error; err != nil { writeError(c, http.StatusInternalServerError, "failed to query users") return } items, err := uc.buildUserAdminItems(users) if err != nil { writeError(c, http.StatusInternalServerError, "failed to load kindergarten names") return } writeSuccess(c, http.StatusOK, "query success", items) } func (uc *UserAdminController) Create(c *gin.Context) { var payload userAdminPayload if err := c.ShouldBindJSON(&payload); err != nil { writeError(c, http.StatusBadRequest, err.Error()) return } if err := validateCreateUserPayload(payload); err != nil { writeError(c, http.StatusBadRequest, err.Error()) return } if err := validateRegionIDs(payload.RegionIDs); err != nil { writeError(c, http.StatusBadRequest, err.Error()) return } user := models.User{ Email: normalizeOptionalString(payload.Email), FlavorType: payload.FlavorType, Phone: normalizeOptionalString(payload.Phone), Password: payload.Password, Role: payload.Role, Username: strings.TrimSpace(payload.Username), Regions: buildUserRegionBindings(payload.RegionIDs), } if payload.IsActive != nil { user.IsActive = *payload.IsActive } if err := uc.DB.Create(&user).Error; err != nil { writeUserAdminDBError(c, err) return } if payload.IsActive != nil && !*payload.IsActive { if err := uc.DB.Model(&models.User{}).Where("id = ?", user.ID).Update("is_active", false).Error; err != nil { writeError(c, http.StatusInternalServerError, "failed to set user status") return } } if err := uc.DB.Preload("Regions").First(&user, user.ID).Error; err != nil { writeError(c, http.StatusInternalServerError, "failed to reload user") return } items, err := uc.buildUserAdminItems([]models.User{user}) if err != nil { writeError(c, http.StatusInternalServerError, "failed to load kindergarten names") return } writeSuccess(c, http.StatusCreated, "create success", items[0]) } func (uc *UserAdminController) Update(c *gin.Context) { user, err := uc.findUserByID(c.Param("id")) if err != nil { respondUserLookupError(c, err) return } var payload userAdminPayload if err := c.ShouldBindJSON(&payload); err != nil { writeError(c, http.StatusBadRequest, err.Error()) return } if err := validateUpdateUserPayload(payload); err != nil { writeError(c, http.StatusBadRequest, err.Error()) return } if err := validateRegionIDs(payload.RegionIDs); err != nil { writeError(c, http.StatusBadRequest, err.Error()) return } user.Username = strings.TrimSpace(payload.Username) user.Email = normalizeOptionalString(payload.Email) user.Phone = normalizeOptionalString(payload.Phone) user.Role = payload.Role user.FlavorType = payload.FlavorType if payload.IsActive != nil { user.IsActive = *payload.IsActive } if strings.TrimSpace(payload.Password) != "" { if err := user.HashPassword(payload.Password); err != nil { writeError(c, http.StatusInternalServerError, "failed to hash password") return } } if err := uc.DB.Transaction(func(tx *gorm.DB) error { revokeTokens := payload.RevokeTokens != nil && *payload.RevokeTokens updateData := map[string]interface{}{ "username": user.Username, "email": user.Email, "phone": user.Phone, "role": user.Role, "flavor_type": user.FlavorType, "is_active": user.IsActive, } if strings.TrimSpace(payload.Password) != "" { updateData["password"] = user.Password } if revokeTokens { updateData["token_invalid_before"] = time.Now().UnixMilli() } if err := tx.Model(&models.User{}).Where("id = ?", user.ID).Updates(updateData).Error; err != nil { return err } if err := tx.Where("user_id = ?", user.ID).Delete(&models.UserRegionBinding{}).Error; err != nil { return err } bindings := buildUserRegionBindings(payload.RegionIDs) if len(bindings) > 0 { for i := range bindings { bindings[i].UserID = user.ID } if err := tx.Create(&bindings).Error; err != nil { return err } } return nil }); err != nil { writeUserAdminDBError(c, err) return } if err := uc.DB.Preload("Regions").First(&user, user.ID).Error; err != nil { writeError(c, http.StatusInternalServerError, "failed to reload user") return } items, err := uc.buildUserAdminItems([]models.User{user}) if err != nil { writeError(c, http.StatusInternalServerError, "failed to load kindergarten names") return } writeSuccess(c, http.StatusOK, "update success", items[0]) } func (uc *UserAdminController) Delete(c *gin.Context) { user, err := uc.findUserByID(c.Param("id")) if err != nil { respondUserLookupError(c, err) return } if isProtectedAdminAccount(user.Username) { writeError(c, http.StatusBadRequest, "cannot delete protected admin user") return } currentUserID, _, ok := currentUser(c) if ok && currentUserID == user.ID { writeError(c, http.StatusBadRequest, "cannot delete current user") return } if err := uc.DB.Delete(&user).Error; err != nil { writeError(c, http.StatusInternalServerError, "failed to delete user") return } writeSuccess(c, http.StatusOK, "delete success", nil) } func (uc *UserAdminController) findUserByID(id string) (models.User, error) { var user models.User numericID, err := strconv.ParseUint(strings.TrimSpace(id), 10, 64) if err != nil { return user, gorm.ErrRecordNotFound } if err := uc.DB.Preload("Regions").First(&user, numericID).Error; err != nil { return user, err } return user, nil } func (uc *UserAdminController) buildUserAdminItems(users []models.User) ([]userAdminListItem, error) { regionIDs := make([]uint32, 0) seenRegions := make(map[uint32]struct{}) for _, user := range users { for _, region := range user.Regions { if _, exists := seenRegions[region.RegionID]; exists { continue } seenRegions[region.RegionID] = struct{}{} regionIDs = append(regionIDs, region.RegionID) } } kindergartenMap := make(map[uint32]string) if len(regionIDs) > 0 { var kindergartens []models.Kindergarten if err := uc.DB.Where("region_id IN ?", regionIDs).Find(&kindergartens).Error; err != nil { return nil, err } for _, item := range kindergartens { kindergartenMap[item.RegionID] = item.Name } } items := make([]userAdminListItem, 0, len(users)) for _, user := range users { item := userAdminListItem{ CreatedAt: user.CreatedAt, Email: user.Email, FlavorType: user.FlavorType, ID: user.ID, IsActive: user.IsActive, Phone: user.Phone, RegionIDs: user.RegionIDs(), Regions: user.Regions, Role: user.Role, TokenInvalidBefore: user.TokenInvalidBefore, UpdatedAt: user.UpdatedAt, Username: user.Username, } for _, regionID := range item.RegionIDs { if kindergartenName, exists := kindergartenMap[regionID]; exists { item.KindergartenNames = append(item.KindergartenNames, kindergartenName) } } items = append(items, item) } return items, nil } func validateCreateUserPayload(payload userAdminPayload) error { if strings.TrimSpace(payload.Username) == "" { return errors.New("username is required") } if strings.TrimSpace(payload.Password) == "" { return errors.New("password is required") } return validateCommonUserPayload(payload) } func validateUpdateUserPayload(payload userAdminPayload) error { if strings.TrimSpace(payload.Username) == "" { return errors.New("username is required") } return validateCommonUserPayload(payload) } func validateCommonUserPayload(payload userAdminPayload) error { if payload.Role == "" { return errors.New("role is required") } if payload.FlavorType == "" { return errors.New("flavorType is required") } return nil } func validateRegionIDs(regionIDs []uint32) error { seen := make(map[uint32]struct{}, len(regionIDs)) for _, regionID := range regionIDs { if regionID == 0 { return errors.New("regionIds contains invalid value") } if _, exists := seen[regionID]; exists { return errors.New("regionIds contains duplicate value") } seen[regionID] = struct{}{} } return nil } func normalizeOptionalString(value *string) *string { if value == nil { return nil } trimmed := strings.TrimSpace(*value) if trimmed == "" { return nil } return &trimmed } func isProtectedAdminAccount(username string) bool { return strings.EqualFold(strings.TrimSpace(username), "admin") } func respondUserLookupError(c *gin.Context, err error) { if errors.Is(err, gorm.ErrRecordNotFound) { writeError(c, http.StatusNotFound, "user not found") return } writeError(c, http.StatusInternalServerError, "failed to query user") } func writeUserAdminDBError(c *gin.Context, err error) { if strings.Contains(strings.ToLower(err.Error()), "unique") { writeError(c, http.StatusConflict, "username, email or phone already exists") return } writeError(c, http.StatusInternalServerError, "failed to persist user") }