feat: Add Academy, Whistleblower, Incidents, Vendor, DSB, SSO, Reporting, Multi-Tenant and Industry backends
Go handlers, models, stores and migrations for all SDK modules. Updates developer portal navigation and BYOEH page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
668
ai-compliance-sdk/internal/api/handlers/incidents_handlers.go
Normal file
668
ai-compliance-sdk/internal/api/handlers/incidents_handlers.go
Normal file
@@ -0,0 +1,668 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/incidents"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// IncidentHandlers handles incident/breach management HTTP requests
|
||||
type IncidentHandlers struct {
|
||||
store *incidents.Store
|
||||
}
|
||||
|
||||
// NewIncidentHandlers creates new incident handlers
|
||||
func NewIncidentHandlers(store *incidents.Store) *IncidentHandlers {
|
||||
return &IncidentHandlers{store: store}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Incident CRUD
|
||||
// ============================================================================
|
||||
|
||||
// CreateIncident creates a new incident
|
||||
// POST /sdk/v1/incidents
|
||||
func (h *IncidentHandlers) CreateIncident(c *gin.Context) {
|
||||
var req incidents.CreateIncidentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
userID := rbac.GetUserID(c)
|
||||
|
||||
detectedAt := time.Now().UTC()
|
||||
if req.DetectedAt != nil {
|
||||
detectedAt = *req.DetectedAt
|
||||
}
|
||||
|
||||
// Auto-calculate 72h deadline per DSGVO Art. 33
|
||||
deadline := incidents.Calculate72hDeadline(detectedAt)
|
||||
|
||||
incident := &incidents.Incident{
|
||||
TenantID: tenantID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Category: req.Category,
|
||||
Status: incidents.IncidentStatusDetected,
|
||||
Severity: req.Severity,
|
||||
DetectedAt: detectedAt,
|
||||
ReportedBy: userID,
|
||||
AffectedDataCategories: req.AffectedDataCategories,
|
||||
AffectedDataSubjectCount: req.AffectedDataSubjectCount,
|
||||
AffectedSystems: req.AffectedSystems,
|
||||
AuthorityNotification: &incidents.AuthorityNotification{
|
||||
Status: incidents.NotificationStatusPending,
|
||||
Deadline: deadline,
|
||||
},
|
||||
DataSubjectNotification: &incidents.DataSubjectNotification{
|
||||
Required: false,
|
||||
Status: incidents.NotificationStatusNotRequired,
|
||||
},
|
||||
Timeline: []incidents.TimelineEntry{
|
||||
{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Action: "incident_created",
|
||||
UserID: userID,
|
||||
Details: "Incident detected and reported",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := h.store.CreateIncident(c.Request.Context(), incident); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"incident": incident,
|
||||
"authority_deadline": deadline,
|
||||
"hours_until_deadline": time.Until(deadline).Hours(),
|
||||
})
|
||||
}
|
||||
|
||||
// GetIncident retrieves an incident by ID
|
||||
// GET /sdk/v1/incidents/:id
|
||||
func (h *IncidentHandlers) GetIncident(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid incident ID"})
|
||||
return
|
||||
}
|
||||
|
||||
incident, err := h.store.GetIncident(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if incident == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "incident not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get measures
|
||||
measures, _ := h.store.ListMeasures(c.Request.Context(), id)
|
||||
|
||||
// Calculate deadline info if authority notification exists
|
||||
var deadlineInfo gin.H
|
||||
if incident.AuthorityNotification != nil {
|
||||
hoursRemaining := time.Until(incident.AuthorityNotification.Deadline).Hours()
|
||||
deadlineInfo = gin.H{
|
||||
"deadline": incident.AuthorityNotification.Deadline,
|
||||
"hours_remaining": hoursRemaining,
|
||||
"overdue": hoursRemaining < 0,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"incident": incident,
|
||||
"measures": measures,
|
||||
"deadline_info": deadlineInfo,
|
||||
})
|
||||
}
|
||||
|
||||
// ListIncidents lists incidents for a tenant
|
||||
// GET /sdk/v1/incidents
|
||||
func (h *IncidentHandlers) ListIncidents(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
|
||||
filters := &incidents.IncidentFilters{
|
||||
Limit: 50,
|
||||
}
|
||||
|
||||
if status := c.Query("status"); status != "" {
|
||||
filters.Status = incidents.IncidentStatus(status)
|
||||
}
|
||||
if severity := c.Query("severity"); severity != "" {
|
||||
filters.Severity = incidents.IncidentSeverity(severity)
|
||||
}
|
||||
if category := c.Query("category"); category != "" {
|
||||
filters.Category = incidents.IncidentCategory(category)
|
||||
}
|
||||
|
||||
incidentList, total, err := h.store.ListIncidents(c.Request.Context(), tenantID, filters)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, incidents.IncidentListResponse{
|
||||
Incidents: incidentList,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateIncident updates an incident
|
||||
// PUT /sdk/v1/incidents/:id
|
||||
func (h *IncidentHandlers) UpdateIncident(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid incident ID"})
|
||||
return
|
||||
}
|
||||
|
||||
incident, err := h.store.GetIncident(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if incident == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "incident not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req incidents.UpdateIncidentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Title != "" {
|
||||
incident.Title = req.Title
|
||||
}
|
||||
if req.Description != "" {
|
||||
incident.Description = req.Description
|
||||
}
|
||||
if req.Category != "" {
|
||||
incident.Category = req.Category
|
||||
}
|
||||
if req.Status != "" {
|
||||
incident.Status = req.Status
|
||||
}
|
||||
if req.Severity != "" {
|
||||
incident.Severity = req.Severity
|
||||
}
|
||||
if req.AffectedDataCategories != nil {
|
||||
incident.AffectedDataCategories = req.AffectedDataCategories
|
||||
}
|
||||
if req.AffectedDataSubjectCount != nil {
|
||||
incident.AffectedDataSubjectCount = *req.AffectedDataSubjectCount
|
||||
}
|
||||
if req.AffectedSystems != nil {
|
||||
incident.AffectedSystems = req.AffectedSystems
|
||||
}
|
||||
|
||||
if err := h.store.UpdateIncident(c.Request.Context(), incident); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"incident": incident})
|
||||
}
|
||||
|
||||
// DeleteIncident deletes an incident
|
||||
// DELETE /sdk/v1/incidents/:id
|
||||
func (h *IncidentHandlers) DeleteIncident(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid incident ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.DeleteIncident(c.Request.Context(), id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "incident deleted"})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Risk Assessment
|
||||
// ============================================================================
|
||||
|
||||
// AssessRisk performs a risk assessment for an incident
|
||||
// POST /sdk/v1/incidents/:id/risk-assessment
|
||||
func (h *IncidentHandlers) AssessRisk(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid incident ID"})
|
||||
return
|
||||
}
|
||||
|
||||
incident, err := h.store.GetIncident(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if incident == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "incident not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req incidents.RiskAssessmentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID := rbac.GetUserID(c)
|
||||
|
||||
// Auto-calculate risk level
|
||||
riskLevel := incidents.CalculateRiskLevel(req.Likelihood, req.Impact)
|
||||
notificationRequired := incidents.IsNotificationRequired(riskLevel)
|
||||
|
||||
assessment := &incidents.RiskAssessment{
|
||||
Likelihood: req.Likelihood,
|
||||
Impact: req.Impact,
|
||||
RiskLevel: riskLevel,
|
||||
AssessedAt: time.Now().UTC(),
|
||||
AssessedBy: userID,
|
||||
Notes: req.Notes,
|
||||
}
|
||||
|
||||
if err := h.store.UpdateRiskAssessment(c.Request.Context(), id, assessment); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update status to assessment
|
||||
incident.Status = incidents.IncidentStatusAssessment
|
||||
h.store.UpdateIncident(c.Request.Context(), incident)
|
||||
|
||||
// Add timeline entry
|
||||
h.store.AddTimelineEntry(c.Request.Context(), id, incidents.TimelineEntry{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Action: "risk_assessed",
|
||||
UserID: userID,
|
||||
Details: fmt.Sprintf("Risk level: %s (likelihood=%d, impact=%d)", riskLevel, req.Likelihood, req.Impact),
|
||||
})
|
||||
|
||||
// If notification is required, update authority notification status
|
||||
if notificationRequired && incident.AuthorityNotification != nil {
|
||||
incident.AuthorityNotification.Status = incidents.NotificationStatusPending
|
||||
h.store.UpdateAuthorityNotification(c.Request.Context(), id, incident.AuthorityNotification)
|
||||
|
||||
// Update status to notification_required
|
||||
incident.Status = incidents.IncidentStatusNotificationRequired
|
||||
h.store.UpdateIncident(c.Request.Context(), incident)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"risk_assessment": assessment,
|
||||
"notification_required": notificationRequired,
|
||||
"incident_status": incident.Status,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Authority Notification (Art. 33)
|
||||
// ============================================================================
|
||||
|
||||
// SubmitAuthorityNotification submits the supervisory authority notification
|
||||
// POST /sdk/v1/incidents/:id/authority-notification
|
||||
func (h *IncidentHandlers) SubmitAuthorityNotification(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid incident ID"})
|
||||
return
|
||||
}
|
||||
|
||||
incident, err := h.store.GetIncident(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if incident == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "incident not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req incidents.SubmitAuthorityNotificationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID := rbac.GetUserID(c)
|
||||
now := time.Now().UTC()
|
||||
|
||||
// Preserve existing deadline
|
||||
deadline := incidents.Calculate72hDeadline(incident.DetectedAt)
|
||||
if incident.AuthorityNotification != nil {
|
||||
deadline = incident.AuthorityNotification.Deadline
|
||||
}
|
||||
|
||||
notification := &incidents.AuthorityNotification{
|
||||
Status: incidents.NotificationStatusSent,
|
||||
Deadline: deadline,
|
||||
SubmittedAt: &now,
|
||||
AuthorityName: req.AuthorityName,
|
||||
ReferenceNumber: req.ReferenceNumber,
|
||||
ContactPerson: req.ContactPerson,
|
||||
Notes: req.Notes,
|
||||
}
|
||||
|
||||
if err := h.store.UpdateAuthorityNotification(c.Request.Context(), id, notification); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Update incident status
|
||||
incident.Status = incidents.IncidentStatusNotificationSent
|
||||
h.store.UpdateIncident(c.Request.Context(), incident)
|
||||
|
||||
// Add timeline entry
|
||||
h.store.AddTimelineEntry(c.Request.Context(), id, incidents.TimelineEntry{
|
||||
Timestamp: now,
|
||||
Action: "authority_notified",
|
||||
UserID: userID,
|
||||
Details: "Authority notification submitted to " + req.AuthorityName,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"authority_notification": notification,
|
||||
"submitted_within_72h": now.Before(deadline),
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Data Subject Notification (Art. 34)
|
||||
// ============================================================================
|
||||
|
||||
// NotifyDataSubjects submits the data subject notification
|
||||
// POST /sdk/v1/incidents/:id/data-subject-notification
|
||||
func (h *IncidentHandlers) NotifyDataSubjects(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid incident ID"})
|
||||
return
|
||||
}
|
||||
|
||||
incident, err := h.store.GetIncident(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if incident == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "incident not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req incidents.NotifyDataSubjectsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID := rbac.GetUserID(c)
|
||||
now := time.Now().UTC()
|
||||
|
||||
affectedCount := req.AffectedCount
|
||||
if affectedCount == 0 {
|
||||
affectedCount = incident.AffectedDataSubjectCount
|
||||
}
|
||||
|
||||
notification := &incidents.DataSubjectNotification{
|
||||
Required: true,
|
||||
Status: incidents.NotificationStatusSent,
|
||||
SentAt: &now,
|
||||
AffectedCount: affectedCount,
|
||||
NotificationText: req.NotificationText,
|
||||
Channel: req.Channel,
|
||||
}
|
||||
|
||||
if err := h.store.UpdateDataSubjectNotification(c.Request.Context(), id, notification); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Add timeline entry
|
||||
h.store.AddTimelineEntry(c.Request.Context(), id, incidents.TimelineEntry{
|
||||
Timestamp: now,
|
||||
Action: "data_subjects_notified",
|
||||
UserID: userID,
|
||||
Details: "Data subjects notified via " + req.Channel + " (" + fmt.Sprintf("%d", affectedCount) + " affected)",
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data_subject_notification": notification,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Measures
|
||||
// ============================================================================
|
||||
|
||||
// AddMeasure adds a corrective measure to an incident
|
||||
// POST /sdk/v1/incidents/:id/measures
|
||||
func (h *IncidentHandlers) AddMeasure(c *gin.Context) {
|
||||
incidentID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid incident ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify incident exists
|
||||
incident, err := h.store.GetIncident(c.Request.Context(), incidentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if incident == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "incident not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req incidents.AddMeasureRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID := rbac.GetUserID(c)
|
||||
|
||||
measure := &incidents.IncidentMeasure{
|
||||
IncidentID: incidentID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
MeasureType: req.MeasureType,
|
||||
Status: incidents.MeasureStatusPlanned,
|
||||
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
|
||||
}
|
||||
|
||||
// Add timeline entry
|
||||
h.store.AddTimelineEntry(c.Request.Context(), incidentID, incidents.TimelineEntry{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Action: "measure_added",
|
||||
UserID: userID,
|
||||
Details: "Measure added: " + req.Title + " (" + string(req.MeasureType) + ")",
|
||||
})
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"measure": measure})
|
||||
}
|
||||
|
||||
// UpdateMeasure updates a measure
|
||||
// PUT /sdk/v1/incidents/measures/:measureId
|
||||
func (h *IncidentHandlers) UpdateMeasure(c *gin.Context) {
|
||||
measureID, err := uuid.Parse(c.Param("measureId"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid measure ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
MeasureType incidents.MeasureType `json:"measure_type,omitempty"`
|
||||
Status incidents.MeasureStatus `json:"status,omitempty"`
|
||||
Responsible string `json:"responsible,omitempty"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
measure := &incidents.IncidentMeasure{
|
||||
ID: measureID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
MeasureType: req.MeasureType,
|
||||
Status: req.Status,
|
||||
Responsible: req.Responsible,
|
||||
DueDate: req.DueDate,
|
||||
}
|
||||
|
||||
if err := h.store.UpdateMeasure(c.Request.Context(), measure); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"measure": measure})
|
||||
}
|
||||
|
||||
// CompleteMeasure marks a measure as completed
|
||||
// POST /sdk/v1/incidents/measures/:measureId/complete
|
||||
func (h *IncidentHandlers) CompleteMeasure(c *gin.Context) {
|
||||
measureID, err := uuid.Parse(c.Param("measureId"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid measure ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.store.CompleteMeasure(c.Request.Context(), measureID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "measure completed"})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Timeline
|
||||
// ============================================================================
|
||||
|
||||
// AddTimelineEntry adds a timeline entry to an incident
|
||||
// POST /sdk/v1/incidents/:id/timeline
|
||||
func (h *IncidentHandlers) AddTimelineEntry(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid incident ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req incidents.AddTimelineEntryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
userID := rbac.GetUserID(c)
|
||||
|
||||
entry := incidents.TimelineEntry{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Action: req.Action,
|
||||
UserID: userID,
|
||||
Details: req.Details,
|
||||
}
|
||||
|
||||
if err := h.store.AddTimelineEntry(c.Request.Context(), id, entry); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"timeline_entry": entry})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Close Incident
|
||||
// ============================================================================
|
||||
|
||||
// CloseIncident closes an incident with root cause analysis
|
||||
// POST /sdk/v1/incidents/:id/close
|
||||
func (h *IncidentHandlers) CloseIncident(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid incident ID"})
|
||||
return
|
||||
}
|
||||
|
||||
incident, err := h.store.GetIncident(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if incident == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "incident not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var req incidents.CloseIncidentRequest
|
||||
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.CloseIncident(c.Request.Context(), id, req.RootCause, req.LessonsLearned); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Add timeline entry
|
||||
h.store.AddTimelineEntry(c.Request.Context(), id, incidents.TimelineEntry{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Action: "incident_closed",
|
||||
UserID: userID,
|
||||
Details: "Incident closed. Root cause: " + req.RootCause,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "incident closed",
|
||||
"root_cause": req.RootCause,
|
||||
"lessons_learned": req.LessonsLearned,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Statistics
|
||||
// ============================================================================
|
||||
|
||||
// GetStatistics returns aggregated incident statistics
|
||||
// GET /sdk/v1/incidents/statistics
|
||||
func (h *IncidentHandlers) 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