Yimaru-BackEnd/internal/web_server/handlers/issue_reporting.go

401 lines
12 KiB
Go

package handlers
import (
"Yimaru-Backend/internal/domain"
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/gofiber/fiber/v2"
)
type createIssueReq struct {
Subject string `json:"subject" validate:"required"`
Description string `json:"description" validate:"required"`
IssueType string `json:"issue_type" validate:"required"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type issueRes struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
UserRole string `json:"user_role"`
Subject string `json:"subject"`
Description string `json:"description"`
IssueType string `json:"issue_type"`
Status string `json:"status"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type updateIssueStatusReq struct {
Status string `json:"status" validate:"required,oneof=pending in_progress resolved rejected"`
}
type issueListRes struct {
Issues []issueRes `json:"issues"`
TotalCount int64 `json:"total_count"`
}
func mapIssueToRes(issue domain.ReportedIssue) issueRes {
return issueRes{
ID: issue.ID,
UserID: issue.UserID,
UserRole: string(issue.UserRole),
Subject: issue.Subject,
Description: issue.Description,
IssueType: string(issue.IssueType),
Status: string(issue.Status),
Metadata: issue.Metadata,
CreatedAt: issue.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: issue.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
// CreateIssue godoc
// @Summary Report an issue
// @Description Allows any authenticated user to report an issue they encountered
// @Tags issues
// @Accept json
// @Produce json
// @Security Bearer
// @Param body body createIssueReq true "Issue report payload"
// @Success 201 {object} domain.Response{data=issueRes}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 401 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues [post]
func (h *Handler) CreateIssue(c *fiber.Ctx) error {
var req createIssueReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Validation failed",
Error: errMsg,
})
}
userID := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role)
issueReq := domain.ReportedIssueReq{
Subject: req.Subject,
Description: req.Description,
IssueType: domain.ReportedIssueType(req.IssueType),
Metadata: req.Metadata,
}
issue, err := h.issueReportingSvc.CreateReportedIssue(c.Context(), issueReq, userID, role)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to create issue report",
Error: err.Error(),
})
}
actorRole := string(role)
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"subject": issue.Subject, "issue_type": string(issue.IssueType)})
go h.activityLogSvc.RecordAction(context.Background(), &userID, &actorRole, domain.ActionIssueCreated, domain.ResourceIssue, &issue.ID, "Reported issue: "+issue.Subject, meta, &ip, &ua)
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Issue reported successfully",
Data: mapIssueToRes(issue),
Success: true,
})
}
// GetIssueByID godoc
// @Summary Get issue by ID
// @Description Returns a single issue report by its ID (admin only)
// @Tags issues
// @Produce json
// @Security Bearer
// @Param id path int true "Issue ID"
// @Success 200 {object} domain.Response{data=issueRes}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Router /api/v1/issues/{id} [get]
func (h *Handler) GetIssueByID(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid issue ID",
Error: err.Error(),
})
}
issue, err := h.issueReportingSvc.GetIssueByID(c.Context(), id)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Issue not found",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Issue retrieved successfully",
Data: mapIssueToRes(issue),
Success: true,
})
}
// GetUserIssues godoc
// @Summary Get issues for a specific user
// @Description Returns paginated issues reported by a specific user (admin only)
// @Tags issues
// @Produce json
// @Security Bearer
// @Param user_id path int true "User ID"
// @Param limit query int false "Limit" default(20)
// @Param offset query int false "Offset" default(0)
// @Success 200 {object} domain.Response{data=issueListRes}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues/user/{user_id} [get]
func (h *Handler) GetUserIssues(c *fiber.Ctx) error {
userIDStr := c.Params("user_id")
userID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid user ID",
Error: err.Error(),
})
}
limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0"))
if limit > 100 {
limit = 100
}
issues, err := h.issueReportingSvc.GetIssuesForUser(c.Context(), userID, limit, offset)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve user issues",
Error: err.Error(),
})
}
totalCount, _ := h.issueReportingSvc.CountIssuesByUser(c.Context(), userID)
var issueResponses []issueRes
for _, issue := range issues {
issueResponses = append(issueResponses, mapIssueToRes(issue))
}
return c.JSON(domain.Response{
Message: "User issues retrieved successfully",
Data: issueListRes{
Issues: issueResponses,
TotalCount: totalCount,
},
Success: true,
})
}
// GetMyIssues godoc
// @Summary Get my reported issues
// @Description Returns paginated issues reported by the authenticated user
// @Tags issues
// @Produce json
// @Security Bearer
// @Param limit query int false "Limit" default(20)
// @Param offset query int false "Offset" default(0)
// @Success 200 {object} domain.Response{data=issueListRes}
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues/me [get]
func (h *Handler) GetMyIssues(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0"))
if limit > 100 {
limit = 100
}
issues, err := h.issueReportingSvc.GetIssuesForUser(c.Context(), userID, limit, offset)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve issues",
Error: err.Error(),
})
}
totalCount, _ := h.issueReportingSvc.CountIssuesByUser(c.Context(), userID)
var issueResponses []issueRes
for _, issue := range issues {
issueResponses = append(issueResponses, mapIssueToRes(issue))
}
return c.JSON(domain.Response{
Message: "Issues retrieved successfully",
Data: issueListRes{
Issues: issueResponses,
TotalCount: totalCount,
},
Success: true,
})
}
// GetAllIssues godoc
// @Summary Get all issues
// @Description Returns all reported issues with pagination (admin only)
// @Tags issues
// @Produce json
// @Security Bearer
// @Param limit query int false "Limit" default(20)
// @Param offset query int false "Offset" default(0)
// @Success 200 {object} domain.Response{data=issueListRes}
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues [get]
func (h *Handler) GetAllIssues(c *fiber.Ctx) error {
limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0"))
if limit > 100 {
limit = 100
}
issues, err := h.issueReportingSvc.GetAllIssues(c.Context(), limit, offset)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve issues",
Error: err.Error(),
})
}
totalCount, _ := h.issueReportingSvc.CountAllIssues(c.Context())
var issueResponses []issueRes
for _, issue := range issues {
issueResponses = append(issueResponses, mapIssueToRes(issue))
}
return c.JSON(domain.Response{
Message: "Issues retrieved successfully",
Data: issueListRes{
Issues: issueResponses,
TotalCount: totalCount,
},
Success: true,
})
}
// UpdateIssueStatus godoc
// @Summary Update issue status
// @Description Updates the status of an issue (admin only)
// @Tags issues
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "Issue ID"
// @Param body body updateIssueStatusReq true "Status update payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues/{id}/status [patch]
func (h *Handler) UpdateIssueStatus(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid issue ID",
Error: err.Error(),
})
}
var req updateIssueStatusReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Validation failed",
Error: errMsg,
})
}
if err := h.issueReportingSvc.UpdateIssueStatus(c.Context(), id, req.Status); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update issue status",
Error: err.Error(),
})
}
actorID := c.Locals("user_id").(int64)
actorRole := string(c.Locals("role").(domain.Role))
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"issue_id": id, "new_status": req.Status})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionIssueStatusUpdated, domain.ResourceIssue, &id, fmt.Sprintf("Updated issue %d status to %s", id, req.Status), meta, &ip, &ua)
return c.JSON(domain.Response{
Message: "Issue status updated successfully",
Success: true,
})
}
// DeleteIssue godoc
// @Summary Delete an issue
// @Description Deletes an issue report (admin only)
// @Tags issues
// @Produce json
// @Security Bearer
// @Param id path int true "Issue ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/issues/{id} [delete]
func (h *Handler) DeleteIssue(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid issue ID",
Error: err.Error(),
})
}
if err := h.issueReportingSvc.DeleteIssue(c.Context(), id); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to delete issue",
Error: err.Error(),
})
}
actorID := c.Locals("user_id").(int64)
actorRole := string(c.Locals("role").(domain.Role))
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"issue_id": id})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionIssueDeleted, domain.ResourceIssue, &id, fmt.Sprintf("Deleted issue ID: %d", id), meta, &ip, &ua)
return c.JSON(domain.Response{
Message: "Issue deleted successfully",
Success: true,
})
}