107 lines
2.7 KiB
Go
107 lines
2.7 KiB
Go
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)
|
|
|
|
// Execute the next handler
|
|
err := c.Next()
|
|
|
|
// 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())
|
|
}
|
|
}
|
|
|
|
audit := entity.AuditTrails{
|
|
Method: c.Method(),
|
|
Path: c.OriginalURL(),
|
|
IP: getIP(c),
|
|
Status: statusCode,
|
|
UserID: userId,
|
|
RequestHeaders: string(headersJSON),
|
|
RequestBody: string(requestBody),
|
|
ResponseBody: string(responseBody),
|
|
DurationMs: time.Since(start).Milliseconds(),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
// 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)
|
|
|
|
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
|
|
}
|