package handlers import ( "net/http" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" "github.com/kms/api-key-service/internal/audit" "github.com/kms/api-key-service/internal/errors" "github.com/kms/api-key-service/internal/services" "github.com/kms/api-key-service/internal/validation" ) // AuditHandler handles audit-related HTTP requests type AuditHandler struct { auditLogger audit.AuditLogger authService services.AuthenticationService validator *validation.Validator errorHandler *errors.ErrorHandler logger *zap.Logger } // NewAuditHandler creates a new audit handler func NewAuditHandler( auditLogger audit.AuditLogger, authService services.AuthenticationService, logger *zap.Logger, ) *AuditHandler { return &AuditHandler{ auditLogger: auditLogger, authService: authService, validator: validation.NewValidator(logger), errorHandler: errors.NewErrorHandler(logger), logger: logger, } } // AuditQueryRequest represents the request for querying audit events type AuditQueryRequest struct { EventTypes []string `json:"event_types,omitempty" form:"event_types"` Statuses []string `json:"statuses,omitempty" form:"statuses"` ActorID string `json:"actor_id,omitempty" form:"actor_id"` ResourceID string `json:"resource_id,omitempty" form:"resource_id"` ResourceType string `json:"resource_type,omitempty" form:"resource_type"` StartTime *string `json:"start_time,omitempty" form:"start_time"` EndTime *string `json:"end_time,omitempty" form:"end_time"` Limit int `json:"limit,omitempty" form:"limit"` Offset int `json:"offset,omitempty" form:"offset"` OrderBy string `json:"order_by,omitempty" form:"order_by"` OrderDesc *bool `json:"order_desc,omitempty" form:"order_desc"` } // AuditStatsRequest represents the request for audit statistics type AuditStatsRequest struct { EventTypes []string `json:"event_types,omitempty" form:"event_types"` StartTime *string `json:"start_time,omitempty" form:"start_time"` EndTime *string `json:"end_time,omitempty" form:"end_time"` GroupBy string `json:"group_by,omitempty" form:"group_by"` } // AuditResponse represents the response structure for audit queries type AuditResponse struct { Events []AuditEventResponse `json:"events"` Total int `json:"total"` Limit int `json:"limit"` Offset int `json:"offset"` } // AuditEventResponse represents a single audit event in API responses type AuditEventResponse struct { ID string `json:"id"` Type string `json:"type"` Status string `json:"status"` Timestamp string `json:"timestamp"` ActorID string `json:"actor_id,omitempty"` ActorIP string `json:"actor_ip,omitempty"` UserAgent string `json:"user_agent,omitempty"` ResourceID string `json:"resource_id,omitempty"` ResourceType string `json:"resource_type,omitempty"` Action string `json:"action"` Description string `json:"description"` Details map[string]interface{} `json:"details,omitempty"` RequestID string `json:"request_id,omitempty"` SessionID string `json:"session_id,omitempty"` } // ListEvents handles GET /audit/events func (h *AuditHandler) ListEvents(c *gin.Context) { // Parse query parameters var req AuditQueryRequest if err := c.ShouldBindQuery(&req); err != nil { h.errorHandler.HandleValidationError(c, "query_params", "Invalid query parameters") return } // Set defaults if req.Limit <= 0 || req.Limit > 1000 { req.Limit = 100 } if req.Offset < 0 { req.Offset = 0 } if req.OrderBy == "" { req.OrderBy = "timestamp" } if req.OrderDesc == nil { orderDesc := true req.OrderDesc = &orderDesc } // Convert request to audit filter filter := &audit.AuditFilter{ ActorID: req.ActorID, ResourceID: req.ResourceID, ResourceType: req.ResourceType, Limit: req.Limit, Offset: req.Offset, OrderBy: req.OrderBy, OrderDesc: *req.OrderDesc, } // Convert event types for _, et := range req.EventTypes { filter.EventTypes = append(filter.EventTypes, audit.EventType(et)) } // Convert statuses for _, st := range req.Statuses { filter.Statuses = append(filter.Statuses, audit.EventStatus(st)) } // Parse time filters if req.StartTime != nil && *req.StartTime != "" { if startTime, err := time.Parse(time.RFC3339, *req.StartTime); err == nil { filter.StartTime = &startTime } else { h.errorHandler.HandleValidationError(c, "start_time", "Invalid start_time format, use RFC3339") return } } if req.EndTime != nil && *req.EndTime != "" { if endTime, err := time.Parse(time.RFC3339, *req.EndTime); err == nil { filter.EndTime = &endTime } else { h.errorHandler.HandleValidationError(c, "end_time", "Invalid end_time format, use RFC3339") return } } // Query audit events events, err := h.auditLogger.QueryEvents(c.Request.Context(), filter) if err != nil { h.logger.Error("Failed to query audit events", zap.Error(err)) h.errorHandler.HandleInternalError(c, err) return } // Convert to response format response := &AuditResponse{ Events: make([]AuditEventResponse, len(events)), Total: len(events), // Note: This is just the count of returned events, not total matching Limit: req.Limit, Offset: req.Offset, } for i, event := range events { response.Events[i] = AuditEventResponse{ ID: event.ID.String(), Type: string(event.Type), Status: string(event.Status), Timestamp: event.Timestamp.Format(time.RFC3339), ActorID: event.ActorID, ActorIP: event.ActorIP, UserAgent: event.UserAgent, ResourceID: event.ResourceID, ResourceType: event.ResourceType, Action: event.Action, Description: event.Description, Details: event.Details, RequestID: event.RequestID, SessionID: event.SessionID, } } c.JSON(http.StatusOK, response) } // GetEvent handles GET /audit/events/:id func (h *AuditHandler) GetEvent(c *gin.Context) { eventIDStr := c.Param("id") eventID, err := uuid.Parse(eventIDStr) if err != nil { h.errorHandler.HandleValidationError(c, "id", "Invalid event ID format") return } // Get the specific audit event event, err := h.auditLogger.GetEventByID(c.Request.Context(), eventID) if err != nil { h.logger.Error("Failed to get audit event", zap.Error(err), zap.String("event_id", eventID.String())) // Check if it's a not found error if err.Error() == "audit event with ID '"+eventID.String()+"' not found" { h.errorHandler.HandleNotFoundError(c, "audit_event", "Audit event not found") } else { h.errorHandler.HandleInternalError(c, err) } return } // Convert to response format response := AuditEventResponse{ ID: event.ID.String(), Type: string(event.Type), Status: string(event.Status), Timestamp: event.Timestamp.Format(time.RFC3339), ActorID: event.ActorID, ActorIP: event.ActorIP, UserAgent: event.UserAgent, ResourceID: event.ResourceID, ResourceType: event.ResourceType, Action: event.Action, Description: event.Description, Details: event.Details, RequestID: event.RequestID, SessionID: event.SessionID, } c.JSON(http.StatusOK, response) } // GetStats handles GET /audit/stats func (h *AuditHandler) GetStats(c *gin.Context) { // Parse query parameters var req AuditStatsRequest if err := c.ShouldBindQuery(&req); err != nil { h.errorHandler.HandleValidationError(c, "query_params", "Invalid query parameters") return } // Convert request to audit stats filter filter := &audit.AuditStatsFilter{ GroupBy: req.GroupBy, } // Convert event types for _, et := range req.EventTypes { filter.EventTypes = append(filter.EventTypes, audit.EventType(et)) } // Parse time filters if req.StartTime != nil && *req.StartTime != "" { if startTime, err := time.Parse(time.RFC3339, *req.StartTime); err == nil { filter.StartTime = &startTime } else { h.errorHandler.HandleValidationError(c, "start_time", "Invalid start_time format, use RFC3339") return } } if req.EndTime != nil && *req.EndTime != "" { if endTime, err := time.Parse(time.RFC3339, *req.EndTime); err == nil { filter.EndTime = &endTime } else { h.errorHandler.HandleValidationError(c, "end_time", "Invalid end_time format, use RFC3339") return } } // Get audit statistics stats, err := h.auditLogger.GetEventStats(c.Request.Context(), filter) if err != nil { h.logger.Error("Failed to get audit statistics", zap.Error(err)) h.errorHandler.HandleInternalError(c, err) return } c.JSON(http.StatusOK, stats) }