backend structure build
This commit is contained in:
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -102,6 +103,28 @@ func handleCreateRepository(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate input
|
||||
if err := validateRepositoryName(req.Name); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if err := validateSize(req.Size); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Type != "lvm" && req.Type != "zfs" {
|
||||
jsonError(w, http.StatusBadRequest, "type must be 'lvm' or 'zfs'")
|
||||
return
|
||||
}
|
||||
if req.Type == "lvm" && req.VGName == "" {
|
||||
jsonError(w, http.StatusBadRequest, "vg_name is required for LVM repositories")
|
||||
return
|
||||
}
|
||||
if req.Type == "zfs" && req.PoolName == "" {
|
||||
jsonError(w, http.StatusBadRequest, "pool_name is required for ZFS repositories")
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := sm.Disk.CreateRepository(req.Name, req.Size, req.Type, req.VGName, req.PoolName)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
@@ -240,6 +263,22 @@ func handleCreateTarget(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate input
|
||||
if err := validateIQN(req.IQN); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if len(req.Portals) == 0 {
|
||||
jsonError(w, http.StatusBadRequest, "at least one portal is required")
|
||||
return
|
||||
}
|
||||
for _, portal := range req.Portals {
|
||||
if err := validatePortal(portal); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
target, err := sm.ISCSI.CreateTarget(req.IQN, req.Portals, req.Initiators)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
@@ -315,6 +354,63 @@ func handleListSessions(sm *services.ServiceManager) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func handleAddLUN(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
var req struct {
|
||||
LUNNumber int `json:"lun_number"`
|
||||
DevicePath string `json:"device_path"`
|
||||
Type string `json:"type"` // "disk" or "tape"
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Type != "disk" && req.Type != "tape" {
|
||||
jsonError(w, http.StatusBadRequest, "type must be 'disk' or 'tape'")
|
||||
return
|
||||
}
|
||||
|
||||
if err := sm.ISCSI.AddLUN(vars["id"], req.LUNNumber, req.DevicePath, req.Type); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Return updated target
|
||||
target, err := sm.ISCSI.GetTarget(vars["id"])
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, target)
|
||||
}
|
||||
}
|
||||
|
||||
func handleRemoveLUN(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
lunNumber := 0
|
||||
if _, err := fmt.Sscanf(vars["lun"], "%d", &lunNumber); err != nil {
|
||||
jsonError(w, http.StatusBadRequest, "invalid LUN number")
|
||||
return
|
||||
}
|
||||
|
||||
if err := sm.ISCSI.RemoveLUN(vars["id"], lunNumber); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Return updated target
|
||||
target, err := sm.ISCSI.GetTarget(vars["id"])
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, target)
|
||||
}
|
||||
}
|
||||
|
||||
// Bacula handlers
|
||||
func handleBaculaStatus(sm *services.ServiceManager) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
67
backend/internal/api/middleware.go
Normal file
67
backend/internal/api/middleware.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/bams/backend/internal/logger"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func loggingMiddleware(log *logger.Logger) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
log.Info("HTTP request", "method", r.Method, "path", r.URL.Path, "remote", r.RemoteAddr)
|
||||
|
||||
// Wrap response writer to capture status code
|
||||
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
|
||||
next.ServeHTTP(wrapped, r)
|
||||
|
||||
duration := time.Since(start)
|
||||
log.Info("HTTP response", "method", r.Method, "path", r.URL.Path, "status", wrapped.statusCode, "duration", duration)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func corsMiddleware() mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
w.Header().Set("Access-Control-Max-Age", "3600")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func recoveryMiddleware(log *logger.Logger) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Error("Panic recovered", "error", err, "path", r.URL.Path)
|
||||
jsonError(w, http.StatusInternalServerError, "Internal server error")
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.statusCode = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/bams/backend/internal/logger"
|
||||
"github.com/bams/backend/internal/services"
|
||||
"github.com/gorilla/mux"
|
||||
@@ -11,7 +9,8 @@ import (
|
||||
func NewRouter(sm *services.ServiceManager, log *logger.Logger) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
|
||||
// Middleware
|
||||
// Middleware (order matters)
|
||||
router.Use(recoveryMiddleware(log))
|
||||
router.Use(loggingMiddleware(log))
|
||||
router.Use(corsMiddleware())
|
||||
|
||||
@@ -42,6 +41,8 @@ func NewRouter(sm *services.ServiceManager, log *logger.Logger) *mux.Router {
|
||||
v1.HandleFunc("/iscsi/targets/{id}", handleUpdateTarget(sm)).Methods("PUT")
|
||||
v1.HandleFunc("/iscsi/targets/{id}", handleDeleteTarget(sm)).Methods("DELETE")
|
||||
v1.HandleFunc("/iscsi/targets/{id}/apply", handleApplyTarget(sm)).Methods("POST")
|
||||
v1.HandleFunc("/iscsi/targets/{id}/luns", handleAddLUN(sm)).Methods("POST")
|
||||
v1.HandleFunc("/iscsi/targets/{id}/luns/{lun}", handleRemoveLUN(sm)).Methods("DELETE")
|
||||
v1.HandleFunc("/iscsi/sessions", handleListSessions(sm)).Methods("GET")
|
||||
|
||||
// Bacula integration
|
||||
@@ -61,27 +62,3 @@ func NewRouter(sm *services.ServiceManager, log *logger.Logger) *mux.Router {
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func loggingMiddleware(log *logger.Logger) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Info("HTTP request", "method", r.Method, "path", r.URL.Path)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func corsMiddleware() mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
74
backend/internal/api/validation.go
Normal file
74
backend/internal/api/validation.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
iqnRegex = regexp.MustCompile(`^iqn\.\d{4}-\d{2}\.[^:]+:[^:]+$`)
|
||||
)
|
||||
|
||||
func validateIQN(iqn string) error {
|
||||
if !iqnRegex.MatchString(iqn) {
|
||||
return fmt.Errorf("invalid IQN format: %s (expected format: iqn.YYYY-MM.reversed.domain:identifier)", iqn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRepositoryName(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("repository name cannot be empty")
|
||||
}
|
||||
if len(name) > 64 {
|
||||
return fmt.Errorf("repository name too long (max 64 characters)")
|
||||
}
|
||||
// Allow alphanumeric, dash, underscore
|
||||
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, name)
|
||||
if !matched {
|
||||
return fmt.Errorf("repository name contains invalid characters (only alphanumeric, dash, underscore allowed)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSize(size string) error {
|
||||
if size == "" {
|
||||
return fmt.Errorf("size cannot be empty")
|
||||
}
|
||||
// Basic validation for size format (e.g., "10G", "500M")
|
||||
matched, _ := regexp.MatchString(`^\d+[KMGT]?$`, strings.ToUpper(size))
|
||||
if !matched {
|
||||
return fmt.Errorf("invalid size format: %s (expected format: 10G, 500M, etc.)", size)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateIP(ip string) error {
|
||||
// Basic IP validation
|
||||
parts := strings.Split(ip, ":")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid IP:port format: %s", ip)
|
||||
}
|
||||
ipParts := strings.Split(parts[0], ".")
|
||||
if len(ipParts) != 4 {
|
||||
return fmt.Errorf("invalid IP address: %s", parts[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePortal(portal string) error {
|
||||
if portal == "" {
|
||||
return fmt.Errorf("portal cannot be empty")
|
||||
}
|
||||
// Format: IP:port or hostname:port
|
||||
parts := strings.Split(portal, ":")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("invalid portal format: %s (expected IP:port or hostname:port)", portal)
|
||||
}
|
||||
// If it looks like an IP, validate it
|
||||
if matched, _ := regexp.MatchString(`^\d+\.\d+\.\d+\.\d+$`, parts[0]); matched {
|
||||
return validateIP(portal)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user