570 lines
18 KiB
Go
570 lines
18 KiB
Go
// Файл: 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,
|
||
})
|
||
}
|