Basdat/internal/middleware/idempotency.go
2025-12-20 00:01:08 +07:00

169 lines
4.8 KiB
Go

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