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
236 lines
7.1 KiB
Go
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)
|
|
}
|