internal/handler/wod_handler.go
Rama main · réplica de lectura del repositorio original.
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"})
}