This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/ai-compliance-sdk/internal/api/handlers/audit_handlers.go
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

446 lines
10 KiB
Go

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
}