Ctrl K
ComunidadGuías…

internal/handler/admin_user_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"
)

// adminUserServiceIface is the package-private surface AdminUserHandler depends
// on. The concrete *service.AdminUserService satisfies it; tests use a stub.
// Keep this list narrow: only methods this handler actually invokes.
type adminUserServiceIface interface {
	CreateUser(actorID int64, fields service.CreateUserFields) (*domain.User, error)
	UpdateProfile(actorID, targetID int64, fields service.ProfileUpdateFields, ifMatchUpdatedAt time.Time) (*domain.User, error)
	ForcePasswordReset(actorID, targetID int64) error
	SetPassword(actorID, targetID int64, newPassword string) error
}

// AdminUserHandler handles admin user management operations
type AdminUserHandler struct {
	userService      *service.UserService
	adminUserService adminUserServiceIface
	logger           *logger.Logger
}

// NewAdminUserHandler creates a new admin user handler
func NewAdminUserHandler(userService *service.UserService, adminUserService adminUserServiceIface, logger *logger.Logger) *AdminUserHandler {
	return &AdminUserHandler{
		userService:      userService,
		adminUserService: adminUserService,
		logger:           logger,
	}
}

// ListUsers handles GET /api/admin/users
// @Summary      List users (Admin)
// @Description  Get a paginated list of all users
// @Tags         admin
// @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{} "Users list with total count"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /admin/users [get]
func (h *AdminUserHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
	// Parse pagination parameters
	limit := 50
	if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
		if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
			limit = parsedLimit
		}
	}

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

	// Get users
	users, total, err := h.userService.ListUsers(limit, offset)
	if err != nil {
		h.logger.Error("Failed to list users: %v", err)
		http.Error(w, "Failed to retrieve users", http.StatusInternalServerError)
		return
	}

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

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

// UnlockUser handles POST /api/admin/users/:id/unlock
// @Summary      Unlock user account (Admin)
// @Description  Unlock a user account that was locked due to failed login attempts
// @Tags         admin
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "User ID"
// @Success      200 {object} MessageResponse "Success message"
// @Failure      400 {object} ErrorResponse "Invalid user ID"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /admin/users/{id}/unlock [post]
func (h *AdminUserHandler) UnlockUser(w http.ResponseWriter, r *http.Request) {
	// Get admin user ID from context
	adminUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// Get target user ID from URL
	targetUserIDStr := chi.URLParam(r, "id")
	targetUserID, err := strconv.ParseInt(targetUserIDStr, 10, 64)
	if err != nil {
		http.Error(w, "Invalid user ID", http.StatusBadRequest)
		return
	}

	// Unlock the account
	if err := h.userService.UnlockAccount(adminUserID, targetUserID); err != nil {
		h.logger.Error("Failed to unlock user account: admin_user_id=%d target_user_id=%d error=%v", adminUserID, targetUserID, err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	h.logger.Info("User account unlocked: admin_user_id=%d target_user_id=%d", adminUserID, targetUserID)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{
		"message": "Account unlocked successfully",
	})
}

// DisableUser handles POST /api/admin/users/:id/disable
// @Summary      Disable user account (Admin)
// @Description  Disable a user account with optional reason
// @Tags         admin
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "User ID"
// @Param        request body object false "Optional disable reason"
// @Success      200 {object} MessageResponse "Success message"
// @Failure      400 {object} ErrorResponse "Invalid user ID or cannot disable"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /admin/users/{id}/disable [post]
func (h *AdminUserHandler) DisableUser(w http.ResponseWriter, r *http.Request) {
	// Get admin user ID from context
	adminUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// Get target user ID from URL
	targetUserIDStr := chi.URLParam(r, "id")
	targetUserID, err := strconv.ParseInt(targetUserIDStr, 10, 64)
	if err != nil {
		http.Error(w, "Invalid user ID", http.StatusBadRequest)
		return
	}

	// Parse request body for reason
	var request struct {
		Reason string `json:"reason"`
	}
	if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
		// Reason is optional
		request.Reason = ""
	}

	// Disable the account
	if err := h.userService.DisableAccount(adminUserID, targetUserID, request.Reason); err != nil {
		h.logger.Error("Failed to disable user account: admin_user_id=%d target_user_id=%d error=%v", adminUserID, targetUserID, err)
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	h.logger.Info("User account disabled: admin_user_id=%d target_user_id=%d reason=%s", adminUserID, targetUserID, request.Reason)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{
		"message": "Account disabled successfully",
	})
}

// EnableUser handles POST /api/admin/users/:id/enable
// @Summary      Enable user account (Admin)
// @Description  Re-enable a previously disabled user account
// @Tags         admin
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "User ID"
// @Success      200 {object} MessageResponse "Success message"
// @Failure      400 {object} ErrorResponse "Invalid user ID"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /admin/users/{id}/enable [post]
func (h *AdminUserHandler) EnableUser(w http.ResponseWriter, r *http.Request) {
	// Get admin user ID from context
	adminUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// Get target user ID from URL
	targetUserIDStr := chi.URLParam(r, "id")
	targetUserID, err := strconv.ParseInt(targetUserIDStr, 10, 64)
	if err != nil {
		http.Error(w, "Invalid user ID", http.StatusBadRequest)
		return
	}

	// Enable the account
	if err := h.userService.EnableAccount(adminUserID, targetUserID); err != nil {
		h.logger.Error("Failed to enable user account: admin_user_id=%d target_user_id=%d error=%v", adminUserID, targetUserID, err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	h.logger.Info("User account enabled: admin_user_id=%d target_user_id=%d", adminUserID, targetUserID)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{
		"message": "Account enabled successfully",
	})
}

// ChangeUserRole handles PUT /api/admin/users/:id/role
// @Summary      Change user role (Admin)
// @Description  Change a user's role to 'user' or 'admin'
// @Tags         admin
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "User ID"
// @Param        request body object true "New role (user or admin)"
// @Success      200 {object} MessageResponse "Success message"
// @Failure      400 {object} ErrorResponse "Invalid user ID or role"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /admin/users/{id}/role [put]
func (h *AdminUserHandler) ChangeUserRole(w http.ResponseWriter, r *http.Request) {
	// Get admin user ID from context
	adminUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// Get target user ID from URL
	targetUserIDStr := chi.URLParam(r, "id")
	targetUserID, err := strconv.ParseInt(targetUserIDStr, 10, 64)
	if err != nil {
		http.Error(w, "Invalid user ID", http.StatusBadRequest)
		return
	}

	// Parse request body
	var request struct {
		Role string `json:"role"`
	}
	if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	// Validate role
	if request.Role != "athlete" && request.Role != "coach" && request.Role != "admin" {
		http.Error(w, "Role must be 'athlete', 'coach', or 'admin'", http.StatusBadRequest)
		return
	}

	// Change the role
	if err := h.userService.ChangeUserRole(adminUserID, targetUserID, request.Role); err != nil {
		h.logger.Error("Failed to change user role: admin_user_id=%d target_user_id=%d new_role=%s error=%v", adminUserID, targetUserID, request.Role, err)
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	h.logger.Info("User role changed: admin_user_id=%d target_user_id=%d new_role=%s", adminUserID, targetUserID, request.Role)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{
		"message": "User role changed successfully",
	})
}

// ToggleEmailVerification handles POST /api/admin/users/:id/toggle-email-verification
// @Summary      Toggle email verification (Admin)
// @Description  Manually set a user's email verification status
// @Tags         admin
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "User ID"
// @Param        request body object true "Verified status (true/false)"
// @Success      200 {object} MessageResponse "Success message"
// @Failure      400 {object} ErrorResponse "Invalid user ID"
// @Failure      500 {object} ErrorResponse "Internal server error"
// @Router       /admin/users/{id}/toggle-email-verification [post]
func (h *AdminUserHandler) ToggleEmailVerification(w http.ResponseWriter, r *http.Request) {
	// Get admin user ID from context
	adminUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// Get target user ID from URL
	targetUserIDStr := chi.URLParam(r, "id")
	targetUserID, err := strconv.ParseInt(targetUserIDStr, 10, 64)
	if err != nil {
		http.Error(w, "Invalid user ID", http.StatusBadRequest)
		return
	}

	// Parse request body
	var request struct {
		Verified bool `json:"verified"`
	}
	if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	// Toggle email verification
	if err := h.userService.SetEmailVerification(adminUserID, targetUserID, request.Verified); err != nil {
		h.logger.Error("Failed to toggle email verification: admin_user_id=%d target_user_id=%d verified=%v error=%v", adminUserID, targetUserID, request.Verified, err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	h.logger.Info("Email verification toggled: admin_user_id=%d target_user_id=%d verified=%v", adminUserID, targetUserID, request.Verified)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{
		"message": "Email verification updated successfully",
	})
}

// GetUserDetails handles GET /api/admin/users/:id
// @Summary      Get user details (Admin)
// @Description  Get detailed information about a specific user
// @Tags         admin
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "User ID"
// @Success      200 {object} map[string]interface{} "User details"
// @Failure      400 {object} ErrorResponse "Invalid user ID"
// @Failure      404 {object} ErrorResponse "User not found"
// @Router       /admin/users/{id} [get]
func (h *AdminUserHandler) GetUserDetails(w http.ResponseWriter, r *http.Request) {
	// Get target user ID from URL
	targetUserIDStr := chi.URLParam(r, "id")
	targetUserID, err := strconv.ParseInt(targetUserIDStr, 10, 64)
	if err != nil {
		http.Error(w, "Invalid user ID", http.StatusBadRequest)
		return
	}

	// Get user with admin details
	user, err := h.userService.GetUserByIDWithAdminDetails(targetUserID)
	if err != nil {
		h.logger.Error("Failed to get user details: target_user_id=%d error=%v", targetUserID, err)
		http.Error(w, "User not found", http.StatusNotFound)
		return
	}

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

// DeleteUser handles DELETE /api/admin/users/:id
// @Summary      Delete user (Admin)
// @Description  Permanently delete a user and all their data
// @Tags         admin
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id path int true "User ID"
// @Success      200 {object} MessageResponse "Success message"
// @Failure      400 {object} ErrorResponse "Invalid user ID or cannot delete"
// @Router       /admin/users/{id} [delete]
func (h *AdminUserHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
	// Get admin user ID from context
	adminUserID, ok := middleware.GetUserID(r.Context())
	if !ok {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}

	// Get target user ID from URL
	targetUserIDStr := chi.URLParam(r, "id")
	targetUserID, err := strconv.ParseInt(targetUserIDStr, 10, 64)
	if err != nil {
		http.Error(w, "Invalid user ID", http.StatusBadRequest)
		return
	}

	// Delete the user
	if err := h.userService.DeleteUser(adminUserID, targetUserID); err != nil {
		h.logger.Error("Failed to delete user: admin_user_id=%d target_user_id=%d error=%v", adminUserID, targetUserID, err)
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	h.logger.Info("User deleted: admin_user_id=%d target_user_id=%d", adminUserID, targetUserID)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{
		"message": "User deleted successfully",
	})
}

// updateProfileRequest is the PATCH /api/admin/users/{id} request body.
// UpdatedAt is required; it is compared against the stored row timestamp for
// optimistic concurrency control. Only present fields are applied.
type updateProfileRequest struct {
	Name          *string    `json:"name,omitempty"`
	Email         *string    `json:"email,omitempty"`
	Birthday      *time.Time `json:"birthday,omitempty"`
	EmailVerified *bool      `json:"email_verified,omitempty"`
	UpdatedAt     time.Time  `json:"updated_at"` // required (precondition)
}

// UpdateProfile handles PATCH /api/admin/users/{id}.
//
//   - Optimistic concurrency: client must send updated_at matching current row.
//   - Partial: only fields present in body are updated.
//   - L2 enforcement: service returns ErrProtectedUser → 403 via WriteError.
//   - On success: 200 with the updated User JSON.
func (h *AdminUserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
	actorID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}
	targetID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
	if err != nil {
		respondError(w, http.StatusNotFound, "Not found")
		return
	}
	var req updateProfileRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "invalid JSON")
		return
	}
	if req.UpdatedAt.IsZero() {
		respondError(w, http.StatusBadRequest, "updated_at must be a valid non-zero timestamp")
		return
	}
	user, err := h.adminUserService.UpdateProfile(actorID, targetID,
		service.ProfileUpdateFields{
			Name:          req.Name,
			Email:         req.Email,
			Birthday:      req.Birthday,
			EmailVerified: req.EmailVerified,
		},
		req.UpdatedAt,
	)
	if err != nil {
		WriteError(w, err)
		return
	}
	respondJSON(w, http.StatusOK, user)
}

// ForcePasswordReset handles POST /api/admin/users/{id}/force-password-reset.
//
//   - Calls service.ForcePasswordReset (sends email + revokes refresh tokens).
//   - L2 enforcement: protected target → 403.
//   - On success: 204 No Content.
func (h *AdminUserHandler) ForcePasswordReset(w http.ResponseWriter, r *http.Request) {
	actorID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}
	targetID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
	if err != nil {
		respondError(w, http.StatusNotFound, "Not found")
		return
	}
	if err := h.adminUserService.ForcePasswordReset(actorID, targetID); err != nil {
		WriteError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// createUserRequest is the POST /api/admin/users request body.
type createUserRequest struct {
	Email         string `json:"email"`
	Password      string `json:"password"`
	Name          string `json:"name"`
	Role          string `json:"role"`
	EmailVerified *bool  `json:"email_verified,omitempty"` // nil → default true
}

// CreateUser handles POST /api/admin/users — admin creates a new user account.
//
//   - 201 with the created user JSON on success
//   - 400 invalid_input on validation failure
//   - 409 duplicate_email if email already exists
//   - 401/403 from auth middleware
func (h *AdminUserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
	actorID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}
	var req createUserRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "invalid JSON")
		return
	}
	// Default EmailVerified=true when the caller omits the field.
	emailVerified := true
	if req.EmailVerified != nil {
		emailVerified = *req.EmailVerified
	}
	user, err := h.adminUserService.CreateUser(actorID, service.CreateUserFields{
		Email:         req.Email,
		Password:      req.Password,
		Name:          req.Name,
		Role:          req.Role,
		EmailVerified: emailVerified,
	})
	if err != nil {
		WriteError(w, err)
		return
	}
	respondJSON(w, http.StatusCreated, user)
}

// setPasswordRequest is the POST /api/admin/users/{id}/password request body.
type setPasswordRequest struct {
	NewPassword string `json:"new_password"`
}

// SetPassword handles POST /api/admin/users/{id}/password — admin sets a
// specific password on a user account.
//
//   - 204 on success (empty body)
//   - 400 on invalid JSON or password-policy failure
//   - 403 from L1 ProtectedUserGuard if target is protected
//   - 404 if {id} is not a valid integer
func (h *AdminUserHandler) SetPassword(w http.ResponseWriter, r *http.Request) {
	actorID, ok := middleware.GetUserID(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "Unauthorized")
		return
	}
	targetID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
	if err != nil {
		respondError(w, http.StatusNotFound, "Not found")
		return
	}
	var req setPasswordRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		respondError(w, http.StatusBadRequest, "invalid JSON")
		return
	}
	if err := h.adminUserService.SetPassword(actorID, targetID, req.NewPassword); err != nil {
		WriteError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}