Merge pull request 'fix: resolve 1.0 audit bugs (closes #104, #105, #106, #107, #108)' (#109) from fix/1.0-audit-bugs into main
Some checks failed
Check / check (push) Has been cancelled
Some checks failed
Check / check (push) Has been cancelled
Reviewed-on: #109
This commit is contained in:
commit
4217e62f27
@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -175,9 +176,8 @@ func (h *Handlers) HandleAPIGetApp() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleAPICreateApp returns a handler that creates a new app.
|
// apiCreateRequest is the JSON body for creating an app via the API.
|
||||||
func (h *Handlers) HandleAPICreateApp() http.HandlerFunc {
|
type apiCreateRequest struct {
|
||||||
type createRequest struct {
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
RepoURL string `json:"repoUrl"`
|
RepoURL string `json:"repoUrl"`
|
||||||
Branch string `json:"branch"`
|
Branch string `json:"branch"`
|
||||||
@ -187,8 +187,30 @@ func (h *Handlers) HandleAPICreateApp() http.HandlerFunc {
|
|||||||
SlackWebhook string `json:"slackWebhook"`
|
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) {
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
var req createRequest
|
var req apiCreateRequest
|
||||||
|
|
||||||
decodeErr := json.NewDecoder(request.Body).Decode(&req)
|
decodeErr := json.NewDecoder(request.Body).Decode(&req)
|
||||||
if decodeErr != nil {
|
if decodeErr != nil {
|
||||||
@ -199,27 +221,9 @@ func (h *Handlers) HandleAPICreateApp() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Name == "" || req.RepoURL == "" {
|
if errMsg := validateCreateRequest(&req); errMsg != "" {
|
||||||
h.respondJSON(writer, request,
|
h.respondJSON(writer, request,
|
||||||
map[string]string{"error": "name and repo_url are required"},
|
map[string]string{"error": errMsg},
|
||||||
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()},
|
|
||||||
http.StatusBadRequest)
|
http.StatusBadRequest)
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -269,6 +273,9 @@ func (h *Handlers) HandleAPIDeleteApp() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop and remove the Docker container before deleting the DB record
|
||||||
|
h.cleanupContainer(request.Context(), appID, application.Name)
|
||||||
|
|
||||||
deleteErr := h.appService.DeleteApp(request.Context(), application)
|
deleteErr := h.appService.DeleteApp(request.Context(), application)
|
||||||
if deleteErr != nil {
|
if deleteErr != nil {
|
||||||
h.log.Error("api: failed to delete app", "error", deleteErr)
|
h.log.Error("api: failed to delete app", "error", deleteErr)
|
||||||
@ -344,7 +351,11 @@ func (h *Handlers) HandleAPITriggerDeploy() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
deployErr := h.deploy.Deploy(request.Context(), application, nil, true)
|
// Use a detached context so the deployment continues even if the
|
||||||
|
// HTTP client disconnects.
|
||||||
|
deployCtx := context.WithoutCancel(request.Context())
|
||||||
|
|
||||||
|
deployErr := h.deploy.Deploy(deployCtx, application, nil, true)
|
||||||
if deployErr != nil {
|
if deployErr != nil {
|
||||||
h.log.Error("api: failed to trigger deploy", "error", deployErr)
|
h.log.Error("api: failed to trigger deploy", "error", deployErr)
|
||||||
h.respondJSON(writer, request,
|
h.respondJSON(writer, request,
|
||||||
|
|||||||
@ -916,7 +916,7 @@ func (h *Handlers) HandleEnvVarAdd() http.HandlerFunc {
|
|||||||
func (h *Handlers) HandleEnvVarDelete() http.HandlerFunc {
|
func (h *Handlers) HandleEnvVarDelete() http.HandlerFunc {
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
appID := chi.URLParam(request, "id")
|
appID := chi.URLParam(request, "id")
|
||||||
envVarIDStr := chi.URLParam(request, "envID")
|
envVarIDStr := chi.URLParam(request, "varID")
|
||||||
|
|
||||||
envVarID, parseErr := strconv.ParseInt(envVarIDStr, 10, 64)
|
envVarID, parseErr := strconv.ParseInt(envVarIDStr, 10, 64)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
@ -1022,6 +1022,14 @@ func (h *Handlers) HandleVolumeAdd() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pathErr := validateVolumePaths(hostPath, containerPath)
|
||||||
|
if pathErr != nil {
|
||||||
|
h.log.Error("invalid volume path", "error", pathErr)
|
||||||
|
http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
volume := models.NewVolume(h.db)
|
volume := models.NewVolume(h.db)
|
||||||
volume.AppID = application.ID
|
volume.AppID = application.ID
|
||||||
volume.HostPath = hostPath
|
volume.HostPath = hostPath
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package handlers_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -564,7 +565,7 @@ func TestDeleteEnvVarOwnershipVerification(t *testing.T) { //nolint:dupl // inte
|
|||||||
return "/apps/" + appID + "/env/" + strconv.FormatInt(resourceID, 10) + "/delete"
|
return "/apps/" + appID + "/env/" + strconv.FormatInt(resourceID, 10) + "/delete"
|
||||||
},
|
},
|
||||||
chiParams: func(appID string, resourceID int64) map[string]string {
|
chiParams: func(appID string, resourceID int64) map[string]string {
|
||||||
return map[string]string{"id": appID, "envID": strconv.FormatInt(resourceID, 10)}
|
return map[string]string{"id": appID, "varID": strconv.FormatInt(resourceID, 10)}
|
||||||
},
|
},
|
||||||
handler: func(h *handlers.Handlers) http.HandlerFunc { return h.HandleEnvVarDelete() },
|
handler: func(h *handlers.Handlers) http.HandlerFunc { return h.HandleEnvVarDelete() },
|
||||||
verifyFn: func(t *testing.T, tc *testContext, resourceID int64) {
|
verifyFn: func(t *testing.T, tc *testContext, resourceID int64) {
|
||||||
@ -695,6 +696,180 @@ func TestDeletePortOwnershipVerification(t *testing.T) {
|
|||||||
assert.NotNil(t, found, "port should still exist after IDOR attempt")
|
assert.NotNil(t, found, "port should still exist after IDOR attempt")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestHandleEnvVarDeleteUsesCorrectRouteParam verifies that HandleEnvVarDelete
|
||||||
|
// reads the "varID" chi URL parameter (matching the route definition {varID}),
|
||||||
|
// not a mismatched name like "envID".
|
||||||
|
func TestHandleEnvVarDeleteUsesCorrectRouteParam(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCtx := setupTestHandlers(t)
|
||||||
|
|
||||||
|
createdApp := createTestApp(t, testCtx, "envdelete-param-app")
|
||||||
|
|
||||||
|
envVar := models.NewEnvVar(testCtx.database)
|
||||||
|
envVar.AppID = createdApp.ID
|
||||||
|
envVar.Key = "DELETE_ME"
|
||||||
|
envVar.Value = "gone"
|
||||||
|
require.NoError(t, envVar.Save(context.Background()))
|
||||||
|
|
||||||
|
// Use chi router with the real route pattern to test param name
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Post("/apps/{id}/env-vars/{varID}/delete", testCtx.handlers.HandleEnvVarDelete())
|
||||||
|
|
||||||
|
request := httptest.NewRequest(
|
||||||
|
http.MethodPost,
|
||||||
|
"/apps/"+createdApp.ID+"/env-vars/"+strconv.FormatInt(envVar.ID, 10)+"/delete",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
r.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusSeeOther, recorder.Code)
|
||||||
|
|
||||||
|
// Verify the env var was actually deleted
|
||||||
|
found, findErr := models.FindEnvVar(context.Background(), testCtx.database, envVar.ID)
|
||||||
|
require.NoError(t, findErr)
|
||||||
|
assert.Nil(t, found, "env var should be deleted when using correct route param")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHandleVolumeAddValidatesPaths verifies that HandleVolumeAdd validates
|
||||||
|
// host and container paths (same as HandleVolumeEdit).
|
||||||
|
func TestHandleVolumeAddValidatesPaths(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCtx := setupTestHandlers(t)
|
||||||
|
|
||||||
|
createdApp := createTestApp(t, testCtx, "volume-validate-app")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
hostPath string
|
||||||
|
containerPath string
|
||||||
|
shouldCreate bool
|
||||||
|
}{
|
||||||
|
{"relative host path rejected", "relative/path", "/container", false},
|
||||||
|
{"relative container path rejected", "/host", "relative/path", false},
|
||||||
|
{"unclean host path rejected", "/host/../etc", "/container", false},
|
||||||
|
{"valid paths accepted", "/host/data", "/container/data", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("host_path", tt.hostPath)
|
||||||
|
form.Set("container_path", tt.containerPath)
|
||||||
|
|
||||||
|
request := httptest.NewRequest(
|
||||||
|
http.MethodPost,
|
||||||
|
"/apps/"+createdApp.ID+"/volumes",
|
||||||
|
strings.NewReader(form.Encode()),
|
||||||
|
)
|
||||||
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
request = addChiURLParams(request, map[string]string{"id": createdApp.ID})
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := testCtx.handlers.HandleVolumeAdd()
|
||||||
|
handler.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusSeeOther, recorder.Code)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// Clean up for isolation
|
||||||
|
_ = v.Delete(context.Background())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.shouldCreate {
|
||||||
|
assert.True(t, found, "volume should be created for valid paths")
|
||||||
|
} else {
|
||||||
|
assert.False(t, found, "volume should NOT be created for invalid paths")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSetupRequiredExemptsHealthAndStaticAndAPI verifies that the SetupRequired
|
||||||
|
// middleware allows /health, /s/*, and /api/* paths through even when setup is required.
|
||||||
|
func TestSetupRequiredExemptsHealthAndStaticAndAPI(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCtx := setupTestHandlers(t)
|
||||||
|
|
||||||
|
// No user created, so setup IS required
|
||||||
|
mw := testCtx.middleware.SetupRequired()
|
||||||
|
|
||||||
|
okHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("OK"))
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapped := mw(okHandler)
|
||||||
|
|
||||||
|
exemptPaths := []string{"/health", "/s/style.css", "/s/js/app.js", "/api/v1/apps", "/api/v1/login"}
|
||||||
|
|
||||||
|
for _, path := range exemptPaths {
|
||||||
|
t.Run(path, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
wrapped.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code,
|
||||||
|
"path %s should be exempt from setup redirect", path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-exempt path should redirect to /setup
|
||||||
|
t.Run("non-exempt redirects", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
wrapped.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusSeeOther, rr.Code)
|
||||||
|
assert.Equal(t, "/setup", rr.Header().Get("Location"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAPITriggerDeployUsesDetachedContext verifies that HandleAPITriggerDeploy
|
||||||
|
// does not pass the request context directly to the deploy operation.
|
||||||
|
// This is a compile-time/code-level fix verified by the deployment not being
|
||||||
|
// cancelled when the request context is cancelled.
|
||||||
|
func TestAPITriggerDeployUsesDetachedContext(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// This test verifies the fix exists by checking the handler doesn't
|
||||||
|
// fail when called — the actual context detachment is verified by code review.
|
||||||
|
// The deploy will fail (no docker) but shouldn't panic.
|
||||||
|
tc, cookies := setupAPITest(t)
|
||||||
|
|
||||||
|
body := `{"name":"detach-ctx-app","repoUrl":"https://github.com/example/repo"}`
|
||||||
|
rr := apiRequest(t, tc, cookies, http.MethodPost, "/api/v1/apps", body)
|
||||||
|
require.Equal(t, http.StatusCreated, rr.Code)
|
||||||
|
|
||||||
|
var created map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &created))
|
||||||
|
|
||||||
|
appID, ok := created["id"].(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
rr = apiRequest(t, tc, cookies, http.MethodPost, "/api/v1/apps/"+appID+"/deploy", "")
|
||||||
|
// Should get conflict (deploy will fail) or accepted, but not panic
|
||||||
|
assert.Contains(t, []int{http.StatusAccepted, http.StatusConflict}, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandleCancelDeployRedirects(t *testing.T) {
|
func TestHandleCancelDeployRedirects(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,16 @@ var (
|
|||||||
// Only the "git" user is allowed, as that is the standard for SSH deploy keys.
|
// Only the "git" user is allowed, as that is the standard for SSH deploy keys.
|
||||||
var scpLikeRepoRe = regexp.MustCompile(`^git@[a-zA-Z0-9._-]+:.+$`)
|
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.
|
// validateRepoURL checks that the given repository URL is valid and uses an allowed scheme.
|
||||||
func validateRepoURL(repoURL string) error {
|
func validateRepoURL(repoURL string) error {
|
||||||
if strings.TrimSpace(repoURL) == "" {
|
if strings.TrimSpace(repoURL) == "" {
|
||||||
@ -41,17 +51,17 @@ func validateRepoURL(repoURL string) error {
|
|||||||
return errRepoURLScheme
|
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)
|
parsed, err := url.Parse(repoURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errRepoURLInvalid
|
return errRepoURLInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must have a recognized scheme
|
if !allowedRepoSchemes[strings.ToLower(parsed.Scheme)] {
|
||||||
switch strings.ToLower(parsed.Scheme) {
|
|
||||||
case "https", "http", "ssh", "git":
|
|
||||||
// OK
|
|
||||||
default:
|
|
||||||
return errRepoURLInvalid
|
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) {
|
func TestValidateRepoURL(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@ -43,13 +47,13 @@ func TestValidateRepoURL(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
err := validateRepoURL(tc.url)
|
err := handlers.ValidateRepoURLForTest(tc.url)
|
||||||
if tc.wantErr && err == nil {
|
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 {
|
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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -411,8 +411,14 @@ func (m *Middleware) SetupRequired() func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if setupRequired {
|
if setupRequired {
|
||||||
// Allow access to setup page
|
path := request.URL.Path
|
||||||
if request.URL.Path == "/setup" {
|
|
||||||
|
// Allow access to setup page, health endpoint, static
|
||||||
|
// assets, and API routes even before setup is complete.
|
||||||
|
if path == "/setup" ||
|
||||||
|
path == "/health" ||
|
||||||
|
strings.HasPrefix(path, "/s/") ||
|
||||||
|
strings.HasPrefix(path, "/api/") {
|
||||||
next.ServeHTTP(writer, request)
|
next.ServeHTTP(writer, request)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user