Files
goyco/internal/handlers/vote_handler.go

291 lines
9.1 KiB
Go

package handlers
import (
"net/http"
"goyco/internal/database"
"goyco/internal/dto"
"goyco/internal/services"
"github.com/go-chi/chi/v5"
)
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
// @tag.name votes
// @tag.description Voting system endpoints. All votes are handled through the same API with identical behavior.
// @tag.name posts
// @tag.description Post management endpoints with integrated vote statistics.
// @tag.name auth
// @tag.description Authentication and user management endpoints.
// @tag.name users
// @tag.description User management endpoints.
// @tag.name api
// @tag.description API information and system metrics.
type VoteHandler struct {
voteService *services.VoteService
}
func NewVoteHandler(voteService *services.VoteService) *VoteHandler {
return &VoteHandler{
voteService: voteService,
}
}
type VoteResponse = CommonResponse
// @Summary Cast a vote on a post
// @Description Vote on a post (upvote, downvote, or remove vote). Authentication is required; the vote is performed on behalf of the current user.
// @Description
// @Description **Vote Types:**
// @Description - `up`: Upvote the post
// @Description - `down`: Downvote the post
// @Description - `none`: Remove existing vote
// @Description
// @Description **Response includes:**
// @Description - Updated post vote counts (up_votes, down_votes, score)
// @Description - Success message
// @Tags votes
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Post ID"
// @Param request body dto.VoteRequest true "Vote data (type: 'up', 'down', or 'none' to remove)"
// @Success 200 {object} VoteResponse "Vote cast successfully with updated post statistics"
// @Failure 401 {object} VoteResponse "Authentication required"
// @Failure 400 {object} VoteResponse "Invalid request data or vote type"
// @Failure 404 {object} VoteResponse "Post not found"
// @Failure 500 {object} VoteResponse "Internal server error"
// @Example 200 {"success": true, "message": "Vote cast successfully", "data": {"post_id": 1, "type": "up", "up_votes": 5, "down_votes": 2, "score": 3, "is_anonymous": false}}
// @Example 400 {"success": false, "error": "Invalid vote type. Must be 'up', 'down', or 'none'"}
// @Router /api/posts/{id}/vote [post]
func (h *VoteHandler) CastVote(w http.ResponseWriter, r *http.Request) {
userID, ok := RequireAuth(w, r)
if !ok {
return
}
postID, ok := ParseUintParam(w, r, "id", "Post")
if !ok {
return
}
req, ok := GetValidatedDTO[dto.VoteRequest](r)
if !ok {
SendErrorResponse(w, "Invalid request", http.StatusBadRequest)
return
}
var voteType database.VoteType
switch req.Type {
case "up":
voteType = database.VoteUp
case "down":
voteType = database.VoteDown
case "none":
voteType = database.VoteNone
default:
SendErrorResponse(w, "Invalid vote type. Must be 'up', 'down', or 'none'", http.StatusBadRequest)
return
}
ipAddress := GetClientIP(r)
userAgent := r.UserAgent()
serviceReq := services.VoteRequest{
UserID: userID,
PostID: postID,
Type: voteType,
IPAddress: ipAddress,
UserAgent: userAgent,
}
response, err := h.voteService.CastVote(serviceReq)
if err != nil {
if err.Error() == "post not found" {
SendErrorResponse(w, err.Error(), http.StatusNotFound)
return
}
if err.Error() == "post ID is required" || err.Error() == "invalid vote type" {
SendErrorResponse(w, err.Error(), http.StatusBadRequest)
return
}
SendErrorResponse(w, "Internal server error", http.StatusInternalServerError)
return
}
SendSuccessResponse(w, "Vote cast successfully", response)
}
// @Summary Remove a vote
// @Description Remove a vote from a post for the authenticated user. This is equivalent to casting a vote with type 'none'.
// @Tags votes
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Post ID"
// @Success 200 {object} VoteResponse "Vote removed successfully with updated post statistics"
// @Failure 401 {object} VoteResponse "Authentication required"
// @Failure 400 {object} VoteResponse "Invalid post ID"
// @Failure 404 {object} VoteResponse "Post not found"
// @Failure 500 {object} VoteResponse "Internal server error"
// @Router /api/posts/{id}/vote [delete]
func (h *VoteHandler) RemoveVote(w http.ResponseWriter, r *http.Request) {
userID, ok := RequireAuth(w, r)
if !ok {
return
}
postID, ok := ParseUintParam(w, r, "id", "Post")
if !ok {
return
}
ipAddress := GetClientIP(r)
userAgent := r.UserAgent()
serviceReq := services.VoteRequest{
UserID: userID,
PostID: postID,
Type: database.VoteNone,
IPAddress: ipAddress,
UserAgent: userAgent,
}
response, err := h.voteService.CastVote(serviceReq)
if err != nil {
if err.Error() == "post not found" {
SendErrorResponse(w, err.Error(), http.StatusNotFound)
return
}
if err.Error() == "post ID is required" {
SendErrorResponse(w, err.Error(), http.StatusBadRequest)
return
}
SendErrorResponse(w, "Internal server error", http.StatusInternalServerError)
return
}
SendSuccessResponse(w, "Vote removed successfully", response)
}
// @Summary Get current user's vote
// @Description Retrieve the current user's vote for a specific post. Requires authentication and returns the vote type if it exists.
// @Description
// @Description **Response:**
// @Description - If vote exists: Returns vote details with contextual metadata (including `is_anonymous`)
// @Description - If no vote: Returns success with null vote data and metadata
// @Tags votes
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Post ID"
// @Success 200 {object} VoteResponse "Vote retrieved successfully"
// @Success 200 {object} VoteResponse "No vote found for this user/post combination"
// @Failure 401 {object} VoteResponse "Authentication required"
// @Failure 400 {object} VoteResponse "Invalid post ID"
// @Failure 500 {object} VoteResponse "Internal server error"
// @Example 200 {"success": true, "message": "Vote retrieved successfully", "data": {"has_vote": true, "vote": {"type": "up", "user_id": 123}, "is_anonymous": false}}
// @Example 200 {"success": true, "message": "No vote found", "data": {"has_vote": false, "vote": null, "is_anonymous": false}}
// @Router /api/posts/{id}/vote [get]
func (h *VoteHandler) GetUserVote(w http.ResponseWriter, r *http.Request) {
userID, ok := RequireAuth(w, r)
if !ok {
return
}
postID, ok := ParseUintParam(w, r, "id", "Post")
if !ok {
return
}
ipAddress := GetClientIP(r)
userAgent := r.UserAgent()
vote, err := h.voteService.GetUserVote(userID, postID, ipAddress, userAgent)
if err != nil {
if err.Error() == "record not found" {
SendSuccessResponse(w, "No vote found", map[string]any{
"has_vote": false,
"vote": nil,
"is_anonymous": false,
})
return
}
SendErrorResponse(w, "Internal server error", http.StatusInternalServerError)
return
}
SendSuccessResponse(w, "Vote retrieved successfully", map[string]any{
"has_vote": true,
"vote": vote,
"is_anonymous": false,
})
}
// @Summary Get post votes
// @Description Retrieve all votes for a specific post. Returns all votes in a single format.
// @Description
// @Description **Authentication Required:** Yes (Bearer token)
// @Description
// @Description **Response includes:**
// @Description - Array of all votes
// @Description - Total vote count
// @Description - Each vote includes type and unauthenticated status
// @Tags votes
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "Post ID"
// @Success 200 {object} VoteResponse "Votes retrieved successfully with count"
// @Failure 400 {object} VoteResponse "Invalid post ID"
// @Failure 401 {object} VoteResponse "Authentication required"
// @Failure 500 {object} VoteResponse "Internal server error"
// @Example 200 {"success": true, "message": "Votes retrieved successfully", "data": {"votes": [{"type": "up", "user_id": 123}, {"type": "down", "vote_hash": "abc123"}], "count": 2}}
// @Router /api/posts/{id}/votes [get]
func (h *VoteHandler) GetPostVotes(w http.ResponseWriter, r *http.Request) {
postID, ok := ParseUintParam(w, r, "id", "Post")
if !ok {
return
}
votes, err := h.voteService.GetPostVotes(postID)
if err != nil {
SendErrorResponse(w, "Internal server error", http.StatusInternalServerError)
return
}
allVotes := make([]any, 0, len(votes))
for _, vote := range votes {
allVotes = append(allVotes, vote)
}
SendSuccessResponse(w, "Votes retrieved successfully", map[string]any{
"votes": allVotes,
"count": len(allVotes),
})
}
func (h *VoteHandler) MountRoutes(r chi.Router, config RouteModuleConfig) {
protected := r
if config.AuthMiddleware != nil {
protected = r.With(config.AuthMiddleware)
}
if config.GeneralRateLimit != nil {
protected = config.GeneralRateLimit(protected)
}
protected.Post("/posts/{id}/vote", WithValidation[dto.VoteRequest](config.ValidationMiddleware, h.CastVote))
protected.Delete("/posts/{id}/vote", h.RemoveVote)
protected.Get("/posts/{id}/vote", h.GetUserVote)
protected.Get("/posts/{id}/votes", h.GetPostVotes)
}