@ -176,9 +176,8 @@ func (h *Handlers) HandleAPIGetApp() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// HandleAPICreateApp returns a handler that creates a new app.
|
||||
func (h *Handlers) HandleAPICreateApp() http.HandlerFunc {
|
||||
type createRequest struct {
|
||||
// apiCreateRequest is the JSON body for creating an app via the API.
|
||||
type apiCreateRequest struct {
|
||||
Name string `json:"name"`
|
||||
RepoURL string `json:"repoUrl"`
|
||||
Branch string `json:"branch"`
|
||||
@ -186,10 +185,32 @@ func (h *Handlers) HandleAPICreateApp() http.HandlerFunc {
|
||||
DockerNetwork string `json:"dockerNetwork"`
|
||||
NtfyTopic string `json:"ntfyTopic"`
|
||||
SlackWebhook string `json:"slackWebhook"`
|
||||
}
|
||||
|
||||
// validateCreateRequest validates the fields of an API create app request.
|
||||
// Returns an error message string or empty string if valid.
|
||||
func validateCreateRequest(req *apiCreateRequest) string {
|
||||
if req.Name == "" || req.RepoURL == "" {
|
||||
return "name and repo_url are required"
|
||||
}
|
||||
|
||||
nameErr := validateAppName(req.Name)
|
||||
if nameErr != nil {
|
||||
return "invalid app name: " + nameErr.Error()
|
||||
}
|
||||
|
||||
repoURLErr := validateRepoURL(req.RepoURL)
|
||||
if repoURLErr != nil {
|
||||
return "invalid repository URL: " + repoURLErr.Error()
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// HandleAPICreateApp returns a handler that creates a new app.
|
||||
func (h *Handlers) HandleAPICreateApp() http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
var req createRequest
|
||||
var req apiCreateRequest
|
||||
|
||||
decodeErr := json.NewDecoder(request.Body).Decode(&req)
|
||||
if decodeErr != nil {
|
||||
@ -200,27 +221,9 @@ func (h *Handlers) HandleAPICreateApp() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" || req.RepoURL == "" {
|
||||
if errMsg := validateCreateRequest(&req); errMsg != "" {
|
||||
h.respondJSON(writer, request,
|
||||
map[string]string{"error": "name and repo_url are required"},
|
||||
http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
nameErr := validateAppName(req.Name)
|
||||
if nameErr != nil {
|
||||
h.respondJSON(writer, request,
|
||||
map[string]string{"error": "invalid app name: " + nameErr.Error()},
|
||||
http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
repoURLErr := validateRepoURL(req.RepoURL)
|
||||
if repoURLErr != nil {
|
||||
h.respondJSON(writer, request,
|
||||
map[string]string{"error": "invalid repository URL: " + repoURLErr.Error()},
|
||||
map[string]string{"error": errMsg},
|
||||
http.StatusBadRequest)
|
||||
|
||||
return
|
||||
|
||||
6
internal/handlers/export_test.go
Normal file
6
internal/handlers/export_test.go
Normal file
@ -0,0 +1,6 @@
|
||||
package handlers
|
||||
|
||||
// ValidateRepoURLForTest exports validateRepoURL for testing.
|
||||
func ValidateRepoURLForTest(repoURL string) error {
|
||||
return validateRepoURL(repoURL)
|
||||
}
|
||||
@ -780,6 +780,7 @@ func TestHandleVolumeAddValidatesPaths(t *testing.T) {
|
||||
// Check if volume was created by listing volumes
|
||||
volumes, _ := createdApp.GetVolumes(context.Background())
|
||||
found := false
|
||||
|
||||
for _, v := range volumes {
|
||||
if v.HostPath == tt.hostPath && v.ContainerPath == tt.containerPath {
|
||||
found = true
|
||||
|
||||
@ -20,6 +20,16 @@ var (
|
||||
// Only the "git" user is allowed, as that is the standard for SSH deploy keys.
|
||||
var scpLikeRepoRe = regexp.MustCompile(`^git@[a-zA-Z0-9._-]+:.+$`)
|
||||
|
||||
// allowedRepoSchemes lists the URL schemes accepted for repository URLs.
|
||||
//
|
||||
//nolint:gochecknoglobals // package-level constant map parsed once
|
||||
var allowedRepoSchemes = map[string]bool{
|
||||
"https": true,
|
||||
"http": true,
|
||||
"ssh": true,
|
||||
"git": true,
|
||||
}
|
||||
|
||||
// validateRepoURL checks that the given repository URL is valid and uses an allowed scheme.
|
||||
func validateRepoURL(repoURL string) error {
|
||||
if strings.TrimSpace(repoURL) == "" {
|
||||
@ -41,17 +51,17 @@ func validateRepoURL(repoURL string) error {
|
||||
return errRepoURLScheme
|
||||
}
|
||||
|
||||
// Parse as standard URL
|
||||
return validateParsedRepoURL(repoURL)
|
||||
}
|
||||
|
||||
// validateParsedRepoURL validates a standard URL-format repository URL.
|
||||
func validateParsedRepoURL(repoURL string) error {
|
||||
parsed, err := url.Parse(repoURL)
|
||||
if err != nil {
|
||||
return errRepoURLInvalid
|
||||
}
|
||||
|
||||
// Must have a recognized scheme
|
||||
switch strings.ToLower(parsed.Scheme) {
|
||||
case "https", "http", "ssh", "git":
|
||||
// OK
|
||||
default:
|
||||
if !allowedRepoSchemes[strings.ToLower(parsed.Scheme)] {
|
||||
return errRepoURLInvalid
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
package handlers
|
||||
package handlers_test
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/upaas/internal/handlers"
|
||||
)
|
||||
|
||||
func TestValidateRepoURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
@ -43,13 +47,13 @@ func TestValidateRepoURL(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := validateRepoURL(tc.url)
|
||||
err := handlers.ValidateRepoURLForTest(tc.url)
|
||||
if tc.wantErr && err == nil {
|
||||
t.Errorf("validateRepoURL(%q) = nil, want error", tc.url)
|
||||
t.Errorf("ValidateRepoURLForTest(%q) = nil, want error", tc.url)
|
||||
}
|
||||
|
||||
if !tc.wantErr && err != nil {
|
||||
t.Errorf("validateRepoURL(%q) = %v, want nil", tc.url, err)
|
||||
t.Errorf("ValidateRepoURLForTest(%q) = %v, want nil", tc.url, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user