package handlers import ( "net/http" "strings" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "sproutgate-backend/internal/models" ) func (h *Handler) ListUsers(c *gin.Context) { users, err := h.store.ListUsers() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load users"}) return } publicUsers := make([]models.UserPublic, 0, len(users)) for _, u := range users { publicUsers = append(publicUsers, u.OwnerPublic()) } c.JSON(http.StatusOK, gin.H{"total": len(publicUsers), "users": publicUsers}) } func (h *Handler) GetPublicUser(c *gin.Context) { account := strings.TrimSpace(c.Param("account")) if account == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "account is required"}) return } users, err := h.store.ListUsers() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load users"}) return } for _, user := range users { if strings.EqualFold(strings.TrimSpace(user.Account), account) { if user.Banned { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } c.JSON(http.StatusOK, gin.H{"user": user.PublicProfile()}) return } } c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) } func (h *Handler) CreateUser(c *gin.Context) { var req createUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } req.Account = strings.TrimSpace(req.Account) if req.Account == "" || strings.TrimSpace(req.Password) == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "account and password are required"}) return } hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"}) return } wu, err := normalizePublicWebsiteURL(req.WebsiteURL) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } record := models.UserRecord{ Account: req.Account, PasswordHash: string(hash), Username: req.Username, Email: req.Email, Level: req.Level, SproutCoins: req.SproutCoins, SecondaryEmails: req.SecondaryEmails, Phone: req.Phone, AvatarURL: req.AvatarURL, WebsiteURL: wu, Bio: req.Bio, CreatedAt: models.NowISO(), UpdatedAt: models.NowISO(), } if err := h.store.CreateUser(record); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"user": record.OwnerPublic()}) } func (h *Handler) UpdateUser(c *gin.Context) { account := strings.TrimSpace(c.Param("account")) if account == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "account is required"}) return } var req updateUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } user, found, err := h.store.GetUser(account) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load user"}) return } if !found { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } if req.Password != nil && strings.TrimSpace(*req.Password) != "" { hash, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"}) return } user.PasswordHash = string(hash) } if req.Username != nil { user.Username = *req.Username } if req.Email != nil { user.Email = *req.Email } if req.Level != nil { user.Level = *req.Level } if req.SproutCoins != nil { user.SproutCoins = *req.SproutCoins } if req.SecondaryEmails != nil { user.SecondaryEmails = *req.SecondaryEmails } if req.Phone != nil { user.Phone = *req.Phone } if req.AvatarURL != nil { user.AvatarURL = *req.AvatarURL } if req.WebsiteURL != nil { wu, err := normalizePublicWebsiteURL(*req.WebsiteURL) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } user.WebsiteURL = wu } if req.Bio != nil { user.Bio = *req.Bio } if req.Banned != nil { user.Banned = *req.Banned if !user.Banned { user.BanReason = "" user.BannedAt = "" } else if strings.TrimSpace(user.BannedAt) == "" { user.BannedAt = models.NowISO() } } if req.BanReason != nil { r := strings.TrimSpace(*req.BanReason) if len(r) > maxBanReasonLen { c.JSON(http.StatusBadRequest, gin.H{"error": "ban reason is too long"}) return } if r != "" && !user.Banned { c.JSON(http.StatusBadRequest, gin.H{"error": "cannot set ban reason while user is not banned"}) return } user.BanReason = r } if err := h.store.SaveUser(user); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save user"}) return } c.JSON(http.StatusOK, gin.H{"user": user.OwnerPublic()}) } func (h *Handler) DeleteUser(c *gin.Context) { account := strings.TrimSpace(c.Param("account")) if account == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "account is required"}) return } if err := h.store.DeleteUser(account); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete user"}) return } c.JSON(http.StatusOK, gin.H{"deleted": true}) } func (h *Handler) AdminMiddleware() gin.HandlerFunc { return func(c *gin.Context) { token := adminTokenFromRequest(c) if token == "" || token != h.store.AdminToken() { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid admin token"}) c.Abort() return } c.Next() } }