narasiahli-be/app/middleware/audit_trails.middleware.go

107 lines
2.7 KiB
Go
Raw Permalink Normal View History

2025-09-19 04:08:42 +00:00
package middleware
import (
"encoding/json"
"log"
"narasi-ahli-be/app/database/entity"
utilSvc "narasi-ahli-be/utils/service"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
func AuditTrailsMiddleware(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
start := time.Now()
requestBody := c.Body()
headersMap := c.GetReqHeaders()
headersJSON, _ := json.Marshal(headersMap)
authHeader := c.Get("Authorization")
userId := utilSvc.GetUserId(authHeader)
2025-11-04 03:15:21 +00:00
// Execute the next handler
2025-09-19 04:08:42 +00:00
err := c.Next()
2025-11-04 03:15:21 +00:00
// Get status code - ensure it's set correctly for errors
statusCode := c.Response().StatusCode()
if err != nil {
// If error occurred, ensure status code reflects the error
// The error handler should have set this, but if not, default to 500
if statusCode == fiber.StatusOK || statusCode == 0 {
statusCode = fiber.StatusInternalServerError
}
}
// Get response body
responseBody := c.Response().Body()
// If response body is empty and there's an error, create error response
if len(responseBody) == 0 && err != nil {
// Create error response JSON matching the error handler format
errorResp := map[string]interface{}{
"success": false,
"code": statusCode,
"message": err.Error(),
}
if errorJSON, marshalErr := json.Marshal(errorResp); marshalErr == nil {
responseBody = errorJSON
} else {
responseBody = []byte(err.Error())
}
}
2025-09-19 04:08:42 +00:00
audit := entity.AuditTrails{
Method: c.Method(),
Path: c.OriginalURL(),
IP: getIP(c),
2025-11-04 03:15:21 +00:00
Status: statusCode,
2025-09-19 04:08:42 +00:00
UserID: userId,
RequestHeaders: string(headersJSON),
RequestBody: string(requestBody),
2025-11-04 03:15:21 +00:00
ResponseBody: string(responseBody),
2025-09-19 04:08:42 +00:00
DurationMs: time.Since(start).Milliseconds(),
CreatedAt: time.Now(),
}
2025-11-04 03:15:21 +00:00
// Save audit trail - use goroutine to avoid blocking
// IMPORTANT: Save synchronously to ensure it completes even if app crashes
// Using goroutine but with proper error handling
go func(auditRecord entity.AuditTrails) {
if saveErr := db.Create(&auditRecord).Error; saveErr != nil {
log.Printf("Failed to save audit trail for %s %s (status: %d): %v",
auditRecord.Method, auditRecord.Path, auditRecord.Status, saveErr)
}
}(audit)
2025-09-19 04:08:42 +00:00
return err
}
}
func StartAuditTrailCleanup(db *gorm.DB, retention int) {
go func() {
for {
time.Sleep(24 * time.Hour)
cutoff := time.Now().AddDate(0, 0, retention)
db.Where("created_at < ?", cutoff).Delete(&entity.AuditTrails{})
log.Printf(" at: %s", cutoff)
}
}()
}
func getIP(c *fiber.Ctx) string {
ip := c.Get("X-Forwarded-For")
if ip == "" {
ip = c.IP()
}
if strings.Contains(ip, ":") {
ip = strings.Split(ip, ":")[0]
}
return ip
}