package handlers import ( "bytes" "fmt" "net/http" "time" "github.com/breakpilot/ai-compliance-sdk/internal/audit" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // AuditHandlers handles audit-related API endpoints type AuditHandlers struct { store *audit.Store exporter *audit.Exporter } // NewAuditHandlers creates new audit handlers func NewAuditHandlers(store *audit.Store, exporter *audit.Exporter) *AuditHandlers { return &AuditHandlers{ store: store, exporter: exporter, } } // QueryLLMAudit queries LLM audit entries func (h *AuditHandlers) QueryLLMAudit(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } filter := &audit.LLMAuditFilter{ TenantID: tenantID, Limit: 50, Offset: 0, } // Parse query parameters if nsID := c.Query("namespace_id"); nsID != "" { if id, err := uuid.Parse(nsID); err == nil { filter.NamespaceID = &id } } if userID := c.Query("user_id"); userID != "" { if id, err := uuid.Parse(userID); err == nil { filter.UserID = &id } } if op := c.Query("operation"); op != "" { filter.Operation = op } if model := c.Query("model"); model != "" { filter.Model = model } if pii := c.Query("pii_detected"); pii != "" { val := pii == "true" filter.PIIDetected = &val } if violations := c.Query("has_violations"); violations == "true" { val := true filter.HasViolations = &val } if startDate := c.Query("start_date"); startDate != "" { if t, err := time.Parse(time.RFC3339, startDate); err == nil { filter.StartDate = &t } } if endDate := c.Query("end_date"); endDate != "" { if t, err := time.Parse(time.RFC3339, endDate); err == nil { filter.EndDate = &t } } if limit := c.Query("limit"); limit != "" { var l int if _, err := parseIntQuery(limit, &l); err == nil && l > 0 && l <= 1000 { filter.Limit = l } } if offset := c.Query("offset"); offset != "" { var o int if _, err := parseIntQuery(offset, &o); err == nil && o >= 0 { filter.Offset = o } } entries, total, err := h.store.QueryLLMAuditEntries(c.Request.Context(), filter) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "entries": entries, "total": total, "limit": filter.Limit, "offset": filter.Offset, }) } // QueryGeneralAudit queries general audit entries func (h *AuditHandlers) QueryGeneralAudit(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } filter := &audit.GeneralAuditFilter{ TenantID: tenantID, Limit: 50, Offset: 0, } // Parse query parameters if nsID := c.Query("namespace_id"); nsID != "" { if id, err := uuid.Parse(nsID); err == nil { filter.NamespaceID = &id } } if userID := c.Query("user_id"); userID != "" { if id, err := uuid.Parse(userID); err == nil { filter.UserID = &id } } if action := c.Query("action"); action != "" { filter.Action = action } if resourceType := c.Query("resource_type"); resourceType != "" { filter.ResourceType = resourceType } if resourceID := c.Query("resource_id"); resourceID != "" { if id, err := uuid.Parse(resourceID); err == nil { filter.ResourceID = &id } } if startDate := c.Query("start_date"); startDate != "" { if t, err := time.Parse(time.RFC3339, startDate); err == nil { filter.StartDate = &t } } if endDate := c.Query("end_date"); endDate != "" { if t, err := time.Parse(time.RFC3339, endDate); err == nil { filter.EndDate = &t } } if limit := c.Query("limit"); limit != "" { var l int if _, err := parseIntQuery(limit, &l); err == nil && l > 0 && l <= 1000 { filter.Limit = l } } if offset := c.Query("offset"); offset != "" { var o int if _, err := parseIntQuery(offset, &o); err == nil && o >= 0 { filter.Offset = o } } entries, total, err := h.store.QueryGeneralAuditEntries(c.Request.Context(), filter) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "entries": entries, "total": total, "limit": filter.Limit, "offset": filter.Offset, }) } // GetUsageStats returns LLM usage statistics func (h *AuditHandlers) GetUsageStats(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } // Default to last 30 days endDate := time.Now().UTC() startDate := endDate.AddDate(0, 0, -30) if sd := c.Query("start_date"); sd != "" { if t, err := time.Parse(time.RFC3339, sd); err == nil { startDate = t } } if ed := c.Query("end_date"); ed != "" { if t, err := time.Parse(time.RFC3339, ed); err == nil { endDate = t } } stats, err := h.store.GetLLMUsageStats(c.Request.Context(), tenantID, startDate, endDate) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "period_start": startDate.Format(time.RFC3339), "period_end": endDate.Format(time.RFC3339), "stats": stats, }) } // ExportLLMAudit exports LLM audit entries func (h *AuditHandlers) ExportLLMAudit(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } // Default to last 30 days endDate := time.Now().UTC() startDate := endDate.AddDate(0, 0, -30) if sd := c.Query("start_date"); sd != "" { if t, err := time.Parse(time.RFC3339, sd); err == nil { startDate = t } } if ed := c.Query("end_date"); ed != "" { if t, err := time.Parse(time.RFC3339, ed); err == nil { endDate = t } } format := audit.FormatJSON if c.Query("format") == "csv" { format = audit.FormatCSV } includePII := c.Query("include_pii") == "true" opts := &audit.ExportOptions{ TenantID: tenantID, StartDate: startDate, EndDate: endDate, Format: format, IncludePII: includePII, } if nsID := c.Query("namespace_id"); nsID != "" { if id, err := uuid.Parse(nsID); err == nil { opts.NamespaceID = &id } } if userID := c.Query("user_id"); userID != "" { if id, err := uuid.Parse(userID); err == nil { opts.UserID = &id } } var buf bytes.Buffer if err := h.exporter.ExportLLMAudit(c.Request.Context(), &buf, opts); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Set appropriate content type contentType := "application/json" ext := "json" if format == audit.FormatCSV { contentType = "text/csv" ext = "csv" } filename := "llm_audit_" + time.Now().Format("20060102") + "." + ext c.Header("Content-Disposition", "attachment; filename="+filename) c.Data(http.StatusOK, contentType, buf.Bytes()) } // ExportGeneralAudit exports general audit entries func (h *AuditHandlers) ExportGeneralAudit(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } endDate := time.Now().UTC() startDate := endDate.AddDate(0, 0, -30) if sd := c.Query("start_date"); sd != "" { if t, err := time.Parse(time.RFC3339, sd); err == nil { startDate = t } } if ed := c.Query("end_date"); ed != "" { if t, err := time.Parse(time.RFC3339, ed); err == nil { endDate = t } } format := audit.FormatJSON if c.Query("format") == "csv" { format = audit.FormatCSV } includePII := c.Query("include_pii") == "true" opts := &audit.ExportOptions{ TenantID: tenantID, StartDate: startDate, EndDate: endDate, Format: format, IncludePII: includePII, } var buf bytes.Buffer if err := h.exporter.ExportGeneralAudit(c.Request.Context(), &buf, opts); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } contentType := "application/json" ext := "json" if format == audit.FormatCSV { contentType = "text/csv" ext = "csv" } filename := "general_audit_" + time.Now().Format("20060102") + "." + ext c.Header("Content-Disposition", "attachment; filename="+filename) c.Data(http.StatusOK, contentType, buf.Bytes()) } // ExportComplianceReport exports a compliance report func (h *AuditHandlers) ExportComplianceReport(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } endDate := time.Now().UTC() startDate := endDate.AddDate(0, 0, -30) if sd := c.Query("start_date"); sd != "" { if t, err := time.Parse(time.RFC3339, sd); err == nil { startDate = t } } if ed := c.Query("end_date"); ed != "" { if t, err := time.Parse(time.RFC3339, ed); err == nil { endDate = t } } format := audit.FormatJSON if c.Query("format") == "csv" { format = audit.FormatCSV } var buf bytes.Buffer if err := h.exporter.ExportComplianceReport(c.Request.Context(), &buf, tenantID, startDate, endDate, format); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } contentType := "application/json" ext := "json" if format == audit.FormatCSV { contentType = "text/csv" ext = "csv" } filename := "compliance_report_" + time.Now().Format("20060102") + "." + ext c.Header("Content-Disposition", "attachment; filename="+filename) c.Data(http.StatusOK, contentType, buf.Bytes()) } // GetComplianceReport returns a compliance report as JSON (for dashboard) func (h *AuditHandlers) GetComplianceReport(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } endDate := time.Now().UTC() startDate := endDate.AddDate(0, 0, -30) if sd := c.Query("start_date"); sd != "" { if t, err := time.Parse(time.RFC3339, sd); err == nil { startDate = t } } if ed := c.Query("end_date"); ed != "" { if t, err := time.Parse(time.RFC3339, ed); err == nil { endDate = t } } report, err := h.exporter.GenerateComplianceReport(c.Request.Context(), tenantID, startDate, endDate) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, report) } // Helper function to parse int from query func parseIntQuery(s string, out *int) (int, error) { var i int _, err := fmt.Sscanf(s, "%d", &i) if err != nil { return 0, err } *out = i return i, nil }