veza/veza-backend-api/internal/handlers/audit.go
senke b7b23ff4da [BE-API-034] be-api: Implement audit log search improvements
- Added additional filters: resource_id, ip_address, user_agent
- Added page-based pagination support in addition to offset-based
- Added CountLogs method to get total count for pagination
- Standardized SearchLogs handler to use RespondSuccess/RespondWithAppError
- Replaced c.Get with GetUserIDUUID helper
- Improved validation for query parameters
- Response includes total count, page, total_pages, and offset metadata

Phase: PHASE-2
Priority: P2
Progress: 41/267 (15.4%)
2025-12-24 11:56:57 +01:00

459 lines
13 KiB
Go

package handlers
import (
"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"
)
// AuditHandler gère les opérations sur les logs d'audit
type AuditHandler struct {
auditService *services.AuditService
logger *zap.Logger
}
// NewAuditHandler crée un nouveau handler d'audit
func NewAuditHandler(
auditService *services.AuditService,
logger *zap.Logger,
) *AuditHandler {
return &AuditHandler{
auditService: auditService,
logger: logger,
}
}
// SearchLogs recherche des logs d'audit
// GET /api/v1/audit/logs
// 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
}
// BE-API-034: Standardize response format with pagination metadata
response := gin.H{
"logs": logs,
"count": len(logs),
"total": total,
"limit": req.Limit,
}
if req.Page > 0 {
response["page"] = req.Page
response["total_pages"] = (int(total) + req.Limit - 1) / req.Limit
} else {
response["offset"] = req.Offset
}
RespondSuccess(c, http.StatusOK, response)
}
}
// GetStats récupère les statistiques d'audit
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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "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 {
c.JSON(http.StatusBadRequest, gin.H{"error": "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 {
c.JSON(http.StatusBadRequest, gin.H{"error": "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()),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get audit stats"})
return
}
c.JSON(http.StatusOK, gin.H{
"user_id": userID,
"start_date": startDate,
"end_date": endDate,
"stats": stats,
})
}
}
// GetUserActivity récupère l'activité d'un utilisateur
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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "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()),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user activity"})
return
}
c.JSON(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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "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()),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to detect suspicious activity"})
return
}
c.JSON(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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"})
return
}
// Récupérer l'IP depuis les paramètres
ipAddress := c.Param("ip")
if ipAddress == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "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),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get IP activity"})
return
}
c.JSON(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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "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()),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "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),
)
c.JSON(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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "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 {
c.JSON(http.StatusBadRequest, gin.H{"error": "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()),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get audit log"})
return
}
if len(logs) == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Audit log not found"})
return
}
// Vérifier que le log appartient à l'utilisateur
log := logs[0]
if log.UserID != nil && *log.UserID != userID {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
c.JSON(http.StatusOK, gin.H{
"log": log,
})
}
}