first commit
This commit is contained in:
569
internal/api/http.go
Normal file
569
internal/api/http.go
Normal file
@@ -0,0 +1,569 @@
|
||||
// Файл: 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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user