Files
Tomas Dvorak 954a1a1080 feat: migrate to DragonflyDB and clean up environment configuration
- Replace Redis with DragonflyDB for better performance and memory efficiency
- Remove redundant environment variables (POSTGRES_*, ENCRYPTION_KEY, OAUTH_SERVICE_URL)
- Consolidate database configuration to use single DB_* variables
- Use JWT_SECRET for both JWT tokens and encryption
- Remove PORT variable redundancy, use BACKEND_PORT consistently
- Clean up docker-compose configurations for dev/prod consistency
- Add DragonflyDB configuration with optimized memory usage
- Remove redis.conf as it's no longer needed
- Update health checks to use Redis-compatible CLI for DragonflyDB
- Add missing VITE_API_URL to production frontend
- Fix GitHub Actions to use correct go.sum path
- Clean up development directories and unused files
2026-03-05 23:51:34 +01:00

177 lines
4.0 KiB
Go

package middleware
import (
"crypto/md5"
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"golang.org/x/net/context"
)
// CacheConfig holds cache configuration
type CacheConfig struct {
Duration time.Duration
KeyPrefix string
Enabled bool
RedisClient *redis.Client
}
// DefaultCacheConfig returns default cache configuration
func DefaultCacheConfig() CacheConfig {
return CacheConfig{
Duration: 5 * time.Minute,
KeyPrefix: "trackeep:",
Enabled: true,
}
}
// CacheMiddleware creates a cache middleware
func CacheMiddleware(config CacheConfig) gin.HandlerFunc {
if !config.Enabled || config.RedisClient == nil {
return func(c *gin.Context) {
c.Next()
}
}
return func(c *gin.Context) {
// Only cache GET requests
if c.Request.Method != http.MethodGet {
c.Next()
return
}
// Skip caching for auth/bootstrap requests and authenticated traffic.
if shouldSkipCache(c) {
c.Next()
return
}
// Generate cache key
cacheKey := generateCacheKey(c, config.KeyPrefix)
// Try to get from cache
cached, err := config.RedisClient.Get(context.Background(), cacheKey).Result()
if err == nil && cached != "" {
// Cache hit
c.Header("X-Cache", "HIT")
c.Header("Content-Type", "application/json")
c.String(http.StatusOK, cached)
c.Abort()
return
}
// Cache miss, continue with request
c.Header("X-Cache", "MISS")
// Capture response
writer := &cachedResponseWriter{
ResponseWriter: c.Writer,
buffer: make([]byte, 0),
}
c.Writer = writer
c.Next()
// Cache the response if successful
if c.Writer.Status() == http.StatusOK && len(writer.buffer) > 0 {
config.RedisClient.Set(
context.Background(),
cacheKey,
string(writer.buffer),
config.Duration,
)
}
}
}
func shouldSkipCache(c *gin.Context) bool {
path := c.Request.URL.Path
if strings.HasPrefix(path, "/api/v1/auth/") {
return true
}
if c.GetHeader("Authorization") != "" || c.GetHeader("Cookie") != "" {
return true
}
return false
}
// generateCacheKey creates a unique cache key for the request
func generateCacheKey(c *gin.Context, prefix string) string {
// Include path, query params, and user ID if available
keyParts := []string{
prefix,
c.Request.URL.Path,
c.Request.URL.RawQuery,
}
// Add user ID for personalized caching
if userID := c.GetString("userID"); userID != "" {
keyParts = append(keyParts, "user:"+userID)
}
// Create hash of the key to avoid long keys
key := strings.Join(keyParts, ":")
hash := md5.Sum([]byte(key))
return fmt.Sprintf("%x", hash)
}
// cachedResponseWriter captures response data for caching
type cachedResponseWriter struct {
gin.ResponseWriter
buffer []byte
}
func (w *cachedResponseWriter) Write(data []byte) (int, error) {
w.buffer = append(w.buffer, data...)
return w.ResponseWriter.Write(data)
}
// InvalidateCache invalidates cache entries matching a pattern
func InvalidateCache(redisClient *redis.Client, pattern string) error {
if redisClient == nil {
return nil
}
keys, err := redisClient.Keys(context.Background(), pattern).Result()
if err != nil {
return err
}
if len(keys) > 0 {
return redisClient.Del(context.Background(), keys...).Err()
}
return nil
}
// CacheInvalidationMiddleware invalidates cache on write operations
func CacheInvalidationMiddleware(redisClient *redis.Client) gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
// Invalidate cache on successful write operations
if c.Writer.Status() >= 200 && c.Writer.Status() < 300 &&
(c.Request.Method == http.MethodPost ||
c.Request.Method == http.MethodPut ||
c.Request.Method == http.MethodDelete) {
// Invalidate user-specific cache
if userID := c.GetString("userID"); userID != "" {
pattern := fmt.Sprintf("trackeep:*user:%s*", userID)
InvalidateCache(redisClient, pattern)
}
// Invalidate general cache for the affected resource
resourcePattern := fmt.Sprintf("trackeep:*%s*", c.Request.URL.Path)
InvalidateCache(redisClient, resourcePattern)
}
}
}