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..30a35a05 --- /dev/null +++ b/internal/util/pathutil/pathutil_test.go @@ -0,0 +1,87 @@ +package pathutil + +import ( + "sync" + "testing" +) + +func TestBuildPath(t *testing.T) { + t.Helper() + + tests := []struct { + name string + prefix string + path string + expected string + }{ + { + 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) { + 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) + } + }) + } +} + +func TestGetPathPrefix(t *testing.T) { + t.Helper() + 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") + } +} 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..ba69de1b 100644 --- a/internal/view/router.go +++ b/internal/view/router.go @@ -1,8 +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" @@ -11,20 +14,31 @@ 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) - apiGroup := app.Group("/api") + // 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) - webGroup := app.Group("", mids.InjectReqctx) + webGroup := baseGroup.Group("", mids.InjectReqctx) web.MountRouter(webGroup, 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/auth/create_first_user.go b/internal/view/web/auth/create_first_user.go index 11d5e5d7..7ce89f26 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()) @@ -40,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"), @@ -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..3947e5e2 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()) @@ -39,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"), @@ -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/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/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/create_backup.go b/internal/view/web/dashboard/backups/create_backup.go index 49e4c31b..6a71de31 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 { @@ -105,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"), @@ -322,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..a3d841a2 100644 --- a/internal/view/web/dashboard/backups/delete_backup.go +++ b/internal/view/web/dashboard/backups/delete_backup.go @@ -1,6 +1,9 @@ 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" "github.com/google/uuid" @@ -27,7 +30,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(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 a397a854..f60cd36b 100644 --- a/internal/view/web/dashboard/backups/duplicate_backup.go +++ b/internal/view/web/dashboard/backups/duplicate_backup.go @@ -1,6 +1,9 @@ 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" "github.com/google/uuid" @@ -27,7 +30,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(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 543aa85f..4a9e2bfe 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(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/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/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/backups/manual_run.go b/internal/view/web/dashboard/backups/manual_run.go index 309d0655..61c64c5a 100644 --- a/internal/view/web/dashboard/backups/manual_run.go +++ b/internal/view/web/dashboard/backups/manual_run.go @@ -2,7 +2,9 @@ package backups import ( "context" + "fmt" + "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 +29,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(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 88a65c16..47815afd 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,13 +42,13 @@ 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 { 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 4996d461..e2066c58 100644 --- a/internal/view/web/dashboard/databases/delete_database.go +++ b/internal/view/web/dashboard/databases/delete_database.go @@ -1,6 +1,9 @@ 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" "github.com/google/uuid" @@ -27,7 +30,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(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/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..28e2f6ed 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" @@ -64,16 +65,16 @@ 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"), ), editDatabaseButton(database), component.OptionsDropdownButton( - htmx.HxPost("/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"), @@ -104,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/create_destination.go b/internal/view/web/dashboard/destinations/create_destination.go index ae0fe3b1..2f8d0c8e 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,13 +46,13 @@ 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 { 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 80b65be2..a782813f 100644 --- a/internal/view/web/dashboard/destinations/delete_destination.go +++ b/internal/view/web/dashboard/destinations/delete_destination.go @@ -1,6 +1,9 @@ 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" "github.com/google/uuid" @@ -28,7 +31,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(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/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..b0534def 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" @@ -62,16 +63,16 @@ 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"), ), editDestinationButton(destination), component.OptionsDropdownButton( - htmx.HxPost("/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"), @@ -130,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/executions/restore_execution.go b/internal/view/web/dashboard/executions/restore_execution.go index 45494f0d..d1acd334 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(fmt.Sprintf("/dashboard/executions/%s/restore", execution.ID))), 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(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 f8224eaa..24183d87 100644 --- a/internal/view/web/dashboard/executions/show_execution.go +++ b/internal/view/web/dashboard/executions/show_execution.go @@ -1,10 +1,12 @@ package executions import ( + "fmt" "net/http" "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 +123,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(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 e62abaf5..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,9 @@ 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" "github.com/google/uuid" @@ -28,7 +31,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(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/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/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/create_webhook.go b/internal/view/web/dashboard/webhooks/create_webhook.go index 73c73a64..691acf4a 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 { @@ -86,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"), @@ -111,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..516d1055 100644 --- a/internal/view/web/dashboard/webhooks/delete_webhook.go +++ b/internal/view/web/dashboard/webhooks/delete_webhook.go @@ -1,6 +1,9 @@ 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" "github.com/google/uuid" @@ -27,7 +30,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(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 6f2684ac..30c73d74 100644 --- a/internal/view/web/dashboard/webhooks/duplicate_webhook.go +++ b/internal/view/web/dashboard/webhooks/duplicate_webhook.go @@ -1,6 +1,9 @@ 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" "github.com/google/uuid" @@ -27,7 +30,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(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 c19a0fc9..615567af 100644 --- a/internal/view/web/dashboard/webhooks/edit_webhook.go +++ b/internal/view/web/dashboard/webhooks/edit_webhook.go @@ -2,10 +2,12 @@ package webhooks import ( "database/sql" + "fmt" "net/http" "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 +103,7 @@ func editWebhookForm( backups []dbgen.Backup, ) nodx.Node { return nodx.FormEl( - htmx.HxPost("/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"), @@ -126,7 +128,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(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/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/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/run_webhook.go b/internal/view/web/dashboard/webhooks/run_webhook.go index 690dd439..95234ba4 100644 --- a/internal/view/web/dashboard/webhooks/run_webhook.go +++ b/internal/view/web/dashboard/webhooks/run_webhook.go @@ -2,8 +2,10 @@ package webhooks import ( "context" + "fmt" "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 +45,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(fmt.Sprintf("/dashboard/webhooks/%s/run", webhookID))), htmx.HxDisabledELT("this"), lucide.Zap(), component.SpanText("Run webhook now"), 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"), 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, ), ), 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"), 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")