package handlers import ( "context" "net/http" "strconv" "time" apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // AuditServiceInterfaceForAuditHandler defines methods needed for audit handler type AuditServiceInterfaceForAuditHandler interface { SearchLogs(ctx context.Context, req *services.AuditLogSearchRequest) ([]*services.AuditLog, error) CountLogs(ctx context.Context, req *services.AuditLogSearchRequest) (int64, error) GetUserActivity(ctx context.Context, userID uuid.UUID, limit int) ([]*services.AuditLog, error) GetIPActivity(ctx context.Context, ipAddress string, limit int) ([]*services.AuditLog, error) GetStats(ctx context.Context, startDate, endDate time.Time) ([]*services.AuditStats, error) DetectSuspiciousActivity(ctx context.Context, hours int) ([]*services.SuspiciousActivity, error) CleanupOldLogs(ctx context.Context, retentionDays int) (int64, error) } // AuditHandler gère les opérations sur les logs d'audit type AuditHandler struct { auditService AuditServiceInterfaceForAuditHandler logger *zap.Logger } // NewAuditHandler crée un nouveau handler d'audit func NewAuditHandler( auditService *services.AuditService, logger *zap.Logger, ) *AuditHandler { return &AuditHandler{ auditService: &auditServiceWrapperForAuditHandler{auditService: auditService}, logger: logger, } } // NewAuditHandlerWithInterface creates a new audit handler with interface (for testing) func NewAuditHandlerWithInterface( auditService AuditServiceInterfaceForAuditHandler, logger *zap.Logger, ) *AuditHandler { return &AuditHandler{ auditService: auditService, logger: logger, } } // auditServiceWrapperForAuditHandler wraps *services.AuditService to implement AuditServiceInterfaceForAuditHandler type auditServiceWrapperForAuditHandler struct { auditService *services.AuditService } func (w *auditServiceWrapperForAuditHandler) SearchLogs(ctx context.Context, req *services.AuditLogSearchRequest) ([]*services.AuditLog, error) { return w.auditService.SearchLogs(ctx, req) } func (w *auditServiceWrapperForAuditHandler) CountLogs(ctx context.Context, req *services.AuditLogSearchRequest) (int64, error) { return w.auditService.CountLogs(ctx, req) } func (w *auditServiceWrapperForAuditHandler) GetUserActivity(ctx context.Context, userID uuid.UUID, limit int) ([]*services.AuditLog, error) { return w.auditService.GetUserActivity(ctx, userID, limit) } func (w *auditServiceWrapperForAuditHandler) GetIPActivity(ctx context.Context, ipAddress string, limit int) ([]*services.AuditLog, error) { return w.auditService.GetIPActivity(ctx, ipAddress, limit) } func (w *auditServiceWrapperForAuditHandler) GetStats(ctx context.Context, startDate, endDate time.Time) ([]*services.AuditStats, error) { return w.auditService.GetStats(ctx, startDate, endDate) } func (w *auditServiceWrapperForAuditHandler) DetectSuspiciousActivity(ctx context.Context, hours int) ([]*services.SuspiciousActivity, error) { return w.auditService.DetectSuspiciousActivity(ctx, hours) } func (w *auditServiceWrapperForAuditHandler) CleanupOldLogs(ctx context.Context, retentionDays int) (int64, error) { return w.auditService.CleanupOldLogs(ctx, retentionDays) } // SearchLogs recherche des logs d'audit // @Summary Search audit logs // @Description Search and filter audit logs with pagination support. Supports filtering by action, resource, date range, IP address, and user agent. // @Tags Audit // @Accept json // @Produce json // @Security BearerAuth // @Param action query string false "Filter by action type" // @Param resource query string false "Filter by resource type" // @Param resource_id query string false "Filter by resource ID (UUID)" // @Param ip_address query string false "Filter by IP address" // @Param user_agent query string false "Filter by user agent" // @Param start_date query string false "Start date filter (YYYY-MM-DD)" // @Param end_date query string false "End date filter (YYYY-MM-DD)" // @Param page query int false "Page number" default(1) // @Param limit query int false "Items per page" default(20) // @Param offset query int false "Offset for pagination" // @Success 200 {object} handlers.APIResponse{data=object{logs=array,pagination=object}} // @Failure 400 {object} handlers.APIResponse "Validation error" // @Failure 401 {object} handlers.APIResponse "Unauthorized" // @Failure 500 {object} handlers.APIResponse "Internal server error" // @Router /audit/logs [get] // BE-API-034: Enhanced with better filtering and pagination func (ah *AuditHandler) SearchLogs() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte userID, ok := GetUserIDUUID(c) if !ok { return // Erreur déjà envoyée par GetUserIDUUID } // Parser les paramètres de recherche req := &services.AuditLogSearchRequest{ UserID: &userID, // Par défaut, chercher les logs de l'utilisateur } // Paramètres optionnels de filtrage if action := c.Query("action"); action != "" { req.Action = action } if resource := c.Query("resource"); resource != "" { req.Resource = resource } // BE-API-034: Added resource_id filter if resourceIDStr := c.Query("resource_id"); resourceIDStr != "" { if resourceID, err := uuid.Parse(resourceIDStr); err == nil { req.ResourceID = &resourceID } } // BE-API-034: Added IP address filter if ipAddress := c.Query("ip_address"); ipAddress != "" { req.IPAddress = ipAddress } // BE-API-034: Added user_agent filter if userAgent := c.Query("user_agent"); userAgent != "" { req.UserAgent = userAgent } if startDateStr := c.Query("start_date"); startDateStr != "" { if startDate, err := time.Parse("2006-01-02", startDateStr); err == nil { req.StartDate = &startDate } else { RespondWithAppError(c, apperrors.NewValidationError("Invalid start_date format. Use YYYY-MM-DD")) return } } if endDateStr := c.Query("end_date"); endDateStr != "" { if endDate, err := time.Parse("2006-01-02", endDateStr); err == nil { req.EndDate = &endDate } else { RespondWithAppError(c, apperrors.NewValidationError("Invalid end_date format. Use YYYY-MM-DD")) return } } // BE-API-034: Improved pagination - support both page/limit and offset/limit if pageStr := c.Query("page"); pageStr != "" { if page, err := strconv.Atoi(pageStr); err == nil && page > 0 { req.Page = page } else { RespondWithAppError(c, apperrors.NewValidationError("Invalid page number. Must be a positive integer")) return } } if limitStr := c.Query("limit"); limitStr != "" { if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 && limit <= 100 { req.Limit = limit } else { RespondWithAppError(c, apperrors.NewValidationError("Invalid limit. Must be between 1 and 100")) return } } else { req.Limit = 50 // Limite par défaut } if offsetStr := c.Query("offset"); offsetStr != "" { if offset, err := strconv.Atoi(offsetStr); err == nil && offset >= 0 { req.Offset = offset } else { RespondWithAppError(c, apperrors.NewValidationError("Invalid offset. Must be a non-negative integer")) return } } // Effectuer la recherche logs, err := ah.auditService.SearchLogs(c.Request.Context(), req) if err != nil { ah.logger.Error("Failed to search audit logs", zap.Error(err), zap.String("user_id", userID.String()), ) RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to search audit logs", err)) return } // BE-API-034: Get total count for pagination total, err := ah.auditService.CountLogs(c.Request.Context(), req) if err != nil { ah.logger.Warn("Failed to count audit logs, continuing without total", zap.Error(err), ) total = int64(len(logs)) // Fallback to current count } // INT-007: Standardize pagination format var pagination PaginationData if req.Page > 0 { pagination = BuildPaginationData(req.Page, req.Limit, total) } else { // Pour offset-based, on calcule la page approximative page := (req.Offset / req.Limit) + 1 pagination = BuildPaginationData(page, req.Limit, total) } RespondSuccess(c, http.StatusOK, gin.H{ "logs": logs, "count": len(logs), "pagination": pagination, }) } } // GetStats récupère les statistiques d'audit // @Summary Get audit statistics // @Description Get audit statistics for the current user, optionally filtered by date range // @Tags Audit // @Accept json // @Produce json // @Security BearerAuth // @Param start_date query string false "Start date (YYYY-MM-DD)" // @Param end_date query string false "End date (YYYY-MM-DD)" // @Success 200 {object} handlers.APIResponse{data=object{stats=object}} // @Failure 400 {object} handlers.APIResponse "Validation error" // @Failure 401 {object} handlers.APIResponse "Unauthorized" // @Failure 500 {object} handlers.APIResponse "Internal server error" // @Router /audit/stats [get] func (ah *AuditHandler) GetStats() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte userIDInterface, exists := c.Get("user_id") if !exists { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeUnauthorized), "User not authenticated") return } userID, ok := userIDInterface.(uuid.UUID) if !ok { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeInternal), "Invalid user ID type") return } // Parser les paramètres de date var startDate, endDate time.Time var err error if startDateStr := c.Query("start_date"); startDateStr != "" { startDate, err = time.Parse("2006-01-02", startDateStr) if err != nil { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeValidation), "Invalid start_date format") return } } else { startDate = time.Now().AddDate(0, 0, -30) // 30 jours par défaut } if endDateStr := c.Query("end_date"); endDateStr != "" { endDate, err = time.Parse("2006-01-02", endDateStr) if err != nil { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeValidation), "Invalid end_date format") return } } else { endDate = time.Now() } // Récupérer les statistiques stats, err := ah.auditService.GetStats(c.Request.Context(), startDate, endDate) if err != nil { ah.logger.Error("Failed to get audit stats", zap.Error(err), zap.String("user_id", userID.String()), ) // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeInternal), "Failed to get audit stats") return } // Action 1.3.2.1: Use wrapped format helper RespondSuccess(c, http.StatusOK, gin.H{ "user_id": userID, "start_date": startDate, "end_date": endDate, "stats": stats, }) } } // GetUserActivity récupère l'activité d'un utilisateur // @Summary Get user activity // @Description Get recent activity logs for the current user // @Tags Audit // @Accept json // @Produce json // @Security BearerAuth // @Param limit query int false "Number of activities to return" default(50) // @Success 200 {object} handlers.APIResponse{data=object{activities=array}} // @Failure 401 {object} handlers.APIResponse "Unauthorized" // @Failure 500 {object} handlers.APIResponse "Internal server error" // @Router /audit/activity [get] func (ah *AuditHandler) GetUserActivity() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte userIDInterface, exists := c.Get("user_id") if !exists { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeUnauthorized), "User not authenticated") return } userID, ok := userIDInterface.(uuid.UUID) if !ok { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeInternal), "Invalid user ID type") return } // Parser le paramètre limit limit := 50 // Limite par défaut if limitStr := c.Query("limit"); limitStr != "" { if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 { limit = parsedLimit } } // Récupérer l'activité activity, err := ah.auditService.GetUserActivity(c.Request.Context(), userID, limit) if err != nil { ah.logger.Error("Failed to get user activity", zap.Error(err), zap.String("user_id", userID.String()), ) // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeInternal), "Failed to get user activity") return } // Action 1.3.2.1: Use wrapped format helper RespondSuccess(c, http.StatusOK, gin.H{ "user_id": userID, "activity": activity, "count": len(activity), }) } } // DetectSuspiciousActivity détecte les activités suspectes func (ah *AuditHandler) DetectSuspiciousActivity() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte userIDInterface, exists := c.Get("user_id") if !exists { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeUnauthorized), "User not authenticated") return } userID, ok := userIDInterface.(uuid.UUID) if !ok { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeInternal), "Invalid user ID type") return } // Parser le paramètre hours hours := 24 // 24 heures par défaut if hoursStr := c.Query("hours"); hoursStr != "" { if parsedHours, err := strconv.Atoi(hoursStr); err == nil && parsedHours > 0 && parsedHours <= 168 { hours = parsedHours } } // Détecter les activités suspectes activities, err := ah.auditService.DetectSuspiciousActivity(c.Request.Context(), hours) if err != nil { ah.logger.Error("Failed to detect suspicious activity", zap.Error(err), zap.String("user_id", userID.String()), ) // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeInternal), "Failed to detect suspicious activity") return } // Action 1.3.2.1: Use wrapped format helper RespondSuccess(c, http.StatusOK, gin.H{ "user_id": userID, "hours": hours, "activities": activities, "count": len(activities), }) } } // GetIPActivity récupère l'activité d'une IP func (ah *AuditHandler) GetIPActivity() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte userIDInterface, exists := c.Get("user_id") if !exists { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeUnauthorized), "User not authenticated") return } userID, ok := userIDInterface.(uuid.UUID) if !ok { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeInternal), "Invalid user ID type") return } // Récupérer l'IP depuis les paramètres ipAddress := c.Param("ip") if ipAddress == "" { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeValidation), "IP address parameter required") return } // Parser le paramètre limit limit := 50 // Limite par défaut if limitStr := c.Query("limit"); limitStr != "" { if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 { limit = parsedLimit } } // Récupérer l'activité de l'IP activity, err := ah.auditService.GetIPActivity(c.Request.Context(), ipAddress, limit) if err != nil { ah.logger.Error("Failed to get IP activity", zap.Error(err), zap.String("user_id", userID.String()), zap.String("ip_address", ipAddress), ) // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeInternal), "Failed to get IP activity") return } // Action 1.3.2.1: Use wrapped format helper RespondSuccess(c, http.StatusOK, gin.H{ "user_id": userID, "ip_address": ipAddress, "activity": activity, "count": len(activity), }) } } // CleanupOldLogs nettoie les anciens logs d'audit func (ah *AuditHandler) CleanupOldLogs() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte userIDInterface, exists := c.Get("user_id") if !exists { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeUnauthorized), "User not authenticated") return } userID, ok := userIDInterface.(uuid.UUID) if !ok { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeInternal), "Invalid user ID type") return } // Parser le paramètre retention_days retentionDays := 90 // 90 jours par défaut if retentionStr := c.Query("retention_days"); retentionStr != "" { if parsedRetention, err := strconv.Atoi(retentionStr); err == nil && parsedRetention > 0 && parsedRetention <= 365 { retentionDays = parsedRetention } } // Nettoyer les anciens logs deletedCount, err := ah.auditService.CleanupOldLogs(c.Request.Context(), retentionDays) if err != nil { ah.logger.Error("Failed to cleanup old audit logs", zap.Error(err), zap.String("user_id", userID.String()), ) // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeInternal), "Failed to cleanup old logs") return } ah.logger.Info("Old audit logs cleaned up", zap.String("user_id", userID.String()), zap.Int64("deleted_count", deletedCount), zap.Int("retention_days", retentionDays), ) // Action 1.3.2.1: Use wrapped format helper RespondSuccess(c, http.StatusOK, gin.H{ "message": "Old audit logs cleaned up successfully", "deleted_count": deletedCount, "retention_days": retentionDays, }) } } // GetAuditLog récupère un log d'audit spécifique func (ah *AuditHandler) GetAuditLog() gin.HandlerFunc { return func(c *gin.Context) { // Récupérer l'ID utilisateur depuis le contexte userIDInterface, exists := c.Get("user_id") if !exists { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeUnauthorized), "User not authenticated") return } userID, ok := userIDInterface.(uuid.UUID) if !ok { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeInternal), "Invalid user ID type") return } // Récupérer l'ID du log depuis les paramètres logIDStr := c.Param("id") logID, err := uuid.Parse(logIDStr) if err != nil { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeValidation), "Invalid log ID") return } // Rechercher le log spécifique req := &services.AuditLogSearchRequest{ UserID: &userID, Limit: 1, } logs, err := ah.auditService.SearchLogs(c.Request.Context(), req) if err != nil { ah.logger.Error("Failed to get audit log", zap.Error(err), zap.String("user_id", userID.String()), zap.String("log_id", logID.String()), ) // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeInternal), "Failed to get audit log") return } if len(logs) == 0 { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeNotFound), "Audit log not found") return } // Vérifier que le log appartient à l'utilisateur log := logs[0] if log.UserID != nil && *log.UserID != userID { // Action 1.3.2.1: Use wrapped format helper for errors RespondWithError(c, int(apperrors.ErrCodeForbidden), "Access denied") return } // Action 1.3.2.1: Use wrapped format helper RespondSuccess(c, http.StatusOK, gin.H{ "log": log, }) } }