/* * Copyright 2026 Safronov Grigorii * * Licensed under the CDDL, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * * You may obtain a copy of the License at * https://opensource.org/licenses/CDDL-1.0 */ // Файл: internal/api/http.go // Назначение: HTTP RESTful API для взаимодействия с СУБД через curl. // Поддерживает CRUD операции, управление индексами, ACL и ограничениями. // Реализован с минимальными блокировками, использует wait-free структуры. // // ИСПРАВЛЕНО: // - добавлено поле metricsCollector и метод SetMetricsCollector; // - регистрация /metrics (Prometheus text exposition) и /-/healthy; // - ДОБАВЛЕНО: поддержка TLS на самом HTTP-сервере futriiX. // Конструктор NewHTTPServerWithTLS принимает certFile/keyFile, // Start() вызывает ListenAndServeTLS при включённом TLS. // Старый NewHTTPServer оставлен как обёртка для обратной совместимости. // // ИСПРАВЛЕНО (2026-10): после замены ACID-транзакций на batch-операции // функции storage.InitTransactionManager, storage.BeginTransactionOnCollection, // storage.CommitCurrentTransaction, storage.AbortCurrentTransaction, // storage.GetCurrentTransactionID, storage.GetActiveTransactions, // storage.HasActiveTransaction были удалены. HTTP-API для транзакций // переписан поверх batch-операций: // - POST /api/transaction/begin — создаёт *storage.Batch и сохраняет // в карте HTTPServer.activeBatches (ключ — ID сессии); // - POST /api/transaction/commit — коммитит batch и удаляет из карты; // - POST /api/transaction/abort — снимает регистрацию batch без коммита; // - GET /api/transaction/ — возвращает список активных batch'ей // текущей сессии. // Старые URL-пути сохранены для совместимости с существующими клиентами. package api import ( "crypto/rand" "encoding/base64" "encoding/json" "fmt" "net/http" "strconv" "strings" "sync" "sync/atomic" "time" "futriis/internal/acl" "futriis/internal/cluster" "futriis/internal/config" "futriis/internal/log" "futriis/internal/metrics" "futriis/internal/storage" ) // ============================================================================= // RATE LIMITER // ============================================================================= // RateLimiterConfigHTTP содержит конфигурацию rate limiter'а type RateLimiterConfigHTTP struct { RequestsPerSecond int BurstSize int CleanupInterval time.Duration MaxEntries int } // DefaultRateLimiterConfigHTTP возвращает конфигурацию по умолчанию func DefaultRateLimiterConfigHTTP() *RateLimiterConfigHTTP { return &RateLimiterConfigHTTP{ RequestsPerSecond: 100, BurstSize: 20, CleanupInterval: 5 * time.Minute, MaxEntries: 10000, } } // TokenBucketHTTP реализует алгоритм token bucket для HTTP API type TokenBucketHTTP struct { tokens float64 maxTokens float64 refillRate float64 lastRefill time.Time mu sync.Mutex } // NewTokenBucketHTTP создаёт новый token bucket func NewTokenBucketHTTP(ratePerSecond float64, burst int) *TokenBucketHTTP { return &TokenBucketHTTP{ tokens: float64(burst), maxTokens: float64(burst), refillRate: ratePerSecond, lastRefill: time.Now(), } } // Allow проверяет, разрешён ли запрос func (tb *TokenBucketHTTP) Allow() bool { tb.mu.Lock() defer tb.mu.Unlock() now := time.Now() elapsed := now.Sub(tb.lastRefill).Seconds() tb.tokens += elapsed * tb.refillRate if tb.tokens > tb.maxTokens { tb.tokens = tb.maxTokens } tb.lastRefill = now if tb.tokens >= 1.0 { tb.tokens -= 1.0 return true } return false } // RateLimiterHTTP управляет rate limiting для HTTP API type RateLimiterHTTP struct { config *RateLimiterConfigHTTP limiters sync.Map rejected atomic.Uint64 allowed atomic.Uint64 stopChan chan struct{} wg sync.WaitGroup } // NewRateLimiterHTTP создаёт новый rate limiter func NewRateLimiterHTTP(config *RateLimiterConfigHTTP) *RateLimiterHTTP { if config == nil { config = DefaultRateLimiterConfigHTTP() } rl := &RateLimiterHTTP{ config: config, stopChan: make(chan struct{}), } rl.wg.Add(1) go rl.cleanupLoop() return rl } // getLimiter возвращает или создаёт лимитер для ключа func (rl *RateLimiterHTTP) getLimiter(key string) *TokenBucketHTTP { if val, ok := rl.limiters.Load(key); ok { return val.(*TokenBucketHTTP) } limiter := NewTokenBucketHTTP(float64(rl.config.RequestsPerSecond), rl.config.BurstSize) actual, _ := rl.limiters.LoadOrStore(key, limiter) return actual.(*TokenBucketHTTP) } // Allow проверяет, разрешён ли запрос для ключа func (rl *RateLimiterHTTP) Allow(key string) bool { limiter := rl.getLimiter(key) allowed := limiter.Allow() if allowed { rl.allowed.Add(1) } else { rl.rejected.Add(1) } return allowed } // Middleware возвращает middleware для HTTP func (rl *RateLimiterHTTP) Middleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { key := r.RemoteAddr if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" { key = forwarded } if !rl.Allow(key) { w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", rl.config.RequestsPerSecond)) w.Header().Set("X-RateLimit-Remaining", "0") w.Header().Set("Retry-After", "1") http.Error(w, "Rate limit exceeded. Please try again later.", http.StatusTooManyRequests) return } next(w, r) } } // cleanupLoop периодически очищает старые лимитеры func (rl *RateLimiterHTTP) cleanupLoop() { defer rl.wg.Done() ticker := time.NewTicker(rl.config.CleanupInterval) defer ticker.Stop() for { select { case <-ticker.C: rl.cleanup() case <-rl.stopChan: return } } } // cleanup удаляет старые лимитеры func (rl *RateLimiterHTTP) cleanup() { count := 0 rl.limiters.Range(func(key, value interface{}) bool { count++ if count > rl.config.MaxEntries { rl.limiters.Delete(key) } return true }) } // GetStats возвращает статистику rate limiter'а func (rl *RateLimiterHTTP) GetStats() map[string]interface{} { count := 0 rl.limiters.Range(func(key, value interface{}) bool { count++ return true }) return map[string]interface{}{ "active_limiters": count, "allowed_requests": rl.allowed.Load(), "rejected_requests": rl.rejected.Load(), "requests_per_second": rl.config.RequestsPerSecond, "burst_size": rl.config.BurstSize, } } // Stop останавливает rate limiter func (rl *RateLimiterHTTP) Stop() { close(rl.stopChan) rl.wg.Wait() } // generateSecureToken генерирует криптостойкий токен func generateSecureToken() string { bytes := make([]byte, 32) _, err := rand.Read(bytes) if err != nil { return fmt.Sprintf("fallback_%d", time.Now().UnixNano()) } return base64.URLEncoding.EncodeToString(bytes) } // ============================================================================= // HTTP SERVER // ============================================================================= // HTTPServer представляет HTTP сервер API type HTTPServer struct { store *storage.Storage coordinator *cluster.RaftCoordinator aclManager *acl.ACLManager logger *log.Logger server *http.Server port int rateLimiter *RateLimiterHTTP sessions sync.Map // ИСПРАВЛЕНО: коллектор метрик (может быть nil, если выключен). metricsCollector *metrics.Collector // ДОБАВЛЕНО: параметры TLS для самого HTTP-сервера. tlsEnabled bool certFile string keyFile string // ДОБАВЛЕНО (2026-10): реестр активных batch'ей по ID сессии. // Ключ — sessionID (string), значение — *storage.Batch. // Используется HTTP-API транзакций: /api/transaction/begin создаёт // batch и регистрирует здесь, /api/transaction/commit извлекает и // коммитит, /api/transaction/abort просто снимает регистрацию. activeBatches sync.Map } // APIResponse представляет стандартный ответ API type APIResponse struct { Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Error string `json:"error,omitempty"` } // NewHTTPServer — обёртка для обратной совместимости. // Использует NewHTTPServerWithTLS с пустым TLS-конфигом (HTTP). func NewHTTPServer(port int, store *storage.Storage, coord *cluster.RaftCoordinator, aclMgr *acl.ACLManager, logger *log.Logger) *HTTPServer { return NewHTTPServerWithTLS(port, store, coord, aclMgr, logger, nil) } // NewHTTPServerWithTLS — основной конструктор HTTP-сервера. // // ДОБАВЛЕНО: если secCfg != nil и secCfg.IsTLSEnabled() == true, // сервер будет слушать HTTPS и использовать указанные cert/key файлы. // Если TLS включён, но cert_file/key_file пустые — TLS отключается, // в лог пишется предупреждение, сервер поднимается как HTTP (fail-soft, // чтобы не уронить процесс из-за опечатки в конфиге). func NewHTTPServerWithTLS( port int, store *storage.Storage, coord *cluster.RaftCoordinator, aclMgr *acl.ACLManager, logger *log.Logger, secCfg *config.SecurityConfig, ) *HTTPServer { s := &HTTPServer{ store: store, coordinator: coord, aclManager: aclMgr, logger: logger, port: port, rateLimiter: NewRateLimiterHTTP(DefaultRateLimiterConfigHTTP()), } // ДОБАВЛЕНО: разбор TLS-конфига. if secCfg != nil && secCfg.IsTLSEnabled() { s.tlsEnabled = true s.certFile = secCfg.GetCertFile() s.keyFile = secCfg.GetKeyFile() if s.certFile == "" || s.keyFile == "" { if s.logger != nil { s.logger.Warn("TLS enabled but cert_file/key_file is empty — falling back to HTTP") } s.tlsEnabled = false } else if s.logger != nil { s.logger.Info(fmt.Sprintf("TLS enabled for HTTP API (cert=%s, min_version=%s)", s.certFile, secCfg.GetTLSMinVersion())) } } else if s.logger != nil { s.logger.Info("HTTP API running without TLS (plain HTTP)") } mux := http.NewServeMux() corsHandler := func(handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Session-ID, Authorization") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusOK) return } s.rateLimiter.Middleware(handler)(w, r) } } authMiddleware := func(handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { sessionID := r.Header.Get("X-Session-ID") if sessionID == "" { auth := r.Header.Get("Authorization") if strings.HasPrefix(auth, "Bearer ") { sessionID = strings.TrimPrefix(auth, "Bearer ") } } if sessionID == "" { s.sendError(w, "Authentication required", http.StatusUnauthorized) return } if _, ok := s.sessions.Load(sessionID); !ok { s.sendError(w, "Invalid or expired session", http.StatusUnauthorized) return } handler(w, r) } } mux.HandleFunc("/api/auth/login", corsHandler(s.handleLogin)) mux.HandleFunc("/api/auth/logout", corsHandler(authMiddleware(s.handleLogout))) mux.HandleFunc("/api/auth/session", corsHandler(authMiddleware(s.handleSessionCheck))) mux.HandleFunc("/api/db/", corsHandler(authMiddleware(s.handleDatabaseRequest))) mux.HandleFunc("/api/index/", corsHandler(authMiddleware(s.handleIndexRequest))) mux.HandleFunc("/api/acl/", corsHandler(authMiddleware(s.handleACLRequest))) mux.HandleFunc("/api/constraint/", corsHandler(authMiddleware(s.handleConstraintRequest))) mux.HandleFunc("/api/cluster/", corsHandler(authMiddleware(s.handleClusterRequest))) mux.HandleFunc("/api/trigger/", corsHandler(authMiddleware(s.handleTriggerRequest))) mux.HandleFunc("/api/transaction/", corsHandler(authMiddleware(s.handleTransactionRequest))) mux.HandleFunc("/api/health", s.handleHealthCheck) mux.HandleFunc("/api/metrics", s.handleMetricsEndpoint) // ИСПРАВЛЕНО: Prometheus endpoints (без аутентификации). mux.HandleFunc("/-/healthy", s.handlePromHealthy) mux.HandleFunc("/metrics", s.handlePromMetrics) s.server = &http.Server{ Addr: fmt.Sprintf(":%d", port), Handler: mux, } return s } // SetMetricsCollector устанавливает коллектор метрик. func (s *HTTPServer) SetMetricsCollector(c *metrics.Collector) { s.metricsCollector = c } // Start запускает HTTP сервер. // ДОБАВЛЕНО: если TLS включён — ListenAndServeTLS(certFile, keyFile). func (s *HTTPServer) Start() error { scheme := "http" if s.tlsEnabled { scheme = "https" } s.logger.Info(fmt.Sprintf("Starting HTTP API server on %s://0.0.0.0:%d", scheme, s.port)) if s.tlsEnabled { return s.server.ListenAndServeTLS(s.certFile, s.keyFile) } return s.server.ListenAndServe() } // Stop останавливает HTTP сервер func (s *HTTPServer) Stop() error { s.rateLimiter.Stop() return s.server.Close() } // IsTLSEnabled сообщает, работает ли сервер по HTTPS. // ДОБАВЛЕНО: удобно для диагностики и логирования в main.go. func (s *HTTPServer) IsTLSEnabled() bool { return s.tlsEnabled } // GetRateLimiterStats возвращает статистику rate limiter'а func (s *HTTPServer) GetRateLimiterStats() map[string]interface{} { return s.rateLimiter.GetStats() } // handlePromHealthy — health-check для Prometheus/Grafana func (s *HTTPServer) handlePromHealthy(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprintln(w, "OK") } // handlePromMetrics — Prometheus text exposition endpoint. func (s *HTTPServer) handlePromMetrics(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8") w.WriteHeader(http.StatusOK) if _, err := metrics.DefaultRegistry().WriteTo(w); err != nil && s.logger != nil { s.logger.Error(fmt.Sprintf("Failed to write metrics: %v", err)) } } // isUserAdmin проверяет, является ли пользователь администратором через ACL func (s *HTTPServer) isUserAdmin(username string) bool { if s.aclManager == nil { return username == "admin" } roles := s.aclManager.GetUserRoles(username) for _, role := range roles { if role == "admin" { return true } } return false } // handleLogin обрабатывает аутентификацию func (s *HTTPServer) handleLogin(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) return } var creds struct { Username string `json:"username"` Password string `json:"password"` } if err := json.NewDecoder(r.Body).Decode(&creds); err != nil { s.sendError(w, "Invalid request body", http.StatusBadRequest) return } sessionID, err := s.aclManager.Authenticate(creds.Username, creds.Password) if err != nil { s.logOperation("LOGIN", creds.Username, "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusUnauthorized) return } token := generateSecureToken() s.sessions.Store(token, creds.Username) s.sessions.Store(sessionID, creds.Username) s.logOperation("LOGIN", creds.Username, "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "session_id": sessionID, "token": token, "username": creds.Username, "is_admin": s.isUserAdmin(creds.Username), }) } // handleLogout обрабатывает выход func (s *HTTPServer) handleLogout(w http.ResponseWriter, r *http.Request) { sessionID := r.Header.Get("X-Session-ID") if sessionID == "" { auth := r.Header.Get("Authorization") if strings.HasPrefix(auth, "Bearer ") { sessionID = strings.TrimPrefix(auth, "Bearer ") } } if sessionID != "" { if username, ok := s.sessions.Load(sessionID); ok { s.logOperation("LOGOUT", username.(string), "success", "", nil) s.sessions.Delete(sessionID) } // Снимаем все активные batch'и сессии. s.discardSessionBatches(sessionID) s.aclManager.Logout(sessionID) } s.sendSuccess(w, map[string]string{"status": "logged out"}) } // handleSessionCheck проверяет активность сессии func (s *HTTPServer) handleSessionCheck(w http.ResponseWriter, r *http.Request) { sessionID := r.Header.Get("X-Session-ID") if sessionID == "" { auth := r.Header.Get("Authorization") if strings.HasPrefix(auth, "Bearer ") { sessionID = strings.TrimPrefix(auth, "Bearer ") } } if sessionID == "" { s.sendError(w, "No session", http.StatusUnauthorized) return } username, ok := s.sessions.Load(sessionID) if !ok { s.sendError(w, "Invalid session", http.StatusUnauthorized) return } s.sendSuccess(w, map[string]interface{}{ "authenticated": true, "username": username, "is_admin": s.isUserAdmin(username.(string)), }) } // handleDatabaseRequest обрабатывает запросы к БД func (s *HTTPServer) handleDatabaseRequest(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/db/") parts := strings.Split(path, "/") if len(parts) < 2 { s.sendError(w, "Invalid path. Use /api/db/{database}/{collection}[/{id}]", http.StatusBadRequest) return } database := parts[0] collection := parts[1] docID := "" if len(parts) > 2 { docID = parts[2] } sessionID := s.getSessionID(r) if sessionID == "" { s.sendError(w, "Authentication required", http.StatusUnauthorized) return } switch r.Method { case http.MethodGet: s.handleGetDocument(w, r, sessionID, database, collection, docID) case http.MethodPost: s.handleInsertDocument(w, r, sessionID, database, collection) case http.MethodPut: s.handleUpdateDocument(w, r, sessionID, database, collection, docID) case http.MethodDelete: s.handleDeleteDocument(w, r, sessionID, database, collection, docID) default: s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) } } // getSessionID извлекает session ID из запроса func (s *HTTPServer) getSessionID(r *http.Request) string { sessionID := r.Header.Get("X-Session-ID") if sessionID == "" { auth := r.Header.Get("Authorization") if strings.HasPrefix(auth, "Bearer ") { sessionID = strings.TrimPrefix(auth, "Bearer ") } } return sessionID } // handleGetDocument обрабатывает GET запросы func (s *HTTPServer) handleGetDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) { if !s.aclManager.CheckPermission(sessionID, database, collection, "read") { s.logOperation("GET_DOCUMENT", fmt.Sprintf("%s.%s", database, collection), "error", "Access denied", nil) s.sendError(w, "Access denied", http.StatusForbidden) return } db, err := s.store.GetDatabase(database) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } coll, err := db.GetCollection(collection) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } query := r.URL.Query() if indexName := query.Get("index"); indexName != "" { indexValue := query.Get("value") docs, err := coll.FindByIndex(indexName, indexValue) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } s.logOperation("GET_DOCUMENT_BY_INDEX", fmt.Sprintf("%s.%s[%s=%s]", database, collection, indexName, indexValue), "success", "", nil) s.sendSuccess(w, docs) return } if docID == "" { limit := 100 if limitStr := query.Get("limit"); limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 1000 { limit = l } } offset := 0 if offsetStr := query.Get("offset"); offsetStr != "" { if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 { offset = o } } allDocs := coll.GetAllDocuments() start := offset end := offset + limit if start > len(allDocs) { start = len(allDocs) } if end > len(allDocs) { end = len(allDocs) } result := allDocs[start:end] s.logOperation("LIST_DOCUMENTS", fmt.Sprintf("%s.%s", database, collection), "success", "", map[string]interface{}{ "total": len(allDocs), "limit": limit, "offset": offset, }) s.sendSuccess(w, map[string]interface{}{ "documents": result, "total": len(allDocs), "limit": limit, "offset": offset, }) return } doc, err := coll.Find(docID) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } s.logOperation("GET_DOCUMENT", fmt.Sprintf("%s.%s.%s", database, collection, docID), "success", "", nil) s.sendSuccess(w, doc) } // handleInsertDocument обрабатывает POST запросы func (s *HTTPServer) handleInsertDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection string) { if !s.aclManager.CheckPermission(sessionID, database, collection, "write") { s.logOperation("INSERT_DOCUMENT", fmt.Sprintf("%s.%s", database, collection), "error", "Access denied", nil) s.sendError(w, "Access denied", http.StatusForbidden) return } db, err := s.store.GetDatabase(database) if err != nil { if err := s.store.CreateDatabase(database); err != nil { s.sendError(w, err.Error(), http.StatusInternalServerError) return } db, _ = s.store.GetDatabase(database) } coll, err := db.GetCollection(collection) if err != nil { if err := db.CreateCollection(collection); err != nil { s.sendError(w, err.Error(), http.StatusInternalServerError) return } coll, _ = db.GetCollection(collection) } var doc map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&doc); err != nil { s.sendError(w, "Invalid JSON", http.StatusBadRequest) return } docID := "" if id, ok := doc["_id"]; ok { if idStr, ok := id.(string); ok { docID = idStr } } if err := coll.InsertFromMap(doc); err != nil { s.logOperation("INSERT_DOCUMENT", fmt.Sprintf("%s.%s/%s", database, collection, docID), "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusBadRequest) return } s.logOperation("INSERT_DOCUMENT", fmt.Sprintf("%s.%s/%s", database, collection, docID), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "inserted", "id": docID, }) } // handleUpdateDocument обрабатывает PUT запросы func (s *HTTPServer) handleUpdateDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) { if docID == "" { s.sendError(w, "Document ID required", http.StatusBadRequest) return } if !s.aclManager.CheckPermission(sessionID, database, collection, "write") { s.logOperation("UPDATE_DOCUMENT", fmt.Sprintf("%s.%s.%s", database, collection, docID), "error", "Access denied", nil) s.sendError(w, "Access denied", http.StatusForbidden) return } db, err := s.store.GetDatabase(database) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } coll, err := db.GetCollection(collection) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } var updates map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { s.sendError(w, "Invalid JSON", http.StatusBadRequest) return } if err := coll.Update(docID, updates); err != nil { s.logOperation("UPDATE_DOCUMENT", fmt.Sprintf("%s.%s.%s", database, collection, docID), "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusBadRequest) return } s.logOperation("UPDATE_DOCUMENT", fmt.Sprintf("%s.%s.%s", database, collection, docID), "success", "", map[string]interface{}{ "updated_fields": len(updates), }) s.sendSuccess(w, map[string]interface{}{ "status": "updated", "id": docID, }) } // handleDeleteDocument обрабатывает DELETE запросы func (s *HTTPServer) handleDeleteDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) { if docID == "" { s.sendError(w, "Document ID required", http.StatusBadRequest) return } if !s.aclManager.CheckPermission(sessionID, database, collection, "delete") { s.logOperation("DELETE_DOCUMENT", fmt.Sprintf("%s.%s.%s", database, collection, docID), "error", "Access denied", nil) s.sendError(w, "Access denied", http.StatusForbidden) return } db, err := s.store.GetDatabase(database) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } coll, err := db.GetCollection(collection) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } permanent := r.URL.Query().Get("permanent") == "true" if permanent { if err := coll.PermanentDelete(docID); err != nil { s.logOperation("PERMANENT_DELETE_DOCUMENT", fmt.Sprintf("%s.%s.%s", database, collection, docID), "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusNotFound) return } s.logOperation("PERMANENT_DELETE_DOCUMENT", fmt.Sprintf("%s.%s.%s", database, collection, docID), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "permanently_deleted", "id": docID, }) return } if err := coll.Delete(docID); err != nil { s.logOperation("DELETE_DOCUMENT", fmt.Sprintf("%s.%s.%s", database, collection, docID), "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusNotFound) return } s.logOperation("DELETE_DOCUMENT", fmt.Sprintf("%s.%s.%s", database, collection, docID), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "deleted", "id": docID, }) } // handleIndexRequest обрабатывает запросы к индексам func (s *HTTPServer) handleIndexRequest(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/index/") parts := strings.Split(path, "/") if len(parts) < 3 { s.sendError(w, "Invalid path. Use /api/index/{database}/{collection}/{action}", http.StatusBadRequest) return } database := parts[0] collection := parts[1] action := parts[2] sessionID := s.getSessionID(r) if !s.aclManager.CheckPermission(sessionID, database, collection, "admin") { s.logOperation("INDEX_OPERATION", fmt.Sprintf("%s.%s.%s", database, collection, action), "error", "Admin access required", nil) s.sendError(w, "Admin access required", http.StatusForbidden) return } db, err := s.store.GetDatabase(database) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } coll, err := db.GetCollection(collection) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } switch action { case "list": indexes := coll.GetIndexesInfo() s.logOperation("LIST_INDEXES", fmt.Sprintf("%s.%s", database, collection), "success", "", nil) s.sendSuccess(w, indexes) case "create": var req struct { Name string `json:"name"` Fields []string `json:"fields"` Unique bool `json:"unique"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.sendError(w, "Invalid request body", http.StatusBadRequest) return } if req.Name == "" { s.sendError(w, "Index name required", http.StatusBadRequest) return } if len(req.Fields) == 0 { s.sendError(w, "At least one field required", http.StatusBadRequest) return } if err := coll.CreateIndex(req.Name, req.Fields, req.Unique); err != nil { s.logOperation("CREATE_INDEX", fmt.Sprintf("%s.%s.%s", database, collection, req.Name), "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusBadRequest) return } s.logOperation("CREATE_INDEX", fmt.Sprintf("%s.%s.%s", database, collection, req.Name), "success", "", map[string]interface{}{ "fields": req.Fields, "unique": req.Unique, }) s.sendSuccess(w, map[string]interface{}{ "status": "index_created", "name": req.Name, }) case "drop": if len(parts) < 4 { s.sendError(w, "Index name required", http.StatusBadRequest) return } indexName := parts[3] if err := coll.DropIndex(indexName); err != nil { s.logOperation("DROP_INDEX", fmt.Sprintf("%s.%s.%s", database, collection, indexName), "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusBadRequest) return } s.logOperation("DROP_INDEX", fmt.Sprintf("%s.%s.%s", database, collection, indexName), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "index_dropped", "name": indexName, }) default: s.sendError(w, "Unknown action", http.StatusBadRequest) } } // handleACLRequest обрабатывает запросы ACL func (s *HTTPServer) handleACLRequest(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/acl/") parts := strings.Split(path, "/") if len(parts) < 1 { s.sendError(w, "Invalid path", http.StatusBadRequest) return } sessionID := s.getSessionID(r) if !s.aclManager.CheckPermission(sessionID, "*", "*", "admin") { s.logOperation("ACL_OPERATION", strings.Join(parts, "/"), "error", "Admin access required", nil) s.sendError(w, "Admin access required", http.StatusForbidden) return } action := parts[0] switch action { case "users": users := s.aclManager.ListUsers() s.sendSuccess(w, users) case "user": if len(parts) < 2 { s.sendError(w, "Username required", http.StatusBadRequest) return } username := parts[1] switch r.Method { case http.MethodGet: userInfo, err := s.aclManager.GetUserInfo(username) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } s.sendSuccess(w, userInfo) case http.MethodPost: var req struct { Password string `json:"password"` Roles []string `json:"roles"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.sendError(w, "Invalid request body", http.StatusBadRequest) return } if err := s.aclManager.CreateUser(username, req.Password, req.Roles); err != nil { s.logOperation("ACL_CREATE_USER", username, "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusBadRequest) return } s.logOperation("ACL_CREATE_USER", username, "success", "", map[string]interface{}{ "roles": req.Roles, }) s.sendSuccess(w, map[string]interface{}{ "status": "user_created", "user": username, }) case http.MethodPut: var req struct { AddRole string `json:"add_role"` RemoveRole string `json:"remove_role"` Password string `json:"password"` Disable bool `json:"disable"` Enable bool `json:"enable"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.sendError(w, "Invalid request body", http.StatusBadRequest) return } if req.AddRole != "" { if err := s.aclManager.AddUserRole(username, req.AddRole); err != nil { s.sendError(w, err.Error(), http.StatusBadRequest) return } } if req.RemoveRole != "" { if err := s.aclManager.RemoveUserRole(username, req.RemoveRole); err != nil { s.sendError(w, err.Error(), http.StatusBadRequest) return } } if req.Password != "" { if err := s.aclManager.ChangePassword(username, req.Password); err != nil { s.sendError(w, err.Error(), http.StatusBadRequest) return } } if req.Disable { if err := s.aclManager.DisableUser(username); err != nil { s.sendError(w, err.Error(), http.StatusBadRequest) return } } if req.Enable { if err := s.aclManager.EnableUser(username); err != nil { s.sendError(w, err.Error(), http.StatusBadRequest) return } } s.logOperation("ACL_UPDATE_USER", username, "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "user_updated", "user": username, }) case http.MethodDelete: if err := s.aclManager.DeleteUser(username); err != nil { s.logOperation("ACL_DELETE_USER", username, "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusBadRequest) return } s.logOperation("ACL_DELETE_USER", username, "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "user_deleted", "user": username, }) default: s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) } case "roles": roles := s.aclManager.ListRoles() s.sendSuccess(w, roles) case "role": if len(parts) < 2 { s.sendError(w, "Role name required", http.StatusBadRequest) return } roleName := parts[1] switch r.Method { case http.MethodGet: perms, err := s.aclManager.GetRolePermissions(roleName) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } s.sendSuccess(w, map[string]interface{}{ "name": roleName, "permissions": perms, }) case http.MethodPost: if err := s.aclManager.CreateRole(roleName); err != nil { s.logOperation("ACL_CREATE_ROLE", roleName, "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusBadRequest) return } s.logOperation("ACL_CREATE_ROLE", roleName, "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "role_created", "role": roleName, }) case http.MethodDelete: if err := s.aclManager.DeleteRole(roleName); err != nil { s.logOperation("ACL_DELETE_ROLE", roleName, "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusBadRequest) return } s.logOperation("ACL_DELETE_ROLE", roleName, "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "role_deleted", "role": roleName, }) default: s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) } case "grant": if len(parts) < 3 { s.sendError(w, "Role and permission required", http.StatusBadRequest) return } roleName := parts[1] permission := parts[2] if err := s.aclManager.GrantPermission(roleName, permission); err != nil { s.logOperation("ACL_GRANT", fmt.Sprintf("%s.%s", roleName, permission), "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusBadRequest) return } s.logOperation("ACL_GRANT", fmt.Sprintf("%s.%s", roleName, permission), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "permission_granted", "role": roleName, "permission": permission, }) case "revoke": if len(parts) < 3 { s.sendError(w, "Role and permission required", http.StatusBadRequest) return } roleName := parts[1] permission := parts[2] if err := s.aclManager.RevokePermission(roleName, permission); err != nil { s.logOperation("ACL_REVOKE", fmt.Sprintf("%s.%s", roleName, permission), "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusBadRequest) return } s.logOperation("ACL_REVOKE", fmt.Sprintf("%s.%s", roleName, permission), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "permission_revoked", "role": roleName, "permission": permission, }) default: s.sendError(w, "Unknown action", http.StatusBadRequest) } } // handleConstraintRequest обрабатывает запросы к ограничениям func (s *HTTPServer) handleConstraintRequest(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/constraint/") parts := strings.Split(path, "/") if len(parts) < 3 { s.sendError(w, "Invalid path. Use /api/constraint/{database}/{collection}/{action}", http.StatusBadRequest) return } database := parts[0] collection := parts[1] action := parts[2] sessionID := s.getSessionID(r) if !s.aclManager.CheckPermission(sessionID, database, collection, "admin") { s.logOperation("CONSTRAINT_OPERATION", fmt.Sprintf("%s.%s.%s", database, collection, action), "error", "Admin access required", nil) s.sendError(w, "Admin access required", http.StatusForbidden) return } db, err := s.store.GetDatabase(database) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } coll, err := db.GetCollection(collection) if err != nil { s.sendError(w, err.Error(), http.StatusNotFound) return } switch action { case "list": constraints := make([]map[string]interface{}, 0) for _, field := range coll.GetRequiredFields() { constraints = append(constraints, map[string]interface{}{ "type": "required", "field": field, }) } for _, field := range coll.GetUniqueConstraints() { constraints = append(constraints, map[string]interface{}{ "type": "unique", "field": field, }) } for field, value := range coll.GetMinConstraints() { constraints = append(constraints, map[string]interface{}{ "type": "min", "field": field, "value": value, }) } for field, value := range coll.GetMaxConstraints() { constraints = append(constraints, map[string]interface{}{ "type": "max", "field": field, "value": value, }) } for field, values := range coll.GetEnumConstraints() { constraints = append(constraints, map[string]interface{}{ "type": "enum", "field": field, "values": values, }) } for field, pattern := range coll.GetRegexConstraints() { constraints = append(constraints, map[string]interface{}{ "type": "regex", "field": field, "pattern": pattern, }) } s.sendSuccess(w, constraints) case "required": if len(parts) < 4 { s.sendError(w, "Field name required", http.StatusBadRequest) return } field := parts[3] coll.AddRequiredField(field) s.logOperation("ADD_REQUIRED", fmt.Sprintf("%s.%s.%s", database, collection, field), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "required_field_added", "field": field, }) case "unique": if len(parts) < 4 { s.sendError(w, "Field name required", http.StatusBadRequest) return } field := parts[3] coll.AddUniqueConstraint(field) s.logOperation("ADD_UNIQUE", fmt.Sprintf("%s.%s.%s", database, collection, field), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "unique_constraint_added", "field": field, }) case "min": if len(parts) < 5 { s.sendError(w, "Field name and value required", http.StatusBadRequest) return } field := parts[3] minVal, err := strconv.ParseFloat(parts[4], 64) if err != nil { s.sendError(w, "Invalid minimum value", http.StatusBadRequest) return } coll.AddMinConstraint(field, minVal) s.logOperation("ADD_MIN", fmt.Sprintf("%s.%s.%s", database, collection, field), "success", "", map[string]interface{}{"value": minVal}) s.sendSuccess(w, map[string]interface{}{ "status": "min_constraint_added", "field": field, "value": minVal, }) case "max": if len(parts) < 5 { s.sendError(w, "Field name and value required", http.StatusBadRequest) return } field := parts[3] maxVal, err := strconv.ParseFloat(parts[4], 64) if err != nil { s.sendError(w, "Invalid maximum value", http.StatusBadRequest) return } coll.AddMaxConstraint(field, maxVal) s.logOperation("ADD_MAX", fmt.Sprintf("%s.%s.%s", database, collection, field), "success", "", map[string]interface{}{"value": maxVal}) s.sendSuccess(w, map[string]interface{}{ "status": "max_constraint_added", "field": field, "value": maxVal, }) case "enum": if len(parts) < 5 { s.sendError(w, "Field name and values required", http.StatusBadRequest) return } field := parts[3] values := make([]interface{}, len(parts)-4) for i := 4; i < len(parts); i++ { if num, err := strconv.ParseFloat(parts[i], 64); err == nil { values[i-4] = num } else { values[i-4] = parts[i] } } coll.AddEnumConstraint(field, values) s.logOperation("ADD_ENUM", fmt.Sprintf("%s.%s.%s", database, collection, field), "success", "", map[string]interface{}{"values": values}) s.sendSuccess(w, map[string]interface{}{ "status": "enum_constraint_added", "field": field, "values": values, }) case "regex": if len(parts) < 5 { s.sendError(w, "Field name and pattern required", http.StatusBadRequest) return } field := parts[3] pattern := parts[4] coll.AddRegexConstraint(field, pattern) s.logOperation("ADD_REGEX", fmt.Sprintf("%s.%s.%s", database, collection, field), "success", "", map[string]interface{}{"pattern": pattern}) s.sendSuccess(w, map[string]interface{}{ "status": "regex_constraint_added", "field": field, "pattern": pattern, }) case "remove": if len(parts) < 5 { s.sendError(w, "Constraint type and field required", http.StatusBadRequest) return } constraintType := parts[3] field := parts[4] switch constraintType { case "required": coll.RemoveRequiredField(field) case "unique": coll.RemoveUniqueConstraint(field) case "min": coll.RemoveMinConstraint(field) case "max": coll.RemoveMaxConstraint(field) case "enum": coll.RemoveEnumConstraint(field) case "regex": coll.RemoveRegexConstraint(field) default: s.sendError(w, "Unknown constraint type", http.StatusBadRequest) return } s.logOperation("REMOVE_CONSTRAINT", fmt.Sprintf("%s.%s.%s.%s", database, collection, constraintType, field), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "constraint_removed", "type": constraintType, "field": field, }) default: s.sendError(w, "Unknown action", http.StatusBadRequest) } } // handleClusterRequest обрабатывает запросы к кластеру func (s *HTTPServer) handleClusterRequest(w http.ResponseWriter, r *http.Request) { sessionID := s.getSessionID(r) if !s.aclManager.CheckPermission(sessionID, "*", "*", "admin") { s.logOperation("CLUSTER_OPERATION", r.URL.Path, "error", "Admin access required", nil) s.sendError(w, "Admin access required", http.StatusForbidden) return } if s.coordinator == nil { s.sendError(w, "Cluster not available", http.StatusServiceUnavailable) return } path := strings.TrimPrefix(r.URL.Path, "/api/cluster/") parts := strings.Split(path, "/") switch r.Method { case http.MethodGet: if len(parts) == 0 || parts[0] == "" || parts[0] == "status" { status := s.coordinator.GetClusterStatus() s.logOperation("CLUSTER_STATUS", "", "success", "", nil) s.sendSuccess(w, status) } else if parts[0] == "health" { status := s.coordinator.GetClusterStatus() s.sendSuccess(w, map[string]interface{}{ "status": status.Health, "total_nodes": status.TotalNodes, "active_nodes": status.ActiveNodes, "syncing_nodes": status.SyncingNodes, "failed_nodes": status.FailedNodes, "replication_factor": status.ReplicationFactor, "leader_id": status.LeaderID, "checked_at": time.Now().UnixMilli(), }) } else if parts[0] == "nodes" { nodes := s.coordinator.GetAllNodes() s.sendSuccess(w, nodes) } else if parts[0] == "leader" { leader := s.coordinator.GetLeader() s.sendSuccess(w, leader) } else if parts[0] == "shards" { shards := s.coordinator.GetAllShards() s.sendSuccess(w, shards) } else if parts[0] == "pipeline" { stats := s.coordinator.GetPipelineStats() s.sendSuccess(w, stats) } else if parts[0] == "batch" { stats := s.coordinator.GetBatchCommitStats() s.sendSuccess(w, stats) } else if parts[0] == "resharding" { stats := s.coordinator.GetReshardingStats() s.sendSuccess(w, stats) } else { s.sendError(w, "Unknown cluster endpoint", http.StatusNotFound) } case http.MethodPost: if len(parts) >= 2 && parts[0] == "replication" && parts[1] == "factor" { var req struct { Factor int `json:"factor"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.sendError(w, "Invalid request body", http.StatusBadRequest) return } if req.Factor < 1 || req.Factor > 5 { s.sendError(w, "Replication factor must be between 1 and 5", http.StatusBadRequest) return } if err := s.coordinator.SetReplicationFactor(req.Factor); err != nil { s.logOperation("SET_REPLICATION_FACTOR", fmt.Sprintf("%d", req.Factor), "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusBadRequest) return } s.logOperation("SET_REPLICATION_FACTOR", fmt.Sprintf("%d", req.Factor), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "updated", "factor": req.Factor, }) } else if len(parts) >= 1 && parts[0] == "resharding" { var req struct { Reason string `json:"reason"` } json.NewDecoder(r.Body).Decode(&req) if req.Reason == "" { req.Reason = "api_trigger" } if err := s.coordinator.TriggerResharding(req.Reason); err != nil { s.logOperation("TRIGGER_RESHARDING", "", "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusBadRequest) return } s.logOperation("TRIGGER_RESHARDING", "", "success", "", map[string]interface{}{"reason": req.Reason}) s.sendSuccess(w, map[string]interface{}{ "status": "resharding_triggered", "reason": req.Reason, }) } else { s.sendError(w, "Unknown cluster endpoint", http.StatusNotFound) } default: s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) } } // handleTriggerRequest обрабатывает запросы к триггерам func (s *HTTPServer) handleTriggerRequest(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/trigger/") parts := strings.Split(path, "/") if len(parts) < 2 { s.sendError(w, "Invalid path. Use /api/trigger/{collection}/{action}", http.StatusBadRequest) return } collection := parts[0] action := parts[1] sessionID := s.getSessionID(r) if !s.aclManager.CheckPermission(sessionID, "", collection, "admin") { s.logOperation("TRIGGER_OPERATION", fmt.Sprintf("%s.%s", collection, action), "error", "Admin access required", nil) s.sendError(w, "Admin access required", http.StatusForbidden) return } tm := storage.GetTriggerManager() if tm == nil { storage.InitTriggerManager(s.logger) tm = storage.GetTriggerManager() } switch action { case "list": triggers := tm.ListTriggers(collection) s.sendSuccess(w, triggers) case "create": if r.Method != http.MethodPost { s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { Name string `json:"name"` Event string `json:"event"` Action string `json:"action"` Condition map[string]interface{} `json:"condition"` Description string `json:"description"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.sendError(w, "Invalid request body", http.StatusBadRequest) return } if req.Name == "" { s.sendError(w, "Trigger name required", http.StatusBadRequest) return } config := make(map[string]interface{}) config["action"] = req.Action config["description"] = req.Description if req.Condition != nil { config["condition"] = req.Condition } event := storage.TriggerEvent(req.Event) if event == "" { event = storage.TriggerAfterInsert } if err := tm.CreateTrigger("", collection, req.Name, event, config); err != nil { s.logOperation("CREATE_TRIGGER", fmt.Sprintf("%s.%s", collection, req.Name), "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusBadRequest) return } s.logOperation("CREATE_TRIGGER", fmt.Sprintf("%s.%s", collection, req.Name), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "trigger_created", "name": req.Name, }) case "drop": if len(parts) < 3 { s.sendError(w, "Trigger name required", http.StatusBadRequest) return } triggerName := parts[2] if err := tm.DropTrigger(collection, "", triggerName); err != nil { s.logOperation("DROP_TRIGGER", fmt.Sprintf("%s.%s", collection, triggerName), "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusNotFound) return } s.logOperation("DROP_TRIGGER", fmt.Sprintf("%s.%s", collection, triggerName), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "trigger_dropped", "name": triggerName, }) case "enable": if len(parts) < 3 { s.sendError(w, "Trigger name required", http.StatusBadRequest) return } triggerName := parts[2] if err := tm.EnableTrigger(collection, "", triggerName); err != nil { s.logOperation("ENABLE_TRIGGER", fmt.Sprintf("%s.%s", collection, triggerName), "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusNotFound) return } s.logOperation("ENABLE_TRIGGER", fmt.Sprintf("%s.%s", collection, triggerName), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "trigger_enabled", "name": triggerName, }) case "disable": if len(parts) < 3 { s.sendError(w, "Trigger name required", http.StatusBadRequest) return } triggerName := parts[2] if err := tm.DisableTrigger(collection, "", triggerName); err != nil { s.logOperation("DISABLE_TRIGGER", fmt.Sprintf("%s.%s", collection, triggerName), "error", err.Error(), nil) s.sendError(w, err.Error(), http.StatusNotFound) return } s.logOperation("DISABLE_TRIGGER", fmt.Sprintf("%s.%s", collection, triggerName), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "trigger_disabled", "name": triggerName, }) case "log": logs := tm.GetTriggerExecutionLog() filtered := make([]interface{}, 0) for _, entry := range logs { if entry.Collection == collection { filtered = append(filtered, entry) } } s.sendSuccess(w, filtered) default: s.sendError(w, "Unknown action", http.StatusBadRequest) } } // ============================================================================= // BATCH API (замена транзакций) // ============================================================================= // getSessionBatch возвращает активный batch для сессии (или nil). func (s *HTTPServer) getSessionBatch(sessionID string) (*storage.Batch, bool) { if sessionID == "" { return nil, false } val, ok := s.activeBatches.Load(sessionID) if !ok { return nil, false } batch, ok := val.(*storage.Batch) return batch, ok } // setSessionBatch сохраняет batch для сессии. func (s *HTTPServer) setSessionBatch(sessionID string, batch *storage.Batch) { if sessionID == "" || batch == nil { return } s.activeBatches.Store(sessionID, batch) } // clearSessionBatch снимает batch с сессии. func (s *HTTPServer) clearSessionBatch(sessionID string) { if sessionID == "" { return } s.activeBatches.Delete(sessionID) } // discardSessionBatches снимает все активные batch'и сессии. // Вызывается при logout. func (s *HTTPServer) discardSessionBatches(sessionID string) { if sessionID == "" { return } if batch, ok := s.getSessionBatch(sessionID); ok && batch != nil { // Снимаем регистрацию в глобальном менеджере (без коммита). if bm := storage.GetBatchManager(); bm != nil { bm.UnregisterBatch(batch) } s.clearSessionBatch(sessionID) } } // handleTransactionRequest обрабатывает запросы к batch-операциям. // // ИСПРАВЛЕНО (2026-10): переписано с транзакций на batch-операции. // URL-пути сохранены для совместимости с существующими клиентами: // - POST /api/transaction/begin — создать batch // - POST /api/transaction/commit — закоммитить batch // - POST /api/transaction/abort — отменить batch (без коммита) // - GET /api/transaction/ — список активных batch'ей сессии func (s *HTTPServer) handleTransactionRequest(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/api/transaction/") parts := strings.Split(path, "/") sessionID := s.getSessionID(r) if !s.aclManager.CheckPermission(sessionID, "*", "*", "write") { s.logOperation("BATCH_OPERATION", "", "error", "Write access required", nil) s.sendError(w, "Write access required", http.StatusForbidden) return } switch r.Method { case http.MethodGet: // Возвращаем список активных batch'ей сессии. active := make([]map[string]interface{}, 0) if batch, ok := s.getSessionBatch(sessionID); ok && batch != nil { active = append(active, map[string]interface{}{ "id": batch.ID, "database": batch.Database, "collection": batch.Collection, "operation_count": len(batch.Operations), "start_time": batch.Timestamp, }) } s.sendSuccess(w, active) case http.MethodPost: if len(parts) == 0 || parts[0] == "" { s.sendError(w, "Action required", http.StatusBadRequest) return } action := parts[0] switch action { case "begin": // Если у сессии уже есть активный batch — возвращаем ошибку. if existing, ok := s.getSessionBatch(sessionID); ok && existing != nil { s.sendError(w, "Session already has active batch", http.StatusConflict) return } // Читаем параметры: database, collection. var req struct { Database string `json:"database"` Collection string `json:"collection"` } _ = json.NewDecoder(r.Body).Decode(&req) batch := storage.NewBatch(req.Database, req.Collection) if batch == nil { s.logOperation("BEGIN_BATCH", "", "error", "failed to create batch", nil) s.sendError(w, "Failed to create batch", http.StatusInternalServerError) return } s.setSessionBatch(sessionID, batch) s.logOperation("BEGIN_BATCH", fmt.Sprintf("%d", batch.ID), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "batch_started", "id": batch.ID, "database": batch.Database, "collection": batch.Collection, }) case "commit": batch, ok := s.getSessionBatch(sessionID) if !ok || batch == nil { s.sendError(w, "No active batch", http.StatusBadRequest) return } if err := batch.Commit(); err != nil { s.logOperation("COMMIT_BATCH", fmt.Sprintf("%d", batch.ID), "error", err.Error(), nil) s.clearSessionBatch(sessionID) s.sendError(w, err.Error(), http.StatusInternalServerError) return } s.logOperation("COMMIT_BATCH", fmt.Sprintf("%d", batch.ID), "success", "", nil) s.clearSessionBatch(sessionID) s.sendSuccess(w, map[string]interface{}{ "status": "batch_committed", "id": batch.ID, }) case "abort": batch, ok := s.getSessionBatch(sessionID) if !ok || batch == nil { s.sendError(w, "No active batch", http.StatusBadRequest) return } // Снимаем регистрацию в глобальном менеджере. if bm := storage.GetBatchManager(); bm != nil { bm.UnregisterBatch(batch) } s.clearSessionBatch(sessionID) s.logOperation("ABORT_BATCH", fmt.Sprintf("%d", batch.ID), "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "batch_aborted", "id": batch.ID, }) default: s.sendError(w, "Unknown action", http.StatusBadRequest) } case http.MethodPut: if len(parts) < 2 { s.sendError(w, "Batch ID and action required", http.StatusBadRequest) return } batchIDStr := parts[0] action := parts[1] batch, ok := s.getSessionBatch(sessionID) if !ok || batch == nil { s.sendError(w, "No active batch", http.StatusBadRequest) return } // Проверяем, что ID совпадает (необязательно, но полезно). if fmt.Sprintf("%d", batch.ID) != batchIDStr { s.sendError(w, "Batch ID mismatch", http.StatusBadRequest) return } switch action { case "commit": if err := batch.Commit(); err != nil { s.logOperation("COMMIT_BATCH", batchIDStr, "error", err.Error(), nil) s.clearSessionBatch(sessionID) s.sendError(w, err.Error(), http.StatusInternalServerError) return } s.logOperation("COMMIT_BATCH", batchIDStr, "success", "", nil) s.clearSessionBatch(sessionID) s.sendSuccess(w, map[string]interface{}{ "status": "batch_committed", "id": batchIDStr, }) case "abort": if bm := storage.GetBatchManager(); bm != nil { bm.UnregisterBatch(batch) } s.clearSessionBatch(sessionID) s.logOperation("ABORT_BATCH", batchIDStr, "success", "", nil) s.sendSuccess(w, map[string]interface{}{ "status": "batch_aborted", "id": batchIDStr, }) default: s.sendError(w, "Unknown action", http.StatusBadRequest) } default: s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed) } } // handleHealthCheck обрабатывает проверку здоровья (без аутентификации) func (s *HTTPServer) handleHealthCheck(w http.ResponseWriter, r *http.Request) { status := "healthy" httpStatus := http.StatusOK if s.store == nil { status = "unhealthy" httpStatus = http.StatusServiceUnavailable } w.Header().Set("Content-Type", "application/json") w.WriteHeader(httpStatus) json.NewEncoder(w).Encode(map[string]interface{}{ "status": status, "timestamp": time.Now().UnixMilli(), "version": "1.0.0", "tls": s.tlsEnabled, }) } // handleMetricsEndpoint обрабатывает запрос метрик (JSON, без аутентификации). // Оставлен для совместимости с Grafana JSON API datasource. // Для Prometheus используйте /metrics. func (s *HTTPServer) handleMetricsEndpoint(w http.ResponseWriter, r *http.Request) { metrics := map[string]interface{}{ "timestamp": time.Now().UnixMilli(), "rate_limiter": s.rateLimiter.GetStats(), "store_stats": s.store.GetStats(), "tls_enabled": s.tlsEnabled, } if s.coordinator != nil { metrics["cluster"] = s.coordinator.GetClusterStatus() metrics["pipeline"] = s.coordinator.GetPipelineStats() metrics["batch_commit"] = s.coordinator.GetBatchCommitStats() metrics["resharding"] = s.coordinator.GetReshardingStats() } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(metrics) } // logOperation логирует операцию func (s *HTTPServer) logOperation(operation, target, status, errMsg string, details map[string]interface{}) { if s.logger == nil { return } logMsg := fmt.Sprintf("[API] %s: %s - %s", operation, target, status) if errMsg != "" { logMsg += " - " + errMsg } if status == "error" { s.logger.Error(logMsg) } else { s.logger.Info(logMsg) } } // sendSuccess отправляет успешный ответ func (s *HTTPServer) sendSuccess(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(APIResponse{ Success: true, Data: data, }) } // sendError отправляет ответ с ошибкой func (s *HTTPServer) sendError(w http.ResponseWriter, errMsg string, statusCode int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) json.NewEncoder(w).Encode(APIResponse{ Success: false, Error: errMsg, }) }