feat: Add Academy, Whistleblower, Incidents SDK modules, pitch-deck, blog and CI/CD config
Some checks failed
ci/woodpecker/push/integration Pipeline failed
ci/woodpecker/push/main Pipeline failed
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Some checks failed
ci/woodpecker/push/integration Pipeline failed
ci/woodpecker/push/main Pipeline failed
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
- Academy, Whistleblower, Incidents frontend pages with API proxies and types - Vendor compliance API proxy route - Go backend handlers and models for all new SDK modules - Investor pitch-deck app with interactive slides - Blog section with DSGVO, AI Act, NIS2, glossary articles - MkDocs documentation site - CI/CD pipelines (Woodpecker, GitHub Actions), security scanning config - Planning and implementation documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/whistleblower"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// WhistleblowerHandlers handles whistleblower HTTP requests
|
||||
type WhistleblowerHandlers struct {
|
||||
store *whistleblower.Store
|
||||
}
|
||||
|
||||
// NewWhistleblowerHandlers creates new whistleblower handlers
|
||||
func NewWhistleblowerHandlers(store *whistleblower.Store) *WhistleblowerHandlers {
|
||||
return &WhistleblowerHandlers{store: store}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Public Handlers (NO auth required — for anonymous reporters)
|
||||
// ============================================================================
|
||||
|
||||
// SubmitReport handles public report submission (no auth required)
|
||||
// POST /sdk/v1/whistleblower/public/submit
|
||||
func (h *WhistleblowerHandlers) SubmitReport(c *gin.Context) {
|
||||
var req whistleblower.PublicReportSubmission
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get tenant ID from header or query param (public endpoint still needs tenant context)
|
||||
tenantIDStr := c.GetHeader("X-Tenant-ID")
|
||||
if tenantIDStr == "" {
|
||||
tenantIDStr = c.Query("tenant_id")
|
||||
}
|
||||
if tenantIDStr == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
tenantID, err := uuid.Parse(tenantIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tenant_id"})
|
||||
return
|
||||
}
|
||||
|
||||
report := &whistleblower.Report{
|
||||
TenantID: tenantID,
|
||||
Category: req.Category,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
IsAnonymous: req.IsAnonymous,
|
||||
}
|
||||
|
||||
// Only set reporter info if not anonymous
|
||||
if !req.IsAnonymous {
|
||||
report.ReporterName = req.ReporterName
|
||||
report.ReporterEmail = req.ReporterEmail
|
||||
report.ReporterPhone = req.ReporterPhone
|
||||
}
|
||||
|
||||
if err := h.store.CreateReport(c.Request.Context(), report); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Return reference number and access key (access key only shown ONCE!)
|
||||
c.JSON(http.StatusCreated, whistleblower.PublicReportResponse{
|
||||
ReferenceNumber: report.ReferenceNumber,
|
||||
AccessKey: report.AccessKey,
|
||||
})
|
||||
}
|
||||
|
||||
// GetReportByAccessKey retrieves a report by access key (for anonymous reporters)
|
||||
// GET /sdk/v1/whistleblower/public/report?access_key=xxx
|
||||
func (h *WhistleblowerHandlers) GetReportByAccessKey(c *gin.Context) {
|
||||
accessKey := c.Query("access_key")
|
||||
if accessKey == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "access_key is required"})
|
||||
return
|
||||
}
|
||||
|
||||
report, err := h.store.GetReportByAccessKey(c.Request.Context(), accessKey)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if report == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "report not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Return limited fields for public access (no access_key, no internal details)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"reference_number": report.ReferenceNumber,
|
||||
"category": report.Category,
|
||||
"status": report.Status,
|
||||
"title": report.Title,
|
||||
"received_at": report.ReceivedAt,
|
||||
"deadline_acknowledgment": report.DeadlineAcknowledgment,
|
||||
"deadline_feedback": report.DeadlineFeedback,
|
||||
"acknowledged_at": report.AcknowledgedAt,
|
||||
"closed_at": report.ClosedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// SendPublicMessage allows a reporter to send a message via access key
|
||||
// POST /sdk/v1/whistleblower/public/message?access_key=xxx
|
||||
func (h *WhistleblowerHandlers) SendPublicMessage(c *gin.Context) {
|
||||
accessKey := c.Query("access_key")
|
||||
if accessKey == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "access_key is required"})
|
||||
return
|
||||
}
|
||||
|
||||
report, err := h.store.GetReportByAccessKey(c.Request.Context(), accessKey)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if report == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "report not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req whistleblower.SendMessageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
msg := &whistleblower.AnonymousMessage{
|
||||
ReportID: report.ID,
|
||||
Direction: whistleblower.MessageDirectionReporterToAdmin,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
if err := h.store.AddMessage(c.Request.Context(), msg); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"message": msg})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Admin Handlers (auth required)
|
||||
// ============================================================================
|
||||
|
||||
// ListReports lists all reports for the tenant
|
||||
// GET /sdk/v1/whistleblower/reports
|
||||
func (h *WhistleblowerHandlers) ListReports(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
|
||||
filters := &whistleblower.ReportFilters{
|
||||
Limit: 50,
|
||||
}
|
||||
|
||||
if status := c.Query("status"); status != "" {
|
||||
filters.Status = whistleblower.ReportStatus(status)
|
||||
}
|
||||
if category := c.Query("category"); category != "" {
|
||||
filters.Category = whistleblower.ReportCategory(category)
|
||||
}
|
||||
|
||||
reports, total, err := h.store.ListReports(c.Request.Context(), tenantID, filters)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, whistleblower.ReportListResponse{
|
||||
Reports: reports,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// GetReport retrieves a report by ID (admin)
|
||||
// GET /sdk/v1/whistleblower/reports/:id
|
||||
func (h *WhistleblowerHandlers) GetReport(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"})
|
||||
return
|
||||
}
|
||||
|
||||
report, err := h.store.GetReport(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if report == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "report not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get messages and measures for full view
|
||||
messages, _ := h.store.ListMessages(c.Request.Context(), id)
|
||||
measures, _ := h.store.ListMeasures(c.Request.Context(), id)
|
||||
|
||||
// Do not expose access key to admin either
|
||||
report.AccessKey = ""
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"report": report,
|
||||
"messages": messages,
|
||||
"measures": measures,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateReport updates a report
|
||||
// PUT /sdk/v1/whistleblower/reports/:id
|
||||
func (h *WhistleblowerHandlers) UpdateReport(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"})
|
||||
return
|
||||
}
|
||||
|
||||
report, err := h.store.GetReport(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if report == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "report not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req whistleblower.ReportUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID := rbac.GetUserID(c)
|
||||
|
||||
if req.Category != "" {
|
||||
report.Category = req.Category
|
||||
}
|
||||
if req.Status != "" {
|
||||
report.Status = req.Status
|
||||
}
|
||||
if req.Title != "" {
|
||||
report.Title = req.Title
|
||||
}
|
||||
if req.Description != "" {
|
||||
report.Description = req.Description
|
||||
}
|
||||
if req.AssignedTo != nil {
|
||||
report.AssignedTo = req.AssignedTo
|
||||
}
|
||||
|
||||
report.AuditTrail = append(report.AuditTrail, whistleblower.AuditEntry{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Action: "report_updated",
|
||||
UserID: userID.String(),
|
||||
Details: "Report updated by admin",
|
||||
})
|
||||
|
||||
if err := h.store.UpdateReport(c.Request.Context(), report); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
report.AccessKey = ""
|
||||
c.JSON(http.StatusOK, gin.H{"report": report})
|
||||
}
|
||||
|
||||
// DeleteReport deletes a report
|
||||
// DELETE /sdk/v1/whistleblower/reports/:id
|
||||
func (h *WhistleblowerHandlers) DeleteReport(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.DeleteReport(c.Request.Context(), id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "report deleted"})
|
||||
}
|
||||
|
||||
// AcknowledgeReport acknowledges a report (within 7-day HinSchG deadline)
|
||||
// POST /sdk/v1/whistleblower/reports/:id/acknowledge
|
||||
func (h *WhistleblowerHandlers) AcknowledgeReport(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"})
|
||||
return
|
||||
}
|
||||
|
||||
report, err := h.store.GetReport(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if report == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "report not found"})
|
||||
return
|
||||
}
|
||||
|
||||
if report.AcknowledgedAt != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "report already acknowledged"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := rbac.GetUserID(c)
|
||||
|
||||
if err := h.store.AcknowledgeReport(c.Request.Context(), id, userID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Optionally send acknowledgment message to reporter
|
||||
var req whistleblower.AcknowledgeRequest
|
||||
if err := c.ShouldBindJSON(&req); err == nil && req.Message != "" {
|
||||
msg := &whistleblower.AnonymousMessage{
|
||||
ReportID: id,
|
||||
Direction: whistleblower.MessageDirectionAdminToReporter,
|
||||
Content: req.Message,
|
||||
}
|
||||
h.store.AddMessage(c.Request.Context(), msg)
|
||||
}
|
||||
|
||||
// Check if deadline was met
|
||||
isOverdue := time.Now().UTC().After(report.DeadlineAcknowledgment)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "report acknowledged",
|
||||
"is_overdue": isOverdue,
|
||||
})
|
||||
}
|
||||
|
||||
// StartInvestigation changes the report status to investigation
|
||||
// POST /sdk/v1/whistleblower/reports/:id/investigate
|
||||
func (h *WhistleblowerHandlers) StartInvestigation(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"})
|
||||
return
|
||||
}
|
||||
|
||||
report, err := h.store.GetReport(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if report == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "report not found"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := rbac.GetUserID(c)
|
||||
|
||||
report.Status = whistleblower.ReportStatusInvestigation
|
||||
report.AuditTrail = append(report.AuditTrail, whistleblower.AuditEntry{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Action: "investigation_started",
|
||||
UserID: userID.String(),
|
||||
Details: "Investigation started",
|
||||
})
|
||||
|
||||
if err := h.store.UpdateReport(c.Request.Context(), report); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "investigation started",
|
||||
"report": report,
|
||||
})
|
||||
}
|
||||
|
||||
// AddMeasure adds a corrective measure to a report
|
||||
// POST /sdk/v1/whistleblower/reports/:id/measures
|
||||
func (h *WhistleblowerHandlers) AddMeasure(c *gin.Context) {
|
||||
reportID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify report exists
|
||||
report, err := h.store.GetReport(c.Request.Context(), reportID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if report == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "report not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req whistleblower.AddMeasureRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID := rbac.GetUserID(c)
|
||||
|
||||
measure := &whistleblower.Measure{
|
||||
ReportID: reportID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Responsible: req.Responsible,
|
||||
DueDate: req.DueDate,
|
||||
}
|
||||
|
||||
if err := h.store.AddMeasure(c.Request.Context(), measure); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update report status to measures_taken if not already
|
||||
if report.Status != whistleblower.ReportStatusMeasuresTaken &&
|
||||
report.Status != whistleblower.ReportStatusClosed {
|
||||
report.Status = whistleblower.ReportStatusMeasuresTaken
|
||||
report.AuditTrail = append(report.AuditTrail, whistleblower.AuditEntry{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Action: "measure_added",
|
||||
UserID: userID.String(),
|
||||
Details: "Corrective measure added: " + req.Title,
|
||||
})
|
||||
h.store.UpdateReport(c.Request.Context(), report)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"measure": measure})
|
||||
}
|
||||
|
||||
// CloseReport closes a report with a resolution
|
||||
// POST /sdk/v1/whistleblower/reports/:id/close
|
||||
func (h *WhistleblowerHandlers) CloseReport(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req whistleblower.CloseReportRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID := rbac.GetUserID(c)
|
||||
|
||||
if err := h.store.CloseReport(c.Request.Context(), id, userID, req.Resolution); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "report closed"})
|
||||
}
|
||||
|
||||
// SendAdminMessage sends a message from admin to reporter
|
||||
// POST /sdk/v1/whistleblower/reports/:id/messages
|
||||
func (h *WhistleblowerHandlers) SendAdminMessage(c *gin.Context) {
|
||||
reportID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify report exists
|
||||
report, err := h.store.GetReport(c.Request.Context(), reportID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if report == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "report not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req whistleblower.SendMessageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
msg := &whistleblower.AnonymousMessage{
|
||||
ReportID: reportID,
|
||||
Direction: whistleblower.MessageDirectionAdminToReporter,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
if err := h.store.AddMessage(c.Request.Context(), msg); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"message": msg})
|
||||
}
|
||||
|
||||
// ListMessages lists messages for a report
|
||||
// GET /sdk/v1/whistleblower/reports/:id/messages
|
||||
func (h *WhistleblowerHandlers) ListMessages(c *gin.Context) {
|
||||
reportID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"})
|
||||
return
|
||||
}
|
||||
|
||||
messages, err := h.store.ListMessages(c.Request.Context(), reportID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"messages": messages,
|
||||
"total": len(messages),
|
||||
})
|
||||
}
|
||||
|
||||
// GetStatistics returns whistleblower statistics for the tenant
|
||||
// GET /sdk/v1/whistleblower/statistics
|
||||
func (h *WhistleblowerHandlers) GetStatistics(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
|
||||
stats, err := h.store.GetStatistics(c.Request.Context(), tenantID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
Reference in New Issue
Block a user