Files
AuthCentral/api/handlers/user_management_handler.go
Beyhan Oğur 8b1fbdee99 first commit
2026-04-26 21:37:58 +03:00

539 lines
15 KiB
Go

package handlers
import (
"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 {
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"})
}