diff --git a/internal/api/webui.go b/internal/api/webui.go
deleted file mode 100644
index 537910a..0000000
--- a/internal/api/webui.go
+++ /dev/null
@@ -1,1642 +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 (
- "embed"
- "encoding/json"
- "fmt"
- "io/fs"
- "net/http"
- "strconv"
- "strings"
- "time"
-
- "futriis/internal/acl"
- "futriis/internal/cluster"
- "futriis/internal/log"
- "futriis/internal/storage"
-)
-
-//go:embed static/*
-var staticFiles embed.FS
-
-// WebUIServer представляет сервер веб-интерфейса
-type WebUIServer struct {
- store *storage.Storage
- coordinator *cluster.RaftCoordinator
- aclManager *acl.ACLManager
- logger *log.Logger
- server *http.Server
- port int
- enabled bool
-}
-
-// NewWebUIServer создаёт новый веб-сервер интерфейса
-func NewWebUIServer(port int, enabled bool, store *storage.Storage, coord *cluster.RaftCoordinator, aclMgr *acl.ACLManager, logger *log.Logger) *WebUIServer {
- return &WebUIServer{
- store: store,
- coordinator: coord,
- aclManager: aclMgr,
- logger: logger,
- port: port,
- enabled: enabled,
- }
-}
-
-// 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)
-
- // API для веб-интерфейса
- mux.HandleFunc("/api/webui/databases", w.handleGetDatabases)
- mux.HandleFunc("/api/webui/collections/", w.handleGetCollections)
- mux.HandleFunc("/api/webui/documents/", w.handleDocuments)
- mux.HandleFunc("/api/webui/cluster/status", w.handleClusterStatus)
- mux.HandleFunc("/api/webui/cluster/nodes", w.handleClusterNodes)
- mux.HandleFunc("/api/webui/stats", w.handleStats)
- mux.HandleFunc("/api/webui/login", w.handleWebLogin)
- mux.HandleFunc("/api/webui/logout", w.handleWebLogout)
- mux.HandleFunc("/api/webui/session", w.handleSessionCheck)
-
- // API для ACL
- mux.HandleFunc("/api/webui/acl/users", w.handleACLUsers)
- mux.HandleFunc("/api/webui/acl/roles", w.handleACLRoles)
- mux.HandleFunc("/api/webui/acl/permissions", w.handleACLPermissions)
- mux.HandleFunc("/api/webui/acl/user/", w.handleACLUser)
- mux.HandleFunc("/api/webui/acl/role/", w.handleACLRole)
-
- // API для транзакций
- mux.HandleFunc("/api/webui/transactions", w.handleTransactions)
- mux.HandleFunc("/api/webui/transaction/", w.handleTransactionAction)
-
- // API для индексов
- mux.HandleFunc("/api/webui/indexes/", w.handleIndexesList)
- mux.HandleFunc("/api/webui/index/", w.handleIndexOperation)
-
- // API для импорта/экспорта
- mux.HandleFunc("/api/webui/export", w.handleExportData)
- mux.HandleFunc("/api/webui/import", w.handleImportData)
-
- // API для триггеров
- mux.HandleFunc("/api/webui/triggers/", w.handleTriggers)
- mux.HandleFunc("/api/webui/trigger/", w.handleTriggerOperation)
- mux.HandleFunc("/api/webui/trigger/log", w.handleTriggerLog)
-
- 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", w.port))
- return w.server.ListenAndServe()
-}
-
-// Stop останавливает веб-сервер
-func (w *WebUIServer) Stop() error {
- if w.server != nil {
- return w.server.Close()
- }
- return nil
-}
-
-// 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
-
-
-
-
-
-
-
-
-
-
- Панель управления
-
- Подключено
-
-
-
-
-
-
-
-
-
-
-
-
-
-`
-
- wr.Header().Set("Content-Type", "text/html; charset=utf-8")
- wr.Write([]byte(html))
-}
-
-// 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.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- 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.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- 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.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "deleted",
- "id": docID,
- })
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}
-
-// 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
- }
-
- w.sendJSONSuccess(wr, stats)
-}
-
-// 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))
-
- sessionID, err := w.aclManager.Authenticate(creds.Username, creds.Password)
- if err != nil {
- w.logger.Debug(fmt.Sprintf("Authentication failed for %s: %v", creds.Username, err))
- w.sendJSONError(wr, "Неверный логин и/или пароль", http.StatusUnauthorized)
- return
- }
-
- w.logger.Debug(fmt.Sprintf("Authentication successful for %s, sessionID=%s", creds.Username, sessionID))
-
- http.SetCookie(wr, &http.Cookie{
- Name: "session_id",
- Value: sessionID,
- Path: "/",
- MaxAge: 86400,
- HttpOnly: true,
- SameSite: http.SameSiteLaxMode,
- })
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "session_id": sessionID,
- "username": creds.Username,
- })
-}
-
-// handleWebLogout обрабатывает выход из веб-интерфейса
-func (w *WebUIServer) handleWebLogout(wr http.ResponseWriter, r *http.Request) {
- if cookie, err := r.Cookie("session_id"); err == nil {
- w.aclManager.Logout(cookie.Value)
- }
-
- http.SetCookie(wr, &http.Cookie{
- Name: "session_id",
- Value: "",
- Path: "/",
- MaxAge: -1,
- HttpOnly: 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
- }
-
- if !w.aclManager.CheckSession(sessionID) {
- w.sendJSONError(wr, "Invalid session", http.StatusUnauthorized)
- return
- }
-
- username := w.aclManager.GetUsername(sessionID)
-
- connectionStatus := "connected"
- if w.coordinator == nil {
- connectionStatus = "disconnected"
- } else if status := w.coordinator.GetClusterStatus(); status.Health == "critical" {
- connectionStatus = "disconnected"
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "authenticated": true,
- "username": username,
- "connection_status": connectionStatus,
- })
-}
-
-// ==================== ACL Handlers ====================
-
-// handleACLUsers возвращает список пользователей
-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 возвращает список ролей
-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 возвращает все разрешения
-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 обрабатывает операции с пользователем
-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.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- 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.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.sendJSONSuccess(wr, map[string]interface{}{"status": "deleted"})
-
- default:
- w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
- }
-}
-
-// handleACLRole обрабатывает операции с ролью
-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.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.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.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.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.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/")
- action := path
-
- switch r.Method {
- case http.MethodPost:
- switch action {
- case "commit":
- if err := storage.CommitCurrentTransaction(); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusInternalServerError)
- return
- }
- 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.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.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- 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.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
- 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"`
- }
-
- 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) + ".msgpack"
- }
-
- 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()
-
- for _, collName := range collections {
- coll, err := db.GetCollection(collName)
- if err != nil {
- continue
- }
-
- docs := coll.GetAllDocuments()
- 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,
- }
- collData = append(collData, docData)
- }
- exportData[collName] = collData
- }
-
- exportData["_metadata"] = map[string]interface{}{
- "database": req.Database,
- "export_time": time.Now().Unix(),
- "version": "1.0",
- "collections": len(collections),
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "export_prepared",
- "database": req.Database,
- "filename": req.Filename,
- "collections": len(collections),
- "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"`
- }
-
- 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 !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
-
- 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 {
- continue
- }
- }
-
- coll, err := db.GetCollection(collName)
- if err != nil {
- continue
- }
-
- for _, docRaw := range collData {
- docMap, ok := docRaw.(map[string]interface{})
- if !ok {
- continue
- }
-
- var docID string
- if id, ok := docMap["_id"].(string); ok {
- docID = id
- } else {
- continue
- }
-
- if existingDoc, _ := coll.Find(docID); existingDoc != nil && !req.Overwrite {
- 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 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 {
- continue
- }
- importedDocuments++
- }
- importedCollections++
- }
-
- w.sendJSONSuccess(wr, map[string]interface{}{
- "status": "imported",
- "database": req.Database,
- "collections": importedCollections,
- "documents": importedDocuments,
- })
-}
-
-// ==================== 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 {
- 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,
- })
- }
- 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.sendJSONError(wr, err.Error(), http.StatusBadRequest)
- return
- }
-
- 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.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
- 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.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
- 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 and event required", http.StatusBadRequest)
- return
- }
- triggerName := parts[3]
- triggerEvent := parts[4]
-
- if err := tm.DropTrigger(collName, triggerEvent, triggerName); err != nil {
- w.sendJSONError(wr, err.Error(), http.StatusNotFound)
- return
- }
- 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
- }
-
- log := tm.GetTriggerExecutionLog()
-
- // Форматируем лог для отправки
- formattedLog := make([]map[string]interface{}, 0, len(log))
- for _, entry := range log {
- 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)
-}
-
-// checkAuth проверяет аутентификацию
-func (w *WebUIServer) checkAuth(r *http.Request) bool {
- sessionID := w.getSessionID(r)
- if sessionID == "" {
- return false
- }
- return w.aclManager.CheckSession(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,
- })
-}