Files
MyClub/internal/controllers/audit_log_controller.go
T
Tomas Dvorak 9ccca365b3 dev day #65
2025-10-19 18:09:28 +02:00

292 lines
8.3 KiB
Go

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)
}