471 lines
13 KiB
Go
471 lines
13 KiB
Go
package handlers
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"database/sql"
|
|
"embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"emly-api-go/internal/models"
|
|
)
|
|
|
|
//go:embed templates/report.txt.tmpl
|
|
var reportTemplateFS embed.FS
|
|
|
|
var reportTmpl = template.Must(
|
|
template.ParseFS(reportTemplateFS, "templates/report.txt.tmpl"),
|
|
)
|
|
|
|
var fileRoles = []struct {
|
|
field string
|
|
role models.FileRole
|
|
defaultMime string
|
|
}{
|
|
{"attachment", models.FileRoleAttachment, "application/octet-stream"},
|
|
{"screenshot", models.FileRoleScreenshot, "image/png"},
|
|
{"log", models.FileRoleLog, "text/plain"},
|
|
}
|
|
|
|
func CreateBugReport(db *sqlx.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
|
jsonError(w, http.StatusBadRequest, "invalid multipart form: "+err.Error())
|
|
return
|
|
}
|
|
|
|
name := r.FormValue("name")
|
|
email := r.FormValue("email")
|
|
description := r.FormValue("description")
|
|
hwid := r.FormValue("hwid")
|
|
hostname := r.FormValue("hostname")
|
|
osUser := r.FormValue("os_user")
|
|
systemInfoStr := r.FormValue("system_info")
|
|
|
|
if name == "" || email == "" || description == "" {
|
|
jsonError(w, http.StatusBadRequest, "name, email and description are required")
|
|
return
|
|
}
|
|
|
|
submitterIP := strings.TrimSpace(strings.SplitN(r.Header.Get("X-Forwarded-For"), ",", 2)[0])
|
|
if submitterIP == "" {
|
|
submitterIP = r.Header.Get("X-Real-IP")
|
|
}
|
|
if submitterIP == "" {
|
|
submitterIP = "unknown"
|
|
}
|
|
|
|
var systemInfo json.RawMessage
|
|
if systemInfoStr != "" && json.Valid([]byte(systemInfoStr)) {
|
|
systemInfo = json.RawMessage(systemInfoStr)
|
|
}
|
|
|
|
log.Printf("[BUGREPORT] Received from name=%s hwid=%s ip=%s", name, hwid, submitterIP)
|
|
|
|
result, err := db.ExecContext(r.Context(),
|
|
"INSERT INTO emly_bugreports_dev.bug_reports (name, email, description, hwid, hostname, os_user, submitter_ip, system_info, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
name, email, description, hwid, hostname, osUser, submitterIP, systemInfo, models.BugReportStatusNew,
|
|
)
|
|
if err != nil {
|
|
jsonError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
reportID, err := result.LastInsertId()
|
|
if err != nil {
|
|
jsonError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
for _, fr := range fileRoles {
|
|
file, header, err := r.FormFile(fr.field)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
defer func(file multipart.File) {
|
|
err := file.Close()
|
|
if err != nil {
|
|
log.Fatalf("closing uploaded file failed: %v", err)
|
|
}
|
|
}(file)
|
|
|
|
data, err := io.ReadAll(file)
|
|
if err != nil {
|
|
jsonError(w, http.StatusInternalServerError, "reading file "+fr.field+": "+err.Error())
|
|
return
|
|
}
|
|
|
|
mimeType := header.Header.Get("Content-Type")
|
|
if mimeType == "" {
|
|
mimeType = fr.defaultMime
|
|
}
|
|
filename := header.Filename
|
|
if filename == "" {
|
|
filename = fr.field + ".bin"
|
|
}
|
|
|
|
log.Printf("[BUGREPORT] File uploaded: role=%s size=%d bytes", fr.role, len(data))
|
|
|
|
_, err = db.ExecContext(r.Context(),
|
|
"INSERT INTO emly_bugreports_dev.bug_report_files (report_id, file_role, filename, mime_type, file_size, data) VALUES (?, ?, ?, ?, ?, ?)",
|
|
reportID, fr.role, filename, mimeType, len(data), data,
|
|
)
|
|
if err != nil {
|
|
jsonError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
log.Printf("[BUGREPORT] Created successfully with id=%d", reportID)
|
|
|
|
jsonCreated(w, map[string]interface{}{
|
|
"success": true,
|
|
"report_id": reportID,
|
|
"message": "Bug report submitted successfully",
|
|
})
|
|
}
|
|
}
|
|
|
|
func GetAllBugReports(db *sqlx.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
page, pageSize := 1, 20
|
|
if p := r.URL.Query().Get("page"); p != "" {
|
|
if v, err := strconv.Atoi(p); err == nil && v > 0 {
|
|
page = v
|
|
}
|
|
}
|
|
if ps := r.URL.Query().Get("page_size"); ps != "" {
|
|
if v, err := strconv.Atoi(ps); err == nil && v > 0 && v <= 100 {
|
|
pageSize = v
|
|
}
|
|
}
|
|
|
|
status := r.URL.Query().Get("status")
|
|
search := r.URL.Query().Get("search")
|
|
offset := (page - 1) * pageSize
|
|
|
|
var conditions []string
|
|
var params []interface{}
|
|
|
|
if status != "" {
|
|
conditions = append(conditions, "br.status = ?")
|
|
params = append(params, status)
|
|
}
|
|
if search != "" {
|
|
like := "%" + search + "%"
|
|
conditions = append(conditions, "(br.hostname LIKE ? OR br.os_user LIKE ? OR br.name LIKE ? OR br.email LIKE ?)")
|
|
params = append(params, like, like, like, like)
|
|
}
|
|
|
|
whereClause := ""
|
|
if len(conditions) > 0 {
|
|
whereClause = "WHERE " + strings.Join(conditions, " AND ")
|
|
}
|
|
|
|
var total int
|
|
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM emly_bugreports_dev.bug_reports br " + whereClause)
|
|
if err := db.GetContext(r.Context(), &total, countQuery, params...); err != nil {
|
|
jsonError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
mainQuery := fmt.Sprintf(`
|
|
SELECT br.*, COUNT(bf.id) as file_count
|
|
FROM emly_bugreports_dev.bug_reports br
|
|
LEFT JOIN emly_bugreports_dev.bug_report_files bf ON bf.report_id = br.id
|
|
` + whereClause + `
|
|
GROUP BY br.id
|
|
ORDER BY br.created_at DESC
|
|
LIMIT ? OFFSET ?`)
|
|
|
|
listParams := append(params, pageSize, offset)
|
|
var reports []models.BugReportListItem
|
|
if err := db.SelectContext(r.Context(), &reports, mainQuery, listParams...); err != nil {
|
|
jsonError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
jsonOK(w, map[string]interface{}{
|
|
"data": reports,
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": pageSize,
|
|
"total_pages": int(math.Ceil(float64(total) / float64(pageSize))),
|
|
})
|
|
}
|
|
}
|
|
|
|
func GetBugReportByID(db *sqlx.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
if id == "" {
|
|
jsonError(w, http.StatusBadRequest, "missing id parameter")
|
|
return
|
|
}
|
|
|
|
var report models.BugReport
|
|
reportErr := db.GetContext(r.Context(), &report, "SELECT * FROM emly_bugreports_dev.bug_reports WHERE id = ?", id)
|
|
if errors.Is(reportErr, sql.ErrNoRows) {
|
|
jsonError(w, http.StatusNotFound, "bug report not found")
|
|
return
|
|
}
|
|
if reportErr != nil {
|
|
jsonError(w, http.StatusInternalServerError, reportErr.Error())
|
|
return
|
|
}
|
|
|
|
type response struct {
|
|
Report models.BugReport `json:"report"`
|
|
}
|
|
|
|
responseData := response{
|
|
Report: report,
|
|
}
|
|
|
|
jsonOK(w, responseData)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
jsonError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
jsonOK(w, 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 == "" {
|
|
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 {
|
|
jsonError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
jsonOK(w, files)
|
|
}
|
|
}
|
|
|
|
func GetBugReportZipById(db *sqlx.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
if id == "" {
|
|
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) {
|
|
jsonError(w, http.StatusNotFound, "bug report not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
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 {
|
|
jsonError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
var sysInfoStr string
|
|
if len(report.SystemInfo) > 0 && string(report.SystemInfo) != "null" {
|
|
if pretty, err := json.MarshalIndent(report.SystemInfo, "", " "); err == nil {
|
|
sysInfoStr = string(pretty)
|
|
}
|
|
}
|
|
|
|
tmplData := struct {
|
|
models.BugReport
|
|
CreatedAt string
|
|
UpdatedAt string
|
|
SystemInfo string
|
|
}{
|
|
BugReport: report,
|
|
CreatedAt: report.CreatedAt.UTC().Format("2006-01-02T15:04:05.000Z"),
|
|
UpdatedAt: report.UpdatedAt.UTC().Format("2006-01-02T15:04:05.000Z"),
|
|
SystemInfo: sysInfoStr,
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
zw := zip.NewWriter(&buf)
|
|
|
|
rf, err := zw.Create("report.txt")
|
|
if err != nil {
|
|
jsonError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if err = reportTmpl.Execute(rf, tmplData); err != nil {
|
|
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 {
|
|
jsonError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
if _, err = ff.Write(file.Data); err != nil {
|
|
jsonError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := zw.Close(); err != nil {
|
|
jsonError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/zip")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"report-%d.zip\"", report.ID))
|
|
_, err = w.Write(buf.Bytes())
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func GetReportFileByFileID(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
|
|
}
|
|
fileId := chi.URLParam(r, "file_id")
|
|
if fileId == "" {
|
|
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) {
|
|
jsonError(w, http.StatusNotFound, "file not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
jsonError(w, http.StatusInternalServerError, 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
|
|
}
|
|
}
|
|
}
|
|
|
|
func GetReportStatusByID(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
|
|
}
|
|
|
|
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 PatchBugReportStatus(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
|
|
}
|
|
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
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 {
|
|
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": "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"})
|
|
}
|
|
}
|