From 2e0447d500018888779db7d9545569f6ee9a66ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=BCli=20Patrik=20P=C3=A9ter?= Date: Fri, 3 Oct 2025 08:21:37 +0200 Subject: [PATCH 1/6] Adds support for path prefix configuration Introduces configurable path prefix to serve application under subpath (e.g., /pgbackweb) Implements validation for path prefixes in environment variables Updates all redirects and routes to use the configured path prefix This allows deploying the app at a custom URL path while maintaining correct functionality. --- README.md | 5 ++ cmd/app/main.go | 6 +- internal/config/env.go | 1 + internal/config/env_validate.go | 4 + internal/util/pathutil/pathutil.go | 35 +++++++++ internal/util/pathutil/pathutil_test.go | 78 +++++++++++++++++++ internal/validate/path_prefix.go | 43 ++++++++++ internal/validate/path_prefix_test.go | 78 +++++++++++++++++++ internal/view/middleware/require_auth.go | 11 ++- internal/view/middleware/require_no_auth.go | 6 +- internal/view/router.go | 12 ++- internal/view/web/auth/create_first_user.go | 5 +- internal/view/web/auth/login.go | 5 +- internal/view/web/auth/logout.go | 5 +- .../web/dashboard/backups/create_backup.go | 3 +- .../dashboard/databases/create_database.go | 3 +- .../destinations/create_destination.go | 3 +- .../web/dashboard/webhooks/create_webhook.go | 3 +- internal/view/web/router.go | 7 +- 19 files changed, 289 insertions(+), 24 deletions(-) create mode 100644 internal/util/pathutil/pathutil.go create mode 100644 internal/util/pathutil/pathutil_test.go create mode 100644 internal/validate/path_prefix.go create mode 100644 internal/validate/path_prefix_test.go diff --git a/README.md b/README.md index 805867d9..064a35bf 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ services: environment: PBW_ENCRYPTION_KEY: "my_secret_key" # Change this to a strong key PBW_POSTGRES_CONN_STRING: "postgresql://postgres:password@postgres:5432/pgbackweb?sslmode=disable" + # PBW_PATH_PREFIX: "/pgbackweb" # Optional: Use this if serving under a subpath TZ: "America/Guatemala" # Set your timezone, optional depends_on: postgres: @@ -119,6 +120,10 @@ You only need to configure the following environment variables: - `PBW_LISTEN_PORT`: Port for the server to listen on, default 8085 (optional) +- `PBW_PATH_PREFIX`: Path prefix for the application URL. Use this when serving + the application under a subpath (e.g., `/pgbackweb`). Must start with `/` and + not end with `/`. Default is empty (optional) + - `TZ`: Your [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) (optional). Default is `UTC`. This impacts logging, backup filenames and diff --git a/cmd/app/main.go b/cmd/app/main.go index 632deec9..322952e5 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -8,6 +8,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/integration" "github.com/eduardolat/pgbackweb/internal/logger" "github.com/eduardolat/pgbackweb/internal/service" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view" "github.com/labstack/echo/v4" ) @@ -18,6 +19,9 @@ func main() { logger.FatalError("error getting environment variables", logger.KV{"error": err}) } + // Initialize the path prefix utility + pathutil.SetPathPrefix(env.PBW_PATH_PREFIX) + cr, err := cron.New() if err != nil { logger.FatalError("error initializing cron scheduler", logger.KV{"error": err}) @@ -44,7 +48,7 @@ func main() { app := echo.New() app.HideBanner = true app.HidePort = true - view.MountRouter(app, servs) + view.MountRouter(app, servs, env) address := env.PBW_LISTEN_HOST + ":" + env.PBW_LISTEN_PORT logger.Info("server started at http://localhost:"+env.PBW_LISTEN_PORT, logger.KV{ diff --git a/internal/config/env.go b/internal/config/env.go index b4067ee7..31d39852 100644 --- a/internal/config/env.go +++ b/internal/config/env.go @@ -12,6 +12,7 @@ type Env struct { PBW_POSTGRES_CONN_STRING string `env:"PBW_POSTGRES_CONN_STRING,required"` PBW_LISTEN_HOST string `env:"PBW_LISTEN_HOST" envDefault:"0.0.0.0"` PBW_LISTEN_PORT string `env:"PBW_LISTEN_PORT" envDefault:"8085"` + PBW_PATH_PREFIX string `env:"PBW_PATH_PREFIX" envDefault:""` } var ( diff --git a/internal/config/env_validate.go b/internal/config/env_validate.go index b538a8b1..07aa366d 100644 --- a/internal/config/env_validate.go +++ b/internal/config/env_validate.go @@ -16,5 +16,9 @@ func validateEnv(env Env) error { return fmt.Errorf("invalid listen port %s, valid values are 1-65535", env.PBW_LISTEN_PORT) } + if !validate.PathPrefix(env.PBW_PATH_PREFIX) { + return fmt.Errorf("invalid path prefix %s, must start with / and not end with / (or be empty)", env.PBW_PATH_PREFIX) + } + return nil } diff --git a/internal/util/pathutil/pathutil.go b/internal/util/pathutil/pathutil.go new file mode 100644 index 00000000..d67e7779 --- /dev/null +++ b/internal/util/pathutil/pathutil.go @@ -0,0 +1,35 @@ +package pathutil + +import "sync" + +var ( + pathPrefix string + pathPrefixOnce sync.Once +) + +// SetPathPrefix sets the path prefix once. This should be called during +// application initialization with the value from the environment config. +func SetPathPrefix(prefix string) { + pathPrefixOnce.Do(func() { + pathPrefix = prefix + }) +} + +// GetPathPrefix returns the configured path prefix. +func GetPathPrefix() string { + return pathPrefix +} + +// BuildPath constructs a full path by prepending the configured path prefix +// to the given path. If no prefix is configured, returns the path as-is. +// +// Examples: +// - BuildPath("/dashboard") with prefix "/pgbackweb" -> "/pgbackweb/dashboard" +// - BuildPath("/dashboard") with no prefix -> "/dashboard" +// - BuildPath("") with prefix "/pgbackweb" -> "/pgbackweb" +func BuildPath(path string) string { + if pathPrefix == "" { + return path + } + return pathPrefix + path +} diff --git a/internal/util/pathutil/pathutil_test.go b/internal/util/pathutil/pathutil_test.go new file mode 100644 index 00000000..2676332f --- /dev/null +++ b/internal/util/pathutil/pathutil_test.go @@ -0,0 +1,78 @@ +package pathutil + +import "testing" + +func TestBuildPath(t *testing.T) { + t.Helper() + + tests := []struct { + name string + prefix string + path string + expected string + shouldSkip bool // Skip if prefix was already set + }{ + { + name: "no prefix configured", + prefix: "", + path: "/dashboard", + expected: "/dashboard", + }, + { + name: "with prefix - dashboard", + prefix: "/pgbackweb", + path: "/dashboard", + expected: "/pgbackweb/dashboard", + }, + { + name: "with prefix - api", + prefix: "/pgbackweb", + path: "/api/v1/health", + expected: "/pgbackweb/api/v1/health", + }, + { + name: "with prefix - root", + prefix: "/pgbackweb", + path: "", + expected: "/pgbackweb", + }, + { + name: "with prefix - auth", + prefix: "/pgbackweb", + path: "/auth/login", + expected: "/pgbackweb/auth/login", + }, + { + name: "empty prefix and empty path", + prefix: "", + path: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset for each test + pathPrefix = tt.prefix + result := BuildPath(tt.path) + if result != tt.expected { + t.Errorf("BuildPath(%q) with prefix %q = %q, want %q", tt.path, tt.prefix, result, tt.expected) + } + }) + } +} + +func TestGetPathPrefix(t *testing.T) { + t.Helper() + + // Set a test prefix + pathPrefix = "/test-prefix" + + result := GetPathPrefix() + if result != "/test-prefix" { + t.Errorf("GetPathPrefix() = %q, want %q", result, "/test-prefix") + } + + // Reset + pathPrefix = "" +} diff --git a/internal/validate/path_prefix.go b/internal/validate/path_prefix.go new file mode 100644 index 00000000..0d4ff44a --- /dev/null +++ b/internal/validate/path_prefix.go @@ -0,0 +1,43 @@ +package validate + +import "strings" + +// PathPrefix validates that a path prefix is correctly formatted. +// +// Valid path prefixes: +// - Empty string (no prefix) +// - Must start with / +// - Must NOT end with / +// - No whitespace allowed +// +// Examples: +// - "" -> true (no prefix) +// - "/api" -> true +// - "/pgbackweb" -> true +// - "/app/v1" -> true +// - "api" -> false (doesn't start with /) +// - "/api/" -> false (ends with /) +// - "/ api" -> false (contains whitespace) +func PathPrefix(pathPrefix string) bool { + // Empty string is valid (no prefix) + if pathPrefix == "" { + return true + } + + // Must start with / + if !strings.HasPrefix(pathPrefix, "/") { + return false + } + + // Must NOT end with / + if strings.HasSuffix(pathPrefix, "/") { + return false + } + + // No whitespace allowed + if strings.ContainsAny(pathPrefix, " \t\n\r") { + return false + } + + return true +} diff --git a/internal/validate/path_prefix_test.go b/internal/validate/path_prefix_test.go new file mode 100644 index 00000000..a770baf2 --- /dev/null +++ b/internal/validate/path_prefix_test.go @@ -0,0 +1,78 @@ +package validate + +import "testing" + +func TestPathPrefix(t *testing.T) { + t.Helper() + + tests := []struct { + name string + input string + expected bool + }{ + { + name: "empty string is valid", + input: "", + expected: true, + }, + { + name: "valid simple path", + input: "/api", + expected: true, + }, + { + name: "valid complex path", + input: "/pgbackweb", + expected: true, + }, + { + name: "valid nested path", + input: "/app/v1", + expected: true, + }, + { + name: "valid deep nested path", + input: "/api/app/v1", + expected: true, + }, + { + name: "invalid - doesn't start with slash", + input: "api", + expected: false, + }, + { + name: "invalid - ends with slash", + input: "/api/", + expected: false, + }, + { + name: "invalid - only slash", + input: "/", + expected: false, + }, + { + name: "invalid - contains space", + input: "/api path", + expected: false, + }, + { + name: "invalid - contains tab", + input: "/api\tpath", + expected: false, + }, + { + name: "invalid - contains newline", + input: "/api\npath", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := PathPrefix(tt.input) + if result != tt.expected { + t.Errorf("PathPrefix(%q) = %v, want %v", tt.input, result, tt.expected) + } + }) + } +} diff --git a/internal/view/middleware/require_auth.go b/internal/view/middleware/require_auth.go index 8541211b..f3fd2082 100644 --- a/internal/view/middleware/require_auth.go +++ b/internal/view/middleware/require_auth.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/logger" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/labstack/echo/v4" htmx "github.com/nodxdev/nodxgo-htmx" @@ -29,11 +30,13 @@ func (m *Middleware) RequireAuth(next echo.HandlerFunc) echo.HandlerFunc { } if usersQty == 0 { - htmx.ServerSetRedirect(c.Response().Header(), "/auth/create-first-user") - return c.Redirect(http.StatusFound, "/auth/create-first-user") + redirectPath := pathutil.BuildPath("/auth/create-first-user") + htmx.ServerSetRedirect(c.Response().Header(), redirectPath) + return c.Redirect(http.StatusFound, redirectPath) } - htmx.ServerSetRedirect(c.Response().Header(), "/auth/login") - return c.Redirect(http.StatusFound, "/auth/login") + redirectPath := pathutil.BuildPath("/auth/login") + htmx.ServerSetRedirect(c.Response().Header(), redirectPath) + return c.Redirect(http.StatusFound, redirectPath) } } diff --git a/internal/view/middleware/require_no_auth.go b/internal/view/middleware/require_no_auth.go index 03c4a5b6..c9a57e0f 100644 --- a/internal/view/middleware/require_no_auth.go +++ b/internal/view/middleware/require_no_auth.go @@ -3,6 +3,7 @@ package middleware import ( "net/http" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/labstack/echo/v4" htmx "github.com/nodxdev/nodxgo-htmx" @@ -13,8 +14,9 @@ func (m *Middleware) RequireNoAuth(next echo.HandlerFunc) echo.HandlerFunc { reqCtx := reqctx.GetCtx(c) if reqCtx.IsAuthed { - htmx.ServerSetRedirect(c.Response().Header(), "/dashboard") - return c.Redirect(http.StatusFound, "/dashboard") + redirectPath := pathutil.BuildPath("/dashboard") + htmx.ServerSetRedirect(c.Response().Header(), redirectPath) + return c.Redirect(http.StatusFound, redirectPath) } return next(c) diff --git a/internal/view/router.go b/internal/view/router.go index 730dda4e..1290c9e9 100644 --- a/internal/view/router.go +++ b/internal/view/router.go @@ -3,6 +3,7 @@ package view import ( "time" + "github.com/eduardolat/pgbackweb/internal/config" "github.com/eduardolat/pgbackweb/internal/service" "github.com/eduardolat/pgbackweb/internal/view/api" "github.com/eduardolat/pgbackweb/internal/view/middleware" @@ -11,20 +12,23 @@ import ( "github.com/labstack/echo/v4" ) -func MountRouter(app *echo.Echo, servs *service.Service) { +func MountRouter(app *echo.Echo, servs *service.Service, env config.Env) { mids := middleware.New(servs) + // Create the base group with the path prefix (if any) + baseGroup := app.Group(env.PBW_PATH_PREFIX) + browserCache := mids.NewBrowserCacheMiddleware( middleware.BrowserCacheMiddlewareConfig{ CacheDuration: time.Hour * 24 * 30, ExcludedFiles: []string{"/robots.txt"}, }, ) - app.Group("", browserCache).StaticFS("", static.StaticFs) + baseGroup.Group("", browserCache).StaticFS("", static.StaticFs) - apiGroup := app.Group("/api") + apiGroup := baseGroup.Group("/api") api.MountRouter(apiGroup, mids, servs) - webGroup := app.Group("", mids.InjectReqctx) + webGroup := baseGroup.Group("", mids.InjectReqctx) web.MountRouter(webGroup, mids, servs) } diff --git a/internal/view/web/auth/create_first_user.go b/internal/view/web/auth/create_first_user.go index 11d5e5d7..7d04e996 100644 --- a/internal/view/web/auth/create_first_user.go +++ b/internal/view/web/auth/create_first_user.go @@ -6,6 +6,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" "github.com/eduardolat/pgbackweb/internal/logger" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/layout" @@ -29,7 +30,7 @@ func (h *handlers) createFirstUserPageHandler(c echo.Context) error { return c.String(http.StatusInternalServerError, "Internal server error") } if usersQty > 0 { - return c.Redirect(http.StatusFound, "/auth/login") + return c.Redirect(http.StatusFound, pathutil.BuildPath("/auth/login")) } return echoutil.RenderNodx(c, http.StatusOK, createFirstUserPage()) @@ -135,6 +136,6 @@ func (h *handlers) createFirstUserHandler(c echo.Context) error { } return respondhtmx.AlertWithRedirect( - c, "User created successfully", "/auth/login", + c, "User created successfully", pathutil.BuildPath("/auth/login"), ) } diff --git a/internal/view/web/auth/login.go b/internal/view/web/auth/login.go index bde5aab6..c0827247 100644 --- a/internal/view/web/auth/login.go +++ b/internal/view/web/auth/login.go @@ -5,6 +5,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/logger" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/layout" @@ -28,7 +29,7 @@ func (h *handlers) loginPageHandler(c echo.Context) error { return c.String(http.StatusInternalServerError, "Internal server error") } if usersQty == 0 { - return c.Redirect(http.StatusFound, "/auth/create-first-user") + return c.Redirect(http.StatusFound, pathutil.BuildPath("/auth/create-first-user")) } return echoutil.RenderNodx(c, http.StatusOK, loginPage()) @@ -108,5 +109,5 @@ func (h *handlers) loginHandler(c echo.Context) error { } h.servs.AuthService.SetSessionCookie(c, session.DecryptedToken) - return respondhtmx.Redirect(c, "/dashboard") + return respondhtmx.Redirect(c, pathutil.BuildPath("/dashboard")) } diff --git a/internal/view/web/auth/logout.go b/internal/view/web/auth/logout.go index 81d3f30d..d70356e1 100644 --- a/internal/view/web/auth/logout.go +++ b/internal/view/web/auth/logout.go @@ -1,6 +1,7 @@ package auth import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/labstack/echo/v4" @@ -15,7 +16,7 @@ func (h *handlers) logoutHandler(c echo.Context) error { } h.servs.AuthService.ClearSessionCookie(c) - return respondhtmx.Redirect(c, "/auth/login") + return respondhtmx.Redirect(c, pathutil.BuildPath("/auth/login")) } func (h *handlers) logoutAllSessionsHandler(c echo.Context) error { @@ -28,5 +29,5 @@ func (h *handlers) logoutAllSessionsHandler(c echo.Context) error { } h.servs.AuthService.ClearSessionCookie(c) - return respondhtmx.Redirect(c, "/auth/login") + return respondhtmx.Redirect(c, pathutil.BuildPath("/auth/login")) } diff --git a/internal/view/web/dashboard/backups/create_backup.go b/internal/view/web/dashboard/backups/create_backup.go index 49e4c31b..a88bbc9a 100644 --- a/internal/view/web/dashboard/backups/create_backup.go +++ b/internal/view/web/dashboard/backups/create_backup.go @@ -7,6 +7,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" "github.com/eduardolat/pgbackweb/internal/staticdata" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -70,7 +71,7 @@ func (h *handlers) createBackupHandler(c echo.Context) error { return respondhtmx.ToastError(c, err.Error()) } - return respondhtmx.Redirect(c, "/dashboard/backups") + return respondhtmx.Redirect(c, pathutil.BuildPath("/dashboard/backups")) } func (h *handlers) createBackupFormHandler(c echo.Context) error { diff --git a/internal/view/web/dashboard/databases/create_database.go b/internal/view/web/dashboard/databases/create_database.go index 88a65c16..0e95a697 100644 --- a/internal/view/web/dashboard/databases/create_database.go +++ b/internal/view/web/dashboard/databases/create_database.go @@ -4,6 +4,7 @@ import ( "database/sql" "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -41,7 +42,7 @@ func (h *handlers) createDatabaseHandler(c echo.Context) error { return respondhtmx.ToastError(c, err.Error()) } - return respondhtmx.Redirect(c, "/dashboard/databases") + return respondhtmx.Redirect(c, pathutil.BuildPath("/dashboard/databases")) } func createDatabaseButton() nodx.Node { diff --git a/internal/view/web/dashboard/destinations/create_destination.go b/internal/view/web/dashboard/destinations/create_destination.go index ae0fe3b1..275944fc 100644 --- a/internal/view/web/dashboard/destinations/create_destination.go +++ b/internal/view/web/dashboard/destinations/create_destination.go @@ -2,6 +2,7 @@ package destinations import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -45,7 +46,7 @@ func (h *handlers) createDestinationHandler(c echo.Context) error { return respondhtmx.ToastError(c, err.Error()) } - return respondhtmx.Redirect(c, "/dashboard/destinations") + return respondhtmx.Redirect(c, pathutil.BuildPath("/dashboard/destinations")) } func createDestinationButton() nodx.Node { diff --git a/internal/view/web/dashboard/webhooks/create_webhook.go b/internal/view/web/dashboard/webhooks/create_webhook.go index 73c73a64..aa6632c7 100644 --- a/internal/view/web/dashboard/webhooks/create_webhook.go +++ b/internal/view/web/dashboard/webhooks/create_webhook.go @@ -6,6 +6,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -54,7 +55,7 @@ func (h *handlers) createWebhookHandler(c echo.Context) error { return respondhtmx.ToastError(c, err.Error()) } - return respondhtmx.Redirect(c, "/dashboard/webhooks") + return respondhtmx.Redirect(c, pathutil.BuildPath("/dashboard/webhooks")) } func (h *handlers) createWebhookFormHandler(c echo.Context) error { diff --git a/internal/view/web/router.go b/internal/view/web/router.go index 2b0ad1b2..ea76b2a8 100644 --- a/internal/view/web/router.go +++ b/internal/view/web/router.go @@ -5,6 +5,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/logger" "github.com/eduardolat/pgbackweb/internal/service" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/middleware" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/auth" @@ -21,7 +22,7 @@ func MountRouter( reqCtx := reqctx.GetCtx(c) if reqCtx.IsAuthed { - return c.Redirect(http.StatusFound, "/dashboard") + return c.Redirect(http.StatusFound, pathutil.BuildPath("/dashboard")) } usersQty, err := servs.UsersService.GetUsersQty(ctx) @@ -35,10 +36,10 @@ func MountRouter( } if usersQty == 0 { - return c.Redirect(http.StatusFound, "/auth/create-first-user") + return c.Redirect(http.StatusFound, pathutil.BuildPath("/auth/create-first-user")) } - return c.Redirect(http.StatusFound, "/auth/login") + return c.Redirect(http.StatusFound, pathutil.BuildPath("/auth/login")) }) authGroup := parent.Group("/auth") From e91d2026e655191e046c0466dfda470d9bc8d143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=BCli=20Patrik=20P=C3=A9ter?= Date: Fri, 3 Oct 2025 08:44:50 +0200 Subject: [PATCH 2/6] Refactors pathutil test suite for better isolation Updates tests to properly manage global state: - Uses defer pattern to restore original values after tests - Resets sync.Once to allow multiple SetPathPrefix calls in tests - Removes shouldSkip field from test cases as it's no longer needed --- internal/util/pathutil/pathutil_test.go | 37 +++++++++++++++---------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/internal/util/pathutil/pathutil_test.go b/internal/util/pathutil/pathutil_test.go index 2676332f..30a35a05 100644 --- a/internal/util/pathutil/pathutil_test.go +++ b/internal/util/pathutil/pathutil_test.go @@ -1,16 +1,18 @@ package pathutil -import "testing" +import ( + "sync" + "testing" +) func TestBuildPath(t *testing.T) { t.Helper() tests := []struct { - name string - prefix string - path string - expected string - shouldSkip bool // Skip if prefix was already set + name string + prefix string + path string + expected string }{ { name: "no prefix configured", @@ -52,8 +54,14 @@ func TestBuildPath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Reset for each test - pathPrefix = tt.prefix + originalPrefix := pathPrefix + pathPrefixOnce = sync.Once{} + SetPathPrefix(tt.prefix) + defer func() { + pathPrefixOnce = sync.Once{} + pathPrefix = originalPrefix + }() + result := BuildPath(tt.path) if result != tt.expected { t.Errorf("BuildPath(%q) with prefix %q = %q, want %q", tt.path, tt.prefix, result, tt.expected) @@ -64,15 +72,16 @@ func TestBuildPath(t *testing.T) { func TestGetPathPrefix(t *testing.T) { t.Helper() - - // Set a test prefix - pathPrefix = "/test-prefix" + originalPrefix := pathPrefix + pathPrefixOnce = sync.Once{} + SetPathPrefix("/test-prefix") + defer func() { + pathPrefixOnce = sync.Once{} + pathPrefix = originalPrefix + }() result := GetPathPrefix() if result != "/test-prefix" { t.Errorf("GetPathPrefix() = %q, want %q", result, "/test-prefix") } - - // Reset - pathPrefix = "" } From 6965e14a41647f34cab66c6c4f0e51e5839eb1c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=BCli=20Patrik=20P=C3=A9ter?= Date: Fri, 3 Oct 2025 09:31:36 +0200 Subject: [PATCH 3/6] Supports subdirectory deployment with configurable path prefix Refactors URL handling to allow serving the app from a subdirectory. Updates all static file paths and dashboard navigation links to use a utility function that prepends the configured path prefix when generating URLs. --- internal/view/router.go | 12 ++++++++++- internal/view/static/static_fs.go | 10 ++++++++- internal/view/web/component/logotype.go | 3 ++- .../dashboard/executions/show_execution.go | 3 ++- internal/view/web/layout/dashboard_aside.go | 21 ++++++++++--------- 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/internal/view/router.go b/internal/view/router.go index 1290c9e9..ba69de1b 100644 --- a/internal/view/router.go +++ b/internal/view/router.go @@ -1,9 +1,11 @@ package view import ( + "io/fs" "time" "github.com/eduardolat/pgbackweb/internal/config" + "github.com/eduardolat/pgbackweb/internal/logger" "github.com/eduardolat/pgbackweb/internal/service" "github.com/eduardolat/pgbackweb/internal/view/api" "github.com/eduardolat/pgbackweb/internal/view/middleware" @@ -24,7 +26,15 @@ func MountRouter(app *echo.Echo, servs *service.Service, env config.Env) { ExcludedFiles: []string{"/robots.txt"}, }, ) - baseGroup.Group("", browserCache).StaticFS("", static.StaticFs) + + // Mount static files + staticFS, err := fs.Sub(static.StaticFs, ".") + if err != nil { + logger.FatalError("failed to create static filesystem", logger.KV{"error": err}) + } + + staticGroup := baseGroup.Group("", browserCache) + staticGroup.StaticFS("/", staticFS) apiGroup := baseGroup.Group("/api") api.MountRouter(apiGroup, mids, servs) diff --git a/internal/view/static/static_fs.go b/internal/view/static/static_fs.go index d1bb3a44..cc6e1967 100644 --- a/internal/view/static/static_fs.go +++ b/internal/view/static/static_fs.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/eduardolat/pgbackweb/internal/util/cryptoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" ) //go:embed * @@ -28,6 +29,9 @@ func GetStaticSHA256() string { // SHA256 hash of the static filesystem to the query parameter. // // The hash is truncated to the first 8 characters for brevity. +// +// This function also prepends the configured path prefix so that static files +// are correctly referenced in HTML when the application is served under a subpath. func GetVersionedFilePath(filePath string) string { hash := GetStaticSHA256() @@ -35,5 +39,9 @@ func GetVersionedFilePath(filePath string) string { hash = hash[:8] } - return filePath + "?v=" + hash + // Prepend the path prefix to ensure static files are found + // when the app is served under a subpath like /pgbackweb + fullPath := pathutil.BuildPath(filePath) + + return fullPath + "?v=" + hash } diff --git a/internal/view/web/component/logotype.go b/internal/view/web/component/logotype.go index 2518435f..cfe13678 100644 --- a/internal/view/web/component/logotype.go +++ b/internal/view/web/component/logotype.go @@ -1,6 +1,7 @@ package component import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" nodx "github.com/nodxdev/nodxgo" ) @@ -12,7 +13,7 @@ func Logotype() nodx.Node { }, nodx.Img( nodx.Class("w-[60px] h-auto"), - nodx.Src("/images/logo.png"), + nodx.Src(pathutil.BuildPath("/images/logo.png")), nodx.Alt("PG Back Web"), ), nodx.SpanEl( diff --git a/internal/view/web/dashboard/executions/show_execution.go b/internal/view/web/dashboard/executions/show_execution.go index f8224eaa..6d3287d4 100644 --- a/internal/view/web/dashboard/executions/show_execution.go +++ b/internal/view/web/dashboard/executions/show_execution.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/google/uuid" @@ -121,7 +122,7 @@ func showExecutionButton( nodx.Class("flex justify-end items-center space-x-2"), deleteExecutionButton(execution.ID), nodx.A( - nodx.Href("/dashboard/executions/"+execution.ID.String()+"/download"), + nodx.Href(pathutil.BuildPath("/dashboard/executions/"+execution.ID.String()+"/download")), nodx.Target("_blank"), nodx.Class("btn btn-primary"), component.SpanText("Download"), diff --git a/internal/view/web/layout/dashboard_aside.go b/internal/view/web/layout/dashboard_aside.go index 9f71fcc3..25459db1 100644 --- a/internal/view/web/layout/dashboard_aside.go +++ b/internal/view/web/layout/dashboard_aside.go @@ -3,6 +3,7 @@ package layout import ( "fmt" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" nodx "github.com/nodxdev/nodxgo" alpine "github.com/nodxdev/nodxgo-alpine" htmx "github.com/nodxdev/nodxgo-htmx" @@ -22,7 +23,7 @@ func dashboardAside() nodx.Node { nodx.Href("https://github.com/eduardolat/pgbackweb"), nodx.Target("_blank"), nodx.Img( - nodx.Src("/images/logo.png"), + nodx.Src(pathutil.BuildPath("/images/logo.png")), nodx.Alt("PG Back Web"), nodx.Class("w-[50px] h-auto"), ), @@ -45,63 +46,63 @@ func dashboardAside() nodx.Node { dashboardAsideItem( lucide.LayoutDashboard, "Summary", - "/dashboard", + pathutil.BuildPath("/dashboard"), true, ), dashboardAsideItem( lucide.Database, "Databases", - "/dashboard/databases", + pathutil.BuildPath("/dashboard/databases"), false, ), dashboardAsideItem( lucide.HardDrive, "Destinations", - "/dashboard/destinations", + pathutil.BuildPath("/dashboard/destinations"), false, ), dashboardAsideItem( lucide.DatabaseBackup, "Backup tasks", - "/dashboard/backups", + pathutil.BuildPath("/dashboard/backups"), false, ), dashboardAsideItem( lucide.List, "Executions", - "/dashboard/executions", + pathutil.BuildPath("/dashboard/executions"), false, ), dashboardAsideItem( lucide.ArchiveRestore, "Restorations", - "/dashboard/restorations", + pathutil.BuildPath("/dashboard/restorations"), false, ), dashboardAsideItem( lucide.Webhook, "Webhooks", - "/dashboard/webhooks", + pathutil.BuildPath("/dashboard/webhooks"), false, ), dashboardAsideItem( lucide.User, "Profile", - "/dashboard/profile", + pathutil.BuildPath("/dashboard/profile"), false, ), dashboardAsideItem( lucide.Info, "About", - "/dashboard/about", + pathutil.BuildPath("/dashboard/about"), false, ), ), From 7658e21aaa147f65cad18ea1ab821efcc18291f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=BCli=20Patrik=20P=C3=A9ter?= Date: Fri, 3 Oct 2025 10:07:35 +0200 Subject: [PATCH 4/6] Refactors URL paths using pathutil.BuildPath function Consistently uses the pathutil.BuildPath function to build URLs across various web components instead of hardcoding them. --- internal/view/web/auth/create_first_user.go | 2 +- internal/view/web/auth/login.go | 2 +- internal/view/web/dashboard/backups/create_backup.go | 4 ++-- internal/view/web/dashboard/backups/delete_backup.go | 3 ++- internal/view/web/dashboard/backups/duplicate_backup.go | 3 ++- internal/view/web/dashboard/backups/edit_backup.go | 3 ++- internal/view/web/dashboard/backups/index.go | 3 ++- internal/view/web/dashboard/backups/manual_run.go | 3 ++- internal/view/web/dashboard/databases/delete_database.go | 3 ++- internal/view/web/dashboard/databases/index.go | 3 ++- internal/view/web/dashboard/databases/list_databases.go | 3 ++- .../view/web/dashboard/destinations/delete_destination.go | 3 ++- internal/view/web/dashboard/destinations/index.go | 3 ++- .../view/web/dashboard/destinations/list_destinations.go | 3 ++- internal/view/web/dashboard/executions/restore_execution.go | 5 +++-- .../view/web/dashboard/executions/soft_delete_execution.go | 3 ++- internal/view/web/dashboard/profile/close_all_sessions.go | 3 ++- internal/view/web/dashboard/profile/update_user.go | 3 ++- internal/view/web/dashboard/webhooks/create_webhook.go | 4 ++-- internal/view/web/dashboard/webhooks/delete_webhook.go | 3 ++- internal/view/web/dashboard/webhooks/duplicate_webhook.go | 3 ++- internal/view/web/dashboard/webhooks/edit_webhook.go | 5 +++-- internal/view/web/dashboard/webhooks/index.go | 3 ++- internal/view/web/dashboard/webhooks/run_webhook.go | 3 ++- internal/view/web/layout/dashboard_header.go | 5 +++-- 25 files changed, 51 insertions(+), 30 deletions(-) diff --git a/internal/view/web/auth/create_first_user.go b/internal/view/web/auth/create_first_user.go index 7d04e996..7ce89f26 100644 --- a/internal/view/web/auth/create_first_user.go +++ b/internal/view/web/auth/create_first_user.go @@ -41,7 +41,7 @@ func createFirstUserPage() nodx.Node { component.H1Text("Create first user"), nodx.FormEl( - htmx.HxPost("/auth/create-first-user"), + htmx.HxPost(pathutil.BuildPath("/auth/create-first-user")), htmx.HxDisabledELT("find button"), nodx.Class("mt-4 space-y-2"), diff --git a/internal/view/web/auth/login.go b/internal/view/web/auth/login.go index c0827247..3947e5e2 100644 --- a/internal/view/web/auth/login.go +++ b/internal/view/web/auth/login.go @@ -40,7 +40,7 @@ func loginPage() nodx.Node { component.H1Text("Login"), nodx.FormEl( - htmx.HxPost("/auth/login"), + htmx.HxPost(pathutil.BuildPath("/auth/login")), htmx.HxDisabledELT("find button"), nodx.Class("mt-4 space-y-2"), diff --git a/internal/view/web/dashboard/backups/create_backup.go b/internal/view/web/dashboard/backups/create_backup.go index a88bbc9a..6a71de31 100644 --- a/internal/view/web/dashboard/backups/create_backup.go +++ b/internal/view/web/dashboard/backups/create_backup.go @@ -106,7 +106,7 @@ func createBackupForm( serverTZ := time.Now().Location().String() return nodx.FormEl( - htmx.HxPost("/dashboard/backups"), + htmx.HxPost(pathutil.BuildPath("/dashboard/backups")), htmx.HxDisabledELT("find button"), nodx.Class("space-y-2 text-base"), @@ -323,7 +323,7 @@ func createBackupButton() nodx.Node { Title: "Create backup task", Content: []nodx.Node{ nodx.Div( - htmx.HxGet("/dashboard/backups/create-form"), + htmx.HxGet(pathutil.BuildPath("/dashboard/backups/create-form")), htmx.HxSwap("outerHTML"), htmx.HxTrigger("intersect once"), nodx.Class("p-10 flex justify-center"), diff --git a/internal/view/web/dashboard/backups/delete_backup.go b/internal/view/web/dashboard/backups/delete_backup.go index c2248b80..3cc267c4 100644 --- a/internal/view/web/dashboard/backups/delete_backup.go +++ b/internal/view/web/dashboard/backups/delete_backup.go @@ -1,6 +1,7 @@ package backups import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -27,7 +28,7 @@ func (h *handlers) deleteBackupHandler(c echo.Context) error { func deleteBackupButton(backupID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxDelete("/dashboard/backups/"+backupID.String()), + htmx.HxDelete(pathutil.BuildPath("/dashboard/backups/"+backupID.String())), htmx.HxConfirm("Are you sure you want to delete this backup task?"), lucide.Trash(), component.SpanText("Delete backup task"), diff --git a/internal/view/web/dashboard/backups/duplicate_backup.go b/internal/view/web/dashboard/backups/duplicate_backup.go index a397a854..832730b2 100644 --- a/internal/view/web/dashboard/backups/duplicate_backup.go +++ b/internal/view/web/dashboard/backups/duplicate_backup.go @@ -1,6 +1,7 @@ package backups import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -27,7 +28,7 @@ func (h *handlers) duplicateBackupHandler(c echo.Context) error { func duplicateBackupButton(backupID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxPost("/dashboard/backups/"+backupID.String()+"/duplicate"), + htmx.HxPost(pathutil.BuildPath("/dashboard/backups/"+backupID.String()+"/duplicate")), htmx.HxConfirm("Are you sure you want to duplicate this backup task?"), lucide.CopyPlus(), component.SpanText("Duplicate backup task"), diff --git a/internal/view/web/dashboard/backups/edit_backup.go b/internal/view/web/dashboard/backups/edit_backup.go index 543aa85f..691cd70c 100644 --- a/internal/view/web/dashboard/backups/edit_backup.go +++ b/internal/view/web/dashboard/backups/edit_backup.go @@ -6,6 +6,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" "github.com/eduardolat/pgbackweb/internal/staticdata" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -90,7 +91,7 @@ func editBackupButton(backup dbgen.BackupsServicePaginateBackupsRow) nodx.Node { Title: "Edit backup task", Content: []nodx.Node{ nodx.FormEl( - htmx.HxPost("/dashboard/backups/"+backup.ID.String()+"/edit"), + htmx.HxPost(pathutil.BuildPath("/dashboard/backups/"+backup.ID.String()+"/edit")), htmx.HxDisabledELT("find button"), nodx.Class("space-y-2 text-base"), diff --git a/internal/view/web/dashboard/backups/index.go b/internal/view/web/dashboard/backups/index.go index 59f1f482..d4eea595 100644 --- a/internal/view/web/dashboard/backups/index.go +++ b/internal/view/web/dashboard/backups/index.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/layout" @@ -50,7 +51,7 @@ func indexPage(reqCtx reqctx.Ctx) nodx.Node { ), nodx.Tbody( component.SkeletonTr(8), - htmx.HxGet("/dashboard/backups/list?page=1"), + htmx.HxGet(pathutil.BuildPath("/dashboard/backups/list?page=1")), htmx.HxTrigger("load"), ), ), diff --git a/internal/view/web/dashboard/backups/manual_run.go b/internal/view/web/dashboard/backups/manual_run.go index 309d0655..0e1a62db 100644 --- a/internal/view/web/dashboard/backups/manual_run.go +++ b/internal/view/web/dashboard/backups/manual_run.go @@ -3,6 +3,7 @@ package backups import ( "context" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -27,7 +28,7 @@ func (h *handlers) manualRunHandler(c echo.Context) error { func manualRunbutton(backupID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxPost("/dashboard/backups/"+backupID.String()+"/run"), + htmx.HxPost(pathutil.BuildPath("/dashboard/backups/"+backupID.String()+"/run")), htmx.HxDisabledELT("this"), lucide.Zap(), component.SpanText("Run backup now"), diff --git a/internal/view/web/dashboard/databases/delete_database.go b/internal/view/web/dashboard/databases/delete_database.go index 4996d461..a35f6ad3 100644 --- a/internal/view/web/dashboard/databases/delete_database.go +++ b/internal/view/web/dashboard/databases/delete_database.go @@ -1,6 +1,7 @@ package databases import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -27,7 +28,7 @@ func (h *handlers) deleteDatabaseHandler(c echo.Context) error { func deleteDatabaseButton(databaseID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxDelete("/dashboard/databases/"+databaseID.String()), + htmx.HxDelete(pathutil.BuildPath("/dashboard/databases/"+databaseID.String())), htmx.HxConfirm("Are you sure you want to delete this database?"), lucide.Trash(), component.SpanText("Delete database"), diff --git a/internal/view/web/dashboard/databases/index.go b/internal/view/web/dashboard/databases/index.go index 6e440f98..44330d27 100644 --- a/internal/view/web/dashboard/databases/index.go +++ b/internal/view/web/dashboard/databases/index.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/layout" @@ -42,7 +43,7 @@ func indexPage(reqCtx reqctx.Ctx) nodx.Node { ), nodx.Tbody( component.SkeletonTr(8), - htmx.HxGet("/dashboard/databases/list?page=1"), + htmx.HxGet(pathutil.BuildPath("/dashboard/databases/list?page=1")), htmx.HxTrigger("load"), ), ), diff --git a/internal/view/web/dashboard/databases/list_databases.go b/internal/view/web/dashboard/databases/list_databases.go index c4138c8f..1406c672 100644 --- a/internal/view/web/dashboard/databases/list_databases.go +++ b/internal/view/web/dashboard/databases/list_databases.go @@ -8,6 +8,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/service/databases" "github.com/eduardolat/pgbackweb/internal/util/echoutil" "github.com/eduardolat/pgbackweb/internal/util/paginateutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" @@ -73,7 +74,7 @@ func listDatabases( ), editDatabaseButton(database), component.OptionsDropdownButton( - htmx.HxPost("/dashboard/databases/"+database.ID.String()+"/test"), + htmx.HxPost(pathutil.BuildPath("/dashboard/databases/"+database.ID.String()+"/test")), htmx.HxDisabledELT("this"), lucide.DatabaseZap(), component.SpanText("Test connection"), diff --git a/internal/view/web/dashboard/destinations/delete_destination.go b/internal/view/web/dashboard/destinations/delete_destination.go index 80b65be2..60a55f10 100644 --- a/internal/view/web/dashboard/destinations/delete_destination.go +++ b/internal/view/web/dashboard/destinations/delete_destination.go @@ -1,6 +1,7 @@ package destinations import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -28,7 +29,7 @@ func (h *handlers) deleteDestinationHandler(c echo.Context) error { func deleteDestinationButton(destinationID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxDelete("/dashboard/destinations/"+destinationID.String()), + htmx.HxDelete(pathutil.BuildPath("/dashboard/destinations/"+destinationID.String())), htmx.HxConfirm("Are you sure you want to delete this destination?"), lucide.Trash(), component.SpanText("Delete destination"), diff --git a/internal/view/web/dashboard/destinations/index.go b/internal/view/web/dashboard/destinations/index.go index 8355c611..25fb1c2c 100644 --- a/internal/view/web/dashboard/destinations/index.go +++ b/internal/view/web/dashboard/destinations/index.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/layout" @@ -55,7 +56,7 @@ func indexPage(reqCtx reqctx.Ctx) nodx.Node { ), nodx.Tbody( component.SkeletonTr(8), - htmx.HxGet("/dashboard/destinations/list?page=1"), + htmx.HxGet(pathutil.BuildPath("/dashboard/destinations/list?page=1")), htmx.HxTrigger("load"), ), ), diff --git a/internal/view/web/dashboard/destinations/list_destinations.go b/internal/view/web/dashboard/destinations/list_destinations.go index b0eee351..ad360ee3 100644 --- a/internal/view/web/dashboard/destinations/list_destinations.go +++ b/internal/view/web/dashboard/destinations/list_destinations.go @@ -8,6 +8,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/service/destinations" "github.com/eduardolat/pgbackweb/internal/util/echoutil" "github.com/eduardolat/pgbackweb/internal/util/paginateutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" @@ -71,7 +72,7 @@ func listDestinations( ), editDestinationButton(destination), component.OptionsDropdownButton( - htmx.HxPost("/dashboard/destinations/"+destination.ID.String()+"/test"), + htmx.HxPost(pathutil.BuildPath("/dashboard/destinations/"+destination.ID.String()+"/test")), htmx.HxDisabledELT("this"), lucide.PlugZap(), component.SpanText("Test connection"), diff --git a/internal/view/web/dashboard/executions/restore_execution.go b/internal/view/web/dashboard/executions/restore_execution.go index 45494f0d..6c59cdb5 100644 --- a/internal/view/web/dashboard/executions/restore_execution.go +++ b/internal/view/web/dashboard/executions/restore_execution.go @@ -7,6 +7,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -107,7 +108,7 @@ func restoreExecutionForm( databases []dbgen.DatabasesServiceGetAllDatabasesRow, ) nodx.Node { return nodx.FormEl( - htmx.HxPost("/dashboard/executions/"+execution.ID.String()+"/restore"), + htmx.HxPost(pathutil.BuildPath("/dashboard/executions/"+execution.ID.String()+"/restore")), htmx.HxConfirm("Are you sure you want to restore this backup?"), htmx.HxDisabledELT("find button"), @@ -222,7 +223,7 @@ func restoreExecutionButton(execution dbgen.ExecutionsServicePaginateExecutionsR Title: "Restore backup execution", Content: []nodx.Node{ nodx.Div( - htmx.HxGet("/dashboard/executions/"+execution.ID.String()+"/restore-form"), + htmx.HxGet(pathutil.BuildPath("/dashboard/executions/"+execution.ID.String()+"/restore-form")), htmx.HxSwap("outerHTML"), htmx.HxTrigger("intersect once"), nodx.Class("p-10 flex justify-center"), diff --git a/internal/view/web/dashboard/executions/soft_delete_execution.go b/internal/view/web/dashboard/executions/soft_delete_execution.go index e62abaf5..c08fa121 100644 --- a/internal/view/web/dashboard/executions/soft_delete_execution.go +++ b/internal/view/web/dashboard/executions/soft_delete_execution.go @@ -1,6 +1,7 @@ package executions import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -28,7 +29,7 @@ func (h *handlers) deleteExecutionHandler(c echo.Context) error { func deleteExecutionButton(executionID uuid.UUID) nodx.Node { return nodx.Button( - htmx.HxDelete("/dashboard/executions/"+executionID.String()), + htmx.HxDelete(pathutil.BuildPath("/dashboard/executions/"+executionID.String())), htmx.HxDisabledELT("this"), htmx.HxConfirm("Are you sure you want to delete this execution? It will delete the backup file from the destination and it can't be recovered."), nodx.Class("btn btn-error btn-outline"), diff --git a/internal/view/web/dashboard/profile/close_all_sessions.go b/internal/view/web/dashboard/profile/close_all_sessions.go index ad3cfe93..1f09ea6b 100644 --- a/internal/view/web/dashboard/profile/close_all_sessions.go +++ b/internal/view/web/dashboard/profile/close_all_sessions.go @@ -2,6 +2,7 @@ package profile import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" nodx "github.com/nodxdev/nodxgo" @@ -15,7 +16,7 @@ func closeAllSessionsForm(sessions []dbgen.Session) nodx.Node { component.H2Text("Close all sessions"), component.PText("This will log you out from all devices including this one."), nodx.Button( - htmx.HxPost("/auth/logout-all"), + htmx.HxPost(pathutil.BuildPath("/auth/logout-all")), htmx.HxDisabledELT("this"), htmx.HxConfirm("Are you sure you want to close all your sessions?"), nodx.Class("mt-2 btn btn-error"), diff --git a/internal/view/web/dashboard/profile/update_user.go b/internal/view/web/dashboard/profile/update_user.go index 2e4f84be..63334023 100644 --- a/internal/view/web/dashboard/profile/update_user.go +++ b/internal/view/web/dashboard/profile/update_user.go @@ -4,6 +4,7 @@ import ( "database/sql" "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/component" @@ -48,7 +49,7 @@ func updateUserForm(user dbgen.User) nodx.Node { return component.CardBox(component.CardBoxParams{ Children: []nodx.Node{ nodx.FormEl( - htmx.HxPost("/dashboard/profile"), + htmx.HxPost(pathutil.BuildPath("/dashboard/profile")), htmx.HxDisabledELT("find button"), nodx.Class("space-y-2"), diff --git a/internal/view/web/dashboard/webhooks/create_webhook.go b/internal/view/web/dashboard/webhooks/create_webhook.go index aa6632c7..691acf4a 100644 --- a/internal/view/web/dashboard/webhooks/create_webhook.go +++ b/internal/view/web/dashboard/webhooks/create_webhook.go @@ -87,7 +87,7 @@ func createWebhookForm( backups []dbgen.Backup, ) nodx.Node { return nodx.FormEl( - htmx.HxPost("/dashboard/webhooks/create"), + htmx.HxPost(pathutil.BuildPath("/dashboard/webhooks/create")), htmx.HxDisabledELT("find button[type='submit']"), nodx.Class("space-y-2"), @@ -112,7 +112,7 @@ func createWebhookButton() nodx.Node { Title: "Create webhook", Content: []nodx.Node{ nodx.Div( - htmx.HxGet("/dashboard/webhooks/create"), + htmx.HxGet(pathutil.BuildPath("/dashboard/webhooks/create")), htmx.HxSwap("outerHTML"), htmx.HxTrigger("intersect once"), nodx.Class("p-10 flex justify-center"), diff --git a/internal/view/web/dashboard/webhooks/delete_webhook.go b/internal/view/web/dashboard/webhooks/delete_webhook.go index 1f93341d..c86a10b5 100644 --- a/internal/view/web/dashboard/webhooks/delete_webhook.go +++ b/internal/view/web/dashboard/webhooks/delete_webhook.go @@ -1,6 +1,7 @@ package webhooks import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -27,7 +28,7 @@ func (h *handlers) deleteWebhookHandler(c echo.Context) error { func deleteWebhookButton(webhookID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxDelete("/dashboard/webhooks/"+webhookID.String()), + htmx.HxDelete(pathutil.BuildPath("/dashboard/webhooks/"+webhookID.String())), htmx.HxConfirm("Are you sure you want to delete this webhook?"), lucide.Trash(), component.SpanText("Delete webhook"), diff --git a/internal/view/web/dashboard/webhooks/duplicate_webhook.go b/internal/view/web/dashboard/webhooks/duplicate_webhook.go index 6f2684ac..03119aa0 100644 --- a/internal/view/web/dashboard/webhooks/duplicate_webhook.go +++ b/internal/view/web/dashboard/webhooks/duplicate_webhook.go @@ -1,6 +1,7 @@ package webhooks import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -27,7 +28,7 @@ func (h *handlers) duplicateWebhookHandler(c echo.Context) error { func duplicateWebhookButton(webhookID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxPost("/dashboard/webhooks/"+webhookID.String()+"/duplicate"), + htmx.HxPost(pathutil.BuildPath("/dashboard/webhooks/"+webhookID.String()+"/duplicate")), htmx.HxConfirm("Are you sure you want to duplicate this webhook?"), lucide.CopyPlus(), component.SpanText("Duplicate webhook"), diff --git a/internal/view/web/dashboard/webhooks/edit_webhook.go b/internal/view/web/dashboard/webhooks/edit_webhook.go index c19a0fc9..5ee23b66 100644 --- a/internal/view/web/dashboard/webhooks/edit_webhook.go +++ b/internal/view/web/dashboard/webhooks/edit_webhook.go @@ -6,6 +6,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/database/dbgen" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -101,7 +102,7 @@ func editWebhookForm( backups []dbgen.Backup, ) nodx.Node { return nodx.FormEl( - htmx.HxPost("/dashboard/webhooks/"+webhook.ID.String()+"/edit"), + htmx.HxPost(pathutil.BuildPath("/dashboard/webhooks/"+webhook.ID.String()+"/edit")), htmx.HxDisabledELT("find button[type='submit']"), nodx.Class("space-y-2"), @@ -126,7 +127,7 @@ func editWebhookButton(webhookID uuid.UUID) nodx.Node { Title: "Edit webhook", Content: []nodx.Node{ nodx.Div( - htmx.HxGet("/dashboard/webhooks/"+webhookID.String()+"/edit"), + htmx.HxGet(pathutil.BuildPath("/dashboard/webhooks/"+webhookID.String()+"/edit")), htmx.HxSwap("outerHTML"), htmx.HxTrigger("intersect once"), nodx.Class("p-10 flex justify-center"), diff --git a/internal/view/web/dashboard/webhooks/index.go b/internal/view/web/dashboard/webhooks/index.go index 06c50057..f46a5127 100644 --- a/internal/view/web/dashboard/webhooks/index.go +++ b/internal/view/web/dashboard/webhooks/index.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/reqctx" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/layout" @@ -45,7 +46,7 @@ func indexPage(reqCtx reqctx.Ctx) nodx.Node { ), nodx.Tbody( component.SkeletonTr(8), - htmx.HxGet("/dashboard/webhooks/list?page=1"), + htmx.HxGet(pathutil.BuildPath("/dashboard/webhooks/list?page=1")), htmx.HxTrigger("load"), ), ), diff --git a/internal/view/web/dashboard/webhooks/run_webhook.go b/internal/view/web/dashboard/webhooks/run_webhook.go index 690dd439..171d2a43 100644 --- a/internal/view/web/dashboard/webhooks/run_webhook.go +++ b/internal/view/web/dashboard/webhooks/run_webhook.go @@ -4,6 +4,7 @@ import ( "context" "github.com/eduardolat/pgbackweb/internal/logger" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" "github.com/google/uuid" @@ -43,7 +44,7 @@ func (h *handlers) runWebhookHandler(c echo.Context) error { func runWebhookButton(webhookID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxPost("/dashboard/webhooks/"+webhookID.String()+"/run"), + htmx.HxPost(pathutil.BuildPath("/dashboard/webhooks/"+webhookID.String()+"/run")), htmx.HxDisabledELT("this"), lucide.Zap(), component.SpanText("Run webhook now"), diff --git a/internal/view/web/layout/dashboard_header.go b/internal/view/web/layout/dashboard_header.go index c929161d..b5d8006f 100644 --- a/internal/view/web/layout/dashboard_header.go +++ b/internal/view/web/layout/dashboard_header.go @@ -1,6 +1,7 @@ package layout import ( + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" nodx "github.com/nodxdev/nodxgo" htmx "github.com/nodxdev/nodxgo-htmx" @@ -28,7 +29,7 @@ func dashboardHeader() nodx.Node { nodx.Div( nodx.Class("flex justify-end items-center space-x-2"), nodx.Div( - htmx.HxGet("/dashboard/health-button"), + htmx.HxGet(pathutil.BuildPath("/dashboard/health-button")), htmx.HxSwap("outerHTML"), htmx.HxTrigger("load once"), ), @@ -40,7 +41,7 @@ func dashboardHeader() nodx.Node { lucide.ExternalLink(), ), nodx.Button( - htmx.HxPost("/auth/logout"), + htmx.HxPost(pathutil.BuildPath("/auth/logout")), htmx.HxDisabledELT("this"), nodx.Class("btn btn-ghost btn-neutral"), component.SpanText("Log out"), From 7b81f8add1d0812d34c5494a48374f951afc2719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=BCli=20Patrik=20P=C3=A9ter?= Date: Fri, 3 Oct 2025 10:23:27 +0200 Subject: [PATCH 5/6] Standardizes URL path formatting in dashboard components Replaces string concatenation with fmt.Sprintf for building URLs This ensures consistent and more secure path handling across all dashboard operations like delete, edit, duplicate, etc. --- internal/view/web/dashboard/backups/delete_backup.go | 4 +++- internal/view/web/dashboard/backups/duplicate_backup.go | 4 +++- internal/view/web/dashboard/backups/edit_backup.go | 2 +- internal/view/web/dashboard/backups/manual_run.go | 3 ++- internal/view/web/dashboard/databases/create_database.go | 2 +- internal/view/web/dashboard/databases/delete_database.go | 4 +++- internal/view/web/dashboard/databases/edit_database.go | 3 ++- internal/view/web/dashboard/databases/list_databases.go | 2 +- .../view/web/dashboard/destinations/create_destination.go | 2 +- .../view/web/dashboard/destinations/delete_destination.go | 4 +++- internal/view/web/dashboard/destinations/edit_destination.go | 3 ++- .../view/web/dashboard/destinations/list_destinations.go | 2 +- internal/view/web/dashboard/executions/restore_execution.go | 4 ++-- internal/view/web/dashboard/executions/show_execution.go | 3 ++- .../view/web/dashboard/executions/soft_delete_execution.go | 4 +++- internal/view/web/dashboard/webhooks/delete_webhook.go | 4 +++- internal/view/web/dashboard/webhooks/duplicate_webhook.go | 4 +++- internal/view/web/dashboard/webhooks/edit_webhook.go | 5 +++-- internal/view/web/dashboard/webhooks/run_webhook.go | 3 ++- 19 files changed, 41 insertions(+), 21 deletions(-) diff --git a/internal/view/web/dashboard/backups/delete_backup.go b/internal/view/web/dashboard/backups/delete_backup.go index 3cc267c4..a3d841a2 100644 --- a/internal/view/web/dashboard/backups/delete_backup.go +++ b/internal/view/web/dashboard/backups/delete_backup.go @@ -1,6 +1,8 @@ package backups import ( + "fmt" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -28,7 +30,7 @@ func (h *handlers) deleteBackupHandler(c echo.Context) error { func deleteBackupButton(backupID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxDelete(pathutil.BuildPath("/dashboard/backups/"+backupID.String())), + htmx.HxDelete(pathutil.BuildPath(fmt.Sprintf("/dashboard/backups/%s", backupID))), htmx.HxConfirm("Are you sure you want to delete this backup task?"), lucide.Trash(), component.SpanText("Delete backup task"), diff --git a/internal/view/web/dashboard/backups/duplicate_backup.go b/internal/view/web/dashboard/backups/duplicate_backup.go index 832730b2..f60cd36b 100644 --- a/internal/view/web/dashboard/backups/duplicate_backup.go +++ b/internal/view/web/dashboard/backups/duplicate_backup.go @@ -1,6 +1,8 @@ package backups import ( + "fmt" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -28,7 +30,7 @@ func (h *handlers) duplicateBackupHandler(c echo.Context) error { func duplicateBackupButton(backupID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxPost(pathutil.BuildPath("/dashboard/backups/"+backupID.String()+"/duplicate")), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/backups/%s/duplicate", backupID))), htmx.HxConfirm("Are you sure you want to duplicate this backup task?"), lucide.CopyPlus(), component.SpanText("Duplicate backup task"), diff --git a/internal/view/web/dashboard/backups/edit_backup.go b/internal/view/web/dashboard/backups/edit_backup.go index 691cd70c..4a9e2bfe 100644 --- a/internal/view/web/dashboard/backups/edit_backup.go +++ b/internal/view/web/dashboard/backups/edit_backup.go @@ -91,7 +91,7 @@ func editBackupButton(backup dbgen.BackupsServicePaginateBackupsRow) nodx.Node { Title: "Edit backup task", Content: []nodx.Node{ nodx.FormEl( - htmx.HxPost(pathutil.BuildPath("/dashboard/backups/"+backup.ID.String()+"/edit")), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/backups/%s/edit", backup.ID))), htmx.HxDisabledELT("find button"), nodx.Class("space-y-2 text-base"), diff --git a/internal/view/web/dashboard/backups/manual_run.go b/internal/view/web/dashboard/backups/manual_run.go index 0e1a62db..61c64c5a 100644 --- a/internal/view/web/dashboard/backups/manual_run.go +++ b/internal/view/web/dashboard/backups/manual_run.go @@ -2,6 +2,7 @@ package backups import ( "context" + "fmt" "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" @@ -28,7 +29,7 @@ func (h *handlers) manualRunHandler(c echo.Context) error { func manualRunbutton(backupID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxPost(pathutil.BuildPath("/dashboard/backups/"+backupID.String()+"/run")), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/backups/%s/run", backupID))), htmx.HxDisabledELT("this"), lucide.Zap(), component.SpanText("Run backup now"), diff --git a/internal/view/web/dashboard/databases/create_database.go b/internal/view/web/dashboard/databases/create_database.go index 0e95a697..47815afd 100644 --- a/internal/view/web/dashboard/databases/create_database.go +++ b/internal/view/web/dashboard/databases/create_database.go @@ -48,7 +48,7 @@ func (h *handlers) createDatabaseHandler(c echo.Context) error { func createDatabaseButton() nodx.Node { htmxAttributes := func(url string) nodx.Node { return nodx.Group( - htmx.HxPost(url), + htmx.HxPost(pathutil.BuildPath(url)), htmx.HxInclude("#add-database-form"), htmx.HxDisabledELT(".add-database-btn"), htmx.HxIndicator("#add-database-loading"), diff --git a/internal/view/web/dashboard/databases/delete_database.go b/internal/view/web/dashboard/databases/delete_database.go index a35f6ad3..e2066c58 100644 --- a/internal/view/web/dashboard/databases/delete_database.go +++ b/internal/view/web/dashboard/databases/delete_database.go @@ -1,6 +1,8 @@ package databases import ( + "fmt" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -28,7 +30,7 @@ func (h *handlers) deleteDatabaseHandler(c echo.Context) error { func deleteDatabaseButton(databaseID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxDelete(pathutil.BuildPath("/dashboard/databases/"+databaseID.String())), + htmx.HxDelete(pathutil.BuildPath(fmt.Sprintf("/dashboard/databases/%s", databaseID))), htmx.HxConfirm("Are you sure you want to delete this database?"), lucide.Trash(), component.SpanText("Delete database"), diff --git a/internal/view/web/dashboard/databases/edit_database.go b/internal/view/web/dashboard/databases/edit_database.go index 6503668e..04d028d5 100644 --- a/internal/view/web/dashboard/databases/edit_database.go +++ b/internal/view/web/dashboard/databases/edit_database.go @@ -4,6 +4,7 @@ import ( "database/sql" "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -55,7 +56,7 @@ func editDatabaseButton( htmxAttributes := func(url string) nodx.Node { return nodx.Group( - htmx.HxPost(url), + htmx.HxPost(pathutil.BuildPath(url)), htmx.HxInclude("#"+formID), htmx.HxDisabledELT("."+btnClass), htmx.HxIndicator("#"+loadingID), diff --git a/internal/view/web/dashboard/databases/list_databases.go b/internal/view/web/dashboard/databases/list_databases.go index 1406c672..ae7934eb 100644 --- a/internal/view/web/dashboard/databases/list_databases.go +++ b/internal/view/web/dashboard/databases/list_databases.go @@ -74,7 +74,7 @@ func listDatabases( ), editDatabaseButton(database), component.OptionsDropdownButton( - htmx.HxPost(pathutil.BuildPath("/dashboard/databases/"+database.ID.String()+"/test")), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/databases/%s/test", database.ID))), htmx.HxDisabledELT("this"), lucide.DatabaseZap(), component.SpanText("Test connection"), diff --git a/internal/view/web/dashboard/destinations/create_destination.go b/internal/view/web/dashboard/destinations/create_destination.go index 275944fc..2f8d0c8e 100644 --- a/internal/view/web/dashboard/destinations/create_destination.go +++ b/internal/view/web/dashboard/destinations/create_destination.go @@ -52,7 +52,7 @@ func (h *handlers) createDestinationHandler(c echo.Context) error { func createDestinationButton() nodx.Node { htmxAttributes := func(url string) nodx.Node { return nodx.Group( - htmx.HxPost(url), + htmx.HxPost(pathutil.BuildPath(url)), htmx.HxInclude("#add-destination-form"), htmx.HxDisabledELT(".add-destination-btn"), htmx.HxIndicator("#add-destination-loading"), diff --git a/internal/view/web/dashboard/destinations/delete_destination.go b/internal/view/web/dashboard/destinations/delete_destination.go index 60a55f10..a782813f 100644 --- a/internal/view/web/dashboard/destinations/delete_destination.go +++ b/internal/view/web/dashboard/destinations/delete_destination.go @@ -1,6 +1,8 @@ package destinations import ( + "fmt" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -29,7 +31,7 @@ func (h *handlers) deleteDestinationHandler(c echo.Context) error { func deleteDestinationButton(destinationID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxDelete(pathutil.BuildPath("/dashboard/destinations/"+destinationID.String())), + htmx.HxDelete(pathutil.BuildPath(fmt.Sprintf("/dashboard/destinations/%s", destinationID))), htmx.HxConfirm("Are you sure you want to delete this destination?"), lucide.Trash(), component.SpanText("Delete destination"), diff --git a/internal/view/web/dashboard/destinations/edit_destination.go b/internal/view/web/dashboard/destinations/edit_destination.go index 47629a9a..982b809b 100644 --- a/internal/view/web/dashboard/destinations/edit_destination.go +++ b/internal/view/web/dashboard/destinations/edit_destination.go @@ -4,6 +4,7 @@ import ( "database/sql" "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -58,7 +59,7 @@ func editDestinationButton( htmxAttributes := func(url string) nodx.Node { return nodx.Group( - htmx.HxPost(url), + htmx.HxPost(pathutil.BuildPath(url)), htmx.HxInclude("#"+formID), htmx.HxDisabledELT("."+btnClass), htmx.HxIndicator("#"+loadingID), diff --git a/internal/view/web/dashboard/destinations/list_destinations.go b/internal/view/web/dashboard/destinations/list_destinations.go index ad360ee3..48f7dfdf 100644 --- a/internal/view/web/dashboard/destinations/list_destinations.go +++ b/internal/view/web/dashboard/destinations/list_destinations.go @@ -72,7 +72,7 @@ func listDestinations( ), editDestinationButton(destination), component.OptionsDropdownButton( - htmx.HxPost(pathutil.BuildPath("/dashboard/destinations/"+destination.ID.String()+"/test")), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/destinations/%s/test", destination.ID))), htmx.HxDisabledELT("this"), lucide.PlugZap(), component.SpanText("Test connection"), diff --git a/internal/view/web/dashboard/executions/restore_execution.go b/internal/view/web/dashboard/executions/restore_execution.go index 6c59cdb5..d1acd334 100644 --- a/internal/view/web/dashboard/executions/restore_execution.go +++ b/internal/view/web/dashboard/executions/restore_execution.go @@ -108,7 +108,7 @@ func restoreExecutionForm( databases []dbgen.DatabasesServiceGetAllDatabasesRow, ) nodx.Node { return nodx.FormEl( - htmx.HxPost(pathutil.BuildPath("/dashboard/executions/"+execution.ID.String()+"/restore")), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/executions/%s/restore", execution.ID))), htmx.HxConfirm("Are you sure you want to restore this backup?"), htmx.HxDisabledELT("find button"), @@ -223,7 +223,7 @@ func restoreExecutionButton(execution dbgen.ExecutionsServicePaginateExecutionsR Title: "Restore backup execution", Content: []nodx.Node{ nodx.Div( - htmx.HxGet(pathutil.BuildPath("/dashboard/executions/"+execution.ID.String()+"/restore-form")), + htmx.HxGet(pathutil.BuildPath(fmt.Sprintf("/dashboard/executions/%s/restore-form", execution.ID))), htmx.HxSwap("outerHTML"), htmx.HxTrigger("intersect once"), nodx.Class("p-10 flex justify-center"), diff --git a/internal/view/web/dashboard/executions/show_execution.go b/internal/view/web/dashboard/executions/show_execution.go index 6d3287d4..24183d87 100644 --- a/internal/view/web/dashboard/executions/show_execution.go +++ b/internal/view/web/dashboard/executions/show_execution.go @@ -1,6 +1,7 @@ package executions import ( + "fmt" "net/http" "path/filepath" @@ -122,7 +123,7 @@ func showExecutionButton( nodx.Class("flex justify-end items-center space-x-2"), deleteExecutionButton(execution.ID), nodx.A( - nodx.Href(pathutil.BuildPath("/dashboard/executions/"+execution.ID.String()+"/download")), + nodx.Href(pathutil.BuildPath(fmt.Sprintf("/dashboard/executions/%s/download", execution.ID))), nodx.Target("_blank"), nodx.Class("btn btn-primary"), component.SpanText("Download"), diff --git a/internal/view/web/dashboard/executions/soft_delete_execution.go b/internal/view/web/dashboard/executions/soft_delete_execution.go index c08fa121..847345f6 100644 --- a/internal/view/web/dashboard/executions/soft_delete_execution.go +++ b/internal/view/web/dashboard/executions/soft_delete_execution.go @@ -1,6 +1,8 @@ package executions import ( + "fmt" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -29,7 +31,7 @@ func (h *handlers) deleteExecutionHandler(c echo.Context) error { func deleteExecutionButton(executionID uuid.UUID) nodx.Node { return nodx.Button( - htmx.HxDelete(pathutil.BuildPath("/dashboard/executions/"+executionID.String())), + htmx.HxDelete(pathutil.BuildPath(fmt.Sprintf("/dashboard/executions/%s", executionID))), htmx.HxDisabledELT("this"), htmx.HxConfirm("Are you sure you want to delete this execution? It will delete the backup file from the destination and it can't be recovered."), nodx.Class("btn btn-error btn-outline"), diff --git a/internal/view/web/dashboard/webhooks/delete_webhook.go b/internal/view/web/dashboard/webhooks/delete_webhook.go index c86a10b5..516d1055 100644 --- a/internal/view/web/dashboard/webhooks/delete_webhook.go +++ b/internal/view/web/dashboard/webhooks/delete_webhook.go @@ -1,6 +1,8 @@ package webhooks import ( + "fmt" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -28,7 +30,7 @@ func (h *handlers) deleteWebhookHandler(c echo.Context) error { func deleteWebhookButton(webhookID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxDelete(pathutil.BuildPath("/dashboard/webhooks/"+webhookID.String())), + htmx.HxDelete(pathutil.BuildPath(fmt.Sprintf("/dashboard/webhooks/%s", webhookID))), htmx.HxConfirm("Are you sure you want to delete this webhook?"), lucide.Trash(), component.SpanText("Delete webhook"), diff --git a/internal/view/web/dashboard/webhooks/duplicate_webhook.go b/internal/view/web/dashboard/webhooks/duplicate_webhook.go index 03119aa0..30c73d74 100644 --- a/internal/view/web/dashboard/webhooks/duplicate_webhook.go +++ b/internal/view/web/dashboard/webhooks/duplicate_webhook.go @@ -1,6 +1,8 @@ package webhooks import ( + "fmt" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/view/web/component" "github.com/eduardolat/pgbackweb/internal/view/web/respondhtmx" @@ -28,7 +30,7 @@ func (h *handlers) duplicateWebhookHandler(c echo.Context) error { func duplicateWebhookButton(webhookID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxPost(pathutil.BuildPath("/dashboard/webhooks/"+webhookID.String()+"/duplicate")), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/webhooks/%s/duplicate", webhookID))), htmx.HxConfirm("Are you sure you want to duplicate this webhook?"), lucide.CopyPlus(), component.SpanText("Duplicate webhook"), diff --git a/internal/view/web/dashboard/webhooks/edit_webhook.go b/internal/view/web/dashboard/webhooks/edit_webhook.go index 5ee23b66..615567af 100644 --- a/internal/view/web/dashboard/webhooks/edit_webhook.go +++ b/internal/view/web/dashboard/webhooks/edit_webhook.go @@ -2,6 +2,7 @@ package webhooks import ( "database/sql" + "fmt" "net/http" "github.com/eduardolat/pgbackweb/internal/database/dbgen" @@ -102,7 +103,7 @@ func editWebhookForm( backups []dbgen.Backup, ) nodx.Node { return nodx.FormEl( - htmx.HxPost(pathutil.BuildPath("/dashboard/webhooks/"+webhook.ID.String()+"/edit")), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/webhooks/%s/edit", webhook.ID))), htmx.HxDisabledELT("find button[type='submit']"), nodx.Class("space-y-2"), @@ -127,7 +128,7 @@ func editWebhookButton(webhookID uuid.UUID) nodx.Node { Title: "Edit webhook", Content: []nodx.Node{ nodx.Div( - htmx.HxGet(pathutil.BuildPath("/dashboard/webhooks/"+webhookID.String()+"/edit")), + htmx.HxGet(pathutil.BuildPath(fmt.Sprintf("/dashboard/webhooks/%s/edit", webhookID))), htmx.HxSwap("outerHTML"), htmx.HxTrigger("intersect once"), nodx.Class("p-10 flex justify-center"), diff --git a/internal/view/web/dashboard/webhooks/run_webhook.go b/internal/view/web/dashboard/webhooks/run_webhook.go index 171d2a43..95234ba4 100644 --- a/internal/view/web/dashboard/webhooks/run_webhook.go +++ b/internal/view/web/dashboard/webhooks/run_webhook.go @@ -2,6 +2,7 @@ package webhooks import ( "context" + "fmt" "github.com/eduardolat/pgbackweb/internal/logger" "github.com/eduardolat/pgbackweb/internal/util/pathutil" @@ -44,7 +45,7 @@ func (h *handlers) runWebhookHandler(c echo.Context) error { func runWebhookButton(webhookID uuid.UUID) nodx.Node { return component.OptionsDropdownButton( - htmx.HxPost(pathutil.BuildPath("/dashboard/webhooks/"+webhookID.String()+"/run")), + htmx.HxPost(pathutil.BuildPath(fmt.Sprintf("/dashboard/webhooks/%s/run", webhookID))), htmx.HxDisabledELT("this"), lucide.Zap(), component.SpanText("Run webhook now"), From f39598db9749de150c1d90c57faefb4580767107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=BCli=20Patrik=20P=C3=A9ter?= Date: Fri, 3 Oct 2025 12:21:43 +0200 Subject: [PATCH 6/6] Refactors URL handling with pathutil.BuildPath Introduces consistent use of pathutil.BuildPath function for building URLs Adds proper path prefixing to image paths in support project data Improves URL generation across various dashboard components WIP on feature/path-prefix --- .../view/web/component/support_project.inc.js | 45 ++++++++++++++++++- .../web/dashboard/backups/list_backups.go | 8 ++-- .../web/dashboard/databases/list_databases.go | 8 ++-- .../destinations/list_destinations.go | 8 ++-- .../view/web/dashboard/executions/index.go | 3 +- .../dashboard/executions/list_executions.go | 3 +- .../view/web/dashboard/restorations/index.go | 3 +- .../restorations/list_restorations.go | 3 +- .../web/dashboard/webhooks/list_webhooks.go | 2 +- .../dashboard/webhooks/webhook_executions.go | 4 +- 10 files changed, 66 insertions(+), 21 deletions(-) diff --git a/internal/view/web/component/support_project.inc.js b/internal/view/web/component/support_project.inc.js index fa348327..c42173a1 100644 --- a/internal/view/web/component/support_project.inc.js +++ b/internal/view/web/component/support_project.inc.js @@ -55,6 +55,47 @@ window.alpineSupportProjectData = function () { } }, + prefixImagePath(path) { + // If the path starts with / and doesn't start with http, add the path prefix + if (path && path.startsWith("/") && !path.startsWith("http")) { + return window.PBW_PATH_PREFIX + path; + } + return path; + }, + + processData(data) { + // Add path prefix to all image URLs + if (data.referralLinks) { + data.referralLinks = data.referralLinks.map((link) => ({ + ...link, + logo: this.prefixImagePath(link.logo), + })); + } + + if (data.sponsors) { + if (data.sponsors.gold) { + data.sponsors.gold = data.sponsors.gold.map((sponsor) => ({ + ...sponsor, + logo: this.prefixImagePath(sponsor.logo), + })); + } + if (data.sponsors.silver) { + data.sponsors.silver = data.sponsors.silver.map((sponsor) => ({ + ...sponsor, + logo: this.prefixImagePath(sponsor.logo), + })); + } + if (data.sponsors.bronze) { + data.sponsors.bronze = data.sponsors.bronze.map((sponsor) => ({ + ...sponsor, + logo: this.prefixImagePath(sponsor.logo), + })); + } + } + + return data; + }, + async getData() { const cacheKey = "pbw-support-project-data"; @@ -63,7 +104,7 @@ window.alpineSupportProjectData = function () { const cached = JSON.parse(cachedJSON); // Cache for 2 minutes if (Date.now() - cached.timestamp < 2 * 60 * 1000) { - return cached.data; + return this.processData(cached.data); } } @@ -80,7 +121,7 @@ window.alpineSupportProjectData = function () { timestamp: Date.now(), }); localStorage.setItem(cacheKey, dataToCache); - return data; + return this.processData(data); } catch { return null; } diff --git a/internal/view/web/dashboard/backups/list_backups.go b/internal/view/web/dashboard/backups/list_backups.go index 8483997b..675590c7 100644 --- a/internal/view/web/dashboard/backups/list_backups.go +++ b/internal/view/web/dashboard/backups/list_backups.go @@ -70,9 +70,9 @@ func listBackups( nodx.Td(component.OptionsDropdown( component.OptionsDropdownA( nodx.Class("btn btn-sm btn-ghost btn-square"), - nodx.Href( + nodx.Href(pathutil.BuildPath( fmt.Sprintf("/dashboard/executions?backup=%s", backup.ID), - ), + )), nodx.Target("_blank"), lucide.List(), component.SpanText("Show executions"), @@ -125,9 +125,9 @@ func listBackups( if pagination.HasNextPage { trs = append(trs, nodx.Tr( - htmx.HxGet(fmt.Sprintf( + htmx.HxGet(pathutil.BuildPath(fmt.Sprintf( "/dashboard/backups/list?page=%d", pagination.NextPage, - )), + ))), htmx.HxTrigger("intersect once"), htmx.HxSwap("afterend"), )) diff --git a/internal/view/web/dashboard/databases/list_databases.go b/internal/view/web/dashboard/databases/list_databases.go index ae7934eb..28e2f6ed 100644 --- a/internal/view/web/dashboard/databases/list_databases.go +++ b/internal/view/web/dashboard/databases/list_databases.go @@ -65,9 +65,9 @@ func listDatabases( nodx.Div( nodx.Class("flex flex-col space-y-1"), component.OptionsDropdownA( - nodx.Href( + nodx.Href(pathutil.BuildPath( fmt.Sprintf("/dashboard/executions?database=%s", database.ID), - ), + )), nodx.Target("_blank"), lucide.List(), component.SpanText("Show executions"), @@ -105,9 +105,9 @@ func listDatabases( if pagination.HasNextPage { trs = append(trs, nodx.Tr( - htmx.HxGet(fmt.Sprintf( + htmx.HxGet(pathutil.BuildPath(fmt.Sprintf( "/dashboard/databases/list?page=%d", pagination.NextPage, - )), + ))), htmx.HxTrigger("intersect once"), htmx.HxSwap("afterend"), )) diff --git a/internal/view/web/dashboard/destinations/list_destinations.go b/internal/view/web/dashboard/destinations/list_destinations.go index 48f7dfdf..b0534def 100644 --- a/internal/view/web/dashboard/destinations/list_destinations.go +++ b/internal/view/web/dashboard/destinations/list_destinations.go @@ -63,9 +63,9 @@ func listDestinations( trs = append(trs, nodx.Tr( nodx.Td(component.OptionsDropdown( component.OptionsDropdownA( - nodx.Href( + nodx.Href(pathutil.BuildPath( fmt.Sprintf("/dashboard/executions?destination=%s", destination.ID), - ), + )), nodx.Target("_blank"), lucide.List(), component.SpanText("Show executions"), @@ -131,9 +131,9 @@ func listDestinations( if pagination.HasNextPage { trs = append(trs, nodx.Tr( - htmx.HxGet(fmt.Sprintf( + htmx.HxGet(pathutil.BuildPath(fmt.Sprintf( "/dashboard/destinations/list?page=%d", pagination.NextPage, - )), + ))), htmx.HxTrigger("intersect once"), htmx.HxSwap("afterend"), )) diff --git a/internal/view/web/dashboard/executions/index.go b/internal/view/web/dashboard/executions/index.go index 5c57b5cb..7216be59 100644 --- a/internal/view/web/dashboard/executions/index.go +++ b/internal/view/web/dashboard/executions/index.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/strutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/reqctx" @@ -61,7 +62,7 @@ func indexPage(reqCtx reqctx.Ctx, queryData execsQueryData) nodx.Node { nodx.Tbody( component.SkeletonTr(8), htmx.HxGet(func() string { - url := "/dashboard/executions/list?page=1" + url := pathutil.BuildPath("/dashboard/executions/list?page=1") if queryData.Database != uuid.Nil { url = strutil.AddQueryParamToUrl(url, "database", queryData.Database.String()) } diff --git a/internal/view/web/dashboard/executions/list_executions.go b/internal/view/web/dashboard/executions/list_executions.go index 04e0199c..c6b8b3e0 100644 --- a/internal/view/web/dashboard/executions/list_executions.go +++ b/internal/view/web/dashboard/executions/list_executions.go @@ -8,6 +8,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/service/executions" "github.com/eduardolat/pgbackweb/internal/util/echoutil" "github.com/eduardolat/pgbackweb/internal/util/paginateutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/strutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/validate" @@ -117,7 +118,7 @@ func listExecutions( if pagination.HasNextPage { trs = append(trs, nodx.Tr( htmx.HxGet(func() string { - url := "/dashboard/executions/list" + url := pathutil.BuildPath("/dashboard/executions/list") url = strutil.AddQueryParamToUrl(url, "page", fmt.Sprintf("%d", pagination.NextPage)) if queryData.Database != uuid.Nil { url = strutil.AddQueryParamToUrl(url, "database", queryData.Database.String()) diff --git a/internal/view/web/dashboard/restorations/index.go b/internal/view/web/dashboard/restorations/index.go index 2b11cc41..2c4bc18c 100644 --- a/internal/view/web/dashboard/restorations/index.go +++ b/internal/view/web/dashboard/restorations/index.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/strutil" "github.com/eduardolat/pgbackweb/internal/validate" "github.com/eduardolat/pgbackweb/internal/view/reqctx" @@ -66,7 +67,7 @@ func indexPage(reqCtx reqctx.Ctx, queryData resQueryData) nodx.Node { nodx.Tbody( component.SkeletonTr(8), htmx.HxGet(func() string { - url := "/dashboard/restorations/list?page=1" + url := pathutil.BuildPath("/dashboard/restorations/list?page=1") if queryData.Execution != uuid.Nil { url = strutil.AddQueryParamToUrl(url, "execution", queryData.Execution.String()) } diff --git a/internal/view/web/dashboard/restorations/list_restorations.go b/internal/view/web/dashboard/restorations/list_restorations.go index ec330b75..5a3a11dc 100644 --- a/internal/view/web/dashboard/restorations/list_restorations.go +++ b/internal/view/web/dashboard/restorations/list_restorations.go @@ -8,6 +8,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/service/restorations" "github.com/eduardolat/pgbackweb/internal/util/echoutil" "github.com/eduardolat/pgbackweb/internal/util/paginateutil" + "github.com/eduardolat/pgbackweb/internal/util/pathutil" "github.com/eduardolat/pgbackweb/internal/util/strutil" "github.com/eduardolat/pgbackweb/internal/util/timeutil" "github.com/eduardolat/pgbackweb/internal/validate" @@ -109,7 +110,7 @@ func listRestorations( if pagination.HasNextPage { trs = append(trs, nodx.Tr( htmx.HxGet(func() string { - url := "/dashboard/restorations/list" + url := pathutil.BuildPath("/dashboard/restorations/list") url = strutil.AddQueryParamToUrl(url, "page", fmt.Sprintf("%d", pagination.NextPage)) if queryData.Execution != uuid.Nil { url = strutil.AddQueryParamToUrl(url, "execution", queryData.Execution.String()) diff --git a/internal/view/web/dashboard/webhooks/list_webhooks.go b/internal/view/web/dashboard/webhooks/list_webhooks.go index d9b22fac..c7b05041 100644 --- a/internal/view/web/dashboard/webhooks/list_webhooks.go +++ b/internal/view/web/dashboard/webhooks/list_webhooks.go @@ -92,7 +92,7 @@ func listWebhooks( if pagination.HasNextPage { trs = append(trs, nodx.Tr( htmx.HxGet(func() string { - url := "/dashboard/webhooks/list" + url := pathutil.BuildPath("/dashboard/webhooks/list") url = strutil.AddQueryParamToUrl(url, "page", fmt.Sprintf("%d", pagination.NextPage)) return url }()), diff --git a/internal/view/web/dashboard/webhooks/webhook_executions.go b/internal/view/web/dashboard/webhooks/webhook_executions.go index c4a9a194..1d8cdaec 100644 --- a/internal/view/web/dashboard/webhooks/webhook_executions.go +++ b/internal/view/web/dashboard/webhooks/webhook_executions.go @@ -88,7 +88,7 @@ func webhookExecutionsList( if pagination.HasNextPage { trs = append(trs, nodx.Tr( htmx.HxGet(func() string { - url := "/dashboard/webhooks/" + webhookID.String() + "/executions" + url := pathutil.BuildPath("/dashboard/webhooks/" + webhookID.String() + "/executions") url = strutil.AddQueryParamToUrl(url, "page", fmt.Sprintf("%d", pagination.NextPage)) return url }()), @@ -258,7 +258,7 @@ func webhookExecutionsButton(webhookID uuid.UUID) nodx.Node { ), nodx.Tbody( htmx.HxGet( - "/dashboard/webhooks/"+webhookID.String()+"/executions?page=1", + pathutil.BuildPath("/dashboard/webhooks/"+webhookID.String()+"/executions?page=1"), ), htmx.HxIndicator("#webhook-executions-loading"), htmx.HxTrigger("intersect once"),