Ctrl K
ComunidadGuías…

internal/handler/wod_handler.go

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

Archivo original ↗Preguntar a KiBot
package handler

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

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

// WODHandler handles WOD (Workout of the Day) endpoints
type WODHandler struct {
	wodService *service.WODService
}

// NewWODHandler creates a new WOD handler
func NewWODHandler(wodService *service.WODService) *WODHandler {
	return &WODHandler{
		wodService: wodService,
	}
}

// CreateWODRequest represents a request to create a custom WOD
// @Description Request to create a new WOD
type CreateWODRequest struct {
	Name        string  `json:"name" example:"My Custom WOD"`
	Source      string  `json:"source,omitempty" example:"custom"`
	Type        string  `json:"type,omitempty" example:"AMRAP"`
	Regime      string  `json:"regime,omitempty" example:"15 min"`
	ScoreType   string  `json:"score_type,omitempty" example:"rounds+reps"`
	Description string  `json:"description,omitempty" example:"15 min AMRAP of 10 KB swings, 15 box jumps"`
	URL         *string `json:"url,omitempty"`
	Notes       *string `json:"notes,omitempty"`
}

// UpdateWODRequest represents a request to update a WOD
// @Description Request to update an existing WOD
type UpdateWODRequest struct {
	Name        string  `json:"name" example:"Updated WOD Name"`
	Source      string  `json:"source,omitempty" example:"custom"`
	Type        string  `json:"type,omitempty" example:"For Time"`
	Regime      string  `json:"regime,omitempty" example:"21-15-9"`
	ScoreType   string  `json:"score_type,omitempty" example:"time"`
	Description string  `json:"description,omitempty" example:"21-15-9 thrusters and pull-ups"`
	URL         *string `json:"url,omitempty"`
	Notes       *string `json:"notes,omitempty"`
}

// WODResponse represents a WOD
// @Description WOD details response
type WODResponse struct {
	ID          int64   `json:"id" example:"1"`
	Name        string  `json:"name" example:"Fran"`
	Source      string  `json:"source,omitempty" example:"CrossFit"`
	Type        string  `json:"type,omitempty" example:"For Time"`
	Regime      string  `json:"regime,omitempty" example:"21-15-9"`
	ScoreType   string  `json:"score_type,omitempty" example:"time"`
	Description string  `json:"description,omitempty" example:"21-15-9 thrusters and pull-ups"`
	URL         *string `json:"url,omitempty"`
	Notes       *string `json:"notes,omitempty"`
	IsStandard  bool    `json:"is_standard" example:"true"`
	CreatedBy   *int64  `json:"created_by,omitempty"`
	CreatedAt   string  `json:"created_at" example:"2024-01-15T10:30:00Z"`
	UpdatedAt   string  `json:"updated_at" example:"2024-01-15T10:30:00Z"`
}

// WODListResponse represents a list of WODs
// @Description List of WODs
type WODListResponse struct {
	WODs []WODResponse `json:"wods"`
}

// CreateWOD creates a new custom WOD
// @Summary      Create a custom WOD
// @Description  Create a new custom WOD (Workout of the Day)
// @Tags         wods
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        request body CreateWODRequest true "WOD details"
// @Success      201 {object} WODResponse "Created WOD"
// @Failure      400 {object} ErrorResponse "Invalid request body or missing name"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /wods [post]
func (h *WODHandler) CreateWOD(w http.ResponseWriter, r *http.Request) {
	// Extract user ID and email from JWT token in context
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}
	userEmail, _ := middleware.GetUserEmail(r.Context())

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

	// Validate required fields
	if req.Name == "" {
		respondError(w, http.StatusBadRequest, "WOD name is required")
		return
	}

	// Create WOD
	wod := &domain.WOD{
		Name:        req.Name,
		Source:      req.Source,
		Type:        req.Type,
		Regime:      req.Regime,
		ScoreType:   req.ScoreType,
		Description: req.Description,
		URL:         req.URL,
		Notes:       req.Notes,
	}

	if err := h.wodService.Create(wod, userID, userEmail); err != nil {
		respondError(w, http.StatusInternalServerError, "Failed to create WOD: "+err.Error())
		return
	}

	// Retrieve created WOD
	created, err := h.wodService.GetByID(wod.ID)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "Failed to retrieve created WOD")
		return
	}

	response := WODResponse{
		ID:          created.ID,
		Name:        created.Name,
		Source:      created.Source,
		Type:        created.Type,
		Regime:      created.Regime,
		ScoreType:   created.ScoreType,
		Description: created.Description,
		URL:         created.URL,
		Notes:       created.Notes,
		IsStandard:  created.IsStandard,
		CreatedBy:   created.CreatedBy,
		CreatedAt:   created.CreatedAt.Format("2006-01-02T15:04:05Z"),
		UpdatedAt:   created.UpdatedAt.Format("2006-01-02T15:04:05Z"),
	}

	respondJSON(w, http.StatusCreated, response)
}

// GetWOD retrieves a WOD by ID
// @Summary      Get WOD by ID
// @Description  Retrieve a single WOD by its ID
// @Tags         wods
// @Accept       json
// @Produce      json
// @Param        id path int true "WOD ID"
// @Success      200 {object} WODResponse "WOD details"
// @Failure      400 {object} ErrorResponse "Invalid WOD ID"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /wods/{id} [get]
func (h *WODHandler) GetWOD(w http.ResponseWriter, r *http.Request) {
	idStr := chi.URLParam(r, "id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil {
		respondError(w, http.StatusBadRequest, "Invalid WOD ID")
		return
	}

	wod, err := h.wodService.GetByID(id)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "Failed to retrieve WOD: "+err.Error())
		return
	}

	response := WODResponse{
		ID:          wod.ID,
		Name:        wod.Name,
		Source:      wod.Source,
		Type:        wod.Type,
		Regime:      wod.Regime,
		ScoreType:   wod.ScoreType,
		Description: wod.Description,
		URL:         wod.URL,
		Notes:       wod.Notes,
		IsStandard:  wod.IsStandard,
		CreatedBy:   wod.CreatedBy,
		CreatedAt:   wod.CreatedAt.Format("2006-01-02T15:04:05Z"),
		UpdatedAt:   wod.UpdatedAt.Format("2006-01-02T15:04:05Z"),
	}

	respondJSON(w, http.StatusOK, response)
}

// ListWODs retrieves all WODs (standard + user's custom)
// @Summary      List all WODs
// @Description  Retrieve all WODs including standard and user's custom WODs
// @Tags         wods
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        limit query int false "Max results (default 100)"
// @Param        offset query int false "Skip N results (default 0)"
// @Param        standard query bool false "Return only standard WODs"
// @Success      200 {object} WODListResponse "List of WODs"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /wods [get]
func (h *WODHandler) ListWODs(w http.ResponseWriter, r *http.Request) {
	// Extract user ID from JWT token in context (optional)
	userID, ok := middleware.GetUserID(r.Context())

	var wods []*domain.WOD
	var err error

	// Parse pagination parameters
	limit := 100 // default
	offset := 0  // default

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

	// Check if user wants only standard WODs
	standardOnly := r.URL.Query().Get("standard") == "true"

	if standardOnly {
		wods, err = h.wodService.ListStandard(limit, offset)
	} else if ok {
		userIDPtr := &userID
		wods, err = h.wodService.ListAll(userIDPtr, limit, offset)
	} else {
		wods, err = h.wodService.ListStandard(limit, offset)
	}

	if err != nil {
		respondError(w, http.StatusInternalServerError, "Failed to retrieve WODs")
		return
	}

	// Build response
	var responses []WODResponse
	for _, wod := range wods {
		response := WODResponse{
			ID:          wod.ID,
			Name:        wod.Name,
			Source:      wod.Source,
			Type:        wod.Type,
			Regime:      wod.Regime,
			ScoreType:   wod.ScoreType,
			Description: wod.Description,
			URL:         wod.URL,
			Notes:       wod.Notes,
			IsStandard:  wod.IsStandard,
			CreatedBy:   wod.CreatedBy,
			CreatedAt:   wod.CreatedAt.Format("2006-01-02T15:04:05Z"),
			UpdatedAt:   wod.UpdatedAt.Format("2006-01-02T15:04:05Z"),
		}
		responses = append(responses, response)
	}

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

// ListStandardWODs returns only standard/pre-seeded WODs
// @Summary      List standard WODs
// @Description  Retrieve only standard (built-in) WODs
// @Tags         wods
// @Accept       json
// @Produce      json
// @Param        limit query int false "Max results (default 100)"
// @Param        offset query int false "Skip N results (default 0)"
// @Success      200 {object} WODListResponse "List of standard WODs"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /wods/standard [get]
func (h *WODHandler) ListStandardWODs(w http.ResponseWriter, r *http.Request) {
	// Parse pagination parameters
	limit := 100 // default
	offset := 0  // default

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

	wods, err := h.wodService.ListStandard(limit, offset)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "Failed to retrieve standard WODs")
		return
	}

	// Build response
	var responses []WODResponse
	for _, wod := range wods {
		response := WODResponse{
			ID:          wod.ID,
			Name:        wod.Name,
			Source:      wod.Source,
			Type:        wod.Type,
			Regime:      wod.Regime,
			ScoreType:   wod.ScoreType,
			Description: wod.Description,
			URL:         wod.URL,
			Notes:       wod.Notes,
			IsStandard:  wod.IsStandard,
			CreatedBy:   wod.CreatedBy,
			CreatedAt:   wod.CreatedAt.Format("2006-01-02T15:04:05Z"),
			UpdatedAt:   wod.UpdatedAt.Format("2006-01-02T15:04:05Z"),
		}
		responses = append(responses, response)
	}

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

// ListMyWODs returns only the authenticated user's custom WODs
// @Summary      List my custom WODs
// @Description  Retrieve only the authenticated user's custom WODs
// @Tags         wods
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        limit query int false "Max results (default 100)"
// @Param        offset query int false "Skip N results (default 0)"
// @Success      200 {object} WODListResponse "List of user's custom WODs"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /wods/mine [get]
func (h *WODHandler) ListMyWODs(w http.ResponseWriter, r *http.Request) {
	// Extract user ID from JWT token in context
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Authentication required")
		return
	}

	// Parse pagination parameters
	limit := 100 // default
	offset := 0  // default

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

	wods, err := h.wodService.ListByUser(userID, limit, offset)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "Failed to retrieve custom WODs")
		return
	}

	// Build response
	var responses []WODResponse
	for _, wod := range wods {
		response := WODResponse{
			ID:          wod.ID,
			Name:        wod.Name,
			Source:      wod.Source,
			Type:        wod.Type,
			Regime:      wod.Regime,
			ScoreType:   wod.ScoreType,
			Description: wod.Description,
			URL:         wod.URL,
			Notes:       wod.Notes,
			IsStandard:  wod.IsStandard,
			CreatedBy:   wod.CreatedBy,
			CreatedAt:   wod.CreatedAt.Format("2006-01-02T15:04:05Z"),
			UpdatedAt:   wod.UpdatedAt.Format("2006-01-02T15:04:05Z"),
		}
		responses = append(responses, response)
	}

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

// SearchWODs searches for WODs by name
// @Summary      Search WODs
// @Description  Search for WODs by name
// @Tags         wods
// @Accept       json
// @Produce      json
// @Param        q query string true "Search query"
// @Success      200 {object} WODListResponse "Matching WODs"
// @Failure      400 {object} ErrorResponse "Missing search query"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /wods/search [get]
func (h *WODHandler) SearchWODs(w http.ResponseWriter, r *http.Request) {
	query := r.URL.Query().Get("q")
	if query == "" {
		respondError(w, http.StatusBadRequest, "Search query is required")
		return
	}

	wods, err := h.wodService.Search(query, 20)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "Failed to search WODs")
		return
	}

	// Build response
	var responses []WODResponse
	for _, wod := range wods {
		response := WODResponse{
			ID:          wod.ID,
			Name:        wod.Name,
			Source:      wod.Source,
			Type:        wod.Type,
			Regime:      wod.Regime,
			ScoreType:   wod.ScoreType,
			Description: wod.Description,
			URL:         wod.URL,
			Notes:       wod.Notes,
			IsStandard:  wod.IsStandard,
			CreatedBy:   wod.CreatedBy,
			CreatedAt:   wod.CreatedAt.Format("2006-01-02T15:04:05Z"),
			UpdatedAt:   wod.UpdatedAt.Format("2006-01-02T15:04:05Z"),
		}
		responses = append(responses, response)
	}

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

// UpdateWOD updates a custom WOD (or any WOD if admin)
// @Summary      Update a WOD
// @Description  Update an existing WOD (admin can update any, users only their own custom WODs)
// @Tags         wods
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "WOD ID"
// @Param        request body UpdateWODRequest true "Updated WOD details"
// @Success      200 {object} WODResponse "Updated WOD"
// @Failure      400 {object} ErrorResponse "Invalid request body"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      403 {object} ErrorResponse "Forbidden - not your WOD"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /wods/{id} [put]
func (h *WODHandler) UpdateWOD(w http.ResponseWriter, r *http.Request) {
	// Extract user ID from JWT token in context
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	// Extract user email for audit logging
	userEmail, _ := middleware.GetUserEmail(r.Context())

	// Check if user is admin
	role, _ := middleware.GetUserRole(r.Context())
	isAdmin := role == "admin"

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

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

	// Build update
	wod := &domain.WOD{
		ID:          id,
		Name:        req.Name,
		Source:      req.Source,
		Type:        req.Type,
		Regime:      req.Regime,
		ScoreType:   req.ScoreType,
		Description: req.Description,
		URL:         req.URL,
		Notes:       req.Notes,
	}

	// Use admin update if user is admin, otherwise regular update
	if isAdmin {
		err = h.wodService.UpdateAsAdmin(wod, userID, userEmail)
	} else {
		err = h.wodService.Update(wod, userID, userEmail)
	}
	if err != nil {
		if err == service.ErrUnauthorized || err == service.ErrWODUnauthorized || err == service.ErrWODOwnership {
			respondError(w, http.StatusForbidden, "You don't have permission to update this WOD")
		} else {
			respondError(w, http.StatusInternalServerError, "Failed to update WOD: "+err.Error())
		}
		return
	}

	// Retrieve updated WOD
	updated, err := h.wodService.GetByID(id)
	if err != nil {
		respondError(w, http.StatusInternalServerError, "Failed to retrieve updated WOD")
		return
	}

	response := WODResponse{
		ID:          updated.ID,
		Name:        updated.Name,
		Source:      updated.Source,
		Type:        updated.Type,
		Regime:      updated.Regime,
		ScoreType:   updated.ScoreType,
		Description: updated.Description,
		URL:         updated.URL,
		Notes:       updated.Notes,
		IsStandard:  updated.IsStandard,
		CreatedBy:   updated.CreatedBy,
		CreatedAt:   updated.CreatedAt.Format("2006-01-02T15:04:05Z"),
		UpdatedAt:   updated.UpdatedAt.Format("2006-01-02T15:04:05Z"),
	}

	respondJSON(w, http.StatusOK, response)
}

// DeleteWOD deletes a custom WOD
// @Summary      Delete a WOD
// @Description  Delete a custom WOD (cannot delete standard WODs)
// @Tags         wods
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "WOD ID"
// @Success      200 {object} MessageResponse "WOD deleted successfully"
// @Failure      400 {object} ErrorResponse "Invalid WOD ID"
// @Failure      401 {object} ErrorResponse "Unauthorized"
// @Failure      403 {object} ErrorResponse "Forbidden - cannot delete this WOD"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /wods/{id} [delete]
func (h *WODHandler) DeleteWOD(w http.ResponseWriter, r *http.Request) {
	// Extract user ID from JWT token in context
	userID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}

	// Extract user email for audit logging
	userEmail, _ := middleware.GetUserEmail(r.Context())

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

	if err := h.wodService.Delete(id, userID, userEmail); err != nil {
		if err == service.ErrUnauthorized {
			respondError(w, http.StatusForbidden, "You don't have permission to delete this WOD")
		} else {
			respondError(w, http.StatusInternalServerError, "Failed to delete WOD: "+err.Error())
		}
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"message": "WOD deleted successfully"})
}