Files
futriis/internal/api/http.go
2026-04-08 21:43:35 +03:00

570 lines
18 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.
// Файл: internal/api/http.go
// Назначение: HTTP RESTful API для взаимодействия с СУБД через curl.
// Поддерживает CRUD операции, управление индексами, ACL и ограничениями.
// Реализован с минимальными блокировками, использует wait-free структуры.
package api
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"futriis/internal/acl"
"futriis/internal/cluster"
"futriis/internal/log"
"futriis/internal/storage"
)
type HTTPServer struct {
store *storage.Storage
coordinator *cluster.RaftCoordinator
aclManager *acl.ACLManager
logger *log.Logger
server *http.Server
port int
}
type APIResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// NewHTTPServer создаёт новый HTTP сервер
func NewHTTPServer(port int, store *storage.Storage, coord *cluster.RaftCoordinator, aclMgr *acl.ACLManager, logger *log.Logger) *HTTPServer {
s := &HTTPServer{
store: store,
coordinator: coord,
aclManager: aclMgr,
logger: logger,
port: port,
}
mux := http.NewServeMux()
// Middleware для аутентификации
mux.HandleFunc("/api/auth/login", s.handleLogin)
mux.HandleFunc("/api/auth/logout", s.handleLogout)
// CRUD операции
mux.HandleFunc("/api/db/", s.handleDatabaseRequest)
// Индексы
mux.HandleFunc("/api/index/", s.handleIndexRequest)
// ACL
mux.HandleFunc("/api/acl/", s.handleACLRequest)
// Constraints
mux.HandleFunc("/api/constraint/", s.handleConstraintRequest)
// Cluster
mux.HandleFunc("/api/cluster/", s.handleClusterRequest)
s.server = &http.Server{
Addr: fmt.Sprintf(":%d", port),
Handler: mux,
}
return s
}
// Start запускает HTTP сервер
func (s *HTTPServer) Start() error {
s.logger.Info("Starting HTTP API server on port " + strconv.Itoa(s.port))
return s.server.ListenAndServe()
}
// Stop останавливает HTTP сервер
func (s *HTTPServer) Stop() error {
return s.server.Close()
}
// handleLogin обрабатывает аутентификацию
func (s *HTTPServer) handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var creds struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
s.sendError(w, "Invalid request body", http.StatusBadRequest)
return
}
sessionID, err := s.aclManager.Authenticate(creds.Username, creds.Password)
if err != nil {
s.sendError(w, err.Error(), http.StatusUnauthorized)
return
}
s.sendSuccess(w, map[string]string{"session_id": sessionID})
}
// handleLogout обрабатывает выход
func (s *HTTPServer) handleLogout(w http.ResponseWriter, r *http.Request) {
sessionID := r.Header.Get("X-Session-ID")
if sessionID != "" {
s.aclManager.Logout(sessionID)
}
s.sendSuccess(w, map[string]string{"status": "logged out"})
}
// handleDatabaseRequest обрабатывает запросы к БД
func (s *HTTPServer) handleDatabaseRequest(w http.ResponseWriter, r *http.Request) {
// URL: /api/db/{database}/{collection}/{document_id}
path := strings.TrimPrefix(r.URL.Path, "/api/db/")
parts := strings.Split(path, "/")
if len(parts) < 2 {
s.sendError(w, "Invalid path. Use /api/db/{database}/{collection}[/{id}]", http.StatusBadRequest)
return
}
database := parts[0]
collection := parts[1]
docID := ""
if len(parts) > 2 {
docID = parts[2]
}
// Проверка аутентификации
sessionID := r.Header.Get("X-Session-ID")
if sessionID == "" {
s.sendError(w, "Authentication required", http.StatusUnauthorized)
return
}
switch r.Method {
case http.MethodGet:
s.handleGetDocument(w, r, sessionID, database, collection, docID)
case http.MethodPost:
s.handleInsertDocument(w, r, sessionID, database, collection)
case http.MethodPut:
s.handleUpdateDocument(w, r, sessionID, database, collection, docID)
case http.MethodDelete:
s.handleDeleteDocument(w, r, sessionID, database, collection, docID)
default:
s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// handleGetDocument обрабатывает GET запросы
func (s *HTTPServer) handleGetDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) {
// Проверка прав
if !s.aclManager.CheckPermission(sessionID, database, collection, "read") {
s.sendError(w, "Access denied", http.StatusForbidden)
return
}
db, err := s.store.GetDatabase(database)
if err != nil {
s.sendError(w, err.Error(), http.StatusNotFound)
return
}
coll, err := db.GetCollection(collection)
if err != nil {
s.sendError(w, err.Error(), http.StatusNotFound)
return
}
// Поиск по индексу или ID
query := r.URL.Query()
if indexName := query.Get("index"); indexName != "" {
indexValue := query.Get("value")
docs, err := coll.FindByIndex(indexName, indexValue)
if err != nil {
s.sendError(w, err.Error(), http.StatusNotFound)
return
}
s.sendSuccess(w, docs)
return
}
if docID == "" {
// Возвращаем все документы
docs := coll.GetAllDocuments()
s.sendSuccess(w, docs)
return
}
doc, err := coll.Find(docID)
if err != nil {
s.sendError(w, err.Error(), http.StatusNotFound)
return
}
s.sendSuccess(w, doc)
}
// handleInsertDocument обрабатывает POST запросы
func (s *HTTPServer) handleInsertDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection string) {
if !s.aclManager.CheckPermission(sessionID, database, collection, "write") {
s.sendError(w, "Access denied", http.StatusForbidden)
return
}
db, err := s.store.GetDatabase(database)
if err != nil {
// Создаём БД если не существует
if err := s.store.CreateDatabase(database); err != nil {
s.sendError(w, err.Error(), http.StatusInternalServerError)
return
}
db, _ = s.store.GetDatabase(database)
}
coll, err := db.GetCollection(collection)
if err != nil {
if err := db.CreateCollection(collection); err != nil {
s.sendError(w, err.Error(), http.StatusInternalServerError)
return
}
coll, _ = db.GetCollection(collection)
}
var doc map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&doc); err != nil {
s.sendError(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := coll.InsertFromMap(doc); err != nil {
s.sendError(w, err.Error(), http.StatusBadRequest)
return
}
s.sendSuccess(w, map[string]string{"status": "inserted"})
}
// handleUpdateDocument обрабатывает PUT запросы
func (s *HTTPServer) handleUpdateDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) {
if docID == "" {
s.sendError(w, "Document ID required", http.StatusBadRequest)
return
}
if !s.aclManager.CheckPermission(sessionID, database, collection, "write") {
s.sendError(w, "Access denied", http.StatusForbidden)
return
}
db, err := s.store.GetDatabase(database)
if err != nil {
s.sendError(w, err.Error(), http.StatusNotFound)
return
}
coll, err := db.GetCollection(collection)
if err != nil {
s.sendError(w, err.Error(), http.StatusNotFound)
return
}
var updates map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&updates); err != nil {
s.sendError(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := coll.Update(docID, updates); err != nil {
s.sendError(w, err.Error(), http.StatusBadRequest)
return
}
s.sendSuccess(w, map[string]string{"status": "updated"})
}
// handleDeleteDocument обрабатывает DELETE запросы
func (s *HTTPServer) handleDeleteDocument(w http.ResponseWriter, r *http.Request, sessionID, database, collection, docID string) {
if docID == "" {
s.sendError(w, "Document ID required", http.StatusBadRequest)
return
}
if !s.aclManager.CheckPermission(sessionID, database, collection, "delete") {
s.sendError(w, "Access denied", http.StatusForbidden)
return
}
db, err := s.store.GetDatabase(database)
if err != nil {
s.sendError(w, err.Error(), http.StatusNotFound)
return
}
coll, err := db.GetCollection(collection)
if err != nil {
s.sendError(w, err.Error(), http.StatusNotFound)
return
}
if err := coll.Delete(docID); err != nil {
s.sendError(w, err.Error(), http.StatusNotFound)
return
}
s.sendSuccess(w, map[string]string{"status": "deleted"})
}
// handleIndexRequest обрабатывает запросы к индексам
func (s *HTTPServer) handleIndexRequest(w http.ResponseWriter, r *http.Request) {
// URL: /api/index/{database}/{collection}/{action}
path := strings.TrimPrefix(r.URL.Path, "/api/index/")
parts := strings.Split(path, "/")
if len(parts) < 3 {
s.sendError(w, "Invalid path. Use /api/index/{database}/{collection}/{action}", http.StatusBadRequest)
return
}
database := parts[0]
collection := parts[1]
action := parts[2]
sessionID := r.Header.Get("X-Session-ID")
if !s.aclManager.CheckPermission(sessionID, database, collection, "admin") {
s.sendError(w, "Admin access required", http.StatusForbidden)
return
}
db, err := s.store.GetDatabase(database)
if err != nil {
s.sendError(w, err.Error(), http.StatusNotFound)
return
}
coll, err := db.GetCollection(collection)
if err != nil {
s.sendError(w, err.Error(), http.StatusNotFound)
return
}
switch action {
case "list":
indexes := coll.GetIndexes()
s.sendSuccess(w, indexes)
case "create":
var req struct {
Name string `json:"name"`
Fields []string `json:"fields"`
Unique bool `json:"unique"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.sendError(w, "Invalid request body", http.StatusBadRequest)
return
}
if err := coll.CreateIndex(req.Name, req.Fields, req.Unique); err != nil {
s.sendError(w, err.Error(), http.StatusBadRequest)
return
}
s.sendSuccess(w, map[string]string{"status": "index created"})
case "drop":
indexName := parts[3]
if err := coll.DropIndex(indexName); err != nil {
s.sendError(w, err.Error(), http.StatusBadRequest)
return
}
s.sendSuccess(w, map[string]string{"status": "index dropped"})
default:
s.sendError(w, "Unknown action", http.StatusBadRequest)
}
}
// handleACLRequest обрабатывает запросы ACL
func (s *HTTPServer) handleACLRequest(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/acl/")
parts := strings.Split(path, "/")
if len(parts) < 1 {
s.sendError(w, "Invalid path", http.StatusBadRequest)
return
}
sessionID := r.Header.Get("X-Session-ID")
if !s.aclManager.CheckPermission(sessionID, "*", "*", "admin") {
s.sendError(w, "Admin access required", http.StatusForbidden)
return
}
action := parts[0]
switch action {
case "users":
users := s.aclManager.ListUsers()
s.sendSuccess(w, users)
case "user":
if len(parts) < 2 {
s.sendError(w, "Username required", http.StatusBadRequest)
return
}
username := parts[1]
switch r.Method {
case http.MethodPost:
var req struct {
Password string `json:"password"`
Roles []string `json:"roles"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.sendError(w, "Invalid request body", http.StatusBadRequest)
return
}
if err := s.aclManager.CreateUser(username, req.Password, req.Roles); err != nil {
s.sendError(w, err.Error(), http.StatusBadRequest)
return
}
s.sendSuccess(w, map[string]string{"status": "user created"})
default:
s.sendError(w, "Method not allowed", http.StatusMethodNotAllowed)
}
case "roles":
roles := s.aclManager.ListRoles()
s.sendSuccess(w, roles)
case "grant":
if len(parts) < 3 {
s.sendError(w, "Role and permission required", http.StatusBadRequest)
return
}
roleName := parts[1]
permission := parts[2]
if err := s.aclManager.GrantPermission(roleName, permission); err != nil {
s.sendError(w, err.Error(), http.StatusBadRequest)
return
}
s.sendSuccess(w, map[string]string{"status": "permission granted"})
default:
s.sendError(w, "Unknown action", http.StatusBadRequest)
}
}
// handleConstraintRequest обрабатывает запросы к ограничениям
func (s *HTTPServer) handleConstraintRequest(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/constraint/")
parts := strings.Split(path, "/")
if len(parts) < 3 {
s.sendError(w, "Invalid path. Use /api/constraint/{database}/{collection}/{action}", http.StatusBadRequest)
return
}
database := parts[0]
collection := parts[1]
action := parts[2]
sessionID := r.Header.Get("X-Session-ID")
if !s.aclManager.CheckPermission(sessionID, database, collection, "admin") {
s.sendError(w, "Admin access required", http.StatusForbidden)
return
}
db, err := s.store.GetDatabase(database)
if err != nil {
s.sendError(w, err.Error(), http.StatusNotFound)
return
}
coll, err := db.GetCollection(collection)
if err != nil {
s.sendError(w, err.Error(), http.StatusNotFound)
return
}
switch action {
case "required":
if len(parts) < 4 {
s.sendError(w, "Field name required", http.StatusBadRequest)
return
}
field := parts[3]
coll.AddRequiredField(field)
s.sendSuccess(w, map[string]string{"status": "required field added"})
case "unique":
if len(parts) < 4 {
s.sendError(w, "Field name required", http.StatusBadRequest)
return
}
field := parts[3]
coll.AddUniqueConstraint(field)
s.sendSuccess(w, map[string]string{"status": "unique constraint added"})
case "min":
if len(parts) < 5 {
s.sendError(w, "Field name and value required", http.StatusBadRequest)
return
}
field := parts[3]
minVal, _ := strconv.ParseFloat(parts[4], 64)
coll.AddMinConstraint(field, minVal)
s.sendSuccess(w, map[string]string{"status": "min constraint added"})
case "max":
if len(parts) < 5 {
s.sendError(w, "Field name and value required", http.StatusBadRequest)
return
}
field := parts[3]
maxVal, _ := strconv.ParseFloat(parts[4], 64)
coll.AddMaxConstraint(field, maxVal)
s.sendSuccess(w, map[string]string{"status": "max constraint added"})
default:
s.sendError(w, "Unknown action", http.StatusBadRequest)
}
}
// handleClusterRequest обрабатывает запросы к кластеру
func (s *HTTPServer) handleClusterRequest(w http.ResponseWriter, r *http.Request) {
sessionID := r.Header.Get("X-Session-ID")
if !s.aclManager.CheckPermission(sessionID, "*", "*", "admin") {
s.sendError(w, "Admin access required", http.StatusForbidden)
return
}
if s.coordinator == nil {
s.sendError(w, "Cluster not available", http.StatusServiceUnavailable)
return
}
status := s.coordinator.GetClusterStatus()
s.sendSuccess(w, status)
}
// sendSuccess отправляет успешный ответ
func (s *HTTPServer) sendSuccess(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(APIResponse{
Success: true,
Data: data,
})
}
// sendError отправляет ответ с ошибкой
func (s *HTTPServer) sendError(w http.ResponseWriter, errMsg string, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(APIResponse{
Success: false,
Error: errMsg,
})
}