package handlers import ( "net/http" "strings" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "sproutgate-backend/internal/models" ) // ListUsers // @Summary 用户列表 // @Tags admin // @Produce json // @Security AdminToken // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]interface{} // @Failure 500 {object} map[string]interface{} // @Router /api/admin/users [get] 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}) } // CreateUser // @Summary 创建用户 // @Tags admin // @Accept json // @Produce json // @Security AdminToken // @Param body body CreateUserRequest true "用户" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} map[string]interface{} // @Failure 401 {object} map[string]interface{} // @Failure 500 {object} map[string]interface{} // @Router /api/admin/users [post] 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()}) } // UpdateUser // @Summary 更新用户 // @Tags admin // @Accept json // @Produce json // @Security AdminToken // @Param account path string true "账号" // @Param body body UpdateUserRequest true "可部分更新" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]interface{} // @Failure 401 {object} map[string]interface{} // @Failure 404 {object} map[string]interface{} // @Failure 500 {object} map[string]interface{} // @Router /api/admin/users/{account} [put] 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 { wasBanned := user.Banned user.Banned = *req.Banned if user.Banned && !wasBanned { user.TokenEpoch++ } 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()}) } // DeleteUser // @Summary 删除用户 // @Tags admin // @Produce json // @Security AdminToken // @Param account path string true "账号" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]interface{} // @Failure 401 {object} map[string]interface{} // @Failure 500 {object} map[string]interface{} // @Router /api/admin/users/{account} [delete] 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() } }