686 lines
23 KiB
Go
686 lines
23 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/geo-platform/tenant-api/internal/ops/domain"
|
|
"github.com/geo-platform/tenant-api/internal/ops/repository"
|
|
"github.com/geo-platform/tenant-api/internal/shared/objectstorage"
|
|
"github.com/geo-platform/tenant-api/internal/shared/response"
|
|
tenantrepo "github.com/geo-platform/tenant-api/internal/tenant/repository"
|
|
)
|
|
|
|
const (
|
|
ActionDesktopBugReportCreate = "desktop_bug_report.create"
|
|
ActionDesktopBugReportUpdate = "desktop_bug_report.update"
|
|
)
|
|
|
|
const (
|
|
DesktopBugReportTypeCrash = "crash"
|
|
DesktopBugReportTypeBug = "bug"
|
|
)
|
|
|
|
const (
|
|
DesktopBugReportStatusNew = "new"
|
|
DesktopBugReportStatusTriaged = "triaged"
|
|
DesktopBugReportStatusInProgress = "in_progress"
|
|
DesktopBugReportStatusResolved = "resolved"
|
|
DesktopBugReportStatusIgnored = "ignored"
|
|
)
|
|
|
|
const (
|
|
DesktopBugReportSeverityLow = "low"
|
|
DesktopBugReportSeverityMedium = "medium"
|
|
DesktopBugReportSeverityHigh = "high"
|
|
DesktopBugReportSeverityCritical = "critical"
|
|
)
|
|
|
|
const maxDesktopBugDumpBytes = 64 * 1024 * 1024
|
|
const maxDesktopBugMultipartBytes = maxDesktopBugDumpBytes + 4*1024*1024
|
|
|
|
type DesktopBugReportService struct {
|
|
reports *repository.DesktopBugReportRepository
|
|
storage objectstorage.Client
|
|
audits *AuditService
|
|
}
|
|
|
|
func NewDesktopBugReportService(
|
|
reports *repository.DesktopBugReportRepository,
|
|
storage objectstorage.Client,
|
|
audits *AuditService,
|
|
) *DesktopBugReportService {
|
|
return &DesktopBugReportService{reports: reports, storage: storage, audits: audits}
|
|
}
|
|
|
|
type DesktopBugReportUploadInput struct {
|
|
ReportType string
|
|
Severity string
|
|
Title string
|
|
Description string
|
|
TenantID *int64
|
|
WorkspaceID *int64
|
|
UserID *int64
|
|
DesktopClientID string
|
|
CrashGUID string
|
|
CrashReportID string
|
|
ProcessType string
|
|
ElectronVersion string
|
|
AppVersion string
|
|
ProductName string
|
|
Platform string
|
|
OSArch string
|
|
DeviceName string
|
|
FormFields map[string]string
|
|
RuntimeSnapshot []byte
|
|
AppLogTail string
|
|
DumpFileName string
|
|
DumpContent []byte
|
|
UploaderIP string
|
|
UserAgent string
|
|
OccurredAt time.Time
|
|
}
|
|
|
|
type DesktopBugReportListInput struct {
|
|
Keyword string
|
|
Status string
|
|
Severity string
|
|
ReportType string
|
|
Platform string
|
|
Process string
|
|
TenantID *int64
|
|
ClientID string
|
|
Page int
|
|
Size int
|
|
}
|
|
|
|
type DesktopBugReportUpdateInput struct {
|
|
Status string
|
|
Severity string
|
|
ResolutionNote *string
|
|
}
|
|
|
|
type DesktopBugReportListResult struct {
|
|
Items []DesktopBugReportView `json:"items"`
|
|
Total int64 `json:"total"`
|
|
Page int `json:"page"`
|
|
Size int `json:"size"`
|
|
}
|
|
|
|
type DesktopBugReportUploadResult struct {
|
|
ID string `json:"id"`
|
|
DumpObjectKey *string `json:"dump_object_key,omitempty"`
|
|
}
|
|
|
|
type DesktopBugReportDownloadURLResult struct {
|
|
DownloadURL string `json:"download_url"`
|
|
}
|
|
|
|
type DesktopBugReportView struct {
|
|
ID string `json:"id"`
|
|
ReportType string `json:"report_type"`
|
|
Status string `json:"status"`
|
|
Severity string `json:"severity"`
|
|
TenantID *int64 `json:"tenant_id,omitempty"`
|
|
TenantName *string `json:"tenant_name,omitempty"`
|
|
WorkspaceID *int64 `json:"workspace_id,omitempty"`
|
|
WorkspaceName *string `json:"workspace_name,omitempty"`
|
|
UserID *int64 `json:"user_id,omitempty"`
|
|
UserName *string `json:"user_name,omitempty"`
|
|
UserPhone *string `json:"user_phone,omitempty"`
|
|
UserEmail *string `json:"user_email,omitempty"`
|
|
DesktopClientID *string `json:"desktop_client_id,omitempty"`
|
|
CrashGUID *string `json:"crash_guid,omitempty"`
|
|
CrashReportID *string `json:"crash_report_id,omitempty"`
|
|
ProcessType *string `json:"process_type,omitempty"`
|
|
ElectronVersion *string `json:"electron_version,omitempty"`
|
|
AppVersion *string `json:"app_version,omitempty"`
|
|
ProductName *string `json:"product_name,omitempty"`
|
|
Platform *string `json:"platform,omitempty"`
|
|
OSArch *string `json:"os_arch,omitempty"`
|
|
DeviceName *string `json:"device_name,omitempty"`
|
|
Title string `json:"title"`
|
|
Description *string `json:"description,omitempty"`
|
|
DumpObjectKey *string `json:"dump_object_key,omitempty"`
|
|
DumpFileName *string `json:"dump_file_name,omitempty"`
|
|
DumpSizeBytes *int64 `json:"dump_size_bytes,omitempty"`
|
|
DumpSHA256 *string `json:"dump_sha256,omitempty"`
|
|
FormFields json.RawMessage `json:"form_fields"`
|
|
RuntimeSnapshot json.RawMessage `json:"runtime_snapshot"`
|
|
AppLogTail *string `json:"app_log_tail,omitempty"`
|
|
ResolutionNote *string `json:"resolution_note,omitempty"`
|
|
UploaderIP *string `json:"uploader_ip,omitempty"`
|
|
UserAgent *string `json:"user_agent,omitempty"`
|
|
OccurredAt time.Time `json:"occurred_at"`
|
|
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
func (s *DesktopBugReportService) UploadPublic(ctx context.Context, in DesktopBugReportUploadInput) (*DesktopBugReportUploadResult, error) {
|
|
in.TenantID = nil
|
|
in.WorkspaceID = nil
|
|
in.UserID = nil
|
|
in.DesktopClientID = ""
|
|
in.FormFields = dropPublicDesktopBugIdentityFields(in.FormFields)
|
|
return s.Upload(ctx, in)
|
|
}
|
|
|
|
func (s *DesktopBugReportService) Upload(ctx context.Context, in DesktopBugReportUploadInput) (*DesktopBugReportUploadResult, error) {
|
|
if s == nil || s.reports == nil {
|
|
return nil, response.ErrServiceUnavailable(50361, "desktop_bug_report_unavailable", "desktop bug report service is unavailable")
|
|
}
|
|
normalized, err := normalizeDesktopBugReportUploadInput(in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var dumpObjectKey *string
|
|
var dumpSHA256 *string
|
|
var dumpSize *int64
|
|
if len(normalized.DumpContent) > 0 {
|
|
if err := s.ensureStorageReady(); err != nil {
|
|
return nil, err
|
|
}
|
|
sum := sha256.Sum256(normalized.DumpContent)
|
|
sha256Hex := hex.EncodeToString(sum[:])
|
|
size := int64(len(normalized.DumpContent))
|
|
objectKey := buildDesktopBugDumpObjectKey(normalized, sha256Hex)
|
|
contentType := detectDesktopBugDumpContentType(normalized.DumpFileName, normalized.DumpContent)
|
|
if err := s.storage.PutBytes(ctx, objectKey, normalized.DumpContent, contentType); err != nil {
|
|
appErr := response.ErrInternal(50061, "desktop_bug_dump_upload_failed", "failed to upload dump to object storage")
|
|
appErr.Cause = err
|
|
return nil, appErr
|
|
}
|
|
dumpObjectKey = &objectKey
|
|
dumpSHA256 = &sha256Hex
|
|
dumpSize = &size
|
|
}
|
|
|
|
formFields, _ := json.Marshal(normalized.FormFields)
|
|
report, err := s.reports.Create(ctx, repository.CreateDesktopBugReportInput{
|
|
ID: uuid.NewString(),
|
|
ReportType: normalized.ReportType,
|
|
Status: DesktopBugReportStatusNew,
|
|
Severity: normalized.Severity,
|
|
TenantID: normalized.TenantID,
|
|
WorkspaceID: normalized.WorkspaceID,
|
|
UserID: normalized.UserID,
|
|
DesktopClientID: optionalUUIDString(normalized.DesktopClientID),
|
|
CrashGUID: optionalText(normalized.CrashGUID),
|
|
CrashReportID: optionalText(normalized.CrashReportID),
|
|
ProcessType: optionalText(normalized.ProcessType),
|
|
ElectronVersion: optionalText(normalized.ElectronVersion),
|
|
AppVersion: optionalText(normalized.AppVersion),
|
|
ProductName: optionalText(normalized.ProductName),
|
|
Platform: optionalText(normalized.Platform),
|
|
OSArch: optionalText(normalized.OSArch),
|
|
DeviceName: optionalText(normalized.DeviceName),
|
|
Title: normalized.Title,
|
|
Description: optionalText(normalized.Description),
|
|
DumpObjectKey: dumpObjectKey,
|
|
DumpFileName: optionalText(normalized.DumpFileName),
|
|
DumpSizeBytes: dumpSize,
|
|
DumpSHA256: dumpSHA256,
|
|
FormFields: formFields,
|
|
RuntimeSnapshot: normalized.RuntimeSnapshot,
|
|
AppLogTail: optionalText(normalized.AppLogTail),
|
|
UploaderIP: optionalText(normalized.UploaderIP),
|
|
UserAgent: optionalText(normalized.UserAgent),
|
|
OccurredAt: normalized.OccurredAt,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &DesktopBugReportUploadResult{ID: report.ID, DumpObjectKey: report.DumpObjectKey}, nil
|
|
}
|
|
|
|
func (s *DesktopBugReportService) UploadForClient(
|
|
ctx context.Context,
|
|
client *tenantrepo.DesktopClient,
|
|
in DesktopBugReportUploadInput,
|
|
) (*DesktopBugReportUploadResult, error) {
|
|
if client == nil {
|
|
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
|
|
}
|
|
tenantID := client.TenantID
|
|
workspaceID := client.WorkspaceID
|
|
userID := client.UserID
|
|
in.TenantID = &tenantID
|
|
in.WorkspaceID = &workspaceID
|
|
in.UserID = &userID
|
|
in.DesktopClientID = client.ID.String()
|
|
if strings.TrimSpace(in.DeviceName) == "" && client.DeviceName != nil {
|
|
in.DeviceName = *client.DeviceName
|
|
}
|
|
if strings.TrimSpace(in.Platform) == "" && client.OS != nil {
|
|
in.Platform = *client.OS
|
|
}
|
|
if strings.TrimSpace(in.OSArch) == "" && client.CPUArch != nil {
|
|
in.OSArch = *client.CPUArch
|
|
}
|
|
if strings.TrimSpace(in.AppVersion) == "" && client.ClientVersion != nil {
|
|
in.AppVersion = *client.ClientVersion
|
|
}
|
|
return s.Upload(ctx, in)
|
|
}
|
|
|
|
func (s *DesktopBugReportService) List(ctx context.Context, in DesktopBugReportListInput) (*DesktopBugReportListResult, error) {
|
|
page := in.Page
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
size := in.Size
|
|
if size <= 0 || size > 200 {
|
|
size = 20
|
|
}
|
|
items, total, err := s.reports.List(ctx, repository.DesktopBugReportFilter{
|
|
Keyword: strings.TrimSpace(in.Keyword),
|
|
Status: normalizeDesktopBugStatus(in.Status),
|
|
Severity: normalizeDesktopBugSeverity(in.Severity),
|
|
ReportType: normalizeDesktopBugReportType(in.ReportType),
|
|
Platform: normalizeDesktopBugToken(in.Platform),
|
|
Process: normalizeDesktopBugToken(in.Process),
|
|
TenantID: in.TenantID,
|
|
ClientID: normalizeUUIDString(in.ClientID),
|
|
Limit: size,
|
|
Offset: (page - 1) * size,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
views := make([]DesktopBugReportView, 0, len(items))
|
|
for i := range items {
|
|
views = append(views, desktopBugReportView(&items[i]))
|
|
}
|
|
return &DesktopBugReportListResult{Items: views, Total: total, Page: page, Size: size}, nil
|
|
}
|
|
|
|
func (s *DesktopBugReportService) Get(ctx context.Context, id string) (*DesktopBugReportView, error) {
|
|
report, err := s.reports.GetByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrDesktopBugReportNotFound) {
|
|
return nil, response.ErrNotFound(40461, "desktop_bug_report_not_found", "客户端 Bug 记录不存在")
|
|
}
|
|
return nil, err
|
|
}
|
|
view := desktopBugReportView(report)
|
|
return &view, nil
|
|
}
|
|
|
|
func (s *DesktopBugReportService) Update(ctx context.Context, actor *Actor, id string, in DesktopBugReportUpdateInput) (*DesktopBugReportView, error) {
|
|
status := normalizeDesktopBugStatus(in.Status)
|
|
if !isAllowedDesktopBugStatus(status) {
|
|
return nil, response.ErrBadRequest(40061, "invalid_desktop_bug_status", "状态必须是 new、triaged、in_progress、resolved 或 ignored")
|
|
}
|
|
severity := normalizeDesktopBugSeverity(in.Severity)
|
|
if !isAllowedDesktopBugSeverity(severity) {
|
|
return nil, response.ErrBadRequest(40062, "invalid_desktop_bug_severity", "严重级别必须是 low、medium、high 或 critical")
|
|
}
|
|
report, err := s.reports.UpdateState(ctx, id, repository.UpdateDesktopBugReportStateInput{
|
|
Status: status,
|
|
Severity: severity,
|
|
ResolutionNote: trimOptionalDesktopBugString(in.ResolutionNote, 4000),
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrDesktopBugReportNotFound) {
|
|
return nil, response.ErrNotFound(40461, "desktop_bug_report_not_found", "客户端 Bug 记录不存在")
|
|
}
|
|
return nil, err
|
|
}
|
|
s.audit(ctx, actor, ActionDesktopBugReportUpdate, report)
|
|
view := desktopBugReportView(report)
|
|
return &view, nil
|
|
}
|
|
|
|
func (s *DesktopBugReportService) ResolveDumpDownloadURL(ctx context.Context, id string) (*DesktopBugReportDownloadURLResult, error) {
|
|
report, err := s.reports.GetByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, repository.ErrDesktopBugReportNotFound) {
|
|
return nil, response.ErrNotFound(40461, "desktop_bug_report_not_found", "客户端 Bug 记录不存在")
|
|
}
|
|
return nil, err
|
|
}
|
|
if report.DumpObjectKey == nil || strings.TrimSpace(*report.DumpObjectKey) == "" {
|
|
return nil, response.ErrNotFound(40462, "desktop_bug_dump_not_found", "该记录没有 dump 文件")
|
|
}
|
|
if err := s.ensureStorageReady(); err != nil {
|
|
return nil, err
|
|
}
|
|
downloadURL, err := s.storage.DownloadURL(*report.DumpObjectKey)
|
|
if err != nil {
|
|
appErr := response.ErrInternal(50062, "desktop_bug_dump_url_failed", "failed to generate dump download URL")
|
|
appErr.Cause = err
|
|
return nil, appErr
|
|
}
|
|
return &DesktopBugReportDownloadURLResult{DownloadURL: downloadURL}, nil
|
|
}
|
|
|
|
func (s *DesktopBugReportService) ensureStorageReady() error {
|
|
if s == nil || s.storage == nil {
|
|
return response.ErrServiceUnavailable(50303, "object_storage_unavailable", "object storage is not configured")
|
|
}
|
|
if err := s.storage.Validate(); err != nil {
|
|
return response.ErrServiceUnavailable(50303, "object_storage_unavailable", "object storage is not configured")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeDesktopBugReportUploadInput(in DesktopBugReportUploadInput) (DesktopBugReportUploadInput, error) {
|
|
in.ReportType = normalizeDesktopBugReportType(in.ReportType)
|
|
if in.ReportType == "" {
|
|
in.ReportType = DesktopBugReportTypeCrash
|
|
}
|
|
if !isAllowedDesktopBugReportType(in.ReportType) {
|
|
return DesktopBugReportUploadInput{}, response.ErrBadRequest(40063, "invalid_desktop_bug_report_type", "上报类型必须是 crash 或 bug")
|
|
}
|
|
in.Severity = normalizeDesktopBugSeverity(in.Severity)
|
|
if in.Severity == "" {
|
|
if in.ReportType == DesktopBugReportTypeCrash {
|
|
in.Severity = DesktopBugReportSeverityHigh
|
|
} else {
|
|
in.Severity = DesktopBugReportSeverityMedium
|
|
}
|
|
}
|
|
if !isAllowedDesktopBugSeverity(in.Severity) {
|
|
return DesktopBugReportUploadInput{}, response.ErrBadRequest(40062, "invalid_desktop_bug_severity", "严重级别必须是 low、medium、high 或 critical")
|
|
}
|
|
in.Title = limitDesktopBugText(firstNonEmptyString(
|
|
in.Title,
|
|
in.FormFields["_title"],
|
|
in.FormFields["title"],
|
|
defaultDesktopBugTitle(in),
|
|
), 200)
|
|
in.Description = limitDesktopBugText(firstNonEmptyString(in.Description, in.FormFields["_description"], in.FormFields["description"]), 4000)
|
|
in.CrashGUID = limitDesktopBugText(firstNonEmptyString(in.CrashGUID, in.FormFields["guid"]), 200)
|
|
in.CrashReportID = limitDesktopBugText(firstNonEmptyString(in.CrashReportID, in.FormFields["crash_report_id"], in.FormFields["id"]), 200)
|
|
in.ProcessType = limitDesktopBugText(firstNonEmptyString(in.ProcessType, in.FormFields["process_type"], in.FormFields["ptype"]), 80)
|
|
in.ElectronVersion = limitDesktopBugText(firstNonEmptyString(in.ElectronVersion, in.FormFields["ver"]), 80)
|
|
in.AppVersion = limitDesktopBugText(firstNonEmptyString(in.AppVersion, in.FormFields["app_version"], in.FormFields["_version"]), 80)
|
|
in.ProductName = limitDesktopBugText(firstNonEmptyString(in.ProductName, in.FormFields["_productName"], in.FormFields["prod"]), 120)
|
|
in.Platform = limitDesktopBugText(firstNonEmptyString(in.Platform, in.FormFields["platform"]), 80)
|
|
in.OSArch = limitDesktopBugText(firstNonEmptyString(in.OSArch, in.FormFields["osarch"], in.FormFields["cpu_arch"]), 80)
|
|
in.DeviceName = limitDesktopBugText(firstNonEmptyString(in.DeviceName, in.FormFields["device_name"]), 200)
|
|
in.DesktopClientID = normalizeUUIDString(firstNonEmptyString(in.DesktopClientID, in.FormFields["client_id"]))
|
|
if len(in.DumpContent) > maxDesktopBugDumpBytes {
|
|
return DesktopBugReportUploadInput{}, response.ErrBadRequest(40064, "desktop_bug_dump_too_large", "dump 文件不能超过 64MB")
|
|
}
|
|
in.DumpFileName = sanitizeDesktopBugFileName(in.DumpFileName)
|
|
if len(in.DumpContent) > 0 && in.DumpFileName == "" {
|
|
in.DumpFileName = "upload_file_minidump.dmp"
|
|
}
|
|
in.AppLogTail = limitDesktopBugText(in.AppLogTail, 20000)
|
|
if len(in.RuntimeSnapshot) == 0 || !json.Valid(in.RuntimeSnapshot) {
|
|
in.RuntimeSnapshot = []byte("{}")
|
|
}
|
|
in.FormFields = sanitizeDesktopBugFormFields(in.FormFields)
|
|
if in.OccurredAt.IsZero() {
|
|
in.OccurredAt = time.Now().UTC()
|
|
}
|
|
return in, nil
|
|
}
|
|
|
|
func defaultDesktopBugTitle(in DesktopBugReportUploadInput) string {
|
|
if in.ReportType == DesktopBugReportTypeBug {
|
|
return "用户反馈"
|
|
}
|
|
process := firstNonEmptyString(in.ProcessType, in.FormFields["process_type"], in.FormFields["ptype"], "unknown")
|
|
platform := firstNonEmptyString(in.Platform, in.FormFields["platform"], "unknown")
|
|
return fmt.Sprintf("客户端崩溃:%s / %s", process, platform)
|
|
}
|
|
|
|
func sanitizeDesktopBugFormFields(fields map[string]string) map[string]string {
|
|
out := make(map[string]string)
|
|
for key, value := range fields {
|
|
normalizedKey := strings.TrimSpace(key)
|
|
if normalizedKey == "" || len(normalizedKey) > 120 {
|
|
continue
|
|
}
|
|
out[normalizedKey] = limitDesktopBugText(value, 2000)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func dropPublicDesktopBugIdentityFields(fields map[string]string) map[string]string {
|
|
if len(fields) == 0 {
|
|
return fields
|
|
}
|
|
out := make(map[string]string, len(fields))
|
|
for key, value := range fields {
|
|
switch strings.ToLower(strings.TrimSpace(key)) {
|
|
case "tenant_id", "_tenant_id", "workspace_id", "_workspace_id", "user_id", "_user_id",
|
|
"client_id", "_client_id", "desktop_client_id", "_desktop_client_id":
|
|
continue
|
|
default:
|
|
out[key] = value
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func buildDesktopBugDumpObjectKey(in DesktopBugReportUploadInput, sha256Hex string) string {
|
|
datePrefix := time.Now().UTC().Format("2006/01/02")
|
|
reportKind := in.ReportType
|
|
if reportKind == "" {
|
|
reportKind = DesktopBugReportTypeCrash
|
|
}
|
|
clientID := normalizeUUIDString(in.DesktopClientID)
|
|
if clientID == "" {
|
|
clientID = "anonymous"
|
|
}
|
|
ext := strings.ToLower(filepath.Ext(in.DumpFileName))
|
|
if ext == "" || len(ext) > 10 {
|
|
ext = ".dmp"
|
|
}
|
|
return fmt.Sprintf("desktop-bug-reports/%s/%s/%s/%s%s", reportKind, datePrefix, clientID, sha256Hex, ext)
|
|
}
|
|
|
|
func detectDesktopBugDumpContentType(fileName string, content []byte) string {
|
|
lower := strings.ToLower(fileName)
|
|
switch {
|
|
case strings.HasSuffix(lower, ".dmp"):
|
|
return "application/vnd.microsoft.portable-executable"
|
|
case strings.HasSuffix(lower, ".gz"):
|
|
return "application/gzip"
|
|
case strings.HasSuffix(lower, ".zip"):
|
|
return "application/zip"
|
|
default:
|
|
if len(content) > 0 {
|
|
return http.DetectContentType(content)
|
|
}
|
|
return "application/octet-stream"
|
|
}
|
|
}
|
|
|
|
func desktopBugReportView(item *domain.DesktopBugReport) DesktopBugReportView {
|
|
return DesktopBugReportView{
|
|
ID: item.ID,
|
|
ReportType: item.ReportType,
|
|
Status: item.Status,
|
|
Severity: item.Severity,
|
|
TenantID: item.TenantID,
|
|
TenantName: item.TenantName,
|
|
WorkspaceID: item.WorkspaceID,
|
|
WorkspaceName: item.WorkspaceName,
|
|
UserID: item.UserID,
|
|
UserName: item.UserName,
|
|
UserPhone: item.UserPhone,
|
|
UserEmail: item.UserEmail,
|
|
DesktopClientID: item.DesktopClientID,
|
|
CrashGUID: item.CrashGUID,
|
|
CrashReportID: item.CrashReportID,
|
|
ProcessType: item.ProcessType,
|
|
ElectronVersion: item.ElectronVersion,
|
|
AppVersion: item.AppVersion,
|
|
ProductName: item.ProductName,
|
|
Platform: item.Platform,
|
|
OSArch: item.OSArch,
|
|
DeviceName: item.DeviceName,
|
|
Title: item.Title,
|
|
Description: item.Description,
|
|
DumpObjectKey: item.DumpObjectKey,
|
|
DumpFileName: item.DumpFileName,
|
|
DumpSizeBytes: item.DumpSizeBytes,
|
|
DumpSHA256: item.DumpSHA256,
|
|
FormFields: item.FormFields,
|
|
RuntimeSnapshot: item.RuntimeSnapshot,
|
|
AppLogTail: item.AppLogTail,
|
|
ResolutionNote: item.ResolutionNote,
|
|
UploaderIP: item.UploaderIP,
|
|
UserAgent: item.UserAgent,
|
|
OccurredAt: item.OccurredAt,
|
|
ResolvedAt: item.ResolvedAt,
|
|
CreatedAt: item.CreatedAt,
|
|
UpdatedAt: item.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func (s *DesktopBugReportService) audit(ctx context.Context, actor *Actor, action string, item *domain.DesktopBugReport) {
|
|
if actor == nil || s.audits == nil || item == nil {
|
|
return
|
|
}
|
|
event := actor.audit(action, "desktop_bug_report", 0, map[string]any{
|
|
"id": item.ID,
|
|
"report_type": item.ReportType,
|
|
"status": item.Status,
|
|
"severity": item.Severity,
|
|
"dump_object_key": item.DumpObjectKey,
|
|
})
|
|
event.TargetID = item.ID
|
|
_ = s.audits.Append(ctx, event)
|
|
}
|
|
|
|
func normalizeDesktopBugReportType(value string) string {
|
|
return strings.ToLower(strings.TrimSpace(value))
|
|
}
|
|
|
|
func normalizeDesktopBugStatus(value string) string {
|
|
return strings.ToLower(strings.TrimSpace(value))
|
|
}
|
|
|
|
func normalizeDesktopBugSeverity(value string) string {
|
|
return strings.ToLower(strings.TrimSpace(value))
|
|
}
|
|
|
|
func normalizeDesktopBugToken(value string) string {
|
|
return strings.ToLower(strings.TrimSpace(value))
|
|
}
|
|
|
|
func isAllowedDesktopBugReportType(value string) bool {
|
|
return value == DesktopBugReportTypeCrash || value == DesktopBugReportTypeBug
|
|
}
|
|
|
|
func isAllowedDesktopBugStatus(value string) bool {
|
|
switch value {
|
|
case DesktopBugReportStatusNew, DesktopBugReportStatusTriaged, DesktopBugReportStatusInProgress, DesktopBugReportStatusResolved, DesktopBugReportStatusIgnored:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isAllowedDesktopBugSeverity(value string) bool {
|
|
switch value {
|
|
case DesktopBugReportSeverityLow, DesktopBugReportSeverityMedium, DesktopBugReportSeverityHigh, DesktopBugReportSeverityCritical:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func optionalText(value string) *string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
return &value
|
|
}
|
|
|
|
func optionalUUIDString(value string) *string {
|
|
value = normalizeUUIDString(value)
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
return &value
|
|
}
|
|
|
|
func normalizeUUIDString(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if _, err := uuid.Parse(value); err != nil {
|
|
return ""
|
|
}
|
|
return value
|
|
}
|
|
|
|
func limitDesktopBugText(value string, limit int) string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || limit <= 0 {
|
|
return ""
|
|
}
|
|
runes := []rune(value)
|
|
if len(runes) <= limit {
|
|
return value
|
|
}
|
|
return string(runes[:limit])
|
|
}
|
|
|
|
func trimOptionalDesktopBugString(value *string, limit int) *string {
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
trimmed := limitDesktopBugText(*value, limit)
|
|
if trimmed == "" {
|
|
return nil
|
|
}
|
|
return &trimmed
|
|
}
|
|
|
|
func sanitizeDesktopBugFileName(value string) string {
|
|
value = strings.TrimSpace(filepath.Base(value))
|
|
if value == "" || value == "." || value == "/" || value == "\\" {
|
|
return ""
|
|
}
|
|
value = strings.Map(func(r rune) rune {
|
|
if r == '\\' || r == '/' || r == '\r' || r == '\n' {
|
|
return -1
|
|
}
|
|
return r
|
|
}, value)
|
|
return limitDesktopBugText(value, 200)
|
|
}
|
|
|
|
var desktopBugISODatePattern = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T`)
|
|
|
|
func ParseDesktopBugOccurredAt(value string) time.Time {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || !desktopBugISODatePattern.MatchString(value) {
|
|
return time.Time{}
|
|
}
|
|
parsed, err := time.Parse(time.RFC3339Nano, value)
|
|
if err != nil {
|
|
return time.Time{}
|
|
}
|
|
return parsed.UTC()
|
|
}
|
|
|
|
func MaxDesktopBugMultipartBytes() int64 {
|
|
return maxDesktopBugMultipartBytes
|
|
}
|
|
|
|
func MaxDesktopBugDumpBytes() int64 {
|
|
return maxDesktopBugDumpBytes
|
|
}
|