// internal/middleware/idempotency.go package middleware import ( "crypto/sha256" "encoding/hex" "fmt" "lost-and-found/internal/utils" "net/http" "sync" "time" "github.com/gin-gonic/gin" ) // ✅ KRITERIA BACKEND: Idempotency (5 Poin) - Advanced Feature // IdempotencyStore menyimpan hasil request yang sudah diproses type IdempotencyStore struct { mu sync.RWMutex results map[string]*IdempotencyResult } // IdempotencyResult menyimpan response dari request sebelumnya type IdempotencyResult struct { StatusCode int Body interface{} Timestamp time.Time } var idempotencyStore = &IdempotencyStore{ results: make(map[string]*IdempotencyResult), } // cleanupIdempotencyStore membersihkan hasil yang sudah lama (> 24 jam) func cleanupIdempotencyStore() { ticker := time.NewTicker(1 * time.Hour) go func() { for range ticker.C { idempotencyStore.mu.Lock() now := time.Now() for key, result := range idempotencyStore.results { if now.Sub(result.Timestamp) > 24*time.Hour { delete(idempotencyStore.results, key) } } idempotencyStore.mu.Unlock() } }() } func init() { cleanupIdempotencyStore() } // IdempotencyMiddleware mencegah double-submit pada endpoint kritis // Endpoint kritis: Payment, Create Order, Transfer, Submit Claim func IdempotencyMiddleware() gin.HandlerFunc { return func(ctx *gin.Context) { // Hanya apply untuk POST/PUT/PATCH methods if ctx.Request.Method != http.MethodPost && ctx.Request.Method != http.MethodPut && ctx.Request.Method != http.MethodPatch { ctx.Next() return } // Get idempotency key from header idempotencyKey := ctx.GetHeader("Idempotency-Key") // Jika tidak ada idempotency key, skip (tidak wajib untuk semua request) if idempotencyKey == "" { ctx.Next() return } // Generate unique key: idempotency-key + user-id + path + method userID, _ := ctx.Get("user_id") uniqueKey := generateUniqueKey(idempotencyKey, fmt.Sprintf("%v", userID), ctx.Request.URL.Path, ctx.Request.Method) // Check if request sudah pernah diproses idempotencyStore.mu.RLock() result, exists := idempotencyStore.results[uniqueKey] idempotencyStore.mu.RUnlock() if exists { // Request sudah pernah diproses, return hasil sebelumnya ctx.JSON(result.StatusCode, gin.H{ "success": true, "message": "Request already processed (idempotent)", "data": result.Body, "idempotent": true, "original_at": result.Timestamp, }) ctx.Abort() return } // Mark request sebagai "processing" untuk prevent concurrent duplicate processingLock := fmt.Sprintf("%s-processing", uniqueKey) idempotencyStore.mu.Lock() if _, processing := idempotencyStore.results[processingLock]; processing { idempotencyStore.mu.Unlock() utils.ErrorResponse(ctx, http.StatusConflict, "Request is being processed", "Duplicate request detected") ctx.Abort() return } // Lock dengan timestamp sebagai marker idempotencyStore.results[processingLock] = &IdempotencyResult{ Timestamp: time.Now(), } idempotencyStore.mu.Unlock() // Custom response writer untuk capture hasil blw := &bodyLogWriter{body: []byte{}, ResponseWriter: ctx.Writer} ctx.Writer = blw // Process request ctx.Next() // Remove processing lock idempotencyStore.mu.Lock() delete(idempotencyStore.results, processingLock) idempotencyStore.mu.Unlock() // Simpan hasil hanya jika sukses (2xx status) if ctx.Writer.Status() >= 200 && ctx.Writer.Status() < 300 { idempotencyStore.mu.Lock() idempotencyStore.results[uniqueKey] = &IdempotencyResult{ StatusCode: ctx.Writer.Status(), Body: blw.body, Timestamp: time.Now(), } idempotencyStore.mu.Unlock() } } } // bodyLogWriter untuk capture response body type bodyLogWriter struct { gin.ResponseWriter body []byte } func (w *bodyLogWriter) Write(b []byte) (int, error) { w.body = append(w.body, b...) return w.ResponseWriter.Write(b) } // generateUniqueKey membuat unique key dari kombinasi parameter func generateUniqueKey(idempotencyKey, userID, path, method string) string { data := fmt.Sprintf("%s:%s:%s:%s", idempotencyKey, userID, path, method) hash := sha256.Sum256([]byte(data)) return hex.EncodeToString(hash[:]) } // ClearIdempotencyCache membersihkan cache (untuk testing/admin) func ClearIdempotencyCache() { idempotencyStore.mu.Lock() defer idempotencyStore.mu.Unlock() idempotencyStore.results = make(map[string]*IdempotencyResult) } // GetIdempotencyCacheSize return ukuran cache (untuk monitoring) func GetIdempotencyCacheSize() int { idempotencyStore.mu.RLock() defer idempotencyStore.mu.RUnlock() return len(idempotencyStore.results) }