All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
- incident_routes.py: 15 Endpoints (CRUD, Risk Assessment, Art. 33/34 Notifications, Measures, Timeline, Close, Stats)
- Neuer Endpoint PUT /{id}/status (nicht in Go vorhanden, Frontend braucht ihn)
- Proxy von ai-compliance-sdk:8090 auf backend-compliance:8002 umgeleitet
- Go incidents_handlers.go + main.go als DEPRECATED markiert
- 50/50 Tests bestanden
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
673 lines
20 KiB
Go
673 lines
20 KiB
Go
// DEPRECATED: Python backend (backend-compliance) is now Source of Truth for Incidents.
|
|
// Frontend proxies to backend-compliance:8002/api/compliance/incidents/*
|
|
// These Go handlers remain for backward compatibility but should not be extended.
|
|
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
|
|
// DEPRECATED: Use Python backend-compliance incident_routes.py instead.
|
|
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)
|
|
}
|
|
|