first commit
This commit is contained in:
543
api/handlers/user_management_handler.go
Normal file
543
api/handlers/user_management_handler.go
Normal file
@@ -0,0 +1,543 @@
|
||||
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"})
|
||||
}
|
||||
Reference in New Issue
Block a user