refactor bug report structure and add API endpoints for bug report management

This commit is contained in:
Flavio Fois
2026-03-17 15:08:54 +01:00
parent 08ff1da469
commit 8097be88a6
6 changed files with 289 additions and 12 deletions

View File

@@ -0,0 +1,243 @@
package handlers
import (
"archive/zip"
"bytes"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/jmoiron/sqlx"
"emly-api-go/internal/models"
)
func GetAllBugReports(db *sqlx.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var reports []models.BugReport
if err := db.SelectContext(r.Context(), &reports, "SELECT * FROM emly_bugreports.bug_reports"); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(reports)
}
}
func GetBugReportByID(db *sqlx.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if id == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "missing id parameter"})
return
}
var report models.BugReport
err := db.GetContext(r.Context(), &report, "SELECT * FROM emly_bugreports.bug_reports WHERE id = ?", id)
if errors.Is(err, sql.ErrNoRows) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"error": "bug report not found"})
return
}
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(report)
}
}
func GetReportsCount(db *sqlx.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var count int
if err := db.GetContext(r.Context(), &count, "SELECT COUNT(*) FROM emly_bugreports.bug_reports"); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]int{"count": count})
}
}
func GetReportFilesByReportID(db *sqlx.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if id == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "missing id parameter"})
return
}
var files []models.BugReportFile
if err := db.SelectContext(r.Context(), &files, "SELECT * FROM emly_bugreports.bug_report_files WHERE report_id = ?", id); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(files)
}
}
func GetBugReportZipById(db *sqlx.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if id == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "missing id parameter"})
return
}
var report models.BugReport
err := db.GetContext(r.Context(), &report, "SELECT * FROM emly_bugreports.bug_reports WHERE id = ?", id)
if errors.Is(err, sql.ErrNoRows) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"error": "bug report not found"})
return
}
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
var files []models.BugReportFile
if err := db.SelectContext(r.Context(), &files, "SELECT * FROM emly_bugreports.bug_report_files WHERE report_id = ?", id); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
reportText := fmt.Sprintf(
"Bug Report #%d\n========================\n\nName: %s\nEmail: %s\nHostname: %s\nOS User: %s\nHWID: %s\nIP: %s\nStatus: %s\nCreated: %s\nUpdated: %s\n\nDescription:\n------------\n%s\n",
report.ID,
report.Name,
report.Email,
report.Hostname,
report.OsUser,
report.HWID,
report.SubmitterIP,
report.Status,
report.CreatedAt.UTC().Format("2006-01-02T15:04:05.000Z"),
report.UpdatedAt.UTC().Format("2006-01-02T15:04:05.000Z"),
report.Description,
)
if len(report.SystemInfo) > 0 && string(report.SystemInfo) != "null" {
pretty, err := json.MarshalIndent(report.SystemInfo, "", " ")
if err == nil {
reportText += fmt.Sprintf("\nSystem Info:\n------------\n%s\n", string(pretty))
}
}
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
rf, err := zw.Create("report.txt")
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
if _, err = rf.Write([]byte(reportText)); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
for _, file := range files {
ff, err := zw.Create(fmt.Sprintf("%s/%s", file.FileRole, file.Filename))
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
if _, err = ff.Write(file.Data); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
}
if err := zw.Close(); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/zip")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"report-%d.zip\"", report.ID))
w.Write(buf.Bytes())
}
}
func GetReportFileByFileID(db *sqlx.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
reportId := chi.URLParam(r, "id")
if reportId == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "missing report id parameter"})
return
}
fileId := chi.URLParam(r, "file_id")
if fileId == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "missing file id parameter"})
return
}
var file models.BugReportFile
err := db.GetContext(r.Context(), &file, "SELECT filename, mime_type, data FROM emly_bugreports.bug_report_files WHERE report_id = ? AND id = ?", reportId, fileId)
if errors.Is(err, sql.ErrNoRows) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"error": "file not found"})
return
}
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
mimeType := file.MimeType
if mimeType == "" {
mimeType = "application/octet-stream"
}
w.Header().Set("Content-Type", mimeType)
w.Header().Set("Content-Disposition", "attachment; filename=\""+file.Filename+"\"")
_, err = w.Write(file.Data)
if err != nil {
return
}
}
}

View File

@@ -8,19 +8,23 @@ import (
type BugReportStatus string
const (
BugReportStatusOpen BugReportStatus = "open"
BugReportStatusInProgress BugReportStatus = "in_progress"
BugReportStatusResolved BugReportStatus = "resolved"
BugReportStatusClosed BugReportStatus = "closed"
BugReportStatusNew BugReportStatus = "new"
BugReportStatusInReview BugReportStatus = "in_review"
BugReportStatusResolved BugReportStatus = "resolved"
BugReportStatusClosed BugReportStatus = "closed"
)
type BugReport struct {
ID int64 `db:"id" json:"id"`
UserID *int64 `db:"user_id" json:"user_id"`
Title string `db:"title" json:"title"`
ID uint64 `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Email string `db:"email" json:"email"`
Description string `db:"description" json:"description"`
Status BugReportStatus `db:"status" json:"status"`
HWID string `db:"hwid" json:"hwid"`
Hostname string `db:"hostname" json:"hostname"`
OsUser string `db:"os_user" json:"os_user"`
SubmitterIP string `db:"submitter_ip" json:"submitter_ip"`
SystemInfo json.RawMessage `db:"system_info" json:"system_info,omitempty"`
Status BugReportStatus `db:"status" json:"status"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}

View File

@@ -12,10 +12,11 @@ const (
type BugReportFile struct {
ID int64 `db:"id" json:"id"`
BugReportID int64 `db:"bug_report_id" json:"bug_report_id"`
BugReportID int64 `db:"report_id" json:"report_id"`
FileRole FileRole `db:"file_role" json:"file_role"`
Filename string `db:"filename" json:"filename"`
MimeType string `db:"mime_type" json:"mime_type"`
Role FileRole `db:"role" json:"role"`
FileSize int64 `db:"file_size" json:"file_size"`
Data []byte `db:"data" json:"-"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}