package middleware import ( "context" "encoding/json" "fmt" "log" "time" "a301_server/pkg/database" "github.com/gofiber/fiber/v2" ) const idempotencyTTL = 10 * time.Minute const redisTimeout = 5 * time.Second type cachedResponse struct { StatusCode int `json:"s"` Body json.RawMessage `json:"b"` } // Idempotency checks the Idempotency-Key header to prevent duplicate transactions. // If the same key is seen again within the TTL, the cached response is returned. func Idempotency(c *fiber.Ctx) error { key := c.Get("Idempotency-Key") if key == "" { return c.Next() } // userID가 있으면 키에 포함하여 사용자 간 캐시 충돌 방지 redisKey := "idempotency:" if uid, ok := c.Locals("userID").(uint); ok { redisKey += fmt.Sprintf("u%d:", uid) } redisKey += key ctx, cancel := context.WithTimeout(context.Background(), redisTimeout) defer cancel() // Check if this key was already processed cached, err := database.RDB.Get(ctx, redisKey).Bytes() if err == nil && len(cached) > 0 { var cr cachedResponse if json.Unmarshal(cached, &cr) == nil { c.Set("Content-Type", "application/json") c.Set("X-Idempotent-Replay", "true") return c.Status(cr.StatusCode).Send(cr.Body) } } // Process the request if err := c.Next(); err != nil { return err } // Cache successful responses (2xx) status := c.Response().StatusCode() if status >= 200 && status < 300 { cr := cachedResponse{StatusCode: status, Body: c.Response().Body()} if data, err := json.Marshal(cr); err == nil { if err := database.RDB.Set(ctx, redisKey, data, idempotencyTTL).Err(); err != nil { log.Printf("WARNING: idempotency cache write failed (key=%s): %v", key, err) } } } return nil }