gohorsejobs/backend/internal/handlers/ticket_handler.go
Tiago Yamamoto 9ee9f6855c feat: implementar múltiplas features
Backend:
- Password reset flow (forgot/reset endpoints, tokens table)
- Profile management (PUT /users/me, skills, experience, education)
- Tickets system (CRUD, messages, stats)
- Activity logs (list, stats)
- Document validator (CNPJ, CPF, EIN support)
- Input sanitizer (XSS prevention)
- Full-text search em vagas (plainto_tsquery)
- Filtros avançados (location, salary, workMode)
- Ordenação (date, salary, relevance)

Frontend:
- Forgot/Reset password pages
- Candidate profile edit page
- Sanitize utilities (sanitize.ts)

Backoffice:
- TicketsModule proxy
- ActivityLogsModule proxy
- Dockerfile otimizado (multi-stage, non-root, healthcheck)

Migrations:
- 013: Profile fields to users
- 014: Password reset tokens
- 015: Tickets table
- 016: Activity logs table
2025-12-27 11:19:47 -03:00

236 lines
7.1 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"strconv"
"github.com/rede5/gohorsejobs/backend/internal/models"
"github.com/rede5/gohorsejobs/backend/internal/services"
)
type TicketHandler struct {
service *services.TicketService
}
func NewTicketHandler(service *services.TicketService) *TicketHandler {
return &TicketHandler{service: service}
}
// CreateTicket creates a new support ticket
// @Summary Create Ticket
// @Description Create a new support ticket
// @Tags Tickets
// @Accept json
// @Produce json
// @Param ticket body models.CreateTicketRequest true "Ticket data"
// @Success 201 {object} models.Ticket
// @Failure 400 {string} string "Invalid Request"
// @Router /api/v1/tickets [post]
func (h *TicketHandler) CreateTicket(w http.ResponseWriter, r *http.Request) {
var req models.CreateTicketRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
if req.Subject == "" || req.Description == "" {
http.Error(w, "Subject and description are required", http.StatusBadRequest)
return
}
// TODO: Get user ID from auth context
var userID *int
ticket, err := h.service.Create(userID, nil, req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(ticket)
}
// GetTickets lists tickets
// @Summary List Tickets
// @Description Get all tickets with optional filters
// @Tags Tickets
// @Produce json
// @Param status query string false "Filter by status"
// @Param priority query string false "Filter by priority"
// @Param limit query int false "Limit results"
// @Param offset query int false "Offset for pagination"
// @Success 200 {array} models.Ticket
// @Router /api/v1/tickets [get]
func (h *TicketHandler) GetTickets(w http.ResponseWriter, r *http.Request) {
status := r.URL.Query().Get("status")
priority := r.URL.Query().Get("priority")
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
if limit == 0 {
limit = 50
}
tickets, err := h.service.List(status, priority, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tickets)
}
// GetTicketByID gets a specific ticket
// @Summary Get Ticket
// @Description Get a ticket by ID
// @Tags Tickets
// @Produce json
// @Param id path int true "Ticket ID"
// @Success 200 {object} models.Ticket
// @Failure 404 {string} string "Not Found"
// @Router /api/v1/tickets/{id} [get]
func (h *TicketHandler) GetTicketByID(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ticket ID", http.StatusBadRequest)
return
}
ticket, err := h.service.GetByID(id)
if err != nil {
http.Error(w, "Ticket not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ticket)
}
// UpdateTicket updates a ticket
// @Summary Update Ticket
// @Description Update ticket status, priority or assignment
// @Tags Tickets
// @Accept json
// @Produce json
// @Param id path int true "Ticket ID"
// @Param ticket body models.UpdateTicketRequest true "Update data"
// @Success 200 {object} models.Ticket
// @Router /api/v1/tickets/{id} [put]
func (h *TicketHandler) UpdateTicket(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ticket ID", http.StatusBadRequest)
return
}
var req models.UpdateTicketRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
ticket, err := h.service.Update(id, req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ticket)
}
// GetTicketMessages gets messages for a ticket
// @Summary Get Ticket Messages
// @Description Get all messages for a ticket
// @Tags Tickets
// @Produce json
// @Param id path int true "Ticket ID"
// @Success 200 {array} models.TicketMessage
// @Router /api/v1/tickets/{id}/messages [get]
func (h *TicketHandler) GetTicketMessages(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ticket ID", http.StatusBadRequest)
return
}
// TODO: Check if user is admin for internal messages
includeInternal := true
messages, err := h.service.GetMessages(id, includeInternal)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(messages)
}
// AddTicketMessage adds a message to a ticket
// @Summary Add Ticket Message
// @Description Add a message/reply to a ticket
// @Tags Tickets
// @Accept json
// @Produce json
// @Param id path int true "Ticket ID"
// @Param message body models.AddTicketMessageRequest true "Message"
// @Success 201 {object} models.TicketMessage
// @Router /api/v1/tickets/{id}/messages [post]
func (h *TicketHandler) AddTicketMessage(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ticket ID", http.StatusBadRequest)
return
}
var req models.AddTicketMessageRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
if req.Message == "" {
http.Error(w, "Message is required", http.StatusBadRequest)
return
}
// TODO: Get user ID from auth context
var userID *int
msg, err := h.service.AddMessage(id, userID, req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(msg)
}
// GetTicketStats gets ticket statistics
// @Summary Get Ticket Stats
// @Description Get ticket statistics for dashboard
// @Tags Tickets
// @Produce json
// @Success 200 {object} models.TicketStats
// @Router /api/v1/tickets/stats [get]
func (h *TicketHandler) GetTicketStats(w http.ResponseWriter, r *http.Request) {
stats, err := h.service.GetStats()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(stats)
}