diff --git a/internal/handlers/bug_report.route.go b/internal/handlers/bug_report.route.go index 84466e9..0dd9910 100644 --- a/internal/handlers/bug_report.route.go +++ b/internal/handlers/bug_report.route.go @@ -31,9 +31,7 @@ var fileRoles = []struct { func CreateBugReport(db *sqlx.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(32 << 20); err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "invalid multipart form: " + err.Error()}) + jsonError(w, http.StatusBadRequest, "invalid multipart form: "+err.Error()) return } @@ -46,9 +44,7 @@ func CreateBugReport(db *sqlx.DB) http.HandlerFunc { systemInfoStr := r.FormValue("system_info") if name == "" || email == "" || description == "" { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "name, email and description are required"}) + jsonError(w, http.StatusBadRequest, "name, email and description are required") return } @@ -72,33 +68,26 @@ func CreateBugReport(db *sqlx.DB) http.HandlerFunc { name, email, description, hwid, hostname, osUser, submitterIP, systemInfo, models.BugReportStatusNew, ) if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + jsonError(w, http.StatusInternalServerError, err.Error()) return } reportID, err := result.LastInsertId() if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + jsonError(w, http.StatusInternalServerError, err.Error()) return } for _, fr := range fileRoles { file, header, err := r.FormFile(fr.field) if err != nil { - // no file uploaded for this role — skip continue } defer file.Close() data, err := io.ReadAll(file) if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "reading file " + fr.field + ": " + err.Error()}) + jsonError(w, http.StatusInternalServerError, "reading file "+fr.field+": "+err.Error()) return } @@ -118,18 +107,14 @@ func CreateBugReport(db *sqlx.DB) http.HandlerFunc { reportID, fr.role, filename, mimeType, len(data), data, ) if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + jsonError(w, http.StatusInternalServerError, err.Error()) return } } log.Printf("[BUGREPORT] Created successfully with id=%d", reportID) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(map[string]interface{}{ + jsonCreated(w, map[string]interface{}{ "success": true, "report_id": reportID, "message": "Bug report submitted successfully", @@ -141,14 +126,11 @@ 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_dev.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()}) + jsonError(w, http.StatusInternalServerError, err.Error()) return } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(reports) + jsonOK(w, reports) } } @@ -156,29 +138,22 @@ 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"}) + jsonError(w, http.StatusBadRequest, "missing id parameter") return } var report models.BugReport err := db.GetContext(r.Context(), &report, "SELECT * FROM emly_bugreports_dev.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"}) + jsonError(w, http.StatusNotFound, "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()}) + jsonError(w, http.StatusInternalServerError, err.Error()) return } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(report) + jsonOK(w, report) } } @@ -186,14 +161,11 @@ 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_dev.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()}) + jsonError(w, http.StatusInternalServerError, err.Error()) return } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]int{"count": count}) + jsonOK(w, map[string]int{"count": count}) } } @@ -201,22 +173,17 @@ 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"}) + jsonError(w, http.StatusBadRequest, "missing id parameter") return } var files []models.BugReportFile if err := db.SelectContext(r.Context(), &files, "SELECT * FROM emly_bugreports_dev.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()}) + jsonError(w, http.StatusInternalServerError, err.Error()) return } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(files) + jsonOK(w, files) } } @@ -224,32 +191,24 @@ 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"}) + jsonError(w, http.StatusBadRequest, "missing id parameter") return } var report models.BugReport err := db.GetContext(r.Context(), &report, "SELECT * FROM emly_bugreports_dev.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"}) + jsonError(w, http.StatusNotFound, "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()}) + jsonError(w, http.StatusInternalServerError, err.Error()) return } var files []models.BugReportFile if err := db.SelectContext(r.Context(), &files, "SELECT * FROM emly_bugreports_dev.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()}) + jsonError(w, http.StatusInternalServerError, err.Error()) return } @@ -279,38 +238,28 @@ func GetBugReportZipById(db *sqlx.DB) http.HandlerFunc { 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()}) + jsonError(w, http.StatusInternalServerError, 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()}) + jsonError(w, http.StatusInternalServerError, 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()}) + jsonError(w, http.StatusInternalServerError, 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()}) + jsonError(w, http.StatusInternalServerError, 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()}) + jsonError(w, http.StatusInternalServerError, err.Error()) return } @@ -324,30 +273,23 @@ 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"}) + jsonError(w, http.StatusBadRequest, "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"}) + jsonError(w, http.StatusBadRequest, "missing file id parameter") return } + var file models.BugReportFile err := db.GetContext(r.Context(), &file, "SELECT filename, mime_type, data FROM emly_bugreports_dev.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"}) + jsonError(w, http.StatusNotFound, "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()}) + jsonError(w, http.StatusInternalServerError, err.Error()) return } @@ -368,65 +310,77 @@ func GetReportStatusByID(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 - } - var reportStatus models.BugReportStatus - if err := db.GetContext(r.Context(), &reportStatus, "SELECT status FROM emly_bugreports_dev.bug_reports WHERE id = ?", reportId); err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + jsonError(w, http.StatusBadRequest, "missing report id parameter") return } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]string{"status": string(reportStatus)}) + var reportStatus models.BugReportStatus + if err := db.GetContext(r.Context(), &reportStatus, "SELECT status FROM emly_bugreports_dev.bug_reports WHERE id = ?", reportId); err != nil { + jsonError(w, http.StatusInternalServerError, err.Error()) + return + } + + jsonOK(w, map[string]string{"status": string(reportStatus)}) } } -func PatchReportStatus(db *sqlx.DB) http.HandlerFunc { +func PatchBugReportStatus(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"}) + jsonError(w, http.StatusBadRequest, "missing report id parameter") return } body, err := io.ReadAll(r.Body) if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "unable to read request body: " + err.Error()}) + jsonError(w, http.StatusBadRequest, "unable to read request body: "+err.Error()) return } reportStatus := models.BugReportStatus(body) result, err := db.ExecContext(r.Context(), "UPDATE emly_bugreports_dev.bug_reports SET status = ? WHERE id = ?", reportStatus, reportId) if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + jsonError(w, http.StatusInternalServerError, err.Error()) return } rowsAffected, err := result.RowsAffected() if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + jsonError(w, http.StatusInternalServerError, err.Error()) return } if rowsAffected == 0 { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "bug report not found"}) + jsonError(w, http.StatusNotFound, "bug report not found") return } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]string{"message": "status updated successfully"}) + jsonOK(w, map[string]string{"message": "status updated successfully"}) + } +} + +func DeleteBugReportByID(db *sqlx.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + reportId := chi.URLParam(r, "id") + if reportId == "" { + jsonError(w, http.StatusBadRequest, "missing report id parameter") + return + } + + result, err := db.ExecContext(r.Context(), "DELETE FROM emly_bugreports_dev.bug_reports WHERE id = ?", reportId) + if err != nil { + jsonError(w, http.StatusInternalServerError, err.Error()) + return + } + rowsAffected, err := result.RowsAffected() + if err != nil { + jsonError(w, http.StatusInternalServerError, err.Error()) + return + } + if rowsAffected == 0 { + jsonError(w, http.StatusNotFound, "bug report not found") + return + } + + jsonOK(w, map[string]string{"message": "bug report deleted successfully"}) } } diff --git a/internal/handlers/response.go b/internal/handlers/response.go new file mode 100644 index 0000000..32ba9cb --- /dev/null +++ b/internal/handlers/response.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "encoding/json" + "net/http" +) + +// jsonError writes a JSON error response with the given status code and message. +func jsonError(w http.ResponseWriter, status int, msg string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(map[string]string{"error": msg}) +} + +// jsonOK writes a 200 JSON response with the given payload. +func jsonOK(w http.ResponseWriter, v any) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(v) +} + +// jsonCreated writes a 201 JSON response with the given payload. +func jsonCreated(w http.ResponseWriter, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(v) +} diff --git a/main.go b/main.go index 5d8df00..9dae06f 100644 --- a/main.go +++ b/main.go @@ -90,7 +90,8 @@ func main() { r.Get("/{id}/files", handlers.GetReportFilesByReportID(db)) r.Get("/{id}/files/{file_id}", handlers.GetReportFileByFileID(db)) r.Get("/{id}/download", handlers.GetBugReportZipById(db)) - r.Patch("/{id}/status", handlers.PatchReportStatus(db)) + r.Patch("/{id}/status", handlers.PatchBugReportStatus(db)) + r.Delete("/{id}", handlers.DeleteBugReportByID(db)) }) }) })