package controllers import ( "encoding/json" "fmt" "net/http" "time" "fotbal-club/internal/models" "fotbal-club/pkg/logger" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // AuditLogController handles audit log operations type AuditLogController struct { DB *gorm.DB } // NewAuditLogController creates a new AuditLogController instance func NewAuditLogController(db *gorm.DB) *AuditLogController { return &AuditLogController{DB: db} } // AuditAction represents common audit actions const ( AuditActionCreate = "CREATE" AuditActionUpdate = "UPDATE" AuditActionDelete = "DELETE" AuditActionLogin = "LOGIN" AuditActionLogout = "LOGOUT" AuditActionView = "VIEW" AuditActionExport = "EXPORT" ) // LogEntry creates an audit log entry func (alc *AuditLogController) LogEntry(c *gin.Context, action, entityType string, entityID *uint, description string, changes interface{}) error { var userID *uint if user, exists := c.Get("user"); exists { if u, ok := user.(*models.User); ok && u != nil { userID = &u.ID } } // Serialize changes to JSON var changesJSON string if changes != nil { if b, err := json.Marshal(changes); err == nil { changesJSON = string(b) } } log := models.AuditLog{ UserID: userID, Action: action, EntityType: entityType, EntityID: entityID, Description: description, IPAddress: c.ClientIP(), UserAgent: c.Request.UserAgent(), Changes: changesJSON, CreatedAt: time.Now(), } if err := alc.DB.Create(&log).Error; err != nil { logger.Error("Failed to create audit log: %v", err) return err } return nil } // LogCreate logs a CREATE action func (alc *AuditLogController) LogCreate(c *gin.Context, entityType string, entityID uint, description string) error { id := entityID return alc.LogEntry(c, AuditActionCreate, entityType, &id, description, nil) } // LogUpdate logs an UPDATE action with before/after changes func (alc *AuditLogController) LogUpdate(c *gin.Context, entityType string, entityID uint, description string, before, after interface{}) error { id := entityID changes := map[string]interface{}{ "before": before, "after": after, } return alc.LogEntry(c, AuditActionUpdate, entityType, &id, description, changes) } // LogDelete logs a DELETE action func (alc *AuditLogController) LogDelete(c *gin.Context, entityType string, entityID uint, description string) error { id := entityID return alc.LogEntry(c, AuditActionDelete, entityType, &id, description, nil) } // LogLogin logs a login action func (alc *AuditLogController) LogLogin(c *gin.Context, userID uint, success bool) error { id := userID description := "User logged in successfully" if !success { description = "Failed login attempt" } return alc.LogEntry(c, AuditActionLogin, "User", &id, description, nil) } // LogLogout logs a logout action func (alc *AuditLogController) LogLogout(c *gin.Context, userID uint) error { id := userID return alc.LogEntry(c, AuditActionLogout, "User", &id, "User logged out", nil) } // GetAuditLogs retrieves audit logs with filtering and pagination (admin only) func (alc *AuditLogController) GetAuditLogs(c *gin.Context) { query := alc.DB.Model(&models.AuditLog{}).Preload("User") // Apply filters query = QueryParser.BuildQueryChain(c, query). WithSearch("action", "entity_type", "description"). WithDateRange("created_at"). Build() // Filter by action if action := c.Query("action"); action != "" { query = query.Where("action = ?", action) } // Filter by entity type if entityType := c.Query("entity_type"); entityType != "" { query = query.Where("entity_type = ?", entityType) } // Filter by user ID if userID := c.Query("user_id"); userID != "" { query = query.Where("user_id = ?", userID) } // Apply sorting (default: newest first) query = QueryParser.ApplySortFromContext(c, query, "created_at", "desc") // Paginate var logs []models.AuditLog meta, err := Paginator.Paginate(c, query, &logs) if err != nil { Respond.InternalError(c, "Failed to retrieve audit logs") return } Respond.SuccessWithMeta(c, logs, meta, "Audit logs retrieved successfully") } // GetAuditLogByID retrieves a single audit log by ID (admin only) func (alc *AuditLogController) GetAuditLogByID(c *gin.Context) { id := c.Param("id") var log models.AuditLog if err := alc.DB.Preload("User").First(&log, id).Error; err != nil { if err == gorm.ErrRecordNotFound { Respond.NotFound(c, "Audit log not found") return } Respond.InternalError(c, "Failed to retrieve audit log") return } Respond.Success(c, log, "Audit log retrieved successfully") } // GetEntityAuditHistory retrieves audit history for a specific entity func (alc *AuditLogController) GetEntityAuditHistory(c *gin.Context) { entityType := c.Param("entity_type") entityID := c.Param("entity_id") query := alc.DB.Model(&models.AuditLog{}). Where("entity_type = ? AND entity_id = ?", entityType, entityID). Preload("User"). Order("created_at DESC") var logs []models.AuditLog meta, err := Paginator.Paginate(c, query, &logs) if err != nil { Respond.InternalError(c, "Failed to retrieve audit history") return } Respond.SuccessWithMeta(c, logs, meta, fmt.Sprintf("Audit history for %s #%s retrieved successfully", entityType, entityID)) } // GetUserActivityLog retrieves activity log for a specific user func (alc *AuditLogController) GetUserActivityLog(c *gin.Context) { userID := c.Param("user_id") query := alc.DB.Model(&models.AuditLog{}). Where("user_id = ?", userID). Preload("User"). Order("created_at DESC") var logs []models.AuditLog meta, err := Paginator.Paginate(c, query, &logs) if err != nil { Respond.InternalError(c, "Failed to retrieve user activity") return } Respond.SuccessWithMeta(c, logs, meta, "User activity log retrieved successfully") } // GetAuditStats returns audit statistics (admin only) func (alc *AuditLogController) GetAuditStats(c *gin.Context) { var stats struct { TotalLogs int64 `json:"total_logs"` ActionCounts map[string]int64 `json:"action_counts"` EntityCounts map[string]int64 `json:"entity_counts"` RecentActions []models.AuditLog `json:"recent_actions"` } // Total logs alc.DB.Model(&models.AuditLog{}).Count(&stats.TotalLogs) // Action counts stats.ActionCounts = make(map[string]int64) var actionCounts []struct { Action string `json:"action"` Count int64 `json:"count"` } alc.DB.Model(&models.AuditLog{}). Select("action, COUNT(*) as count"). Group("action"). Scan(&actionCounts) for _, ac := range actionCounts { stats.ActionCounts[ac.Action] = ac.Count } // Entity counts stats.EntityCounts = make(map[string]int64) var entityCounts []struct { EntityType string `json:"entity_type"` Count int64 `json:"count"` } alc.DB.Model(&models.AuditLog{}). Select("entity_type, COUNT(*) as count"). Where("entity_type IS NOT NULL AND entity_type != ''"). Group("entity_type"). Scan(&entityCounts) for _, ec := range entityCounts { stats.EntityCounts[ec.EntityType] = ec.Count } // Recent actions alc.DB.Model(&models.AuditLog{}). Preload("User"). Order("created_at DESC"). Limit(10). Find(&stats.RecentActions) c.JSON(http.StatusOK, stats) } // CleanupOldLogs deletes audit logs older than specified days (admin only) func (alc *AuditLogController) CleanupOldLogs(c *gin.Context) { var req struct { DaysOld int `json:"days_old" binding:"required,min=30"` } if err := c.ShouldBindJSON(&req); err != nil { Respond.BadRequest(c, "Invalid request: days_old must be at least 30") return } cutoffDate := time.Now().AddDate(0, 0, -req.DaysOld) result := alc.DB.Where("created_at < ?", cutoffDate).Delete(&models.AuditLog{}) if result.Error != nil { logger.Error("Failed to cleanup old audit logs: %v", result.Error) Respond.InternalError(c, "Failed to cleanup old audit logs") return } logger.Info("Cleaned up %d audit logs older than %d days", result.RowsAffected, req.DaysOld) Respond.Success(c, gin.H{ "deleted_count": result.RowsAffected, "cutoff_date": cutoffDate, }, fmt.Sprintf("Successfully deleted %d audit logs older than %d days", result.RowsAffected, req.DaysOld)) } // Global AuditLogController instance (needs to be initialized with DB) var AuditLogger *AuditLogController // InitAuditLogger initializes the global audit logger func InitAuditLogger(db *gorm.DB) { AuditLogger = NewAuditLogController(db) }