diff --git a/app/admin/admin.go b/app/admin/admin.go index e394104..921eaa5 100644 --- a/app/admin/admin.go +++ b/app/admin/admin.go @@ -188,6 +188,15 @@ func (h *Handler) jwtMiddleware(next http.Handler) http.Handler { return } + // Повторно валидируем токен в nubes API на каждый UI API запрос, + // чтобы отозванные токены не оставались валидными до повторного логина. + ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) + defer cancel() + if err := auth.PingNubesAPI(ctx, h.nubesEndpoint, token); err != nil { + jsonErr(w, http.StatusForbidden, "token rejected by cloud API: "+err.Error()) + return + } + // Проверяем что тенант существует (был создан при /ui/api/auth) jwtTenant, ok := h.store.GetBySub(claims.Sub) if !ok { diff --git a/app/persistence/redis.go b/app/persistence/redis.go index cbe31fc..ef7871f 100644 --- a/app/persistence/redis.go +++ b/app/persistence/redis.go @@ -12,6 +12,7 @@ import ( "context" "encoding/json" "fmt" + "sync" "time" "github.com/redis/go-redis/v9" @@ -24,6 +25,12 @@ import ( // nil означает режим "только память" — все функции тихо no-op. var Client *redis.Client +var ( + seqMu sync.Mutex + queueWriteSeq = map[string]uint64{} + tenantWriteSeq = map[string]uint64{} +) + const ( // redisHashTenants — HASH: tenantID → JSON тенанта redisHashTenants = "ssq:tenants" @@ -85,7 +92,11 @@ func SaveQueue(key string, queue *models.Queue) { log.Warnf("persistence: queue %q too large for Redis (%d bytes), skipping", key, len(data)) return } + writeSeq := nextQueueWriteSeq(key) asyncWrite(func() { + if !isLatestQueueWriteSeq(key, writeSeq) { + return + } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := Client.HSet(ctx, redisHashQueues, key, string(data)).Err(); err != nil { @@ -99,7 +110,11 @@ func DeleteQueue(key string) { if Client == nil { return } + writeSeq := nextQueueWriteSeq(key) asyncWrite(func() { + if !isLatestQueueWriteSeq(key, writeSeq) { + return + } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := Client.HDel(ctx, redisHashQueues, key).Err(); err != nil { @@ -153,7 +168,11 @@ func SaveTenantRaw(id string, jsonData []byte) { // Копируем bytes — вызывающий может переиспользовать буфер dataCopy := make([]byte, len(jsonData)) copy(dataCopy, jsonData) + writeSeq := nextTenantWriteSeq(id) asyncWrite(func() { + if !isLatestTenantWriteSeq(id, writeSeq) { + return + } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := Client.HSet(ctx, redisHashTenants, id, string(dataCopy)).Err(); err != nil { @@ -167,7 +186,11 @@ func DeleteTenant(id string) { if Client == nil { return } + writeSeq := nextTenantWriteSeq(id) asyncWrite(func() { + if !isLatestTenantWriteSeq(id, writeSeq) { + return + } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := Client.HDel(ctx, redisHashTenants, id).Err(); err != nil { @@ -176,6 +199,32 @@ func DeleteTenant(id string) { }) } +func nextQueueWriteSeq(key string) uint64 { + seqMu.Lock() + defer seqMu.Unlock() + queueWriteSeq[key]++ + return queueWriteSeq[key] +} + +func isLatestQueueWriteSeq(key string, seq uint64) bool { + seqMu.Lock() + defer seqMu.Unlock() + return queueWriteSeq[key] == seq +} + +func nextTenantWriteSeq(id string) uint64 { + seqMu.Lock() + defer seqMu.Unlock() + tenantWriteSeq[id]++ + return tenantWriteSeq[id] +} + +func isLatestTenantWriteSeq(id string, seq uint64) bool { + seqMu.Lock() + defer seqMu.Unlock() + return tenantWriteSeq[id] == seq +} + // LoadAllTenantsRaw — загружает всех тенантов из Redis при старте. // Возвращает map[tenantID]rawJSON — десериализацию делает tenant_store.go. func LoadAllTenantsRaw() (map[string][]byte, error) { diff --git a/app/router/router.go b/app/router/router.go index 48f17d4..504ee8c 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -26,7 +26,7 @@ import ( func New(tenantStore *tenant.TenantStore, adminToken string) http.Handler { r := mux.NewRouter() - // Fix #3.5: глобальный лимит размера тела HTTP запроса — 1MB + // Fix #3.5: глобальный лимит размера тела HTTP запроса — 3MB // Защита от OOM при отправке гигантских batch/message requests r.Use(requestBodyLimitMiddleware) @@ -57,12 +57,12 @@ func New(tenantStore *tenant.TenantStore, adminToken string) http.Handler { return r } -// requestBodyLimitMiddleware — ограничивает размер тела HTTP запроса до 1MB. +// requestBodyLimitMiddleware — ограничивает размер тела HTTP запроса до 3MB. // Защита от OOM при отправке гигантских messages/batch (Fix #3.5). func requestBodyLimitMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Body != nil { - r.Body = http.MaxBytesReader(w, r.Body, 1*1024*1024) // 1MB + r.Body = http.MaxBytesReader(w, r.Body, 3*1024*1024) // 3MB } next.ServeHTTP(w, r) })