diff --git a/internal/api/webui.go b/internal/api/webui.go
deleted file mode 100644
index 9b25d6e..0000000
--- a/internal/api/webui.go
+++ /dev/null
@@ -1,3113 +0,0 @@
-/*
- * 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/webui.go
-// Назначение: Веб-интерфейс в стиле dashboard для управления СУБД futriis
-
-package api
-
-import (
- "bufio"
- "crypto/rand"
- "embed"
- "encoding/base64"
- "encoding/json"
- "fmt"
- "io/fs"
- "net/http"
- "os"
- "path/filepath"
- "runtime"
- "strconv"
- "strings"
- "sync"
- "time"
-
- "futriis/internal/acl"
- "futriis/internal/cluster"
- "futriis/internal/log"
- "futriis/internal/plugin"
- "futriis/internal/storage"
-)
-
-//go:embed static/*
-var staticFiles embed.FS
-
-// LogEntry представляет запись в логе веб-интерфейса
-type LogEntry struct {
- Timestamp int64 `json:"timestamp"`
- Operation string `json:"operation"`
- Target string `json:"target"`
- User string `json:"user"`
- Status string `json:"status"`
- ErrorMsg string `json:"error_msg,omitempty"`
- Details map[string]interface{} `json:"details,omitempty"`
-}
-
-// LogBuffer буферизирует логи веб-интерфейса
-type LogBuffer struct {
- entries []LogEntry
- mu sync.RWMutex
- maxSize int
- filePath string
- file *os.File
- writer *bufio.Writer
-}
-
-// NewLogBuffer создаёт новый буфер логов
-func NewLogBuffer(filePath string, maxSize int) (*LogBuffer, error) {
- dir := filepath.Dir(filePath)
- if err := os.MkdirAll(dir, 0755); err != nil {
- return nil, fmt.Errorf("failed to create log directory: %v", err)
- }
-
- file, err := os.OpenFile(filePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
- if err != nil {
- return nil, fmt.Errorf("failed to open log file: %v", err)
- }
-
- return &LogBuffer{
- entries: make([]LogEntry, 0, maxSize),
- maxSize: maxSize,
- filePath: filePath,
- file: file,
- writer: bufio.NewWriter(file),
- }, nil
-}
-
-// Add добавляет запись в лог
-func (lb *LogBuffer) Add(entry LogEntry) {
- lb.mu.Lock()
- defer lb.mu.Unlock()
-
- lb.entries = append(lb.entries, entry)
- if len(lb.entries) > lb.maxSize {
- lb.entries = lb.entries[len(lb.entries)-lb.maxSize:]
- }
-
- data, err := json.Marshal(entry)
- if err == nil {
- lb.writer.Write(data)
- lb.writer.Write([]byte("\n"))
- lb.writer.Flush()
- }
-}
-
-// GetEntries возвращает все записи лога
-func (lb *LogBuffer) GetEntries() []LogEntry {
- lb.mu.RLock()
- defer lb.mu.RUnlock()
-
- result := make([]LogEntry, len(lb.entries))
- copy(result, lb.entries)
- return result
-}
-
-// Close закрывает буфер логов
-func (lb *LogBuffer) Close() error {
- if lb.writer != nil {
- lb.writer.Flush()
- }
- if lb.file != nil {
- return lb.file.Close()
- }
- return nil
-}
-
-// ========== SystemMetrics ==========
-
-// SystemMetrics представляет системные метрики
-type SystemMetrics struct {
- mu sync.RWMutex
- RequestCount uint64 `json:"request_count"`
- ErrorCount uint64 `json:"error_count"`
- AverageLatencyMs float64 `json:"average_latency_ms"`
- P99LatencyMs float64 `json:"p99_latency_ms"`
- ActiveConnections int `json:"active_connections"`
- GoroutineCount int `json:"goroutine_count"`
- MemoryAllocBytes uint64 `json:"memory_alloc_bytes"`
- MemoryTotalBytes uint64 `json:"memory_total_bytes"`
- GCPerSecond float64 `json:"gc_per_second"`
- CPUPercent float64 `json:"cpu_percent"`
- StartTime int64 `json:"start_time"`
- LastMetricTime int64 `json:"last_metric_time"`
- EndpointMetrics map[string]*EndpointMetric `json:"endpoint_metrics"`
- latencyHistory []float64
- maxHistorySize int
-}
-
-// EndpointMetric представляет метрики для конкретного эндпоинта
-type EndpointMetric struct {
- Count uint64 `json:"count"`
- ErrorCount uint64 `json:"error_count"`
- TotalLatencyMs float64 `json:"total_latency_ms"`
- AvgLatencyMs float64 `json:"avg_latency_ms"`
- MaxLatencyMs float64 `json:"max_latency_ms"`
- MinLatencyMs float64 `json:"min_latency_ms"`
-}
-
-// NewSystemMetrics создаёт новый сборщик метрик
-func NewSystemMetrics() *SystemMetrics {
- return &SystemMetrics{
- StartTime: time.Now().UnixMilli(),
- EndpointMetrics: make(map[string]*EndpointMetric),
- latencyHistory: make([]float64, 0, 1000),
- maxHistorySize: 10000,
- }
-}
-
-// sortFloat64sLocal локальная функция сортировки
-func sortFloat64sLocal(a []float64) {
- for i := 0; i < len(a)-1; i++ {
- for j := i + 1; j < len(a); j++ {
- if a[i] > a[j] {
- a[i], a[j] = a[j], a[i]
- }
- }
- }
-}
-
-// RecordRequest записывает информацию о запросе
-func (sm *SystemMetrics) RecordRequest(endpoint string, latencyMs float64, isError bool) {
- sm.mu.Lock()
- defer sm.mu.Unlock()
-
- sm.RequestCount++
- if isError {
- sm.ErrorCount++
- }
-
- em, exists := sm.EndpointMetrics[endpoint]
- if !exists {
- em = &EndpointMetric{
- MinLatencyMs: latencyMs,
- MaxLatencyMs: latencyMs,
- }
- sm.EndpointMetrics[endpoint] = em
- }
-
- em.Count++
- if isError {
- em.ErrorCount++
- }
- em.TotalLatencyMs += latencyMs
- em.AvgLatencyMs = em.TotalLatencyMs / float64(em.Count)
- if latencyMs > em.MaxLatencyMs {
- em.MaxLatencyMs = latencyMs
- }
- if latencyMs < em.MinLatencyMs {
- em.MinLatencyMs = latencyMs
- }
-
- sm.AverageLatencyMs = (sm.AverageLatencyMs*float64(sm.RequestCount-1) + latencyMs) / float64(sm.RequestCount)
-
- sm.latencyHistory = append(sm.latencyHistory, latencyMs)
- if len(sm.latencyHistory) > sm.maxHistorySize {
- sm.latencyHistory = sm.latencyHistory[len(sm.latencyHistory)-sm.maxHistorySize:]
- }
-
- if len(sm.latencyHistory) > 0 {
- sorted := make([]float64, len(sm.latencyHistory))
- copy(sorted, sm.latencyHistory)
- sortFloat64sLocal(sorted)
- p99Index := int(float64(len(sorted)) * 0.99)
- if p99Index < len(sorted) {
- sm.P99LatencyMs = sorted[p99Index]
- }
- }
-}
-
-// UpdateSystemInfo обновляет системную информацию
-func (sm *SystemMetrics) UpdateSystemInfo() {
- sm.mu.Lock()
- defer sm.mu.Unlock()
-
- var memStats runtime.MemStats
- runtime.ReadMemStats(&memStats)
-
- sm.MemoryAllocBytes = memStats.Alloc
- sm.MemoryTotalBytes = memStats.TotalAlloc
- sm.GoroutineCount = runtime.NumGoroutine()
- sm.LastMetricTime = time.Now().UnixMilli()
-
- gcPauseTotal := memStats.PauseTotalNs
- timeDiff := float64(sm.LastMetricTime - sm.StartTime) / 1000.0
- if timeDiff > 0 {
- sm.GCPerSecond = float64(gcPauseTotal) / timeDiff / 1e9
- }
-}
-
-// GetMetrics возвращает копию метрик
-func (sm *SystemMetrics) GetMetrics() map[string]interface{} {
- sm.mu.RLock()
- defer sm.mu.RUnlock()
-
- sm.UpdateSystemInfo()
-
- return map[string]interface{}{
- "request_count": sm.RequestCount,
- "error_count": sm.ErrorCount,
- "average_latency_ms": sm.AverageLatencyMs,
- "p99_latency_ms": sm.P99LatencyMs,
- "active_connections": sm.ActiveConnections,
- "goroutine_count": sm.GoroutineCount,
- "memory_alloc_bytes": sm.MemoryAllocBytes,
- "memory_total_bytes": sm.MemoryTotalBytes,
- "gc_per_second": sm.GCPerSecond,
- "start_time": sm.StartTime,
- "last_metric_time": sm.LastMetricTime,
- "uptime_seconds": (time.Now().UnixMilli() - sm.StartTime) / 1000,
- "endpoint_metrics": sm.EndpointMetrics,
- }
-}
-
-// RecordConnection записывает изменение количества соединений
-func (sm *SystemMetrics) RecordConnection(delta int) {
- sm.mu.Lock()
- defer sm.mu.Unlock()
- sm.ActiveConnections += delta
-}
-
-// ========== Health Check ==========
-
-// HealthStatus представляет статус здоровья системы
-type HealthStatus struct {
- Status string `json:"status"`
- Timestamp int64 `json:"timestamp"`
- Components map[string]string `json:"components"`
- Details map[string]interface{} `json:"details"`
- Message string `json:"message,omitempty"`
-}
-
-// HealthChecker проверяет здоровье компонентов
-type HealthChecker struct {
- store *storage.Storage
- coordinator *cluster.RaftCoordinator
- logger *log.Logger
- mu sync.RWMutex
- lastStatus *HealthStatus
-}
-
-// NewHealthChecker создаёт новый проверяльщик здоровья
-func NewHealthChecker(store *storage.Storage, coord *cluster.RaftCoordinator, logger *log.Logger) *HealthChecker {
- hc := &HealthChecker{
- store: store,
- coordinator: coord,
- logger: logger,
- }
-
- go hc.periodicCheck()
-
- return hc
-}
-
-// periodicCheck периодически проверяет здоровье
-func (hc *HealthChecker) periodicCheck() {
- ticker := time.NewTicker(30 * time.Second)
- defer ticker.Stop()
-
- for range ticker.C {
- hc.Check()
- }
-}
-
-// Check выполняет проверку здоровья
-func (hc *HealthChecker) Check() *HealthStatus {
- hc.mu.Lock()
- defer hc.mu.Unlock()
-
- status := &HealthStatus{
- Timestamp: time.Now().UnixMilli(),
- Components: make(map[string]string),
- Details: make(map[string]interface{}),
- }
-
- overallStatus := "healthy"
-
- if hc.store == nil {
- status.Components["storage"] = "unavailable"
- overallStatus = "unhealthy"
- } else {
- databases := hc.store.ListDatabases()
- status.Details["database_count"] = len(databases)
- if len(databases) >= 0 {
- status.Components["storage"] = "healthy"
- } else {
- status.Components["storage"] = "degraded"
- if overallStatus == "healthy" {
- overallStatus = "degraded"
- }
- }
- }
-
- if hc.coordinator == nil {
- status.Components["cluster"] = "disabled"
- } else {
- clusterStatus := hc.coordinator.GetClusterStatus()
- status.Details["cluster_health"] = clusterStatus.Health
- status.Details["active_nodes"] = clusterStatus.ActiveNodes
- status.Details["total_nodes"] = clusterStatus.TotalNodes
-
- if clusterStatus.Health == "critical" {
- status.Components["cluster"] = "critical"
- overallStatus = "unhealthy"
- } else if clusterStatus.Health == "degraded" {
- status.Components["cluster"] = "degraded"
- if overallStatus == "healthy" {
- overallStatus = "degraded"
- }
- } else {
- status.Components["cluster"] = "healthy"
- }
- }
-
- status.Status = overallStatus
- hc.lastStatus = status
-
- return status
-}
-
-// GetLastStatus возвращает последний статус здоровья
-func (hc *HealthChecker) GetLastStatus() *HealthStatus {
- hc.mu.RLock()
- defer hc.mu.RUnlock()
- return hc.lastStatus
-}
-
-// ========== WebUIServer ==========
-
-// WebUIServer представляет сервер веб-интерфейса
-type WebUIServer struct {
- store *storage.Storage
- coordinator *cluster.RaftCoordinator
- aclManager *acl.ACLManager
- logger *log.Logger
- server *http.Server
- port int
- enabled bool
- credentialMgr *CredentialManager
- logBuffer *LogBuffer
- pluginManager *plugin.PluginManager
- metrics *SystemMetrics
- healthChecker *HealthChecker
- rateLimiter *RateLimiter
-}
-
-// NewWebUIServer создаёт новый веб-сервер интерфейса
-func NewWebUIServer(port int, enabled bool, store *storage.Storage, coord *cluster.RaftCoordinator, aclMgr *acl.ACLManager, logger *log.Logger) *WebUIServer {
- credMgr := NewCredentialManager()
-
- if err := credMgr.Load(); err != nil {
- logger.Warn(fmt.Sprintf("Failed to load credentials: %v, using defaults", err))
- if err := credMgr.CreateDefault(); err != nil {
- logger.Error(fmt.Sprintf("Failed to create default credentials: %v", err))
- }
- }
-
- logBuffer, err := NewLogBuffer("futriis/webui.log", 10000)
- if err != nil {
- logger.Warn(fmt.Sprintf("Failed to create log buffer: %v", err))
- logBuffer = &LogBuffer{entries: make([]LogEntry, 0, 1000)}
- }
-
- pluginManager := plugin.NewPluginManager("futriis/plugins", logger, store, true)
-
- w := &WebUIServer{
- store: store,
- coordinator: coord,
- aclManager: aclMgr,
- logger: logger,
- port: port,
- enabled: enabled,
- credentialMgr: credMgr,
- logBuffer: logBuffer,
- pluginManager: pluginManager,
- metrics: NewSystemMetrics(),
- rateLimiter: NewRateLimiter(DefaultRateLimiterConfig()),
- }
-
- w.healthChecker = NewHealthChecker(store, coord, logger)
-
- return w
-}
-
-// generateSecureSessionID генерирует криптостойкий session ID
-func generateSecureSessionID(username string) string {
- // 32 байта = 256 бит энтропии
- bytes := make([]byte, 32)
- _, err := rand.Read(bytes)
- if err != nil {
- // Fallback: используем комбинацию времени + username + random
- return fmt.Sprintf("fallback_%d_%s", time.Now().UnixNano(), username)
- }
-
- // Кодируем в base64 URL-safe
- sessionID := base64.URLEncoding.EncodeToString(bytes)
-
- // Добавляем username для трассировки (не влияет на безопасность)
- return fmt.Sprintf("%s_%s", sessionID[:24], username)
-}
-
-// middlewareWithMetrics добавляет метрики к обработчикам
-func (w *WebUIServer) middlewareWithMetrics(handler http.HandlerFunc, endpoint string) http.HandlerFunc {
- return func(wr http.ResponseWriter, r *http.Request) {
- start := time.Now()
-
- rw := &responseWriter{ResponseWriter: wr, statusCode: http.StatusOK}
-
- // Применяем rate limiting
- if !w.rateLimiter.Allow(r.RemoteAddr) {
- w.rateLimiter.rejected.Add(1)
- wr.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", w.rateLimiter.config.RequestsPerSecond))
- wr.Header().Set("X-RateLimit-Remaining", "0")
- wr.Header().Set("Retry-After", "1")
- http.Error(wr, "Rate limit exceeded. Please try again later.", http.StatusTooManyRequests)
- return
- }
- w.rateLimiter.allowed.Add(1)
-
- handler(rw, r)
-
- latency := float64(time.Since(start).Microseconds()) / 1000.0
- isError := rw.statusCode >= 400
-
- w.metrics.RecordRequest(endpoint, latency, isError)
-
- if w.logger != nil && isError {
- w.logger.Debugf("[METRICS] %s %s: %d in %.2fms", r.Method, endpoint, rw.statusCode, latency)
- }
- }
-}
-
-// responseWriter захватывает статус код
-type responseWriter struct {
- http.ResponseWriter
- statusCode int
-}
-
-func (rw *responseWriter) WriteHeader(code int) {
- rw.statusCode = code
- rw.ResponseWriter.WriteHeader(code)
-}
-
-// logOperation логирует операцию веб-интерфейса
-func (w *WebUIServer) logOperation(operation, target, status, errMsg string, details map[string]interface{}) {
- entry := LogEntry{
- Timestamp: time.Now().UnixMilli(),
- Operation: operation,
- Target: target,
- User: w.credentialMgr.GetCurrentUsername(),
- Status: status,
- ErrorMsg: errMsg,
- Details: details,
- }
-
- if w.logBuffer != nil {
- w.logBuffer.Add(entry)
- }
-
- if w.logger != nil {
- if status == "error" {
- w.logger.Error(fmt.Sprintf("[WEBUI] %s: %s - %s", operation, target, errMsg))
- } else {
- w.logger.Info(fmt.Sprintf("[WEBUI] %s: %s", operation, target))
- }
- }
-}
-
-// Start запускает веб-сервер интерфейса
-func (w *WebUIServer) Start() error {
- if !w.enabled {
- w.logger.Info("Web UI is disabled in configuration")
- return nil
- }
-
- mux := http.NewServeMux()
-
- staticFS, err := fs.Sub(staticFiles, "static")
- if err != nil {
- return fmt.Errorf("failed to load static files: %v", err)
- }
- mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
-
- mux.HandleFunc("/", w.handleWebIndex)
-
- // Observability endpoints
- mux.HandleFunc("/api/webui/metrics", w.middlewareWithMetrics(w.handleMetrics, "metrics"))
- mux.HandleFunc("/api/webui/health", w.middlewareWithMetrics(w.handleHealth, "health"))
- mux.HandleFunc("/api/webui/pipeline/status", w.middlewareWithMetrics(w.handlePipelineStatus, "pipeline_status"))
- mux.HandleFunc("/api/webui/resharding/trigger", w.middlewareWithMetrics(w.handleReshardingTrigger, "resharding_trigger"))
-
- // API для веб-интерфейса
- mux.HandleFunc("/api/webui/databases", w.middlewareWithMetrics(w.handleGetDatabases, "databases"))
- mux.HandleFunc("/api/webui/collections/", w.middlewareWithMetrics(w.handleGetCollections, "collections"))
- mux.HandleFunc("/api/webui/documents/", w.middlewareWithMetrics(w.handleDocuments, "documents"))
- mux.HandleFunc("/api/webui/cluster/status", w.middlewareWithMetrics(w.handleClusterStatus, "cluster_status"))
- mux.HandleFunc("/api/webui/cluster/nodes", w.middlewareWithMetrics(w.handleClusterNodes, "cluster_nodes"))
- mux.HandleFunc("/api/webui/stats", w.middlewareWithMetrics(w.handleStats, "stats"))
- mux.HandleFunc("/api/webui/login", w.middlewareWithMetrics(w.handleWebLogin, "login"))
- mux.HandleFunc("/api/webui/logout", w.middlewareWithMetrics(w.handleWebLogout, "logout"))
- mux.HandleFunc("/api/webui/session", w.middlewareWithMetrics(w.handleSessionCheck, "session"))
- mux.HandleFunc("/api/webui/change-password", w.middlewareWithMetrics(w.handleChangePassword, "change_password"))
- mux.HandleFunc("/api/webui/user/avatar", w.middlewareWithMetrics(w.handleUserAvatar, "user_avatar"))
- mux.HandleFunc("/api/webui/user/info", w.middlewareWithMetrics(w.handleUserInfo, "user_info"))
- mux.HandleFunc("/api/webui/admin/users", w.middlewareWithMetrics(w.handleAdminUsers, "admin_users"))
- mux.HandleFunc("/api/webui/logs", w.middlewareWithMetrics(w.handleWebLogs, "logs"))
- mux.HandleFunc("/api/webui/plugins", w.middlewareWithMetrics(w.handlePlugins, "plugins"))
- mux.HandleFunc("/api/webui/plugin/", w.middlewareWithMetrics(w.handlePluginOperation, "plugin_operation"))
- mux.HandleFunc("/api/webui/acl/users", w.middlewareWithMetrics(w.handleACLUsers, "acl_users"))
- mux.HandleFunc("/api/webui/acl/roles", w.middlewareWithMetrics(w.handleACLRoles, "acl_roles"))
- mux.HandleFunc("/api/webui/acl/permissions", w.middlewareWithMetrics(w.handleACLPermissions, "acl_permissions"))
- mux.HandleFunc("/api/webui/acl/user/", w.middlewareWithMetrics(w.handleACLUser, "acl_user"))
- mux.HandleFunc("/api/webui/acl/role/", w.middlewareWithMetrics(w.handleACLRole, "acl_role"))
- mux.HandleFunc("/api/webui/transactions", w.middlewareWithMetrics(w.handleTransactions, "transactions"))
- mux.HandleFunc("/api/webui/transaction/", w.middlewareWithMetrics(w.handleTransactionAction, "transaction_action"))
- mux.HandleFunc("/api/webui/indexes/", w.middlewareWithMetrics(w.handleIndexesList, "indexes_list"))
- mux.HandleFunc("/api/webui/index/", w.middlewareWithMetrics(w.handleIndexOperation, "index_operation"))
- mux.HandleFunc("/api/webui/export", w.middlewareWithMetrics(w.handleExportData, "export"))
- mux.HandleFunc("/api/webui/import", w.middlewareWithMetrics(w.handleImportData, "import"))
- mux.HandleFunc("/api/webui/triggers/", w.middlewareWithMetrics(w.handleTriggers, "triggers"))
- mux.HandleFunc("/api/webui/trigger/", w.middlewareWithMetrics(w.handleTriggerOperation, "trigger_operation"))
- mux.HandleFunc("/api/webui/trigger/log", w.middlewareWithMetrics(w.handleTriggerLog, "trigger_log"))
- mux.HandleFunc("/api/webui/constraints/", w.middlewareWithMetrics(w.handleConstraintsList, "constraints_list"))
- mux.HandleFunc("/api/webui/constraint/", w.middlewareWithMetrics(w.handleConstraintOperation, "constraint_operation"))
-
- w.server = &http.Server{
- Addr: fmt.Sprintf(":%d", w.port),
- Handler: mux,
- ReadTimeout: 30 * time.Second,
- WriteTimeout: 30 * time.Second,
- }
-
- w.logger.Info(fmt.Sprintf("Web UI started on port %d with observability enabled", w.port))
- return w.server.ListenAndServe()
-}
-
-// Stop останавливает веб-сервер
-func (w *WebUIServer) Stop() error {
- if w.logBuffer != nil {
- w.logBuffer.Close()
- }
- if w.server != nil {
- return w.server.Close()
- }
- return nil
-}
-
-// checkAuth проверяет аутентификацию
-func (w *WebUIServer) checkAuth(r *http.Request) bool {
- sessionID := w.getSessionID(r)
- return sessionID != ""
-}
-
-// getSessionID возвращает ID сессии из cookie
-func (w *WebUIServer) getSessionID(r *http.Request) string {
- if cookie, err := r.Cookie("session_id"); err == nil {
- return cookie.Value
- }
- return ""
-}
-
-// sendJSONSuccess отправляет успешный JSON ответ
-func (w *WebUIServer) sendJSONSuccess(wr http.ResponseWriter, data interface{}) {
- wr.Header().Set("Content-Type", "application/json")
- wr.WriteHeader(http.StatusOK)
- json.NewEncoder(wr).Encode(map[string]interface{}{
- "success": true,
- "data": data,
- })
-}
-
-// sendJSONError отправляет JSON ответ с ошибкой
-func (w *WebUIServer) sendJSONError(wr http.ResponseWriter, errMsg string, statusCode int) {
- wr.Header().Set("Content-Type", "application/json")
- wr.WriteHeader(statusCode)
- json.NewEncoder(wr).Encode(map[string]interface{}{
- "success": false,
- "error": errMsg,
- })
-}
-
-// handleWebIndex возвращает главную HTML страницу
-func (w *WebUIServer) handleWebIndex(wr http.ResponseWriter, r *http.Request) {
- if r.URL.Path != "/" {
- http.NotFound(wr, r)
- return
- }
-
- html := `
-
-
-
-
- Futriis Database Management System
-
-
-
-
-
-
-
-
-
-
- Панель управления futriis 3i² (02.04.2026)
-
- Подключено
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`
-
- wr.Header().Set("Content-Type", "text/html; charset=utf-8")
- wr.Write([]byte(html))
-}
-
-// ========== Observability Handlers ==========
-
-// handleMetrics возвращает метрики системы
-func (w *WebUIServer) handleMetrics(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- metrics := w.metrics.GetMetrics()
-
- if w.coordinator != nil {
- metrics["pipeline_stats"] = w.coordinator.GetPipelineStats()
- metrics["batch_commit_stats"] = w.coordinator.GetBatchCommitStats()
- metrics["resharding_stats"] = w.coordinator.GetReshardingStats()
- metrics["joint_consensus_status"] = w.coordinator.GetJointConsensusStatus()
- }
-
- metrics["rate_limiter_stats"] = w.rateLimiter.GetStats()
-
- w.sendJSONSuccess(wr, metrics)
-}
-
-// handleHealth возвращает статус здоровья
-func (w *WebUIServer) handleHealth(wr http.ResponseWriter, r *http.Request) {
- status := w.healthChecker.Check()
- w.sendJSONSuccess(wr, status)
-}
-
-// handlePipelineStatus возвращает статус пайплайна
-func (w *WebUIServer) handlePipelineStatus(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- if w.coordinator == nil {
- w.sendJSONError(wr, "Cluster not available", http.StatusServiceUnavailable)
- return
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "pipeline": w.coordinator.GetPipelineStats(),
- "batch_commit": w.coordinator.GetBatchCommitStats(),
- "resharding": w.coordinator.GetReshardingStats(),
- "joint_consensus": w.coordinator.GetJointConsensusStatus(),
- })
-}
-
-// handleReshardingTrigger запускает перераспределение шардов
-func (w *WebUIServer) handleReshardingTrigger(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- if w.coordinator == nil {
- w.sendJSONError(wr, "Cluster not available", http.StatusServiceUnavailable)
- return
- }
-
- username := w.credentialMgr.GetCurrentUsername()
- if !w.credentialMgr.IsAdmin(username) {
- w.sendJSONError(wr, "Forbidden", http.StatusForbidden)
- return
- }
-
- var req struct {
- Reason string `json:"reason"`
- }
- json.NewDecoder(r.Body).Decode(&req)
-
- if req.Reason == "" {
- req.Reason = "manual_trigger"
- }
-
- if err := w.coordinator.TriggerResharding(req.Reason); err != nil {
- w.logOperation("RESHARDING_TRIGGER", "", "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusInternalServerError)
- return
- }
-
- w.logOperation("RESHARDING_TRIGGER", "", "success", "", map[string]interface{}{
- "reason": req.Reason,
- })
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "resharding_triggered",
- "reason": req.Reason,
- })
-}
-
-// ========== Authentication Handlers ==========
-
-// handleWebLogin обрабатывает вход в веб-интерфейс
-func (w *WebUIServer) handleWebLogin(wr http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- w.sendJSONError(wr, "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 {
- w.sendJSONError(wr, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- w.logger.Debug(fmt.Sprintf("Login attempt: username=%s", creds.Username))
-
- if !w.credentialMgr.Validate(creds.Username, creds.Password) {
- w.logger.Debug(fmt.Sprintf("Authentication failed for %s", creds.Username))
- w.logOperation("LOGIN", creds.Username, "error", "Invalid credentials", nil)
- w.sendJSONError(wr, "Неверный логин и/или пароль", http.StatusUnauthorized)
- return
- }
-
- // Криптостойкая генерация session ID
- sessionID := generateSecureSessionID(creds.Username)
-
- w.credentialMgr.SetCurrentUsername(creds.Username)
- w.credentialMgr.UpdateLastLogin(creds.Username)
-
- w.logger.Debug(fmt.Sprintf("Authentication successful for %s, sessionID=%s", creds.Username, sessionID[:20]+"..."))
- w.logOperation("LOGIN", creds.Username, "success", "", nil)
-
- // Устанавливаем secure cookie
- http.SetCookie(wr, &http.Cookie{
- Name: "session_id",
- Value: sessionID,
- Path: "/",
- MaxAge: 86400,
- HttpOnly: true,
- Secure: true,
- SameSite: http.SameSiteStrictMode,
- })
-
- avatarData := ""
- if avatar, err := w.credentialMgr.GetAvatar(creds.Username); err == nil && avatar != "" {
- avatarData = avatar
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "session_id": sessionID,
- "username": creds.Username,
- "avatar": avatarData,
- "is_admin": w.credentialMgr.IsAdmin(creds.Username),
- })
-}
-
-// handleWebLogout обрабатывает выход из веб-интерфейса
-func (w *WebUIServer) handleWebLogout(wr http.ResponseWriter, r *http.Request) {
- username := w.credentialMgr.GetCurrentUsername()
- w.logOperation("LOGOUT", username, "success", "", nil)
-
- http.SetCookie(wr, &http.Cookie{
- Name: "session_id",
- Value: "",
- Path: "/",
- MaxAge: -1,
- HttpOnly: true,
- Secure: true,
- })
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "logged out",
- })
-}
-
-// handleSessionCheck проверяет активность сессии
-func (w *WebUIServer) handleSessionCheck(wr http.ResponseWriter, r *http.Request) {
- sessionID := w.getSessionID(r)
- if sessionID == "" {
- w.sendJSONError(wr, "No session", http.StatusUnauthorized)
- return
- }
-
- username := w.credentialMgr.GetCurrentUsername()
- if username == "" {
- username = "admin"
- }
-
- connectionStatus := "connected"
- if w.coordinator == nil {
- connectionStatus = "disconnected"
- } else if status := w.coordinator.GetClusterStatus(); status.Health == "critical" {
- connectionStatus = "disconnected"
- }
-
- avatarData := ""
- if avatar, err := w.credentialMgr.GetAvatar(username); err == nil && avatar != "" {
- avatarData = avatar
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "authenticated": true,
- "username": username,
- "avatar": avatarData,
- "is_admin": w.credentialMgr.IsAdmin(username),
- "connection_status": connectionStatus,
- })
-}
-
-// handleChangePassword обрабатывает смену пароля
-func (w *WebUIServer) handleChangePassword(wr http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- sessionID := w.getSessionID(r)
- if sessionID == "" {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- var req struct {
- CurrentPassword string `json:"current_password"`
- NewPassword string `json:"new_password"`
- }
-
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- w.sendJSONError(wr, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- if req.CurrentPassword == "" || req.NewPassword == "" {
- w.sendJSONError(wr, "Current and new password are required", http.StatusBadRequest)
- return
- }
-
- if len(req.NewPassword) < 4 {
- w.sendJSONError(wr, "New password must be at least 4 characters", http.StatusBadRequest)
- return
- }
-
- username := w.credentialMgr.GetCurrentUsername()
- if username == "" {
- username = "admin"
- }
-
- if err := w.credentialMgr.ChangePassword(username, req.CurrentPassword, req.NewPassword); err != nil {
- w.logOperation("CHANGE_PASSWORD", username, "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- w.logger.Info(fmt.Sprintf("Password changed for user: %s", username))
- w.logOperation("CHANGE_PASSWORD", username, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "password_changed",
- "message": "Пароль успешно изменён",
- })
-}
-
-// handleUserAvatar обрабатывает загрузку и получение аватара пользователя
-func (w *WebUIServer) handleUserAvatar(wr http.ResponseWriter, r *http.Request) {
- sessionID := w.getSessionID(r)
- if sessionID == "" {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- username := w.credentialMgr.GetCurrentUsername()
- if username == "" {
- username = "admin"
- }
-
- switch r.Method {
- case http.MethodGet:
- avatar, err := w.credentialMgr.GetAvatar(username)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
- w.sendJSONSuccess(wr, map[string]interface{}{
- "avatar": avatar,
- })
-
- case http.MethodPost:
- if err := r.ParseMultipartForm(2 << 20); err != nil {
- w.sendJSONError(wr, "Failed to parse form: "+err.Error(), http.StatusBadRequest)
- return
- }
-
- file, handler, err := r.FormFile("avatar")
- if err != nil {
- w.sendJSONError(wr, "Failed to get avatar file: "+err.Error(), http.StatusBadRequest)
- return
- }
- defer file.Close()
-
- if handler.Size > 2<<20 {
- w.sendJSONError(wr, "Avatar file too large (max 2MB)", http.StatusBadRequest)
- return
- }
-
- contentType := handler.Header.Get("Content-Type")
- if contentType != "image/jpeg" && contentType != "image/png" && contentType != "image/gif" {
- w.sendJSONError(wr, "Invalid image type. Use JPEG, PNG or GIF", http.StatusBadRequest)
- return
- }
-
- fileData := make([]byte, handler.Size)
- if _, err := file.Read(fileData); err != nil {
- w.sendJSONError(wr, "Failed to read file: "+err.Error(), http.StatusInternalServerError)
- return
- }
-
- avatarBase64 := "data:" + contentType + ";base64," + base64.StdEncoding.EncodeToString(fileData)
-
- if err := w.credentialMgr.SetAvatar(username, avatarBase64); err != nil {
- w.sendJSONError(wr, "Failed to save avatar: "+err.Error(), http.StatusInternalServerError)
- return
- }
-
- w.logger.Info(fmt.Sprintf("Avatar uploaded for user: %s", username))
- w.logOperation("AVATAR_UPLOAD", username, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "avatar_uploaded",
- "avatar": avatarBase64,
- })
-
- case http.MethodDelete:
- if err := w.credentialMgr.DeleteAvatar(username); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
- w.logOperation("AVATAR_DELETE", username, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "avatar_deleted",
- })
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}
-
-// handleUserInfo возвращает информацию о пользователе
-func (w *WebUIServer) handleUserInfo(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- username := w.credentialMgr.GetCurrentUsername()
- if username == "" {
- username = "admin"
- }
-
- avatar, _ := w.credentialMgr.GetAvatar(username)
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "username": username,
- "avatar": avatar,
- "is_admin": w.credentialMgr.IsAdmin(username),
- })
-}
-
-// ========== Database Handlers ==========
-
-// handleGetDatabases возвращает список баз данных
-func (w *WebUIServer) handleGetDatabases(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- databases := w.store.ListDatabases()
-
- dbInfo := make([]map[string]interface{}, 0)
- for _, dbName := range databases {
- db, err := w.store.GetDatabase(dbName)
- if err == nil {
- collections := db.ListCollections()
- dbInfo = append(dbInfo, map[string]interface{}{
- "name": dbName,
- "collections": len(collections),
- "collections_list": collections,
- })
- }
- }
-
- w.sendJSONSuccess(wr, dbInfo)
-}
-
-// handleGetCollections возвращает список коллекций в базе данных
-func (w *WebUIServer) handleGetCollections(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/collections/")
- parts := strings.Split(path, "/")
-
- if len(parts) < 1 || parts[0] == "" {
- w.sendJSONError(wr, "Database name required", http.StatusBadRequest)
- return
- }
-
- dbName := parts[0]
- db, err := w.store.GetDatabase(dbName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- collections := db.ListCollections()
- collectionsInfo := make([]map[string]interface{}, 0)
-
- for _, collName := range collections {
- coll, err := db.GetCollection(collName)
- if err == nil {
- collectionsInfo = append(collectionsInfo, map[string]interface{}{
- "name": collName,
- "count": coll.Count(),
- "size": coll.Size(),
- "indexes": coll.GetIndexes(),
- })
- }
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "database": dbName,
- "collections": collectionsInfo,
- })
-}
-
-// handleDocuments обрабатывает CRUD операции с документами
-func (w *WebUIServer) handleDocuments(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/documents/")
- parts := strings.Split(path, "/")
-
- if len(parts) < 2 {
- w.sendJSONError(wr, "Invalid path. Use /api/webui/documents/{database}/{collection}", http.StatusBadRequest)
- return
- }
-
- dbName := parts[0]
- collName := parts[1]
-
- db, err := w.store.GetDatabase(dbName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- coll, err := db.GetCollection(collName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- switch r.Method {
- case http.MethodGet:
- limit := 50
- if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
- if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 500 {
- limit = l
- }
- }
-
- offset := 0
- if offsetStr := r.URL.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)
- }
-
- docs := make([]map[string]interface{}, 0)
- for _, doc := range allDocs[start:end] {
- docs = append(docs, map[string]interface{}{
- "id": doc.ID,
- "fields": doc.GetFields(),
- "created_at": doc.CreatedAt,
- "updated_at": doc.UpdatedAt,
- "version": doc.Version,
- })
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "documents": docs,
- "total": len(allDocs),
- "limit": limit,
- "offset": offset,
- })
-
- case http.MethodPost:
- var docData map[string]interface{}
- if err := json.NewDecoder(r.Body).Decode(&docData); err != nil {
- w.sendJSONError(wr, "Invalid JSON", http.StatusBadRequest)
- return
- }
-
- if err := coll.InsertFromMap(docData); err != nil {
- w.logOperation("INSERT_DOCUMENT", fmt.Sprintf("%s.%s", dbName, collName), "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- w.logOperation("INSERT_DOCUMENT", fmt.Sprintf("%s.%s/%v", dbName, collName, docData["_id"]), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "inserted",
- "id": docData["_id"],
- })
-
- case http.MethodPut:
- docID := r.URL.Query().Get("id")
- if docID == "" {
- w.sendJSONError(wr, "Document ID required", http.StatusBadRequest)
- return
- }
-
- var updates map[string]interface{}
- if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
- w.sendJSONError(wr, "Invalid JSON", http.StatusBadRequest)
- return
- }
-
- if err := coll.Update(docID, updates); err != nil {
- w.logOperation("UPDATE_DOCUMENT", fmt.Sprintf("%s.%s/%s", dbName, collName, docID), "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- w.logOperation("UPDATE_DOCUMENT", fmt.Sprintf("%s.%s/%s", dbName, collName, docID), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "updated",
- "id": docID,
- })
-
- case http.MethodDelete:
- docID := r.URL.Query().Get("id")
- if docID == "" {
- w.sendJSONError(wr, "Document ID required", http.StatusBadRequest)
- return
- }
-
- if err := coll.Delete(docID); err != nil {
- w.logOperation("DELETE_DOCUMENT", fmt.Sprintf("%s.%s/%s", dbName, collName, docID), "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- w.logOperation("DELETE_DOCUMENT", fmt.Sprintf("%s.%s/%s", dbName, collName, docID), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "deleted",
- "id": docID,
- })
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}
-
-// ========== Cluster Handlers ==========
-
-// handleClusterStatus возвращает статус кластера
-func (w *WebUIServer) handleClusterStatus(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- if w.coordinator == nil {
- w.sendJSONError(wr, "Cluster not available", http.StatusServiceUnavailable)
- return
- }
-
- status := w.coordinator.GetClusterStatus()
- w.sendJSONSuccess(wr, status)
-}
-
-// handleClusterNodes возвращает список узлов кластера
-func (w *WebUIServer) handleClusterNodes(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- if w.coordinator == nil {
- w.sendJSONError(wr, "Cluster not available", http.StatusServiceUnavailable)
- return
- }
-
- nodes := w.coordinator.GetAllNodes()
- w.sendJSONSuccess(wr, nodes)
-}
-
-// handleStats возвращает статистику системы
-func (w *WebUIServer) handleStats(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- databases := w.store.ListDatabases()
- totalDocs := int64(0)
- totalCollections := 0
-
- for _, dbName := range databases {
- db, _ := w.store.GetDatabase(dbName)
- if db != nil {
- collections := db.ListCollections()
- totalCollections += len(collections)
- for _, collName := range collections {
- coll, _ := db.GetCollection(collName)
- if coll != nil {
- totalDocs += coll.Count()
- }
- }
- }
- }
-
- stats := map[string]interface{}{
- "databases": len(databases),
- "collections": totalCollections,
- "documents": totalDocs,
- "storage_used_mb": float64(w.store.GetPageSize()*int64(len(databases))) / (1024 * 1024),
- "uptime_seconds": time.Now().Unix(),
- "cluster_enabled": w.coordinator != nil,
- "replication_factor": 0,
- }
-
- if w.coordinator != nil {
- stats["replication_factor"] = w.coordinator.GetReplicationFactor()
- stats["cluster_health"] = w.coordinator.GetClusterStatus().Health
- stats["pipeline_enabled"] = true
- stats["batch_commit_enabled"] = true
- stats["resharding_enabled"] = true
- }
-
- w.sendJSONSuccess(wr, stats)
-}
-
-// ========== Logs Handlers ==========
-
-// handleWebLogs возвращает логи веб-интерфейса
-func (w *WebUIServer) handleWebLogs(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- username := w.credentialMgr.GetCurrentUsername()
- if !w.credentialMgr.IsAdmin(username) {
- w.sendJSONError(wr, "Forbidden", http.StatusForbidden)
- return
- }
-
- limit := 100
- if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
- if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 5000 {
- limit = l
- }
- }
-
- entries := w.logBuffer.GetEntries()
- if len(entries) > limit {
- entries = entries[len(entries)-limit:]
- }
-
- w.sendJSONSuccess(wr, entries)
-}
-
-// ========== Plugin Handlers ==========
-
-// handlePlugins возвращает список плагинов
-func (w *WebUIServer) handlePlugins(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- username := w.credentialMgr.GetCurrentUsername()
- if !w.credentialMgr.IsAdmin(username) {
- w.sendJSONError(wr, "Forbidden", http.StatusForbidden)
- return
- }
-
- switch r.Method {
- case http.MethodGet:
- plugins := w.pluginManager.ListPlugins()
- pluginList := make([]map[string]interface{}, 0, len(plugins))
- for _, p := range plugins {
- pluginList = append(pluginList, map[string]interface{}{
- "name": p.Name,
- "version": p.Version(),
- "author": p.Author(),
- "description": p.Description(),
- "file_path": p.FilePath,
- "loaded_at": p.LoadedAt(),
- })
- }
- w.sendJSONSuccess(wr, pluginList)
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}
-
-// handlePluginOperation обрабатывает операции с плагинами
-func (w *WebUIServer) handlePluginOperation(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- username := w.credentialMgr.GetCurrentUsername()
- if !w.credentialMgr.IsAdmin(username) {
- w.sendJSONError(wr, "Forbidden", http.StatusForbidden)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/plugin/")
- parts := strings.Split(path, "/")
-
- if len(parts) < 2 {
- w.sendJSONError(wr, "Plugin name and action required", http.StatusBadRequest)
- return
- }
-
- pluginName := parts[0]
- action := parts[1]
-
- switch action {
- case "start":
- if r.Method != http.MethodPost {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
- if err := w.pluginManager.StartPlugin(pluginName); err != nil {
- w.logOperation("PLUGIN_START", pluginName, "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- w.logOperation("PLUGIN_START", pluginName, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "started"})
-
- case "stop":
- if r.Method != http.MethodPost {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
- if err := w.pluginManager.StopPlugin(pluginName); err != nil {
- w.logOperation("PLUGIN_STOP", pluginName, "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- w.logOperation("PLUGIN_STOP", pluginName, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "stopped"})
-
- case "reload":
- if r.Method != http.MethodPost {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
- if err := w.pluginManager.UnloadPlugin(pluginName); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- w.logOperation("PLUGIN_RELOAD", pluginName, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "reloaded"})
-
- case "delete":
- if r.Method != http.MethodDelete {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
- if err := w.pluginManager.UnloadPlugin(pluginName); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- pluginPath := filepath.Join(w.pluginManager.GetPluginsDir(), pluginName+".lua")
- if err := os.Remove(pluginPath); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- w.logOperation("PLUGIN_DELETE", pluginName, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "deleted"})
-
- case "upload":
- if r.Method != http.MethodPost {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
- if err := r.ParseMultipartForm(1024 * 1024); err != nil {
- w.sendJSONError(wr, "Failed to parse form", http.StatusBadRequest)
- return
- }
-
- file, handler, err := r.FormFile("plugin")
- if err != nil {
- w.sendJSONError(wr, "Failed to get plugin file", http.StatusBadRequest)
- return
- }
- defer file.Close()
-
- if !strings.HasSuffix(handler.Filename, ".lua") {
- w.sendJSONError(wr, "Only .lua files are allowed", http.StatusBadRequest)
- return
- }
-
- pluginPath := filepath.Join(w.pluginManager.GetPluginsDir(), handler.Filename)
-
- dst, err := os.Create(pluginPath)
- if err != nil {
- w.sendJSONError(wr, "Failed to create plugin file", http.StatusInternalServerError)
- return
- }
- defer dst.Close()
-
- if _, err := file.Seek(0, 0); err != nil {
- w.sendJSONError(wr, "Failed to read file", http.StatusInternalServerError)
- return
- }
-
- if _, err := dst.ReadFrom(file); err != nil {
- w.sendJSONError(wr, "Failed to write plugin file", http.StatusInternalServerError)
- return
- }
-
- w.logOperation("PLUGIN_UPLOAD", handler.Filename, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "uploaded", "filename": handler.Filename})
-
- default:
- w.sendJSONError(wr, "Unknown action", http.StatusBadRequest)
- }
-}
-
-// ========== Admin Handlers ==========
-
-// handleAdminUsers обрабатывает административные операции с пользователями
-func (w *WebUIServer) handleAdminUsers(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- username := w.credentialMgr.GetCurrentUsername()
- if !w.credentialMgr.IsAdmin(username) {
- w.sendJSONError(wr, "Forbidden", http.StatusForbidden)
- return
- }
-
- switch r.Method {
- case http.MethodGet:
- users := w.credentialMgr.ListUsers()
- w.sendJSONSuccess(wr, users)
-
- case http.MethodPost:
- var req struct {
- Username string `json:"username"`
- Password string `json:"password"`
- IsAdmin bool `json:"is_admin"`
- }
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- w.sendJSONError(wr, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- if err := w.credentialMgr.CreateUser(req.Username, req.Password, req.IsAdmin); err != nil {
- w.logOperation("ADMIN_CREATE_USER", req.Username, "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- w.logOperation("ADMIN_CREATE_USER", req.Username, "success", "", map[string]interface{}{
- "is_admin": req.IsAdmin,
- })
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "created"})
-
- case http.MethodDelete:
- usernameToDelete := r.URL.Query().Get("username")
- if usernameToDelete == "" {
- w.sendJSONError(wr, "Username required", http.StatusBadRequest)
- return
- }
-
- if err := w.credentialMgr.DeleteUser(usernameToDelete); err != nil {
- w.logOperation("ADMIN_DELETE_USER", usernameToDelete, "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- w.logOperation("ADMIN_DELETE_USER", usernameToDelete, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "deleted"})
-
- case http.MethodPut:
- var req struct {
- Username string `json:"username"`
- NewPassword string `json:"new_password"`
- }
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- w.sendJSONError(wr, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- if err := w.credentialMgr.AdminChangePassword(req.Username, req.NewPassword); err != nil {
- w.logOperation("ADMIN_CHANGE_PASSWORD", req.Username, "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- w.logOperation("ADMIN_CHANGE_PASSWORD", req.Username, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "password_changed"})
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}
-
-// ========== ACL Handlers ==========
-
-// handleACLUsers возвращает список пользователей ACL
-func (w *WebUIServer) handleACLUsers(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- users := w.aclManager.ListUsers()
- usersInfo := make([]map[string]interface{}, 0)
-
- for _, username := range users {
- user, err := w.aclManager.GetUserInfo(username)
- if err == nil {
- usersInfo = append(usersInfo, map[string]interface{}{
- "username": user.Username,
- "roles": user.Roles,
- "active": user.Active,
- "created_at": user.CreatedAt,
- "last_login": user.LastLogin,
- })
- }
- }
-
- w.sendJSONSuccess(wr, usersInfo)
-}
-
-// handleACLRoles возвращает список ролей ACL
-func (w *WebUIServer) handleACLRoles(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- roles := w.aclManager.ListRoles()
- rolesInfo := make([]map[string]interface{}, 0)
-
- for _, roleName := range roles {
- perms, err := w.aclManager.GetRolePermissions(roleName)
- if err == nil {
- rolesInfo = append(rolesInfo, map[string]interface{}{
- "name": roleName,
- "permissions": perms,
- })
- }
- }
-
- w.sendJSONSuccess(wr, rolesInfo)
-}
-
-// handleACLPermissions возвращает все разрешения ACL
-func (w *WebUIServer) handleACLPermissions(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- roles := w.aclManager.ListRoles()
- allPermissions := make(map[string][]string)
-
- for _, roleName := range roles {
- perms, _ := w.aclManager.GetRolePermissions(roleName)
- allPermissions[roleName] = perms
- }
-
- w.sendJSONSuccess(wr, allPermissions)
-}
-
-// handleACLUser обрабатывает операции с конкретным пользователем ACL
-func (w *WebUIServer) handleACLUser(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/acl/user/")
- username := path
-
- if username == "" {
- w.sendJSONError(wr, "Username required", http.StatusBadRequest)
- return
- }
-
- switch r.Method {
- case http.MethodPost:
- var req struct {
- Password string `json:"password"`
- Roles []string `json:"roles"`
- }
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- w.sendJSONError(wr, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- if err := w.aclManager.CreateUser(username, req.Password, req.Roles); err != nil {
- w.logOperation("ACL_CREATE_USER", username, "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- w.logOperation("ACL_CREATE_USER", username, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "created"})
-
- 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 {
- w.sendJSONError(wr, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- if req.AddRole != "" {
- if err := w.aclManager.AddUserRole(username, req.AddRole); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- }
-
- if req.RemoveRole != "" {
- if err := w.aclManager.RemoveUserRole(username, req.RemoveRole); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- }
-
- if req.Password != "" {
- if err := w.aclManager.ChangePassword(username, req.Password); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- }
-
- if req.Disable {
- if err := w.aclManager.DisableUser(username); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- }
-
- if req.Enable {
- if err := w.aclManager.EnableUser(username); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- }
-
- w.logOperation("ACL_UPDATE_USER", username, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "updated"})
-
- case http.MethodDelete:
- if err := w.aclManager.DeleteUser(username); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- w.logOperation("ACL_DELETE_USER", username, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "deleted"})
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}
-
-// handleACLRole обрабатывает операции с ролью ACL
-func (w *WebUIServer) handleACLRole(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/acl/role/")
- parts := strings.Split(path, "/")
-
- roleName := parts[0]
- if roleName == "" {
- w.sendJSONError(wr, "Role name required", http.StatusBadRequest)
- return
- }
-
- switch r.Method {
- case http.MethodPost:
- if err := w.aclManager.CreateRole(roleName); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- w.logOperation("ACL_CREATE_ROLE", roleName, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "created"})
-
- case http.MethodPut:
- if len(parts) < 3 {
- w.sendJSONError(wr, "Action and permission required", http.StatusBadRequest)
- return
- }
-
- action := parts[1]
- permission := strings.Join(parts[2:], "/")
-
- switch action {
- case "grant":
- if err := w.aclManager.GrantPermission(roleName, permission); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- case "revoke":
- if err := w.aclManager.RevokePermission(roleName, permission); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- default:
- w.sendJSONError(wr, "Unknown action", http.StatusBadRequest)
- return
- }
- w.logOperation("ACL_UPDATE_ROLE", roleName, "success", "", map[string]interface{}{"action": action, "permission": permission})
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "updated"})
-
- case http.MethodDelete:
- if err := w.aclManager.DeleteRole(roleName); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- w.logOperation("ACL_DELETE_ROLE", roleName, "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "deleted"})
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}
-
-// ========== Transaction Handlers ==========
-
-// handleTransactions обрабатывает операции с транзакциями
-func (w *WebUIServer) handleTransactions(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- switch r.Method {
- case http.MethodGet:
- txList := storage.GetActiveTransactions()
- w.sendJSONSuccess(wr, txList)
-
- case http.MethodPost:
- var req struct {
- Action string `json:"action"`
- }
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- w.sendJSONError(wr, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- switch req.Action {
- case "start_session":
- if err := storage.InitTransactionManager("futriis.wal"); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusInternalServerError)
- return
- }
- w.logOperation("TRANSACTION_START_SESSION", "", "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "session_started"})
-
- case "start_transaction":
- databases := w.store.ListDatabases()
- if len(databases) == 0 {
- w.sendJSONError(wr, "No database available", http.StatusBadRequest)
- return
- }
-
- db, err := w.store.GetDatabase(databases[0])
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- collections := db.ListCollections()
- if len(collections) == 0 {
- w.sendJSONError(wr, "No collection available", http.StatusBadRequest)
- return
- }
-
- coll, err := db.GetCollection(collections[0])
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- if err := storage.BeginTransactionOnCollection(coll); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusInternalServerError)
- return
- }
- w.logOperation("TRANSACTION_START", "", "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "transaction_started"})
-
- default:
- w.sendJSONError(wr, "Unknown action", http.StatusBadRequest)
- }
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}
-
-// handleTransactionAction обрабатывает действия с транзакцией
-func (w *WebUIServer) handleTransactionAction(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/transaction/")
-
- switch r.Method {
- case http.MethodPost:
- switch path {
- case "commit":
- if err := storage.CommitCurrentTransaction(); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusInternalServerError)
- return
- }
- w.logOperation("TRANSACTION_COMMIT", "", "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "committed"})
-
- case "abort":
- if err := storage.AbortCurrentTransaction(); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusInternalServerError)
- return
- }
- w.logOperation("TRANSACTION_ABORT", "", "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "aborted"})
-
- default:
- w.sendJSONError(wr, "Unknown action", http.StatusBadRequest)
- }
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}
-
-// ========== Index Handlers ==========
-
-// handleIndexesList возвращает список индексов
-func (w *WebUIServer) handleIndexesList(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/indexes/")
- parts := strings.Split(path, "/")
-
- if len(parts) < 2 {
- w.sendJSONError(wr, "Database and collection required", http.StatusBadRequest)
- return
- }
-
- dbName := parts[0]
- collName := parts[1]
-
- db, err := w.store.GetDatabase(dbName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- coll, err := db.GetCollection(collName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- switch r.Method {
- case http.MethodGet:
- indexes := coll.GetIndexesInfo()
- w.sendJSONSuccess(wr, indexes)
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}
-
-// handleIndexOperation обрабатывает операции с индексами
-func (w *WebUIServer) handleIndexOperation(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/index/")
- parts := strings.Split(path, "/")
-
- if len(parts) < 3 {
- w.sendJSONError(wr, "Database, collection and action required", http.StatusBadRequest)
- return
- }
-
- dbName := parts[0]
- collName := parts[1]
- action := parts[2]
-
- db, err := w.store.GetDatabase(dbName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- coll, err := db.GetCollection(collName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- switch action {
- 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 {
- w.sendJSONError(wr, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- if err := coll.CreateIndex(req.Name, req.Fields, req.Unique); err != nil {
- w.logOperation("INDEX_CREATE", fmt.Sprintf("%s.%s.%s", dbName, collName, req.Name), "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- w.logOperation("INDEX_CREATE", fmt.Sprintf("%s.%s.%s", dbName, collName, req.Name), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "created"})
-
- case "drop":
- if len(parts) < 4 {
- w.sendJSONError(wr, "Index name required", http.StatusBadRequest)
- return
- }
- indexName := parts[3]
- if err := coll.DropIndex(indexName); err != nil {
- w.logOperation("INDEX_DROP", fmt.Sprintf("%s.%s.%s", dbName, collName, indexName), "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- w.logOperation("INDEX_DROP", fmt.Sprintf("%s.%s.%s", dbName, collName, indexName), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "dropped"})
-
- default:
- w.sendJSONError(wr, "Unknown action", http.StatusBadRequest)
- }
-}
-
-// ========== Import/Export Handlers ==========
-
-// handleExportData обрабатывает экспорт данных
-func (w *WebUIServer) handleExportData(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- if r.Method != http.MethodPost {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- var req struct {
- Database string `json:"database"`
- Filename string `json:"filename"`
- Format string `json:"format"`
- }
-
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- w.sendJSONError(wr, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- if req.Database == "" {
- w.sendJSONError(wr, "Database name required", http.StatusBadRequest)
- return
- }
-
- if req.Filename == "" {
- req.Filename = req.Database + "_export_" + strconv.FormatInt(time.Now().Unix(), 10)
- if req.Format == "msgpack" {
- req.Filename += ".msgpack"
- } else {
- req.Filename += ".json"
- }
- }
-
- if req.Format == "" {
- req.Format = "json"
- }
-
- db, err := w.store.GetDatabase(req.Database)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- exportData := make(map[string]interface{})
- collections := db.ListCollections()
- totalDocuments := 0
-
- for _, collName := range collections {
- coll, err := db.GetCollection(collName)
- if err != nil {
- continue
- }
-
- docs := coll.GetAllDocuments()
- totalDocuments += len(docs)
- collData := make([]map[string]interface{}, 0, len(docs))
- for _, doc := range docs {
- docData := map[string]interface{}{
- "_id": doc.ID,
- "fields": doc.GetFields(),
- "created_at": doc.CreatedAt,
- "updated_at": doc.UpdatedAt,
- "version": doc.Version,
- }
- if doc.DeletedAt > 0 {
- docData["deleted_at"] = doc.DeletedAt
- }
- collData = append(collData, docData)
- }
- exportData[collName] = collData
- }
-
- exportData["_metadata"] = map[string]interface{}{
- "database": req.Database,
- "export_time": time.Now().Unix(),
- "export_time_str": time.Now().Format("2006-01-02 15:04:05.000"),
- "version": "1.0",
- "collections": len(collections),
- "total_documents": totalDocuments,
- "format": req.Format,
- "exported_by": w.credentialMgr.GetCurrentUsername(),
- }
-
- w.logOperation("EXPORT", req.Database, "success", "", map[string]interface{}{
- "collections": len(collections),
- "total_documents": totalDocuments,
- "format": req.Format,
- })
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "export_prepared",
- "database": req.Database,
- "filename": req.Filename,
- "format": req.Format,
- "collections": len(collections),
- "total_documents": totalDocuments,
- "data": exportData,
- })
-}
-
-// handleImportData обрабатывает импорт данных
-func (w *WebUIServer) handleImportData(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- if r.Method != http.MethodPost {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- var req struct {
- Database string `json:"database"`
- Data map[string]interface{} `json:"data"`
- Overwrite bool `json:"overwrite"`
- Format string `json:"format"`
- }
-
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- w.sendJSONError(wr, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- if req.Database == "" {
- w.sendJSONError(wr, "Database name required", http.StatusBadRequest)
- return
- }
-
- if req.Data == nil {
- w.sendJSONError(wr, "Import data required", http.StatusBadRequest)
- return
- }
-
- if req.Format == "" {
- req.Format = "json"
- }
-
- if !w.store.ExistsDatabase(req.Database) {
- if err := w.store.CreateDatabase(req.Database); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusInternalServerError)
- return
- }
- }
-
- db, err := w.store.GetDatabase(req.Database)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- importedCollections := 0
- importedDocuments := 0
- skippedDocuments := 0
- failedDocuments := 0
-
- for key, value := range req.Data {
- if key == "_metadata" {
- continue
- }
-
- collName := key
- collData, ok := value.([]interface{})
- if !ok {
- continue
- }
-
- if _, err := db.GetCollection(collName); err != nil {
- if err := db.CreateCollection(collName); err != nil {
- w.logOperation("IMPORT_COLLECTION_CREATE", collName, "error", err.Error(), nil)
- continue
- }
- }
-
- coll, err := db.GetCollection(collName)
- if err != nil {
- continue
- }
-
- for _, docRaw := range collData {
- docMap, ok := docRaw.(map[string]interface{})
- if !ok {
- failedDocuments++
- continue
- }
-
- var docID string
- if id, ok := docMap["_id"].(string); ok {
- docID = id
- } else {
- failedDocuments++
- continue
- }
-
- if existingDoc, _ := coll.Find(docID); existingDoc != nil && !req.Overwrite {
- skippedDocuments++
- continue
- }
-
- doc := storage.NewDocumentWithID(docID)
-
- if fields, ok := docMap["fields"].(map[string]interface{}); ok {
- for k, v := range fields {
- doc.SetField(k, v)
- }
- }
-
- if createdAt, ok := docMap["created_at"]; ok {
- switch v := createdAt.(type) {
- case float64:
- doc.CreatedAt = int64(v)
- case int64:
- doc.CreatedAt = v
- }
- }
-
- if updatedAt, ok := docMap["updated_at"]; ok {
- switch v := updatedAt.(type) {
- case float64:
- doc.UpdatedAt = int64(v)
- case int64:
- doc.UpdatedAt = v
- }
- }
-
- if deletedAt, ok := docMap["deleted_at"]; ok {
- switch v := deletedAt.(type) {
- case float64:
- doc.DeletedAt = int64(v)
- case int64:
- doc.DeletedAt = v
- }
- }
-
- if version, ok := docMap["version"]; ok {
- switch v := version.(type) {
- case float64:
- doc.Version = uint64(v)
- case int64:
- doc.Version = uint64(v)
- case uint64:
- doc.Version = v
- }
- }
-
- if err := coll.Insert(doc); err != nil {
- failedDocuments++
- continue
- }
- importedDocuments++
- }
- importedCollections++
- }
-
- w.logOperation("IMPORT", req.Database, "success", "", map[string]interface{}{
- "collections": importedCollections,
- "documents": importedDocuments,
- "skipped": skippedDocuments,
- "failed": failedDocuments,
- "overwrite": req.Overwrite,
- "format": req.Format,
- })
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "imported",
- "database": req.Database,
- "collections": importedCollections,
- "documents": importedDocuments,
- "skipped": skippedDocuments,
- "failed": failedDocuments,
- "format": req.Format,
- })
-}
-
-// ========== Trigger Handlers ==========
-
-// handleTriggers возвращает список триггеров
-func (w *WebUIServer) handleTriggers(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/triggers/")
- parts := strings.Split(path, "/")
-
- tm := storage.GetTriggerManager()
- if tm == nil {
- storage.InitTriggerManager(w.logger)
- tm = storage.GetTriggerManager()
- }
-
- switch r.Method {
- case http.MethodGet:
- if len(parts) >= 2 && parts[0] != "" && parts[1] != "" {
- dbName := parts[0]
- collName := parts[1]
-
- db, err := w.store.GetDatabase(dbName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- if _, err := db.GetCollection(collName); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- triggers := tm.ListTriggers(collName)
-
- triggerList := make([]map[string]interface{}, 0, len(triggers))
- for _, trigger := range triggers {
- triggerList = append(triggerList, map[string]interface{}{
- "name": trigger.Name,
- "collection": trigger.Collection,
- "event": trigger.Event,
- "action": trigger.Action,
- "enabled": trigger.Enabled,
- "description": trigger.Description,
- "created_at": trigger.CreatedAt,
- "updated_at": trigger.UpdatedAt,
- "has_condition": trigger.Condition != nil,
- "operations_count": len(trigger.Operations),
- })
- }
- w.sendJSONSuccess(wr, triggerList)
- } else {
- triggers := tm.ListTriggers("")
- triggerList := make([]map[string]interface{}, 0, len(triggers))
- for _, trigger := range triggers {
- triggerList = append(triggerList, map[string]interface{}{
- "name": trigger.Name,
- "collection": trigger.Collection,
- "event": trigger.Event,
- "action": trigger.Action,
- "enabled": trigger.Enabled,
- "description": trigger.Description,
- "created_at": trigger.CreatedAt,
- })
- }
- w.sendJSONSuccess(wr, triggerList)
- }
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}
-
-// handleTriggerOperation обрабатывает операции с триггерами
-func (w *WebUIServer) handleTriggerOperation(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/trigger/")
- parts := strings.Split(path, "/")
-
- if len(parts) < 3 {
- w.sendJSONError(wr, "Invalid path. Use /api/webui/trigger/{db}/{collection}/{action}", http.StatusBadRequest)
- return
- }
-
- dbName := parts[0]
- collName := parts[1]
- action := parts[2]
-
- tm := storage.GetTriggerManager()
- if tm == nil {
- storage.InitTriggerManager(w.logger)
- tm = storage.GetTriggerManager()
- }
-
- switch action {
- case "create":
- if r.Method != http.MethodPost {
- w.sendJSONError(wr, "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"`
- Operations []map[string]interface{} `json:"operations"`
- Description string `json:"description"`
- }
-
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- w.sendJSONError(wr, "Invalid request body", http.StatusBadRequest)
- return
- }
-
- if req.Name == "" {
- w.sendJSONError(wr, "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
- }
-
- if len(req.Operations) > 0 {
- ops := make([]interface{}, len(req.Operations))
- for i, op := range req.Operations {
- ops[i] = op
- }
- config["operations"] = ops
- }
-
- event := storage.TriggerEvent(req.Event)
- if event == "" {
- event = storage.TriggerAfterInsert
- }
-
- if err := tm.CreateTrigger(dbName, collName, req.Name, event, config); err != nil {
- w.logOperation("TRIGGER_CREATE", fmt.Sprintf("%s.%s.%s", dbName, collName, req.Name), "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- w.logOperation("TRIGGER_CREATE", fmt.Sprintf("%s.%s.%s", dbName, collName, req.Name), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "created",
- "name": req.Name,
- })
-
- case "enable":
- if r.Method != http.MethodPost {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return }
-
- if len(parts) < 4 {
- w.sendJSONError(wr, "Trigger name required", http.StatusBadRequest)
- return
- }
- triggerName := parts[3]
-
- if err := tm.EnableTrigger(collName, "", triggerName); err != nil {
- w.logOperation("TRIGGER_ENABLE", fmt.Sprintf("%s.%s.%s", dbName, collName, triggerName), "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
- w.logOperation("TRIGGER_ENABLE", fmt.Sprintf("%s.%s.%s", dbName, collName, triggerName), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "enabled", "name": triggerName})
-
- case "disable":
- if r.Method != http.MethodPost {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- if len(parts) < 4 {
- w.sendJSONError(wr, "Trigger name required", http.StatusBadRequest)
- return
- }
- triggerName := parts[3]
-
- if err := tm.DisableTrigger(collName, "", triggerName); err != nil {
- w.logOperation("TRIGGER_DISABLE", fmt.Sprintf("%s.%s.%s", dbName, collName, triggerName), "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
- w.logOperation("TRIGGER_DISABLE", fmt.Sprintf("%s.%s.%s", dbName, collName, triggerName), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "disabled", "name": triggerName})
-
- case "delete":
- if r.Method != http.MethodDelete {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- if len(parts) < 4 {
- w.sendJSONError(wr, "Trigger name required", http.StatusBadRequest)
- return
- }
- triggerName := parts[3]
- triggerEvent := ""
- if len(parts) > 4 {
- triggerEvent = parts[4]
- }
-
- if err := tm.DropTrigger(collName, triggerEvent, triggerName); err != nil {
- w.logOperation("TRIGGER_DELETE", fmt.Sprintf("%s.%s.%s", dbName, collName, triggerName), "error", err.Error(), nil)
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
- w.logOperation("TRIGGER_DELETE", fmt.Sprintf("%s.%s.%s", dbName, collName, triggerName), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{"status": "deleted", "name": triggerName})
-
- default:
- w.sendJSONError(wr, "Unknown action", http.StatusBadRequest)
- }
-}
-
-// handleTriggerLog возвращает лог выполнения триггеров
-func (w *WebUIServer) handleTriggerLog(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- if r.Method != http.MethodGet {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- tm := storage.GetTriggerManager()
- if tm == nil {
- w.sendJSONSuccess(wr, []interface{}{})
- return
- }
-
- logEntries := tm.GetTriggerExecutionLog()
-
- formattedLog := make([]map[string]interface{}, 0, len(logEntries))
- for _, entry := range logEntries {
- formattedLog = append(formattedLog, map[string]interface{}{
- "trigger_name": entry.TriggerName,
- "event": entry.Event,
- "collection": entry.Collection,
- "database": entry.Database,
- "document_id": entry.DocumentID,
- "operation": entry.Operation,
- "timestamp": entry.Timestamp.UnixMilli(),
- "user": entry.User,
- "role": entry.Role,
- "custom_data": entry.CustomData,
- })
- }
-
- w.sendJSONSuccess(wr, formattedLog)
-}
-
-// ========== Constraint Handlers ==========
-
-// handleConstraintsList возвращает список ограничений
-func (w *WebUIServer) handleConstraintsList(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- if r.Method != http.MethodGet {
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/constraints/")
- parts := strings.Split(path, "/")
-
- if len(parts) < 2 {
- w.sendJSONError(wr, "Database and collection required", http.StatusBadRequest)
- return
- }
-
- dbName := parts[0]
- collName := parts[1]
-
- db, err := w.store.GetDatabase(dbName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- coll, err := db.GetCollection(collName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- constraintsList := make([]map[string]interface{}, 0)
-
- requiredFields := coll.GetRequiredFields()
- for _, field := range requiredFields {
- constraintsList = append(constraintsList, map[string]interface{}{
- "type": "required",
- "field": field,
- })
- }
-
- uniqueFields := coll.GetUniqueConstraints()
- for _, field := range uniqueFields {
- constraintsList = append(constraintsList, map[string]interface{}{
- "type": "unique",
- "field": field,
- })
- }
-
- minConstraints := coll.GetMinConstraints()
- for field, value := range minConstraints {
- constraintsList = append(constraintsList, map[string]interface{}{
- "type": "min",
- "field": field,
- "value": value,
- })
- }
-
- maxConstraints := coll.GetMaxConstraints()
- for field, value := range maxConstraints {
- constraintsList = append(constraintsList, map[string]interface{}{
- "type": "max",
- "field": field,
- "value": value,
- })
- }
-
- enumConstraints := coll.GetEnumConstraints()
- for field, values := range enumConstraints {
- constraintsList = append(constraintsList, map[string]interface{}{
- "type": "enum",
- "field": field,
- "values": values,
- })
- }
-
- regexConstraints := coll.GetRegexConstraints()
- for field, pattern := range regexConstraints {
- constraintsList = append(constraintsList, map[string]interface{}{
- "type": "regex",
- "field": field,
- "pattern": pattern,
- })
- }
-
- w.sendJSONSuccess(wr, constraintsList)
-}
-
-// handleConstraintOperation обрабатывает операции с ограничениями
-func (w *WebUIServer) handleConstraintOperation(wr http.ResponseWriter, r *http.Request) {
- if !w.checkAuth(r) {
- w.sendJSONError(wr, "Unauthorized", http.StatusUnauthorized)
- return
- }
-
- path := strings.TrimPrefix(r.URL.Path, "/api/webui/constraint/")
- parts := strings.Split(path, "/")
-
- if len(parts) < 3 {
- w.sendJSONError(wr, "Invalid path. Use /api/webui/constraint/{db}/{collection}/{action}", http.StatusBadRequest)
- return
- }
-
- dbName := parts[0]
- collName := parts[1]
- action := parts[2]
-
- db, err := w.store.GetDatabase(dbName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- coll, err := db.GetCollection(collName)
- if err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- switch r.Method {
- case http.MethodPost:
- switch action {
- case "required":
- if len(parts) < 4 {
- w.sendJSONError(wr, "Field name required", http.StatusBadRequest)
- return
- }
- field := parts[3]
- coll.AddRequiredField(field)
- w.logOperation("CONSTRAINT_ADD", fmt.Sprintf("%s.%s.required.%s", dbName, collName, field), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "added",
- "type": "required",
- "field": field,
- })
-
- case "unique":
- if len(parts) < 4 {
- w.sendJSONError(wr, "Field name required", http.StatusBadRequest)
- return
- }
- field := parts[3]
- coll.AddUniqueConstraint(field)
- w.logOperation("CONSTRAINT_ADD", fmt.Sprintf("%s.%s.unique.%s", dbName, collName, field), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "added",
- "type": "unique",
- "field": field,
- })
-
- case "min":
- if len(parts) < 5 {
- w.sendJSONError(wr, "Field name and value required", http.StatusBadRequest)
- return
- }
- field := parts[3]
- minVal, err := strconv.ParseFloat(parts[4], 64)
- if err != nil {
- w.sendJSONError(wr, "Invalid minimum value", http.StatusBadRequest)
- return
- }
- coll.AddMinConstraint(field, minVal)
- w.logOperation("CONSTRAINT_ADD", fmt.Sprintf("%s.%s.min.%s", dbName, collName, field), "success", "", map[string]interface{}{"value": minVal})
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "added",
- "type": "min",
- "field": field,
- "value": minVal,
- })
-
- case "max":
- if len(parts) < 5 {
- w.sendJSONError(wr, "Field name and value required", http.StatusBadRequest)
- return
- }
- field := parts[3]
- maxVal, err := strconv.ParseFloat(parts[4], 64)
- if err != nil {
- w.sendJSONError(wr, "Invalid maximum value", http.StatusBadRequest)
- return
- }
- coll.AddMaxConstraint(field, maxVal)
- w.logOperation("CONSTRAINT_ADD", fmt.Sprintf("%s.%s.max.%s", dbName, collName, field), "success", "", map[string]interface{}{"value": maxVal})
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "added",
- "type": "max",
- "field": field,
- "value": maxVal,
- })
-
- case "enum":
- if len(parts) < 5 {
- w.sendJSONError(wr, "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)
- w.logOperation("CONSTRAINT_ADD", fmt.Sprintf("%s.%s.enum.%s", dbName, collName, field), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "added",
- "type": "enum",
- "field": field,
- "values": values,
- })
-
- case "regex":
- if len(parts) < 5 {
- w.sendJSONError(wr, "Field name and pattern required", http.StatusBadRequest)
- return
- }
- field := parts[3]
- pattern := parts[4]
- coll.AddRegexConstraint(field, pattern)
- w.logOperation("CONSTRAINT_ADD", fmt.Sprintf("%s.%s.regex.%s", dbName, collName, field), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "added",
- "type": "regex",
- "field": field,
- "pattern": pattern,
- })
-
- default:
- w.sendJSONError(wr, "Unknown constraint type", http.StatusBadRequest)
- }
-
- case http.MethodDelete:
- if len(parts) < 4 {
- w.sendJSONError(wr, "Constraint type and field required", http.StatusBadRequest)
- return
- }
- constraintType := parts[2]
- field := parts[3]
-
- 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:
- w.sendJSONError(wr, "Unknown constraint type", http.StatusBadRequest)
- return
- }
-
- w.logOperation("CONSTRAINT_REMOVE", fmt.Sprintf("%s.%s.%s.%s", dbName, collName, constraintType, field), "success", "", nil)
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "removed",
- "type": constraintType,
- "field": field,
- })
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}