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>
446 lines
10 KiB
Go
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
|
|
}
|