package user import ( "context" "encoding/json" "log" "net/http" "sync" "time" "crussell/db" "crussell/clock" "crussell/mw" ) var ( gdprExportCache = make(map[string]*gdprCacheEntry) gdprExportCacheMu sync.RWMutex ) type gdprCacheEntry struct { data json.RawMessage expiresAt time.Time generating bool } // CleanupGDPRExportCache removes expired entries from the GDPR export cache. // Called by the centralised jobs scheduler. func CleanupGDPRExportCache(ctx context.Context) (int, error) { gdprExportCacheMu.Lock() defer gdprExportCacheMu.Unlock() now := clock.Now() var n int for k, v := range gdprExportCache { if now.After(v.expiresAt) { delete(gdprExportCache, k) n++ } } return n, nil } // GET /api/user/gdpr-export func GetGDPRExportHandler(w http.ResponseWriter, r *http.Request) { userID, ok := mw.GetUserID(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } gdprExportCacheMu.Lock() if entry, found := gdprExportCache[userID]; found && clock.Now().Before(entry.expiresAt) { if entry.generating { gdprExportCacheMu.Unlock() w.Header().Set("X-Cache", "GENERATING") _, _ = w.Write([]byte(`{"status":"generating"}`)) return } gdprExportCacheMu.Unlock() w.Header().Set("X-Cache", "HIT") _, _ = w.Write(entry.data) return } gdprExportCache[userID] = &gdprCacheEntry{generating: true} gdprExportCacheMu.Unlock() go func() { defer func() { if r := recover(); r != nil { log.Printf("Panic recovered in GDPR export query: %v", r) } }() var result json.RawMessage err := db.Conn.QueryRow(context.Background(), `SELECT export_all_user_data($1)`, userID).Scan(&result) if err != nil { log.Printf("GDPR export failed for user %s: %v", userID, err) gdprExportCacheMu.Lock() delete(gdprExportCache, userID) gdprExportCacheMu.Unlock() return } gdprExportCacheMu.Lock() gdprExportCache[userID] = &gdprCacheEntry{ data: result, expiresAt: clock.Now().Add(12 * time.Hour), } gdprExportCacheMu.Unlock() }() w.Header().Set("X-Cache", "MISS") _, _ = w.Write([]byte(`{"status":"generating"}`)) }