419 lines
13 KiB
Go
419 lines
13 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)
|
|
|
|
go func() {
|
|
admins, _, err := h.userSvc.GetAllUsers(context.Background(), domain.UserFilter{Role: string(domain.RoleAdmin)})
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, admin := range admins {
|
|
h.sendInAppNotification(admin.ID, domain.NOTIFICATION_TYPE_ISSUE_CREATED, "New Issue Reported", "A new issue \""+issue.Subject+"\" has been reported.")
|
|
}
|
|
}()
|
|
|
|
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)
|
|
|
|
go func() {
|
|
issue, err := h.issueReportingSvc.GetIssueByID(context.Background(), id)
|
|
if err != nil {
|
|
return
|
|
}
|
|
h.sendInAppNotification(issue.UserID, domain.NOTIFICATION_TYPE_ISSUE_STATUS_UPDATED, "Issue Status Updated", fmt.Sprintf("Your issue \"%s\" has been updated to: %s", issue.Subject, req.Status))
|
|
}()
|
|
|
|
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,
|
|
})
|
|
}
|