diff --git a/beszel/internal/hub/hub.go b/beszel/internal/hub/hub.go index 3fc4cad..ac130f4 100644 --- a/beszel/internal/hub/hub.go +++ b/beszel/internal/hub/hub.go @@ -12,6 +12,7 @@ import ( "crypto/ed25519" "encoding/pem" "fmt" + "io/fs" "log" "net" "net/http" @@ -40,15 +41,18 @@ type Hub struct { rm *records.RecordManager systemStats *core.Collection containerStats *core.Collection + appURL string } func NewHub(app *pocketbase.PocketBase) *Hub { - return &Hub{ + hub := &Hub{ app: app, am: alerts.NewAlertManager(app), um: users.NewUserManager(app), rm: records.NewRecordManager(app), } + hub.appURL, _ = GetEnv("APP_URL") + return hub } // GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key. @@ -82,6 +86,10 @@ func (h *Hub) Run() { settings := h.app.Settings() // batch requests (for global alerts) settings.Batch.Enabled = true + // set URL if BASE_URL env is set + if h.appURL != "" { + settings.Meta.AppURL = h.appURL + } // set auth settings usersCollection, err := h.app.FindCollectionByNameOrId("users") if err != nil { @@ -118,19 +126,39 @@ func (h *Hub) Run() { Scheme: "http", Host: "localhost:5173", }) - se.Router.Any("/", func(e *core.RequestEvent) error { + se.Router.Any("/{path...}", func(e *core.RequestEvent) error { proxy.ServeHTTP(e.Response, e.Request) return nil }) default: + // parse app url + parsedURL, err := url.Parse(h.appURL) + if err != nil { + return err + } + // fix base paths in html if using subpath + basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/" + indexFile, _ := fs.ReadFile(site.DistDirFS, "index.html") + indexContent := strings.ReplaceAll(string(indexFile), "./", basePath) + // set up static asset serving + staticPaths := [2]string{"/static/", "/assets/"} + serveStatic := apis.Static(site.DistDirFS, false) + // get CSP configuration csp, cspExists := GetEnv("CSP") - s := apis.Static(site.DistDirFS, true) + // add route se.Router.Any("/{path...}", func(e *core.RequestEvent) error { + // serve static assets if path is in staticPaths + for i := range staticPaths { + if strings.Contains(e.Request.URL.Path, staticPaths[i]) { + e.Response.Header().Set("Cache-Control", "public, max-age=2592000") + return serveStatic(e) + } + } if cspExists { e.Response.Header().Del("X-Frame-Options") e.Response.Header().Set("Content-Security-Policy", csp) } - return s(e) + return e.HTML(http.StatusOK, indexContent) }) } return se.Next() diff --git a/beszel/site/index.html b/beszel/site/index.html index e772f4b..2b5dcb6 100644 --- a/beszel/site/index.html +++ b/beszel/site/index.html @@ -1,10 +1,11 @@ - + Beszel +
diff --git a/beszel/site/src/components/add-system.tsx b/beszel/site/src/components/add-system.tsx index fca744f..90ca2dd 100644 --- a/beszel/site/src/components/add-system.tsx +++ b/beszel/site/src/components/add-system.tsx @@ -18,7 +18,7 @@ import { Copy, PlusIcon } from "lucide-react" import { useState, useRef, MutableRefObject } from "react" import { useStore } from "@nanostores/react" import { cn, copyToClipboard, isReadOnlyUser } from "@/lib/utils" -import { navigate } from "./router" +import { basePath, navigate } from "./router" import { Trans } from "@lingui/macro" import { i18n } from "@lingui/core" @@ -60,7 +60,7 @@ export function AddSystemButton({ className }: { className?: string }) { try { setOpen(false) await pb.collection("systems").create(data) - navigate("/") + navigate(basePath) // console.log(record) } catch (e) { console.log(e) diff --git a/beszel/site/src/components/command-palette.tsx b/beszel/site/src/components/command-palette.tsx index 812efb6..dafff9f 100644 --- a/beszel/site/src/components/command-palette.tsx +++ b/beszel/site/src/components/command-palette.tsx @@ -23,8 +23,9 @@ import { useEffect } from "react" import { useStore } from "@nanostores/react" import { $systems } from "@/lib/stores" import { isAdmin } from "@/lib/utils" -import { navigate } from "./router" +import { $router, basePath, navigate } from "./router" import { Trans, t } from "@lingui/macro" +import { getPagePath } from "@nanostores/router" export default function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) { const systems = useStore($systems) @@ -55,7 +56,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp { - navigate(`/system/${encodeURIComponent(system.name)}`) + navigate(getPagePath($router, "system", { name: system.name })) setOpen(false) }} > @@ -72,7 +73,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp { - navigate("/") + navigate(basePath) setOpen(false) }} > @@ -86,7 +87,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp { - navigate("/settings/general") + navigate(getPagePath($router, "settings", { name: "general" })) setOpen(false) }} > @@ -101,7 +102,7 @@ export default function CommandPalette({ open, setOpen }: { open: boolean; setOp { - navigate("/settings/notifications") + navigate(getPagePath($router, "settings", { name: "notifications" })) setOpen(false) }} > diff --git a/beszel/site/src/components/login/auth-form.tsx b/beszel/site/src/components/login/auth-form.tsx index f3b2bbf..c983932 100644 --- a/beszel/site/src/components/login/auth-form.tsx +++ b/beszel/site/src/components/login/auth-form.tsx @@ -9,8 +9,9 @@ import { toast } from "../ui/use-toast" import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { useCallback, useState } from "react" import { AuthMethodsList, OAuth2AuthConfig } from "pocketbase" -import { Link } from "../router" +import { $router, Link, prependBasePath } from "../router" import { Trans, t } from "@lingui/macro" +import { getPagePath } from "@nanostores/router" const honeypot = v.literal("") const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`)) @@ -260,11 +261,11 @@ export function UserAuthForm({ ) : ( { - e.currentTarget.src = "/static/lock.svg" - }} + // onError={(e) => { + // e.currentTarget.src = "/static/lock.svg" + // }} /> )} {provider.displayName} @@ -278,7 +279,7 @@ export function UserAuthForm({ @@ -311,7 +312,7 @@ export function UserAuthForm({ {passwordEnabled && !isFirstRun && ( Forgot password? diff --git a/beszel/site/src/components/login/login.tsx b/beszel/site/src/components/login/login.tsx index 19898f2..296a925 100644 --- a/beszel/site/src/components/login/login.tsx +++ b/beszel/site/src/components/login/login.tsx @@ -34,7 +34,7 @@ export default function () { const subtitle = useMemo(() => { if (isFirstRun) { return t`Please create an admin account` - } else if (page?.path === "/forgot-password") { + } else if (page?.route === "forgot_password") { return t`Enter email address to reset password` } else { return t`Please sign in to your account` @@ -59,7 +59,7 @@ export default function () {

{subtitle}

- {page?.path === "/forgot-password" ? ( + {page?.route === "forgot_password" ? ( ) : ( diff --git a/beszel/site/src/components/navbar.tsx b/beszel/site/src/components/navbar.tsx index c397e65..034ae9d 100644 --- a/beszel/site/src/components/navbar.tsx +++ b/beszel/site/src/components/navbar.tsx @@ -10,7 +10,7 @@ import { UserIcon, UsersIcon, } from "lucide-react" -import { Link } from "./router" +import { $router, basePath, Link, prependBasePath } from "./router" import { LangToggle } from "./lang-toggle" import { ModeToggle } from "./mode-toggle" import { Logo } from "./logo" @@ -27,6 +27,7 @@ import { } from "@/components/ui/dropdown-menu" import { AddSystemButton } from "./add-system" import { Trans } from "@lingui/macro" +import { getPagePath } from "@nanostores/router" const CommandPalette = lazy(() => import("./command-palette")) @@ -35,7 +36,7 @@ const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0 export default function Navbar() { return (
- + @@ -44,7 +45,7 @@ export default function Navbar() { @@ -63,7 +64,7 @@ export default function Navbar() { {isAdmin() && ( <> - + Users @@ -71,7 +72,7 @@ export default function Navbar() { - + Systems @@ -79,7 +80,7 @@ export default function Navbar() { - + Logs @@ -87,7 +88,7 @@ export default function Navbar() { - + Backups diff --git a/beszel/site/src/components/router.tsx b/beszel/site/src/components/router.tsx index a76745d..968bc45 100644 --- a/beszel/site/src/components/router.tsx +++ b/beszel/site/src/components/router.tsx @@ -1,16 +1,36 @@ import { createRouter } from "@nanostores/router" -export const $router = createRouter( - { - home: "/", - server: "/system/:name", - settings: "/settings/:name?", - forgot_password: "/forgot-password", - }, - { links: false } -) +const routes = { + home: "/", + system: `/system/:name`, + settings: `/settings/:name?`, + forgot_password: `/forgot-password`, +} as const -/** Navigate to url using router */ +/** + * The base path of the application. + * This is used to prepend the base path to all routes. + */ +export const basePath = window.BASE_PATH || "" + +/** + * Prepends the base path to the given path. + * @param path The path to prepend the base path to. + * @returns The path with the base path prepended. + */ +export const prependBasePath = (path: string) => (basePath + path).replaceAll("//", "/") + +// prepend base path to routes +for (const route in routes) { + // @ts-ignore need as const above to get nanostores to parse types properly + routes[route] = prependBasePath(routes[route]) +} + +export const $router = createRouter(routes, { links: false }) + +/** Navigate to url using router + * Base path is automatically prepended if serving from subpath + */ export const navigate = (urlString: string) => { $router.open(urlString) } diff --git a/beszel/site/src/components/routes/home.tsx b/beszel/site/src/components/routes/home.tsx index be2121f..f38e1fb 100644 --- a/beszel/site/src/components/routes/home.tsx +++ b/beszel/site/src/components/routes/home.tsx @@ -7,8 +7,9 @@ import { Separator } from "../ui/separator" import { alertInfo, updateRecordList, updateSystemList } from "@/lib/utils" import { AlertRecord, SystemRecord } from "@/types" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { Link } from "../router" +import { $router, Link } from "../router" import { Plural, t, Trans } from "@lingui/macro" +import { getPagePath } from "@nanostores/router" const SystemsTable = lazy(() => import("../systems-table/systems-table")) @@ -83,7 +84,7 @@ export default function Home() { diff --git a/beszel/site/src/components/routes/settings/layout.tsx b/beszel/site/src/components/routes/settings/layout.tsx index 8c50556..a23ff19 100644 --- a/beszel/site/src/components/routes/settings/layout.tsx +++ b/beszel/site/src/components/routes/settings/layout.tsx @@ -4,7 +4,7 @@ import { SidebarNav } from "./sidebar-nav.tsx" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx" import { useStore } from "@nanostores/react" import { $router } from "@/components/router.tsx" -import { redirectPage } from "@nanostores/router" +import { getPagePath, redirectPage } from "@nanostores/router" import { BellIcon, FileSlidersIcon, SettingsIcon } from "lucide-react" import { $userSettings, pb } from "@/lib/stores.ts" import { toast } from "@/components/ui/use-toast.ts" @@ -49,17 +49,17 @@ export default function SettingsLayout() { const sidebarNavItems = [ { title: _(t({ message: `General`, comment: "Context: General settings" })), - href: "/settings/general", + href: getPagePath($router, "settings", { name: "general" }), icon: SettingsIcon, }, { title: t`Notifications`, - href: "/settings/notifications", + href: getPagePath($router, "settings", { name: "notifications" }), icon: BellIcon, }, { title: t`YAML Config`, - href: "/settings/config", + href: getPagePath($router, "settings", { name: "config" }), icon: FileSlidersIcon, admin: true, }, @@ -69,8 +69,8 @@ export default function SettingsLayout() { useEffect(() => { document.title = t`Settings` + " / Beszel" - // redirect to account page if no page is specified - if (page?.path === "/settings") { + // @ts-ignore redirect to account page if no page is specified + if (!page?.params?.name) { redirectPage($router, "settings", { name: "general" }) } }, []) diff --git a/beszel/site/src/components/routes/settings/notifications.tsx b/beszel/site/src/components/routes/settings/notifications.tsx index 3b73f5b..183b3d1 100644 --- a/beszel/site/src/components/routes/settings/notifications.tsx +++ b/beszel/site/src/components/routes/settings/notifications.tsx @@ -13,6 +13,7 @@ import { saveSettings } from "./layout" import * as v from "valibot" import { isAdmin } from "@/lib/utils" import { Trans, t } from "@lingui/macro" +import { prependBasePath } from "@/components/router" interface ShoutrrrUrlCardProps { url: string @@ -94,7 +95,7 @@ const SettingsNotificationsPage = ({ userSettings }: { userSettings: UserSetting

Please{" "} - + configure an SMTP server {" "} to ensure alerts are delivered. diff --git a/beszel/site/src/components/routes/settings/sidebar-nav.tsx b/beszel/site/src/components/routes/settings/sidebar-nav.tsx index 8690e9b..3ee7aad 100644 --- a/beszel/site/src/components/routes/settings/sidebar-nav.tsx +++ b/beszel/site/src/components/routes/settings/sidebar-nav.tsx @@ -22,7 +22,7 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) { <> {/* Mobile View */}

- diff --git a/beszel/site/src/components/systems-table/systems-table.tsx b/beszel/site/src/components/systems-table/systems-table.tsx index 669a562..9ee2d95 100644 --- a/beszel/site/src/components/systems-table/systems-table.tsx +++ b/beszel/site/src/components/systems-table/systems-table.tsx @@ -65,13 +65,14 @@ import { $hubVersion, $systems, pb } from "@/lib/stores" import { useStore } from "@nanostores/react" import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils" import AlertsButton from "../alerts/alert-button" -import { Link, navigate } from "../router" +import { $router, Link, navigate } from "../router" import { EthernetIcon } from "../ui/icons" import { Trans, t } from "@lingui/macro" import { useLingui } from "@lingui/react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card" import { Input } from "../ui/input" import { ClassValue } from "clsx" +import { getPagePath } from "@nanostores/router" type ViewMode = "table" | "grid" @@ -406,7 +407,7 @@ export default function SystemsTable() { onClick={(e) => { const target = e.target as HTMLElement if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) { - navigate(`/system/${encodeURIComponent(row.original.name)}`) + navigate(getPagePath($router, "system", { name: row.original.name })) } }} > @@ -489,7 +490,7 @@ export default function SystemsTable() { })} {row.original.name} diff --git a/beszel/site/src/lib/stores.ts b/beszel/site/src/lib/stores.ts index 39edc8b..4fdce7b 100644 --- a/beszel/site/src/lib/stores.ts +++ b/beszel/site/src/lib/stores.ts @@ -1,9 +1,10 @@ import PocketBase from "pocketbase" import { atom, map, WritableAtom } from "nanostores" import { AlertRecord, ChartTimes, SystemRecord, UserSettings } from "@/types" +import { basePath } from "@/components/router" /** PocketBase JS Client */ -export const pb = new PocketBase("/") +export const pb = new PocketBase(basePath) /** Store if user is authenticated */ export const $authenticated = atom(pb.authStore.isValid) diff --git a/beszel/site/src/main.tsx b/beszel/site/src/main.tsx index a290c1d..84e0ae0 100644 --- a/beszel/site/src/main.tsx +++ b/beszel/site/src/main.tsx @@ -63,9 +63,9 @@ const App = () => { if (!page) { return

404

- } else if (page.path === "/") { + } else if (page.route === "home") { return - } else if (page.route === "server") { + } else if (page.route === "system") { return } else if (page.route === "settings") { return ( diff --git a/beszel/site/src/types.d.ts b/beszel/site/src/types.d.ts index 62886ea..0c6648e 100644 --- a/beszel/site/src/types.d.ts +++ b/beszel/site/src/types.d.ts @@ -1,5 +1,12 @@ import { RecordModel } from "pocketbase" +// global window properties +declare global { + interface Window { + BASE_PATH: string + } +} + export interface SystemRecord extends RecordModel { name: string host: string diff --git a/beszel/site/vite.config.ts b/beszel/site/vite.config.ts index 5491f75..51fa339 100644 --- a/beszel/site/vite.config.ts +++ b/beszel/site/vite.config.ts @@ -4,6 +4,7 @@ import react from "@vitejs/plugin-react-swc" import { lingui } from "@lingui/vite-plugin" export default defineConfig({ + base: "./", plugins: [ react({ plugins: [["@lingui/swc-plugin", {}]],