Split 7 files exceeding the 500 LOC hard cap into 16 files, all under 500 LOC. No exported symbols renamed; zero behavior changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
285 lines
7.9 KiB
Go
285 lines
7.9 KiB
Go
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
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
messages, _ := h.store.ListMessages(c.Request.Context(), id)
|
|
measures, _ := h.store.ListMeasures(c.Request.Context(), id)
|
|
|
|
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"})
|
|
}
|