package handlers import ( "errors" "net/http" "os" "strconv" "strings" "time" "gauth-central/internal/database" "gauth-central/internal/models" "gauth-central/internal/services" "gauth-central/pkg/utils" "github.com/gin-gonic/gin" ) type UserManagementHandler struct { userService *services.UserManagementService } func NewUserManagementHandler(userService *services.UserManagementService) *UserManagementHandler { return &UserManagementHandler{ userService: userService, } } // GetAllUsers godoc // @Summary Get all users (Admin only) // @Tags Admin - User Management // @Security ApiKeyAuth // @Produce json // @Param page query int false "Page number" default(1) // @Param limit query int false "Items per page" default(10) // @Success 200 {object} map[string]interface{} // @Router /admin/users [get] func (h *UserManagementHandler) GetAllUsers(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) if page < 1 { page = 1 } if limit < 1 || limit > 100 { limit = 10 } users, total, err := h.userService.GetAllUsers(page, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"}) return } c.JSON(http.StatusOK, gin.H{ "users": users, "pagination": gin.H{ "page": page, "limit": limit, "total": total, "totalPages": (total + int64(limit) - 1) / int64(limit), }, }) } // GetUserByID godoc // @Summary Get user by ID (Admin only) // @Tags Admin - User Management // @Security ApiKeyAuth // @Produce json // @Param id path string true "User ID" // @Success 200 {object} models.User // @Router /admin/users/{id} [get] func (h *UserManagementHandler) GetUserByID(c *gin.Context) { userID := c.Param("id") user, err := h.userService.GetUserByID(userID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } c.JSON(http.StatusOK, user) } // CreateUser godoc // @Summary Create new user (Admin only) // @Tags Admin - User Management // @Security ApiKeyAuth // @Accept multipart/form-data // @Produce json // @Param email formData string true "Email" // @Param password formData string true "Password" // @Param user_name formData string true "Username" // @Param email_verified formData boolean false "Email verified" // @Param roles formData string false "Roles (comma separated: admin,user)" // @Param avatar formData file false "Avatar image" // @Success 201 {object} models.User // @Router /admin/users [post] func (h *UserManagementHandler) CreateUser(c *gin.Context) { // Parse multipart form if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse form data"}) return } email := c.PostForm("email") password := c.PostForm("password") userName := c.PostForm("user_name") emailVerified := c.PostForm("email_verified") == "true" rolesStr := c.PostForm("roles") // Validate required fields if email == "" || password == "" || userName == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "email, password, and user_name are required"}) return } // Parse roles var roles []string if rolesStr != "" { roles = strings.Split(rolesStr, ",") // Trim spaces for i, role := range roles { roles[i] = strings.TrimSpace(role) } } user, err := h.userService.CreateUser( email, password, userName, emailVerified, roles, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user: " + err.Error()}) return } // Handle avatar upload if provided avatarFile, err := c.FormFile("avatar") if err == nil && avatarFile != nil { // Validate file size (max 5MB) if avatarFile.Size > 5*1024*1024 { c.JSON(http.StatusBadRequest, gin.H{"error": "Avatar file size exceeds 5MB limit"}) return } // Use utils.SaveOptimizedImage // Default options (WebP, 800px width) avatarURL, err := utils.SaveOptimizedImage(avatarFile, "./uploads/avatars", user.ID.String(), nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar: " + err.Error()}) return } // Update user avatar database.DB.Model(&user).Update("avatar", avatarURL) user.Avatar = avatarURL } c.JSON(http.StatusCreated, user) } // UpdateUser godoc // @Summary Update user (Admin only) // @Tags Admin - User Management // @Security ApiKeyAuth // @Accept multipart/form-data // @Produce json // @Param id path string true "User ID" // @Param email formData string false "Email" // @Param password formData string false "Password" // @Param user_name formData string false "Username" // @Param email_verified formData boolean false "Email verified" // @Param roles formData string false "Roles (comma separated: admin,user)" // @Param avatar formData file false "Avatar image" // @Success 200 {object} map[string]interface{} // @Router /admin/users/{id} [put] func (h *UserManagementHandler) UpdateUser(c *gin.Context) { userID := c.Param("id") // Try to parse as multipart form first contentType := c.GetHeader("Content-Type") isMultipart := strings.Contains(contentType, "multipart/form-data") updates := make(map[string]interface{}) var roles []string if isMultipart { // Parse multipart form if err := c.Request.ParseMultipartForm(32 << 20); err != nil { // 32MB max c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse form data"}) return } // Get form values if email := c.PostForm("email"); email != "" { updates["email"] = email } if password := c.PostForm("password"); password != "" { updates["password"] = password } if userName := c.PostForm("user_name"); userName != "" { updates["user_name"] = userName } if emailVerified := c.PostForm("email_verified"); emailVerified != "" { updates["email_verified"] = emailVerified == "true" } if rolesStr := c.PostForm("roles"); rolesStr != "" { roles = strings.Split(rolesStr, ",") for i, role := range roles { roles[i] = strings.TrimSpace(role) } } } else { // Parse as JSON var input struct { Email *string `json:"email"` Password *string `json:"password"` UserName *string `json:"user_name"` Avatar *string `json:"avatar"` EmailVerified *bool `json:"email_verified"` Roles []string `json:"roles"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if input.Email != nil { updates["email"] = *input.Email } if input.Password != nil { updates["password"] = *input.Password } if input.UserName != nil { updates["user_name"] = *input.UserName } if input.Avatar != nil { updates["avatar"] = *input.Avatar } if input.EmailVerified != nil { updates["email_verified"] = *input.EmailVerified } if input.Roles != nil { roles = input.Roles } } // Update basic user fields if len(updates) > 0 { if err := h.userService.UpdateUser(userID, updates); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"}) return } } // Update roles if provided if len(roles) > 0 { if err := h.userService.AssignRoles(userID, roles); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update roles: " + err.Error()}) return } } // Handle avatar upload if multipart and file provided if isMultipart { avatarFile, err := c.FormFile("avatar") if err == nil && avatarFile != nil { // Validate file size (max 5MB) if avatarFile.Size > 5*1024*1024 { c.JSON(http.StatusBadRequest, gin.H{"error": "Avatar file size exceeds 5MB limit"}) return } // Get user to check for old avatar var user models.User if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } // Delete old avatar if exists and is local if user.Avatar != "" && strings.HasPrefix(user.Avatar, "/uploads/") { oldPath := "." + user.Avatar os.Remove(oldPath) } // Use utils.SaveOptimizedImage avatarURL, err := utils.SaveOptimizedImage(avatarFile, "./uploads/avatars", userID, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save avatar: " + err.Error()}) return } // Update user avatar if err := database.DB.Model(&user).Update("avatar", avatarURL).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update avatar in database"}) return } } } // Get updated user to return user, err := h.userService.GetUserByID(userID) if err != nil { c.JSON(http.StatusOK, gin.H{"message": "User updated successfully"}) return } c.JSON(http.StatusOK, gin.H{ "message": "User updated successfully", "user": user, }) } // DeleteUser godoc // @Summary Delete user (Admin only) // @Tags Admin - User Management // @Security ApiKeyAuth // @Param id path string true "User ID" // @Param hard query boolean false "Hard delete (permanent)" default(false) // @Success 200 {object} map[string]interface{} // @Router /admin/users/{id} [delete] func (h *UserManagementHandler) DeleteUser(c *gin.Context) { userID := c.Param("id") hardDelete := c.Query("hard") == "true" // Prevent deleting self currentUserID := c.GetString("user_id") if userID == currentUserID { c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete your own account"}) return } if err := h.userService.DeleteUser(userID, hardDelete); err != nil { if errors.Is(err, services.ErrUserNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"}) return } deleteType := "soft" if hardDelete { deleteType = "permanently" } c.JSON(http.StatusOK, gin.H{"message": "User deleted " + deleteType + " successfully"}) } // AssignRoles godoc // @Summary Assign roles to user (Admin only) // @Tags Admin - User Management // @Security ApiKeyAuth // @Accept json // @Produce json // @Param id path string true "User ID" // @Param roles body object true "Roles" // @Success 200 {object} map[string]interface{} // @Router /admin/users/{id}/roles [post] func (h *UserManagementHandler) AssignRoles(c *gin.Context) { userID := c.Param("id") var input struct { Roles []string `json:"roles" binding:"required"` // ["admin", "user"] } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.userService.AssignRoles(userID, input.Roles); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to assign roles: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Roles assigned successfully"}) } // RemoveRole godoc // @Summary Remove role from user (Admin only) // @Tags Admin - User Management // @Security ApiKeyAuth // @Param id path string true "User ID" // @Param role path string true "Role name" // @Success 200 {object} map[string]interface{} // @Router /admin/users/{id}/roles/{role} [delete] func (h *UserManagementHandler) RemoveRole(c *gin.Context) { userID := c.Param("id") roleName := c.Param("role") if err := h.userService.RemoveRole(userID, roleName); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove role"}) return } c.JSON(http.StatusOK, gin.H{"message": "Role removed successfully"}) } // SearchUsers godoc // @Summary Search users (Admin only) // @Tags Admin - User Management // @Security ApiKeyAuth // @Produce json // @Param q query string true "Search query" // @Param page query int false "Page number" default(1) // @Param limit query int false "Items per page" default(10) // @Success 200 {object} map[string]interface{} // @Router /admin/users/search [get] func (h *UserManagementHandler) SearchUsers(c *gin.Context) { query := c.Query("q") if query == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Search query required"}) return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) if page < 1 { page = 1 } if limit < 1 || limit > 100 { limit = 10 } users, total, err := h.userService.SearchUsers(query, page, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search users"}) return } c.JSON(http.StatusOK, gin.H{ "users": users, "pagination": gin.H{ "page": page, "limit": limit, "total": total, "totalPages": (total + int64(limit) - 1) / int64(limit), }, }) } // GetDeletedUsers godoc // @Summary Get all soft deleted users (Admin only) // @Tags Admin - User Management // @Security ApiKeyAuth // @Produce json // @Param page query int false "Page number" default(1) // @Param limit query int false "Items per page" default(10) // @Success 200 {object} map[string]interface{} // @Router /admin/users/deleted [get] func (h *UserManagementHandler) GetDeletedUsers(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) if page < 1 { page = 1 } if limit < 1 || limit > 100 { limit = 10 } users, total, err := h.userService.GetDeletedUsers(page, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch deleted users"}) return } // Transform users to include deleted_at field type DeletedUserResponse struct { ID string `json:"id"` UserName string `json:"username"` Email string `json:"email"` Avatar string `json:"avatar,omitempty"` EmailVerified bool `json:"email_verified"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt *time.Time `json:"deleted_at"` Roles []models.Role `json:"roles,omitempty"` SocialAccounts []models.SocialAccount `json:"social_accounts,omitempty"` } deletedUsers := make([]DeletedUserResponse, len(users)) for i, user := range users { var deletedAt *time.Time if user.DeletedAt.Valid { deletedAt = &user.DeletedAt.Time } emailVerified := false if user.EmailVerified != nil { emailVerified = *user.EmailVerified } deletedUsers[i] = DeletedUserResponse{ ID: user.ID.String(), UserName: user.UserName, Email: user.Email, Avatar: user.Avatar, EmailVerified: emailVerified, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, DeletedAt: deletedAt, Roles: user.Roles, SocialAccounts: user.SocialAccounts, } } c.JSON(http.StatusOK, gin.H{ "users": deletedUsers, "pagination": gin.H{ "page": page, "limit": limit, "total": total, "totalPages": (total + int64(limit) - 1) / int64(limit), }, }) } // RestoreUser godoc // @Summary Restore a soft deleted user (Admin only) // @Tags Admin - User Management // @Security ApiKeyAuth // @Param id path string true "User ID" // @Success 200 {object} map[string]interface{} // @Router /admin/users/{id}/restore [post] func (h *UserManagementHandler) RestoreUser(c *gin.Context) { userID := c.Param("id") if err := h.userService.RestoreUser(userID); err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "User restored successfully"}) }