refactor bug report structure and add API endpoints for bug report management
This commit is contained in:
4
go.mod
4
go.mod
@@ -3,14 +3,14 @@ module emly-api-go
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
github.com/go-chi/chi/v5 v5.2.4
|
||||
github.com/go-chi/httprate v0.14.1
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5
|
||||
)
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1,9 +1,13 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
|
||||
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
|
||||
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-chi/httprate v0.14.1 h1:EKZHYEZ58Cg6hWcYzoZILsv7ppb46Wt4uQ738IRtpZs=
|
||||
github.com/go-chi/httprate v0.14.1/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
|
||||
243
internal/handlers/bug_report_handler.route.go
Normal file
243
internal/handlers/bug_report_handler.route.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
25
main.go
25
main.go
@@ -47,6 +47,14 @@ func main() {
|
||||
})
|
||||
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
// Add a header called X-Server
|
||||
r.Use(func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Server", "emly-api-go")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
|
||||
// Health – public, no API key required
|
||||
r.Get("/health", handlers.Health(db))
|
||||
|
||||
@@ -60,6 +68,23 @@ func main() {
|
||||
r.Get("/example", handlers.ExampleGet)
|
||||
r.Post("/example", handlers.ExamplePost)
|
||||
})
|
||||
|
||||
r.Route("/bug-reports", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(apimw.APIKeyAuth(db))
|
||||
|
||||
// Tighter rate-limit on protected group: 30 req / min per IP
|
||||
r.Use(httprate.LimitByIP(30, time.Minute))
|
||||
|
||||
r.Get("/", handlers.GetAllBugReports(db))
|
||||
r.Get("/{id}", handlers.GetBugReportByID(db))
|
||||
r.Get("/count", handlers.GetReportsCount(db))
|
||||
r.Get("/{id}/files", handlers.GetReportFilesByReportID(db))
|
||||
r.Get("/{id}/files/{file_id}", handlers.GetReportFileByFileID(db))
|
||||
r.Get("/{id}/zip", handlers.GetBugReportZipById(db))
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
addr := fmt.Sprintf(":%s", cfg.Port)
|
||||
|
||||
Reference in New Issue
Block a user