Ctrl K
ComunidadGuías…

internal/handler/user_import_handler.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handler

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/johnzastrow/actalog/internal/domain"
	"github.com/johnzastrow/actalog/internal/service"
	"github.com/johnzastrow/actalog/pkg/logger"
	"github.com/johnzastrow/actalog/pkg/middleware"
)

// getUserIDFromContext extracts the user ID from the request context
func getUserIDFromContext(ctx context.Context) int64 {
	if userID, ok := ctx.Value(middleware.UserIDKey).(int64); ok {
		return userID
	}
	return 0
}

// UserImportHandler handles user import/export and batch operations
type UserImportHandler struct {
	userImportService *service.UserImportService
	logger            *logger.Logger
}

// NewUserImportHandler creates a new user import handler
func NewUserImportHandler(userImportService *service.UserImportService, logger *logger.Logger) *UserImportHandler {
	return &UserImportHandler{
		userImportService: userImportService,
		logger:            logger,
	}
}

// PreviewUserImport validates and previews user CSV import
// @Summary      Preview user import
// @Description  Validate and preview user CSV import before confirming
// @Tags         admin-users
// @Accept       multipart/form-data
// @Produce      json
// @Security     BearerAuth
// @Param        file formData file true "CSV file to import"
// @Success      200 {object} service.UserImportResult "Import preview with validation results"
// @Failure      400 {object} ErrorResponse "Invalid file or validation failed"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Router       /admin/users/import/preview [post]
func (h *UserImportHandler) PreviewUserImport(w http.ResponseWriter, r *http.Request) {
	// Parse multipart form
	if err := r.ParseMultipartForm(maxUploadSize); err != nil {
		h.logger.Error("action=preview_user_import outcome=failure error=%v", err)
		respondError(w, http.StatusBadRequest, "Failed to parse multipart form")
		return
	}

	// Get file from form
	file, _, err := r.FormFile("file")
	if err != nil {
		h.logger.Error("action=preview_user_import outcome=failure error=%v", err)
		respondError(w, http.StatusBadRequest, "No file uploaded")
		return
	}
	defer file.Close()

	// Preview import
	result, err := h.userImportService.PreviewUserImport(file)
	if err != nil {
		h.logger.Error("action=preview_user_import outcome=failure error=%v", err)
		respondError(w, http.StatusBadRequest, fmt.Sprintf("Import preview failed: %v", err))
		return
	}

	h.logger.Info("action=preview_user_import outcome=success total_rows=%d valid_rows=%d duplicate_rows=%d",
		result.TotalRows, result.ValidRows, result.DuplicateRows)
	respondJSON(w, http.StatusOK, result)
}

// ConfirmUserImport confirms and executes user CSV import
// @Summary      Confirm user import
// @Description  Execute user CSV import after preview
// @Tags         admin-users
// @Accept       multipart/form-data
// @Produce      json
// @Security     BearerAuth
// @Param        file formData file true "CSV file to import"
// @Param        skip_duplicates formData bool false "Skip duplicate entries"
// @Success      200 {object} service.UserImportResult "Import results with counts"
// @Failure      400 {object} ErrorResponse "Invalid file"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      500 {object} ErrorResponse "Import failed"
// @Router       /admin/users/import/confirm [post]
func (h *UserImportHandler) ConfirmUserImport(w http.ResponseWriter, r *http.Request) {
	// Get admin user ID from context for audit logging
	adminUserID := getUserIDFromContext(r.Context())

	// Parse multipart form
	if err := r.ParseMultipartForm(maxUploadSize); err != nil {
		h.logger.Error("action=confirm_user_import outcome=failure error=%v", err)
		respondError(w, http.StatusBadRequest, "Failed to parse multipart form")
		return
	}

	// Get file from form
	file, _, err := r.FormFile("file")
	if err != nil {
		h.logger.Error("action=confirm_user_import outcome=failure error=%v", err)
		respondError(w, http.StatusBadRequest, "No file uploaded")
		return
	}
	defer file.Close()

	// Parse options from form data
	skipDuplicates := r.FormValue("skip_duplicates") == "true"

	// Confirm import with audit logging
	result, err := h.userImportService.ConfirmUserImportWithAudit(file, skipDuplicates, adminUserID)
	if err != nil {
		h.logger.Error("action=confirm_user_import outcome=failure error=%v", err)
		respondError(w, http.StatusInternalServerError, fmt.Sprintf("Import failed: %v", err))
		return
	}

	h.logger.Info("action=confirm_user_import outcome=success created=%d skipped=%d",
		result.CreatedCount, result.SkippedCount)
	respondJSON(w, http.StatusOK, result)
}

// ExportUsers exports users to CSV format
// @Summary      Export users
// @Description  Export users to CSV format (email, name)
// @Tags         admin-users
// @Produce      text/csv
// @Security     BearerAuth
// @Success      200 {file} file "Exported CSV file"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /admin/users/export [get]
func (h *UserImportHandler) ExportUsers(w http.ResponseWriter, r *http.Request) {
	// Get admin user ID from context for audit logging
	adminUserID := getUserIDFromContext(r.Context())

	data, err := h.userImportService.ExportUsersToCSVWithAudit(adminUserID)
	if err != nil {
		h.logger.Error("action=export_users outcome=failure error=%v", err)
		respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to export users: %v", err))
		return
	}

	// Set response headers for download
	w.Header().Set("Content-Type", "text/csv")
	w.Header().Set("Content-Disposition", "attachment; filename=users_export.csv")
	w.Header().Set("Content-Length", strconv.Itoa(len(data)))

	// Write data
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write(data)

	h.logger.Info("action=export_users outcome=success")
}

// ListUsersWithFilter lists users with filter criteria
// @Summary      List users with filter
// @Description  Get paginated list of users with optional search and date filters
// @Tags         admin-users
// @Produce      json
// @Security     BearerAuth
// @Param        search query string false "Search in name and email"
// @Param        created_after query string false "Filter users created after this date (YYYY-MM-DD)"
// @Param        created_before query string false "Filter users created before this date (YYYY-MM-DD)"
// @Param        limit query int false "Number of results per page (default 50)"
// @Param        offset query int false "Offset for pagination"
// @Success      200 {object} map[string]interface{} "Users list with total count"
// @Failure      400 {object} ErrorResponse "Invalid parameters"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /admin/users/filter [get]
func (h *UserImportHandler) ListUsersWithFilter(w http.ResponseWriter, r *http.Request) {
	// Parse query parameters
	filter := domain.UserListFilter{
		Search: r.URL.Query().Get("search"),
	}

	// Parse date filters
	if createdAfter := r.URL.Query().Get("created_after"); createdAfter != "" {
		parsed, err := time.Parse("2006-01-02", createdAfter)
		if err != nil {
			respondError(w, http.StatusBadRequest, "Invalid created_after format. Use YYYY-MM-DD")
			return
		}
		filter.CreatedAfter = &parsed
	}

	if createdBefore := r.URL.Query().Get("created_before"); createdBefore != "" {
		parsed, err := time.Parse("2006-01-02", createdBefore)
		if err != nil {
			respondError(w, http.StatusBadRequest, "Invalid created_before format. Use YYYY-MM-DD")
			return
		}
		// Add 1 day to include the entire day
		parsed = parsed.Add(24 * time.Hour)
		filter.CreatedBefore = &parsed
	}

	// Parse pagination
	limit := 50
	if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
		if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 {
			limit = parsed
			if limit > 1000 {
				limit = 1000 // Cap at 1000
			}
		}
	}

	offset := 0
	if offsetStr := r.URL.Query().Get("offset"); offsetStr != "" {
		if parsed, err := strconv.Atoi(offsetStr); err == nil && parsed >= 0 {
			offset = parsed
		}
	}

	users, count, err := h.userImportService.ListUsersWithFilter(filter, limit, offset)
	if err != nil {
		h.logger.Error("action=list_users_with_filter outcome=failure error=%v", err)
		respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to list users: %v", err))
		return
	}

	// Clear password hashes before returning
	for _, user := range users {
		user.PasswordHash = ""
	}

	respondJSON(w, http.StatusOK, map[string]interface{}{
		"users": users,
		"count": count,
	})
}

// BatchPasswordResetRequest represents the request body for batch password reset
type BatchPasswordResetRequest struct {
	UserIDs []int64 `json:"user_ids"`
}

// SendBatchPasswordResetEmails sends password reset emails to selected users
// @Summary      Send batch password reset emails
// @Description  Send password reset emails to multiple users at once
// @Tags         admin-users
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        request body BatchPasswordResetRequest true "User IDs to send reset emails to"
// @Success      200 {object} service.BatchPasswordResetResult "Result with success/failure counts"
// @Failure      400 {object} ErrorResponse "Invalid request"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /admin/users/batch-password-reset [post]
func (h *UserImportHandler) SendBatchPasswordResetEmails(w http.ResponseWriter, r *http.Request) {
	// Get admin user ID from context for audit logging
	adminUserID := getUserIDFromContext(r.Context())

	var req BatchPasswordResetRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		h.logger.Error("action=batch_password_reset outcome=failure error=%v", err)
		respondError(w, http.StatusBadRequest, "Invalid request body")
		return
	}

	if len(req.UserIDs) == 0 {
		respondError(w, http.StatusBadRequest, "No user IDs provided")
		return
	}

	if len(req.UserIDs) > 100 {
		respondError(w, http.StatusBadRequest, "Maximum 100 users can be selected at once")
		return
	}

	result, err := h.userImportService.SendBatchPasswordResetEmailsWithAudit(req.UserIDs, adminUserID)
	if err != nil {
		h.logger.Error("action=batch_password_reset outcome=failure error=%v", err)
		respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to send emails: %v", err))
		return
	}

	h.logger.Info("action=batch_password_reset outcome=success sent=%d failed=%d",
		result.SuccessfullySent, result.FailedCount)
	respondJSON(w, http.StatusOK, result)
}