1741 lines
61 KiB
Go
1741 lines
61 KiB
Go
|
|
/*
|
|||
|
|
* 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 структуры.
|
|||
|
|
|
|||
|
|
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/log"
|
|||
|
|
"futriis/internal/storage"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// RateLimiterConfig содержит конфигурацию 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) {
|
|||
|
|
// Используем IP + User-Agent как ключ
|
|||
|
|
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)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 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 // map[string]string (sessionID -> username)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// APIResponse представляет стандартный ответ API
|
|||
|
|
type APIResponse struct {
|
|||
|
|
Success bool `json:"success"`
|
|||
|
|
Data interface{} `json:"data,omitempty"`
|
|||
|
|
Error string `json:"error,omitempty"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewHTTPServer создаёт новый HTTP сервер
|
|||
|
|
func NewHTTPServer(port int, store *storage.Storage, coord *cluster.RaftCoordinator, aclMgr *acl.ACLManager, logger *log.Logger) *HTTPServer {
|
|||
|
|
s := &HTTPServer{
|
|||
|
|
store: store,
|
|||
|
|
coordinator: coord,
|
|||
|
|
aclManager: aclMgr,
|
|||
|
|
logger: logger,
|
|||
|
|
port: port,
|
|||
|
|
rateLimiter: NewRateLimiterHTTP(DefaultRateLimiterConfigHTTP()),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
mux := http.NewServeMux()
|
|||
|
|
|
|||
|
|
// CORS middleware wrapper с rate limiting
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Применяем rate limiting
|
|||
|
|
s.rateLimiter.Middleware(handler)(w, r)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Middleware для аутентификации
|
|||
|
|
authMiddleware := func(handler http.HandlerFunc) http.HandlerFunc {
|
|||
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
sessionID := r.Header.Get("X-Session-ID")
|
|||
|
|
if sessionID == "" {
|
|||
|
|
// Также проверяем Authorization header
|
|||
|
|
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)))
|
|||
|
|
|
|||
|
|
// CRUD операции
|
|||
|
|
mux.HandleFunc("/api/db/", corsHandler(authMiddleware(s.handleDatabaseRequest)))
|
|||
|
|
|
|||
|
|
// Индексы
|
|||
|
|
mux.HandleFunc("/api/index/", corsHandler(authMiddleware(s.handleIndexRequest)))
|
|||
|
|
|
|||
|
|
// ACL
|
|||
|
|
mux.HandleFunc("/api/acl/", corsHandler(authMiddleware(s.handleACLRequest)))
|
|||
|
|
|
|||
|
|
// Constraints
|
|||
|
|
mux.HandleFunc("/api/constraint/", corsHandler(authMiddleware(s.handleConstraintRequest)))
|
|||
|
|
|
|||
|
|
// Cluster
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
s.server = &http.Server{
|
|||
|
|
Addr: fmt.Sprintf(":%d", port),
|
|||
|
|
Handler: mux,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return s
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Start запускает HTTP сервер
|
|||
|
|
func (s *HTTPServer) Start() error {
|
|||
|
|
s.logger.Info("Starting HTTP API server on port " + strconv.Itoa(s.port))
|
|||
|
|
return s.server.ListenAndServe()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Stop останавливает HTTP сервер
|
|||
|
|
func (s *HTTPServer) Stop() error {
|
|||
|
|
s.rateLimiter.Stop()
|
|||
|
|
return s.server.Close()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetRateLimiterStats возвращает статистику rate limiter'а
|
|||
|
|
func (s *HTTPServer) GetRateLimiterStats() map[string]interface{} {
|
|||
|
|
return s.rateLimiter.GetStats()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// isUserAdmin проверяет, является ли пользователь администратором через ACL
|
|||
|
|
func (s *HTTPServer) isUserAdmin(username string) bool {
|
|||
|
|
if s.aclManager == nil {
|
|||
|
|
return username == "admin"
|
|||
|
|
}
|
|||
|
|
// Проверяем через ACL - пользователь с ролью 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)
|
|||
|
|
|
|||
|
|
// Также сохраняем связь с оригинальным sessionID
|
|||
|
|
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)
|
|||
|
|
}
|
|||
|
|
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) {
|
|||
|
|
// URL: /api/db/{database}/{collection}/{document_id}
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Поиск по индексу или ID
|
|||
|
|
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 для жёсткого удаления
|
|||
|
|
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) {
|
|||
|
|
// URL: /api/index/{database}/{collection}/{action}
|
|||
|
|
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" {
|
|||
|
|
// Используем GetClusterStatus для получения информации о здоровье
|
|||
|
|
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)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// handleTransactionRequest обрабатывает запросы к транзакциям
|
|||
|
|
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("TRANSACTION_OPERATION", "", "error", "Write access required", nil)
|
|||
|
|
s.sendError(w, "Write access required", http.StatusForbidden)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
switch r.Method {
|
|||
|
|
case http.MethodGet:
|
|||
|
|
transactions := storage.GetActiveTransactions()
|
|||
|
|
s.sendSuccess(w, transactions)
|
|||
|
|
|
|||
|
|
case http.MethodPost:
|
|||
|
|
if len(parts) == 0 || parts[0] == "" {
|
|||
|
|
s.sendError(w, "Action required", http.StatusBadRequest)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
action := parts[0]
|
|||
|
|
switch action {
|
|||
|
|
case "begin":
|
|||
|
|
if err := storage.InitTransactionManager("futriis.wal"); err != nil {
|
|||
|
|
s.logOperation("BEGIN_TRANSACTION", "", "error", err.Error(), nil)
|
|||
|
|
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
databases := s.store.ListDatabases()
|
|||
|
|
if len(databases) == 0 {
|
|||
|
|
s.sendError(w, "No database available", http.StatusBadRequest)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
db, err := s.store.GetDatabase(databases[0])
|
|||
|
|
if err != nil {
|
|||
|
|
s.sendError(w, err.Error(), http.StatusBadRequest)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
collections := db.ListCollections()
|
|||
|
|
if len(collections) == 0 {
|
|||
|
|
s.sendError(w, "No collection available", http.StatusBadRequest)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
coll, err := db.GetCollection(collections[0])
|
|||
|
|
if err != nil {
|
|||
|
|
s.sendError(w, err.Error(), http.StatusBadRequest)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := storage.BeginTransactionOnCollection(coll); err != nil {
|
|||
|
|
s.logOperation("BEGIN_TRANSACTION", "", "error", err.Error(), nil)
|
|||
|
|
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
s.logOperation("BEGIN_TRANSACTION", "", "success", "", nil)
|
|||
|
|
s.sendSuccess(w, map[string]interface{}{
|
|||
|
|
"status": "transaction_started",
|
|||
|
|
"id": storage.GetCurrentTransactionID(),
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
case "commit":
|
|||
|
|
if err := storage.CommitCurrentTransaction(); err != nil {
|
|||
|
|
s.logOperation("COMMIT_TRANSACTION", "", "error", err.Error(), nil)
|
|||
|
|
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
s.logOperation("COMMIT_TRANSACTION", "", "success", "", nil)
|
|||
|
|
s.sendSuccess(w, map[string]interface{}{
|
|||
|
|
"status": "transaction_committed",
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
case "abort":
|
|||
|
|
if err := storage.AbortCurrentTransaction(); err != nil {
|
|||
|
|
s.logOperation("ABORT_TRANSACTION", "", "error", err.Error(), nil)
|
|||
|
|
s.sendError(w, err.Error(), http.StatusInternalServerError)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
s.logOperation("ABORT_TRANSACTION", "", "success", "", nil)
|
|||
|
|
s.sendSuccess(w, map[string]interface{}{
|
|||
|
|
"status": "transaction_aborted",
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
s.sendError(w, "Unknown action", http.StatusBadRequest)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
case http.MethodPut:
|
|||
|
|
if len(parts) < 2 {
|
|||
|
|
s.sendError(w, "Transaction ID and action required", http.StatusBadRequest)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
txID := parts[0]
|
|||
|
|
action := parts[1]
|
|||
|
|
|
|||
|
|
switch action {
|
|||
|
|
case "commit":
|
|||
|
|
// В реальной реализации здесь был бы двухфазный коммит
|
|||
|
|
s.logOperation("COMMIT_TRANSACTION", txID, "success", "", nil)
|
|||
|
|
s.sendSuccess(w, map[string]interface{}{
|
|||
|
|
"status": "transaction_committed",
|
|||
|
|
"id": txID,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
case "abort":
|
|||
|
|
s.logOperation("ABORT_TRANSACTION", txID, "success", "", nil)
|
|||
|
|
s.sendSuccess(w, map[string]interface{}{
|
|||
|
|
"status": "transaction_aborted",
|
|||
|
|
"id": txID,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
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",
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// handleMetricsEndpoint обрабатывает запрос метрик (без аутентификации)
|
|||
|
|
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(),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
})
|
|||
|
|
}
|