Upload files to "internal/api"
This commit is contained in:
640
internal/api/webui.go
Normal file
640
internal/api/webui.go
Normal file
@@ -0,0 +1,640 @@
|
|||||||
|
/*
|
||||||
|
* 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.handleIndex)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleIndex возвращает главную HTML страницу
|
||||||
|
func (w *WebUIServer) handleIndex(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">
|
||||||
|
<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">×</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))
|
||||||
|
|
||||||
|
// Пытаемся аутентифицировать через ACL менеджер
|
||||||
|
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))
|
||||||
|
|
||||||
|
// Устанавливаем cookie сессии
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user