internal/handler/data_change_log_handler.go
Rama main · réplica de lectura del repositorio original.
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"
)
// DataChangeLogHandler handles HTTP requests for data change logs
type DataChangeLogHandler struct {
service *service.DataChangeLogService
logger *logger.Logger
}
// NewDataChangeLogHandler creates a new data change log handler
func NewDataChangeLogHandler(service *service.DataChangeLogService, logger *logger.Logger) *DataChangeLogHandler {
return &DataChangeLogHandler{
service: service,
logger: logger,
}
}
// GetDataChangeLog handles GET /api/data-change-logs/:id
// @Summary Get data change log (Admin)
// @Description Retrieve a specific data change log entry by ID
// @Tags admin
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Data Change Log ID"
// @Success 200 {object} domain.DataChangeLog "Data change log entry"
// @Failure 400 {object} ErrorResponse "Invalid ID"
// @Failure 403 {object} ErrorResponse "Forbidden"
// @Failure 404 {object} ErrorResponse "Log not found"
// @Router /data-change-logs/{id} [get]
func (h *DataChangeLogHandler) GetDataChangeLog(w http.ResponseWriter, r *http.Request) {
// Only admins can access individual data change logs
userRole, _ := middleware.GetUserRole(r.Context())
if userRole != "admin" {
if h.logger != nil {
h.logger.Warn("Unauthorized access attempt to data change log")
}
respondError(w, http.StatusForbidden, "Forbidden")
return
}
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid ID")
return
}
log, err := h.service.GetByID(id)
if err != nil {
if h.logger != nil {
h.logger.Error("Failed to get data change log: %v", err)
}
respondError(w, http.StatusNotFound, "Data change log not found")
return
}
respondJSON(w, http.StatusOK, log)
}
// ListDataChangeLogs handles GET /api/data-change-logs
// @Summary List data change logs (Admin)
// @Description Get a paginated list of data change logs with optional filters
// @Tags admin
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param entity_type query string false "Filter by entity type"
// @Param entity_id query int false "Filter by entity ID"
// @Param operation query string false "Filter by operation (create, update, delete)"
// @Param user_id query int false "Filter by user ID"
// @Param user_email query string false "Filter by user email"
// @Param start_date query string false "Filter by start date (RFC3339 or YYYY-MM-DD)"
// @Param end_date query string false "Filter by end date (RFC3339 or YYYY-MM-DD)"
// @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{} "Data change logs with total count"
// @Failure 400 {object} ErrorResponse "Invalid parameters"
// @Failure 403 {object} ErrorResponse "Forbidden"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /data-change-logs [get]
func (h *DataChangeLogHandler) ListDataChangeLogs(w http.ResponseWriter, r *http.Request) {
// Only admins can list all data change logs
userRole, _ := middleware.GetUserRole(r.Context())
if userRole != "admin" {
if h.logger != nil {
h.logger.Warn("Unauthorized access attempt to data change logs list")
}
respondError(w, http.StatusForbidden, "Forbidden")
return
}
// Parse query parameters
filters := domain.DataChangeLogFilters{}
if entityType := r.URL.Query().Get("entity_type"); entityType != "" {
filters.EntityType = &entityType
}
if entityIDStr := r.URL.Query().Get("entity_id"); entityIDStr != "" {
entityID, err := strconv.ParseInt(entityIDStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid entity_id")
return
}
filters.EntityID = &entityID
}
if operation := r.URL.Query().Get("operation"); operation != "" {
filters.Operation = &operation
}
if userIDStr := r.URL.Query().Get("user_id"); userIDStr != "" {
userID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid user_id")
return
}
filters.UserID = &userID
}
if userEmail := r.URL.Query().Get("user_email"); userEmail != "" {
filters.UserEmail = &userEmail
}
if startDateStr := r.URL.Query().Get("start_date"); startDateStr != "" {
startDate, err := time.Parse(time.RFC3339, startDateStr)
if err != nil {
// Try simpler date format
startDate, err = time.Parse("2006-01-02", startDateStr)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid start_date format (use RFC3339 or YYYY-MM-DD)")
return
}
}
filters.StartDate = &startDate
}
if endDateStr := r.URL.Query().Get("end_date"); endDateStr != "" {
endDate, err := time.Parse(time.RFC3339, endDateStr)
if err != nil {
// Try simpler date format
endDate, err = time.Parse("2006-01-02", endDateStr)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid end_date format (use RFC3339 or YYYY-MM-DD)")
return
}
// Set to end of day
endDate = endDate.Add(24*time.Hour - time.Second)
}
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 {
if h.logger != nil {
h.logger.Error("Failed to list data change logs: %v", err)
}
respondError(w, http.StatusInternalServerError, "Failed to retrieve data change logs")
return
}
count, err := h.service.Count(filters)
if err != nil {
if h.logger != nil {
h.logger.Error("Failed to count data change logs: %v", err)
}
// Continue anyway, just set count to 0
count = 0
}
response := map[string]interface{}{
"logs": logs,
"total": count,
"limit": limit,
"offset": offset,
}
respondJSON(w, http.StatusOK, response)
}
// GetEntityHistory handles GET /api/data-change-logs/entity/:entity_type/:entity_id
// @Summary Get entity history (Admin)
// @Description Get the change history for a specific entity
// @Tags admin
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param entity_type path string true "Entity type (e.g., user, workout, wod)"
// @Param entity_id path int true "Entity ID"
// @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{} "Entity change history"
// @Failure 400 {object} ErrorResponse "Invalid entity_id"
// @Failure 403 {object} ErrorResponse "Forbidden"
// @Failure 500 {object} ErrorResponse "Internal server error"
// @Router /data-change-logs/entity/{entity_type}/{entity_id} [get]
func (h *DataChangeLogHandler) GetEntityHistory(w http.ResponseWriter, r *http.Request) {
// Only admins can view entity history
userRole, _ := middleware.GetUserRole(r.Context())
if userRole != "admin" {
respondError(w, http.StatusForbidden, "Forbidden")
return
}
entityType := chi.URLParam(r, "entity_type")
entityIDStr := chi.URLParam(r, "entity_id")
entityID, err := strconv.ParseInt(entityIDStr, 10, 64)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid entity_id")
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
}
}
logs, err := h.service.GetByEntityID(entityType, entityID, limit, offset)
if err != nil {
if h.logger != nil {
h.logger.Error("Failed to get entity history: %v", err)
}
respondError(w, http.StatusInternalServerError, "Failed to retrieve entity history")
return
}
response := map[string]interface{}{
"entity_type": entityType,
"entity_id": entityID,
"logs": logs,
}
respondJSON(w, http.StatusOK, response)
}
// CleanupOldLogs handles POST /api/admin/data-change-logs/cleanup
// @Summary Cleanup old data change logs (Admin)
// @Description Delete data change logs older than specified retention period
// @Tags admin
// @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/data-change-logs/cleanup [post]
func (h *DataChangeLogHandler) CleanupOldLogs(w http.ResponseWriter, r *http.Request) {
// Only admins can cleanup data change logs
userRole, _ := middleware.GetUserRole(r.Context())
if userRole != "admin" {
respondError(w, http.StatusForbidden, "Forbidden")
return
}
var request struct {
RetentionDays int `json:"retention_days"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body")
return
}
if request.RetentionDays <= 0 {
respondError(w, http.StatusBadRequest, "retention_days must be positive")
return
}
deletedCount, err := h.service.CleanupOldLogs(request.RetentionDays)
if err != nil {
if h.logger != nil {
h.logger.Error("Failed to cleanup old data change logs: %v", err)
}
respondError(w, http.StatusInternalServerError, "Failed to cleanup data change logs")
return
}
if h.logger != nil {
adminUserID, _ := middleware.GetUserID(r.Context())
h.logger.Info("Cleaned up old data change logs: retention_days=%d, deleted_count=%d, admin_user_id=%d",
request.RetentionDays, deletedCount, adminUserID)
}
response := map[string]interface{}{
"deleted_count": deletedCount,
"message": "Old data change logs deleted successfully",
}
respondJSON(w, http.StatusOK, response)
}