This commit is contained in:
@@ -9,8 +9,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/auth"
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/errors"
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/models"
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/storage"
|
||||
"gitea.avt.data-center.id/othman.suseno/atlas/internal/validation"
|
||||
)
|
||||
|
||||
// pathParam is now in router_helpers.go
|
||||
@@ -45,12 +47,18 @@ func (a *App) handleCreatePool(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
writeError(w, errors.ErrBadRequest("invalid request body"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" || len(req.VDEVs) == 0 {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name and vdevs are required"})
|
||||
// Validate pool name
|
||||
if err := validation.ValidateZFSName(req.Name); err != nil {
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.VDEVs) == 0 {
|
||||
writeError(w, errors.ErrValidation("at least one vdev is required"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -224,17 +232,31 @@ func (a *App) handleListZVOLs(w http.ResponseWriter, r *http.Request) {
|
||||
func (a *App) handleCreateZVOL(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Size uint64 `json:"size"` // in bytes
|
||||
Size string `json:"size"` // human-readable format (e.g., "10G")
|
||||
Options map[string]string `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
writeError(w, errors.ErrBadRequest("invalid request body"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" || req.Size == 0 {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name and size are required"})
|
||||
// Validate ZVOL name
|
||||
if err := validation.ValidateZFSName(req.Name); err != nil {
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate size format
|
||||
if err := validation.ValidateSize(req.Size); err != nil {
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse size to bytes
|
||||
sizeBytes, err := a.parseSizeString(req.Size)
|
||||
if err != nil {
|
||||
writeError(w, errors.ErrValidation(fmt.Sprintf("invalid size: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -242,7 +264,7 @@ func (a *App) handleCreateZVOL(w http.ResponseWriter, r *http.Request) {
|
||||
req.Options = make(map[string]string)
|
||||
}
|
||||
|
||||
if err := a.zfs.CreateZVOL(req.Name, req.Size, req.Options); err != nil {
|
||||
if err := a.zfs.CreateZVOL(req.Name, sizeBytes, req.Options); err != nil {
|
||||
log.Printf("create zvol error: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
@@ -314,8 +336,16 @@ func (a *App) handleCreateSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Dataset == "" || req.Name == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "dataset and name are required"})
|
||||
// Validate dataset name
|
||||
if err := validation.ValidateZFSName(req.Dataset); err != nil {
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate snapshot name (can contain @ but we'll validate the base name)
|
||||
snapshotBaseName := strings.ReplaceAll(req.Name, "@", "")
|
||||
if err := validation.ValidateZFSName(snapshotBaseName); err != nil {
|
||||
writeError(w, errors.ErrValidation("invalid snapshot name"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -325,10 +355,10 @@ func (a *App) handleCreateSnapshot(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
snapshotName := fmt.Sprintf("%s@%s", req.Dataset, req.Name)
|
||||
snap, err := a.zfs.GetSnapshot(snapshotName)
|
||||
fullSnapshotName := fmt.Sprintf("%s@%s", req.Dataset, req.Name)
|
||||
snap, err := a.zfs.GetSnapshot(fullSnapshotName)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"message": "snapshot created", "name": snapshotName})
|
||||
writeJSON(w, http.StatusCreated, map[string]string{"message": "snapshot created", "name": fullSnapshotName})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -477,11 +507,27 @@ func (a *App) handleCreateSMBShare(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" || req.Dataset == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name and dataset are required"})
|
||||
// Validate share name
|
||||
if err := validation.ValidateShareName(req.Name); err != nil {
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate dataset name
|
||||
if err := validation.ValidateZFSName(req.Dataset); err != nil {
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Sanitize path if provided
|
||||
if req.Path != "" {
|
||||
req.Path = validation.SanitizePath(req.Path)
|
||||
if err := validation.ValidatePath(req.Path); err != nil {
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate dataset exists
|
||||
datasets, err := a.zfs.ListDatasets("")
|
||||
if err != nil {
|
||||
@@ -509,20 +555,22 @@ func (a *App) handleCreateSMBShare(w http.ResponseWriter, r *http.Request) {
|
||||
share, err := a.smbStore.Create(req.Name, req.Path, req.Dataset, req.Description, req.ReadOnly, req.GuestOK, req.ValidUsers)
|
||||
if err != nil {
|
||||
if err == storage.ErrSMBShareExists {
|
||||
writeJSON(w, http.StatusConflict, map[string]string{"error": "share name already exists"})
|
||||
writeError(w, errors.ErrConflict("share name already exists"))
|
||||
return
|
||||
}
|
||||
log.Printf("create SMB share error: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
writeError(w, errors.ErrInternal("failed to create SMB share").WithDetails(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Apply configuration to Samba service
|
||||
// Apply configuration to Samba service (with graceful degradation)
|
||||
shares := a.smbStore.List()
|
||||
if err := a.smbService.ApplyConfiguration(shares); err != nil {
|
||||
log.Printf("apply SMB configuration error: %v", err)
|
||||
// Don't fail the request, but log the error
|
||||
// In production, you might want to queue this for retry
|
||||
// Log but don't fail the request - desired state is stored
|
||||
// Service configuration can be retried later
|
||||
if svcErr := a.handleServiceError("SMB", err); svcErr != nil {
|
||||
log.Printf("SMB service configuration failed (non-fatal): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, share)
|
||||
@@ -629,11 +677,29 @@ func (a *App) handleCreateNFSExport(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Dataset == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "dataset is required"})
|
||||
// Validate dataset name
|
||||
if err := validation.ValidateZFSName(req.Dataset); err != nil {
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate and sanitize path if provided
|
||||
if req.Path != "" {
|
||||
req.Path = validation.SanitizePath(req.Path)
|
||||
if err := validation.ValidatePath(req.Path); err != nil {
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate clients
|
||||
for i, client := range req.Clients {
|
||||
if err := validation.ValidateCIDR(client); err != nil {
|
||||
writeError(w, errors.ErrValidation(fmt.Sprintf("client[%d]: %s", i, err.Error())))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate dataset exists
|
||||
datasets, err := a.zfs.ListDatasets("")
|
||||
if err != nil {
|
||||
@@ -786,14 +852,9 @@ func (a *App) handleCreateISCSITarget(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.IQN == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "iqn is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Basic IQN format validation (iqn.yyyy-mm.reversed.domain:identifier)
|
||||
if !strings.HasPrefix(req.IQN, "iqn.") {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid IQN format (must start with 'iqn.')"})
|
||||
// Validate IQN format
|
||||
if err := validation.ValidateIQN(req.IQN); err != nil {
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1065,8 +1126,14 @@ func (a *App) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username == "" || req.Password == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username and password are required"})
|
||||
// Validate username (login is less strict - just check not empty)
|
||||
if req.Username == "" {
|
||||
writeError(w, errors.ErrValidation("username is required"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password == "" {
|
||||
writeError(w, errors.ErrValidation("password is required"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1116,11 +1183,26 @@ func (a *App) handleCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username == "" || req.Password == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username and password are required"})
|
||||
// Validate username
|
||||
if err := validation.ValidateUsername(req.Username); err != nil {
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate password
|
||||
if err := validation.ValidatePassword(req.Password); err != nil {
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate email if provided
|
||||
if req.Email != "" {
|
||||
if err := validation.ValidateEmail(req.Email); err != nil {
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Role == "" {
|
||||
req.Role = models.RoleViewer // Default role
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user