diff --git a/beszel/internal/hub/hub.go b/beszel/internal/hub/hub.go index 3f56ca5b..abaf93d6 100644 --- a/beszel/internal/hub/hub.go +++ b/beszel/internal/hub/hub.go @@ -19,6 +19,7 @@ import ( "time" "github.com/google/uuid" + "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" @@ -112,6 +113,8 @@ func (h *Hub) initialize(e *core.ServeEvent) error { // set URL if BASE_URL env is set if h.appURL != "" { settings.Meta.AppURL = h.appURL + } else { + h.appURL = settings.Meta.AppURL } if err := e.App.Save(settings); err != nil { return err @@ -297,3 +300,30 @@ func (h *Hub) MakeLink(parts ...string) string { } return base } + +type SystemInfo struct { + Name string `json:"name"` + Id string `json:"id"` + Status string `json:"status"` + Port uint16 `json:"port"` + Host string `json:"host"` + Info string `json:"info"` +} + +func (h *Hub) getUserSystemsFromRequest(req *http.Request) ([]SystemInfo, error) { + systems := []SystemInfo{} + token, err := req.Cookie("beszauth") + if err != nil { + return systems, err + } + if token.Value != "" { + user, err := h.FindAuthRecordByToken(token.Value) + if err != nil { + return systems, err + } + h.DB().NewQuery("SELECT s.id, s.info, s.status, s.name, s.port, s.host FROM systems s JOIN json_each(s.users) AS je WHERE je.value = {:user_id}").Bind(dbx.Params{ + "user_id": user.Id, + }).All(&systems) + } + return systems, err +} diff --git a/beszel/internal/hub/server_development.go b/beszel/internal/hub/server_development.go index e7a34d54..1fac115c 100644 --- a/beszel/internal/hub/server_development.go +++ b/beszel/internal/hub/server_development.go @@ -3,18 +3,96 @@ package hub import ( + "beszel" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" "net/http/httputil" "net/url" + "strings" "github.com/pocketbase/pocketbase/core" ) +// responseModifier wraps an http.RoundTripper to modify HTML responses +type responseModifier struct { + transport http.RoundTripper + hub *Hub +} + +// RoundTrip implements http.RoundTripper interface with response modification +func (rm *responseModifier) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := rm.transport.RoundTrip(req) + if err != nil { + return resp, err + } + + // Only modify HTML responses + contentType := resp.Header.Get("Content-Type") + if !strings.Contains(contentType, "text/html") { + return resp, nil + } + + // Read the response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return resp, err + } + resp.Body.Close() + + // Modify the HTML content here + modifiedBody := rm.modifyHTML(string(body), req) + + // Create a new response with the modified body + resp.Body = io.NopCloser(strings.NewReader(modifiedBody)) + resp.ContentLength = int64(len(modifiedBody)) + resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(modifiedBody))) + + return resp, nil +} + +// modifyHTML applies modifications to HTML content +func (rm *responseModifier) modifyHTML(html string, req *http.Request) string { + parsedURL, err := url.Parse(rm.hub.appURL) + if err != nil { + return html + } + // fix base paths in html if using subpath + basePath := strings.TrimSuffix(parsedURL.Path, "/") + "/" + html = strings.ReplaceAll(html, "./", basePath) + html = strings.Replace(html, "{{V}}", beszel.Version, 1) + slog.Info("modifying HTML", "appURL", rm.hub.appURL) + html = strings.Replace(html, "{{HUB_URL}}", rm.hub.appURL, 1) + + systems, err := rm.hub.getUserSystemsFromRequest(req) + if err != nil { + return html + } + systemsJson, err := json.Marshal(systems) + if err != nil { + return html + } + html = strings.Replace(html, "'{SYSTEMS}'", string(systemsJson), 1) + + return html +} + // startServer sets up the development server for Beszel func (h *Hub) startServer(se *core.ServeEvent) error { + slog.Info("starting server", "appURL", h.appURL) proxy := httputil.NewSingleHostReverseProxy(&url.URL{ Scheme: "http", Host: "localhost:5173", }) + + // Set up custom transport with response modification + proxy.Transport = &responseModifier{ + transport: http.DefaultTransport, + hub: h, + } + se.Router.GET("/{path...}", func(e *core.RequestEvent) error { proxy.ServeHTTP(e.Response, e.Request) return nil diff --git a/beszel/internal/hub/server_production.go b/beszel/internal/hub/server_production.go index 831d0b50..4db12f99 100644 --- a/beszel/internal/hub/server_production.go +++ b/beszel/internal/hub/server_production.go @@ -5,7 +5,9 @@ package hub import ( "beszel" "beszel/site" + "encoding/json" "io/fs" + "log/slog" "net/http" "net/url" "strings" @@ -24,9 +26,9 @@ func (h *Hub) startServer(se *core.ServeEvent) error { // 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) - indexContent = strings.Replace(indexContent, "{{V}}", beszel.Version, 1) - indexContent = strings.Replace(indexContent, "{{HUB_URL}}", h.appURL, 1) + html := strings.ReplaceAll(string(indexFile), "./", basePath) + html = strings.Replace(html, "{{V}}", beszel.Version, 1) + html = strings.Replace(html, "{{HUB_URL}}", h.appURL, 1) // set up static asset serving staticPaths := [2]string{"/static/", "/assets/"} serveStatic := apis.Static(site.DistDirFS, false) @@ -45,7 +47,16 @@ func (h *Hub) startServer(se *core.ServeEvent) error { e.Response.Header().Del("X-Frame-Options") e.Response.Header().Set("Content-Security-Policy", csp) } - return e.HTML(http.StatusOK, indexContent) + systems, err := h.getUserSystemsFromRequest(e.Request) + if err != nil { + slog.Error("error getting user systems", "error", err) + } + systemsJson, err := json.Marshal(systems) + if err != nil { + slog.Error("error marshalling user systems", "error", err) + } + html = strings.Replace(html, "'{SYSTEMS}'", string(systemsJson), 1) + return e.HTML(http.StatusOK, html) }) return nil } diff --git a/beszel/site/index.html b/beszel/site/index.html index db01b2f4..7b1861b0 100644 --- a/beszel/site/index.html +++ b/beszel/site/index.html @@ -10,7 +10,8 @@ globalThis.BESZEL = { BASE_PATH: "%BASE_URL%", HUB_VERSION: "{{V}}", - HUB_URL: "{{HUB_URL}}" + HUB_URL: "{{HUB_URL}}", + SYSTEMS: '{SYSTEMS}' } diff --git a/beszel/site/src/lib/api.ts b/beszel/site/src/lib/api.ts index a81e9232..23a18b63 100644 --- a/beszel/site/src/lib/api.ts +++ b/beszel/site/src/lib/api.ts @@ -12,6 +12,13 @@ export const pb = new PocketBase(basePath) export const isAdmin = () => pb.authStore.record?.role === "admin" export const isReadOnlyUser = () => pb.authStore.record?.role === "readonly" +export const updateCookieToken = () => { + console.log("setting token", pb.authStore.token) + document.cookie = `beszauth=${pb.authStore.token}; path=/; expires=${new Date( + Date.now() + 7 * 24 * 60 * 60 * 1000 + ).toString()}` +} + export const verifyAuth = () => { pb.collection("users") .authRefresh() diff --git a/beszel/site/src/lib/systemsManager.ts b/beszel/site/src/lib/systemsManager.ts index c5392000..b245e28f 100644 --- a/beszel/site/src/lib/systemsManager.ts +++ b/beszel/site/src/lib/systemsManager.ts @@ -141,7 +141,13 @@ export async function subscribe() { } /** Refresh all systems with latest data from the hub */ -export async function refresh() { +export async function refresh(records: SystemRecord[] = []) { + if (records.length) { + for (const record of records) { + add(record) + } + return + } try { const records = await fetchSystems() if (!records.length) { diff --git a/beszel/site/src/main.tsx b/beszel/site/src/main.tsx index d241d475..1d42daab 100644 --- a/beszel/site/src/main.tsx +++ b/beszel/site/src/main.tsx @@ -5,7 +5,7 @@ import ReactDOM from "react-dom/client" import { ThemeProvider } from "./components/theme-provider.tsx" import { DirectionProvider } from "@radix-ui/react-direction" import { $authenticated, $publicKey, $copyContent, $direction } from "./lib/stores.ts" -import { pb, updateUserSettings } from "./lib/api.ts" +import { pb, updateUserSettings, updateCookieToken } from "./lib/api.ts" import * as systemsManager from "./lib/systemsManager.ts" import { useStore } from "@nanostores/react" import { Toaster } from "./components/ui/toaster.tsx" @@ -27,8 +27,10 @@ const App = memo(() => { useEffect(() => { // change auth store on auth change + updateCookieToken() pb.authStore.onChange(() => { $authenticated.set(pb.authStore.isValid) + updateCookieToken() }) // get version / public key pb.send("/api/beszel/getkey", {}).then((data) => { @@ -36,11 +38,17 @@ const App = memo(() => { }) // get user settings updateUserSettings() + const startingSystems = globalThis.BESZEL.SYSTEMS + for (const system of startingSystems) { + // if (typeof system.info === "string") { + system.info = JSON.parse(system.info as unknown as string) + // } + } // need to get system list before alerts systemsManager.init() systemsManager // get current systems list - .refresh() + .refresh(startingSystems) // subscribe to new system updates .then(systemsManager.subscribe) // get current alerts @@ -51,6 +59,7 @@ const App = memo(() => { // updateFavicon("favicon.svg") alertManager.unsubscribe() systemsManager.unsubscribe() + globalThis.BESZEL.SYSTEMS = [] } }, []) diff --git a/beszel/site/src/types.d.ts b/beszel/site/src/types.d.ts index cf47909d..093a0ec8 100644 --- a/beszel/site/src/types.d.ts +++ b/beszel/site/src/types.d.ts @@ -7,6 +7,8 @@ declare global { BASE_PATH: string HUB_VERSION: string HUB_URL: string + /** initial list of systems */ + SYSTEMS: SystemRecord[] } } diff --git a/beszel/site/vite.config.ts b/beszel/site/vite.config.ts index 06c6da38..ce6a8ad3 100644 --- a/beszel/site/vite.config.ts +++ b/beszel/site/vite.config.ts @@ -3,7 +3,6 @@ import path from "path" import tailwindcss from "@tailwindcss/vite" import react from "@vitejs/plugin-react-swc" import { lingui } from "@lingui/vite-plugin" -import { version } from "./package.json" export default defineConfig({ base: "./", @@ -13,13 +12,6 @@ export default defineConfig({ }), lingui(), tailwindcss(), - { - name: "replace version in index.html during dev", - apply: "serve", - transformIndexHtml(html) { - return html.replace("{{V}}", version).replace("{{HUB_URL}}", "") - }, - }, ], esbuild: { legalComments: "external",