Ctrl K
ComunidadGuías…

internal/handler/audit_log_handler.go

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

Archivo original ↗Preguntar a KiBot
package handler

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

	"github.com/go-chi/chi/v5"
	"github.com/johnzastrow/actalog/internal/domain"
	"github.com/johnzastrow/actalog/internal/service"
	"github.com/johnzastrow/actalog/pkg/logger"
	"github.com/johnzastrow/actalog/pkg/middleware"
)

// AuditLogHandler handles HTTP requests for audit logs
type AuditLogHandler struct {
	service *service.AuditLogService
	logger  *logger.Logger
}

// NewAuditLogHandler creates a new audit log handler
func NewAuditLogHandler(service *service.AuditLogService, logger *logger.Logger) *AuditLogHandler {
	return &AuditLogHandler{
		service: service,
		logger:  logger,
	}
}

// GetAuditLog handles GET /api/audit-logs/:id
// @Summary      Get audit log entry (Admin)
// @Description  Retrieve a specific audit log entry by ID
// @Tags         audit
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "Audit Log ID"
// @Success      200 {object} domain.AuditLog "Audit log entry"
// @Failure      400 {object} ErrorResponse "Invalid ID"
// @Failure      403 {object} ErrorResponse "Forbidden"
// @Failure      404 {object} ErrorResponse "Audit log not found"
// @Router       /audit-logs/{id} [get]
func (h *AuditLogHandler) GetAuditLog(w http.ResponseWriter, r *http.Request) {
	// Only admins can access individual audit logs
	userRole, _ := middleware.GetUserRole(r.Context())
	if userRole != "admin" {
		h.logger.Warn("Unauthorized access attempt to audit log: user_role=%s path=%s", userRole, r.URL.Path)
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		http.Error(w, "Invalid ID", http.StatusBadRequest)
		return
	}

	log, err := h.service.GetByID(id)
	if err != nil {
		h.logger.Error("Failed to get audit log: id=%d error=%v", id, err)
		http.Error(w, "Audit log not found", http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(log)
}

// ListAuditLogs handles GET /api/audit-logs
// @Summary      List audit logs (Admin)
// @Description  Get a paginated list of audit logs with optional filters
// @Tags         audit
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        user_id query int false "Filter by user ID"
// @Param        target_user_id query int false "Filter by target user ID"
// @Param        event_type query string false "Filter by event type"
// @Param        ip_address query string false "Filter by IP address"
// @Param        start_date query string false "Filter by start date (RFC3339)"
// @Param        end_date query string false "Filter by end date (RFC3339)"
// @Param        limit query int false "Max results (default 50)"
// @Param        offset query int false "Skip N results (default 0)"
// @Success      200 {object} map[string]interface{} "Audit logs list with total count"
// @Failure      400 {object} ErrorResponse "Invalid parameters"
// @Failure      403 {object} ErrorResponse "Forbidden"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /audit-logs [get]
func (h *AuditLogHandler) ListAuditLogs(w http.ResponseWriter, r *http.Request) {
	// Only admins can list all audit logs
	userRole, _ := middleware.GetUserRole(r.Context())
	if userRole != "admin" {
		h.logger.Warn("Unauthorized access attempt to audit logs list: user_role=%s", userRole)
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	// Parse query parameters
	filters := domain.AuditLogFilters{}

	if userIDStr := r.URL.Query().Get("user_id"); userIDStr != "" {
		userID, err := strconv.ParseInt(userIDStr, 10, 64)
		if err != nil {
			http.Error(w, "Invalid user_id", http.StatusBadRequest)
			return
		}
		filters.UserID = &userID
	}

	if targetUserIDStr := r.URL.Query().Get("target_user_id"); targetUserIDStr != "" {
		targetUserID, err := strconv.ParseInt(targetUserIDStr, 10, 64)
		if err != nil {
			http.Error(w, "Invalid target_user_id", http.StatusBadRequest)
			return
		}
		filters.TargetUserID = &targetUserID
	}

	if eventType := r.URL.Query().Get("event_type"); eventType != "" {
		filters.EventType = &eventType
	}

	if ipAddress := r.URL.Query().Get("ip_address"); ipAddress != "" {
		filters.IPAddress = &ipAddress
	}

	if startDateStr := r.URL.Query().Get("start_date"); startDateStr != "" {
		startDate, err := time.Parse(time.RFC3339, startDateStr)
		if err != nil {
			http.Error(w, "Invalid start_date format (use RFC3339)", http.StatusBadRequest)
			return
		}
		filters.StartDate = &startDate
	}

	if endDateStr := r.URL.Query().Get("end_date"); endDateStr != "" {
		endDate, err := time.Parse(time.RFC3339, endDateStr)
		if err != nil {
			http.Error(w, "Invalid end_date format (use RFC3339)", http.StatusBadRequest)
			return
		}
		filters.EndDate = &endDate
	}

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

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

	// Get logs and count
	logs, err := h.service.List(filters, limit, offset)
	if err != nil {
		h.logger.Error("Failed to list audit logs: %v", err)
		http.Error(w, "Failed to retrieve audit logs", http.StatusInternalServerError)
		return
	}

	count, err := h.service.Count(filters)
	if err != nil {
		h.logger.Error("Failed to count audit logs: %v", err)
		// Continue anyway, just set count to 0
		count = 0
	}

	response := map[string]interface{}{
		"logs":   logs,
		"total":  count,
		"limit":  limit,
		"offset": offset,
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(response)
}

// GetMyAuditLogs handles GET /api/users/me/audit-logs
// @Summary      Get my audit logs
// @Description  Get audit logs for the current user only
// @Tags         audit
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        limit query int false "Max results (default 50)"
// @Param        offset query int false "Skip N results (default 0)"
// @Success      200 {object} map[string]interface{} "User's audit logs with total count"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /users/me/audit-logs [get]
func (h *AuditLogHandler) GetMyAuditLogs(w http.ResponseWriter, r *http.Request) {
	userID, ok := middleware.GetUserID(r.Context())
	if !ok || userID == 0 {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

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

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

	// Get logs for current user
	logs, err := h.service.GetByUserID(userID, limit, offset)
	if err != nil {
		h.logger.Error("Failed to get user audit logs: user_id=%d error=%v", userID, err)
		http.Error(w, "Failed to retrieve audit logs", http.StatusInternalServerError)
		return
	}

	// Count total logs for this user
	filters := domain.AuditLogFilters{
		UserID: &userID,
	}
	count, err := h.service.Count(filters)
	if err != nil {
		h.logger.Error("Failed to count user audit logs: user_id=%d error=%v", userID, err)
		count = 0
	}

	response := map[string]interface{}{
		"logs":   logs,
		"total":  count,
		"limit":  limit,
		"offset": offset,
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(response)
}

// CleanupOldLogs handles POST /api/admin/audit-logs/cleanup
// @Summary      Cleanup old audit logs (Admin)
// @Description  Delete audit logs older than specified retention period
// @Tags         audit
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        request body object true "Retention days (must be positive)"
// @Success      200 {object} map[string]interface{} "Deleted count and success message"
// @Failure      400 {object} ErrorResponse "Invalid request"
// @Failure      403 {object} ErrorResponse "Forbidden"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /admin/audit-logs/cleanup [post]
func (h *AuditLogHandler) CleanupOldLogs(w http.ResponseWriter, r *http.Request) {
	// Only admins can cleanup audit logs
	userRole, _ := middleware.GetUserRole(r.Context())
	if userRole != "admin" {
		http.Error(w, "Forbidden", http.StatusForbidden)
		return
	}

	var request struct {
		RetentionDays int `json:"retention_days"`
	}

	if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	if request.RetentionDays <= 0 {
		http.Error(w, "retention_days must be positive", http.StatusBadRequest)
		return
	}

	deletedCount, err := h.service.CleanupOldLogs(request.RetentionDays)
	if err != nil {
		h.logger.Error("Failed to cleanup old audit logs: retention_days=%d error=%v", request.RetentionDays, err)
		http.Error(w, "Failed to cleanup audit logs", http.StatusInternalServerError)
		return
	}

	adminUserID, _ := middleware.GetUserID(r.Context())
	h.logger.Info("Cleaned up old audit logs: retention_days=%d deleted_count=%d admin_user_id=%d", request.RetentionDays, deletedCount, adminUserID)

	response := map[string]interface{}{
		"deleted_count": deletedCount,
		"message":       "Old audit logs deleted successfully",
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(response)
}