Files
futriix/internal/api/webui.go

1941 lines
67 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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)
// API для ограничений (constraints)
mux.HandleFunc("/api/webui/constraints/", w.handleConstraintsList)
mux.HandleFunc("/api/webui/constraint/", w.handleConstraintOperation)
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 := `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>Futriis Database Management System</title>
<link rel="stylesheet" href="/static/style.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="dashboard-container">
<nav class="sidebar">
<div class="sidebar-header">
<div class="logo">
<img src="/static/logo.png" alt="Futriis" style="width: 112px; height: 53px; object-fit: contain;">
</div>
<button class="menu-toggle" id="menuToggle">
<i class="fas fa-bars"></i>
</button>
</div>
<ul class="nav-menu">
<li class="nav-item">
<a href="#" class="nav-link" data-section="dashboard">
<i class="fas fa-tachometer-alt"></i>
<span>Панель управления</span>
</a>
</li>
<li class="nav-item has-submenu">
<a href="#" class="nav-link" data-submenu="crud">
<i class="fas fa-table"></i>
<span>Управление СУБД</span>
<i class="fas fa-chevron-down"></i>
</a>
<ul class="submenu">
<li><a href="#" data-action="create-db"><i class="fas fa-plus-circle"></i>Создать БД</a></li>
<li><a href="#" data-action="create-collection"><i class="fas fa-layer-group"></i>Создать коллекцию</a></li>
<li><a href="#" data-action="insert-doc"><i class="fas fa-file-import"></i>Вставить документ</a></li>
<li><a href="#" data-action="find-doc"><i class="fas fa-search"></i>Найти документ</a></li>
<li><a href="#" data-action="update-doc"><i class="fas fa-edit"></i>Обновить документ</a></li>
<li><a href="#" data-action="delete-doc"><i class="fas fa-trash-alt"></i>Удалить документ</a></li>
</ul>
</li>
<li class="nav-item has-submenu">
<a href="#" class="nav-link" data-submenu="constraints">
<i class="fas fa-check-double"></i>
<span>Ограничения</span>
<i class="fas fa-chevron-down"></i>
</a>
<ul class="submenu">
<li><a href="#" data-section="constraints-list"><i class="fas fa-list"></i>Список ограничений</a></li>
<li><a href="#" data-action="constraint-add-required"><i class="fas fa-exclamation-circle"></i>Обязательное поле</a></li>
<li><a href="#" data-action="constraint-add-unique"><i class="fas fa-unique"></i>Уникальность</a></li>
<li><a href="#" data-action="constraint-add-min"><i class="fas fa-greater-than"></i>Минимум</a></li>
<li><a href="#" data-action="constraint-add-max"><i class="fas fa-less-than"></i>Максимум</a></li>
<li><a href="#" data-action="constraint-add-enum"><i class="fas fa-list-ul"></i>Перечисление</a></li>
<li><a href="#" data-action="constraint-add-regex"><i class="fas fa-code"></i>Регулярное выражение</a></li>
</ul>
</li>
<li class="nav-item has-submenu">
<a href="#" class="nav-link" data-submenu="acl">
<i class="fas fa-lock"></i>
<span>ACL управление</span>
<i class="fas fa-chevron-down"></i>
</a>
<ul class="submenu">
<li><a href="#" data-section="acl-users"><i class="fas fa-users"></i>Пользователи</a></li>
<li><a href="#" data-section="acl-roles"><i class="fas fa-user-tag"></i>Роли</a></li>
<li><a href="#" data-section="acl-permissions"><i class="fas fa-key"></i>Разрешения</a></li>
<li><a href="#" data-action="acl-create-user"><i class="fas fa-user-plus"></i>Создать пользователя</a></li>
<li><a href="#" data-action="acl-create-role"><i class="fas fa-plus-circle"></i>Создать роль</a></li>
</ul>
</li>
<li class="nav-item has-submenu">
<a href="#" class="nav-link" data-submenu="transactions">
<i class="fas fa-exchange-alt"></i>
<span>Транзакции</span>
<i class="fas fa-chevron-down"></i>
</a>
<ul class="submenu">
<li><a href="#" data-action="tx-start-session"><i class="fas fa-play"></i>Начать сессию</a></li>
<li><a href="#" data-action="tx-start"><i class="fas fa-play-circle"></i>Начать транзакцию</a></li>
<li><a href="#" data-action="tx-commit"><i class="fas fa-check-circle"></i>Зафиксировать</a></li>
<li><a href="#" data-action="tx-abort"><i class="fas fa-times-circle"></i>Отменить</a></li>
<li><a href="#" data-section="tx-list"><i class="fas fa-list"></i>Список транзакций</a></li>
</ul>
</li>
<li class="nav-item has-submenu">
<a href="#" class="nav-link" data-submenu="indexes">
<i class="fas fa-search"></i>
<span>Индексы</span>
<i class="fas fa-chevron-down"></i>
</a>
<ul class="submenu">
<li><a href="#" data-section="indexes-list"><i class="fas fa-list"></i>Список индексов</a></li>
<li><a href="#" data-action="index-create"><i class="fas fa-plus"></i>Создать индекс</a></li>
<li><a href="#" data-action="index-drop"><i class="fas fa-trash"></i>Удалить индекс</a></li>
</ul>
</li>
<li class="nav-item has-submenu">
<a href="#" class="nav-link" data-submenu="triggers">
<i class="fas fa-bolt"></i>
<span>Триггеры</span>
<i class="fas fa-chevron-down"></i>
</a>
<ul class="submenu">
<li><a href="#" data-section="triggers-list"><i class="fas fa-list"></i>Список триггеров</a></li>
<li><a href="#" data-action="trigger-create"><i class="fas fa-plus"></i>Создать триггер</a></li>
<li><a href="#" data-section="trigger-log"><i class="fas fa-history"></i>Лог выполнения</a></li>
</ul>
</li>
<li class="nav-item has-submenu">
<a href="#" class="nav-link" data-submenu="import-export">
<i class="fas fa-database"></i>
<span>Импорт/Экспорт</span>
<i class="fas fa-chevron-down"></i>
</a>
<ul class="submenu">
<li><a href="#" data-section="export-data"><i class="fas fa-upload"></i>Экспорт данных</a></li>
<li><a href="#" data-section="import-data"><i class="fas fa-download"></i>Импорт данных</a></li>
</ul>
</li>
<li class="nav-item">
<a href="#" class="nav-link" data-section="cluster">
<i class="fas fa-network-wired"></i>
<span>Управление кластером</span>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link" data-section="audit">
<i class="fas fa-history"></i>
<span>Аудит</span>
</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link" data-section="settings">
<i class="fas fa-cog"></i>
<span>Настройки</span>
</a>
</li>
</ul>
<div class="sidebar-footer">
<div class="user-info" id="userInfo">
<i class="fas fa-user-circle"></i>
<span>Гость</span>
</div>
<button class="logout-btn" id="logoutBtn">
<i class="fas fa-sign-out-alt"></i>
<span>Выход</span>
</button>
</div>
</nav>
<main class="main-content">
<header class="top-bar">
<h1 id="pageTitle">Панель управления</h1>
<div class="connection-status" id="connectionStatus">
<span>Подключено</span>
</div>
</header>
<div class="content-area" id="contentArea">
<div class="loading-spinner">
<i class="fas fa-spinner fa-pulse"></i>
<p>Загрузка...</p>
</div>
</div>
</main>
</div>
<div id="modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modalTitle">Заголовок</h2>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body" id="modalBody"></div>
<div class="modal-footer">
<button class="btn btn-secondary modal-close">Отмена</button>
<button class="btn btn-primary" id="modalConfirm">Подтвердить</button>
</div>
</div>
</div>
<div id="notificationContainer" class="notification-container"></div>
<script src="/static/app.js"></script>
</body>
</html>`
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)
}
// ==================== Constraints 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)
// Required fields
requiredFields := coll.GetRequiredFields()
for _, field := range requiredFields {
constraintsList = append(constraintsList, map[string]interface{}{
"type": "required",
"field": field,
})
}
// Unique constraints
uniqueFields := coll.GetUniqueConstraints()
for _, field := range uniqueFields {
constraintsList = append(constraintsList, map[string]interface{}{
"type": "unique",
"field": field,
})
}
// Min constraints
minConstraints := coll.GetMinConstraints()
for field, value := range minConstraints {
constraintsList = append(constraintsList, map[string]interface{}{
"type": "min",
"field": field,
"value": value,
})
}
// Max constraints
maxConstraints := coll.GetMaxConstraints()
for field, value := range maxConstraints {
constraintsList = append(constraintsList, map[string]interface{}{
"type": "max",
"field": field,
"value": value,
})
}
// Enum constraints
enumConstraints := coll.GetEnumConstraints()
for field, values := range enumConstraints {
constraintsList = append(constraintsList, map[string]interface{}{
"type": "enum",
"field": field,
"values": values,
})
}
// Regex constraints
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.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.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.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.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++ {
// Try to parse as number first
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.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.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.sendJSONSuccess(wr, map[string]interface{}{
"status": "removed",
"type": constraintType,
"field": field,
})
default:
w.sendJSONError(wr, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// 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,
})
}