From 8db87e549735c8f94ee3d28860ca14734eb01789 Mon Sep 17 00:00:00 2001 From: hank Date: Thu, 11 Sep 2025 12:45:43 -0400 Subject: [PATCH 1/6] Update Crowdin configuration file --- i18n.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n.yml b/i18n.yml index c5882ca..ec7b8cb 100644 --- a/i18n.yml +++ b/i18n.yml @@ -1,3 +1,3 @@ files: - - source: /internal/site/src/locales/en/en.po + - source: /internal/site/src/locales/en/ translation: /internal/site/src/locales/%two_letters_code%/%two_letters_code%.po From a7d07310b600a858d291e5d331d81662ac85df7f Mon Sep 17 00:00:00 2001 From: henrygd Date: Thu, 11 Sep 2025 14:01:09 -0400 Subject: [PATCH 2/6] Add `AUTO_LOGIN` environment variable for automatic login. (#399) --- internal/hub/hub.go | 48 ++++++++++++++++------------------ internal/hub/hub_test.go | 54 +++++++++++++++++++++++++++++++++++++++ supplemental/CHANGELOG.md | 4 +++ 3 files changed, 80 insertions(+), 26 deletions(-) diff --git a/internal/hub/hub.go b/internal/hub/hub.go index 89fb4a9..890abae 100644 --- a/internal/hub/hub.go +++ b/internal/hub/hub.go @@ -175,35 +175,31 @@ func (h *Hub) registerCronJobs(_ *core.ServeEvent) error { // custom middlewares func (h *Hub) registerMiddlewares(se *core.ServeEvent) { + // authorizes request with user matching the provided email + authorizeRequestWithEmail := func(e *core.RequestEvent, email string) (err error) { + if e.Auth != nil || email == "" { + return e.Next() + } + isAuthRefresh := e.Request.URL.Path == "/api/collections/users/auth-refresh" && e.Request.Method == http.MethodPost + e.Auth, err = e.App.FindFirstRecordByData("users", "email", email) + if err != nil || !isAuthRefresh { + return e.Next() + } + // auth refresh endpoint, make sure token is set in header + token, _ := e.Auth.NewAuthToken() + e.Request.Header.Set("Authorization", token) + return e.Next() + } + // authenticate with trusted header + if autoLogin, _ := GetEnv("AUTO_LOGIN"); autoLogin != "" { + se.Router.BindFunc(func(e *core.RequestEvent) error { + return authorizeRequestWithEmail(e, autoLogin) + }) + } // authenticate with trusted header if trustedHeader, _ := GetEnv("TRUSTED_AUTH_HEADER"); trustedHeader != "" { se.Router.BindFunc(func(e *core.RequestEvent) error { - if e.Auth != nil { - return e.Next() - } - trustedEmail := e.Request.Header.Get(trustedHeader) - if trustedEmail == "" { - return e.Next() - } - isAuthRefresh := e.Request.URL.Path == "/api/collections/users/auth-refresh" && e.Request.Method == http.MethodPost - if !isAuthRefresh { - authRecord, err := e.App.FindAuthRecordByEmail("users", trustedEmail) - if err == nil { - e.Auth = authRecord - } - return e.Next() - } - // if auth refresh endpoint, find user record directly and generate token - user, err := e.App.FindFirstRecordByData("users", "email", trustedEmail) - if err != nil { - return e.Next() - } - e.Auth = user - // need to set the authorization header for the client sdk to pick up the token - if token, err := user.NewAuthToken(); err == nil { - e.Request.Header.Set("Authorization", token) - } - return e.Next() + return authorizeRequestWithEmail(e, e.Request.Header.Get(trustedHeader)) }) } } diff --git a/internal/hub/hub_test.go b/internal/hub/hub_test.go index 47b5300..2b8762e 100644 --- a/internal/hub/hub_test.go +++ b/internal/hub/hub_test.go @@ -712,6 +712,60 @@ func TestCreateUserEndpointAvailability(t *testing.T) { }) } +func TestAutoLoginMiddleware(t *testing.T) { + var hubs []*beszelTests.TestHub + + defer func() { + defer os.Unsetenv("AUTO_LOGIN") + for _, hub := range hubs { + hub.Cleanup() + } + }() + + os.Setenv("AUTO_LOGIN", "user@test.com") + + testAppFactory := func(t testing.TB) *pbTests.TestApp { + hub, _ := beszelTests.NewTestHub(t.TempDir()) + hubs = append(hubs, hub) + hub.StartHub() + return hub.TestApp + } + + scenarios := []beszelTests.ApiScenario{ + { + Name: "GET /getkey - without auto login should fail", + Method: http.MethodGet, + URL: "/api/beszel/getkey", + ExpectedStatus: 401, + ExpectedContent: []string{"requires valid"}, + TestAppFactory: testAppFactory, + }, + { + Name: "GET /getkey - with auto login should fail if no matching user", + Method: http.MethodGet, + URL: "/api/beszel/getkey", + ExpectedStatus: 401, + ExpectedContent: []string{"requires valid"}, + TestAppFactory: testAppFactory, + }, + { + Name: "GET /getkey - with auto login should succeed", + Method: http.MethodGet, + URL: "/api/beszel/getkey", + ExpectedStatus: 200, + ExpectedContent: []string{"\"key\":", "\"v\":"}, + TestAppFactory: testAppFactory, + BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) { + beszelTests.CreateUser(app, "user@test.com", "password123") + }, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } +} + func TestTrustedHeaderMiddleware(t *testing.T) { var hubs []*beszelTests.TestHub diff --git a/supplemental/CHANGELOG.md b/supplemental/CHANGELOG.md index f719a84..323f856 100644 --- a/supplemental/CHANGELOG.md +++ b/supplemental/CHANGELOG.md @@ -1,5 +1,9 @@ ## 0.12.8 +- Add `TRUSTED_AUTH_HEADER` environment variable for authentication forwarding. (#399) + +- Add `AUTO_LOGIN` environment variable for automatic login. (#399) + - Add FreeBSD support for agent install script and update command. ## 0.12.7 From bcdb4c92b55b50a772e68cb1d76538becdf70f9d Mon Sep 17 00:00:00 2001 From: henrygd Date: Thu, 11 Sep 2025 15:07:37 -0400 Subject: [PATCH 3/6] add freebsd to list of copyable commands --- internal/site/biome.json | 7 ++++-- internal/site/src/components/add-system.tsx | 8 ++++++- .../routes/settings/tokens-fingerprints.tsx | 22 ++++++++++++++----- supplemental/CHANGELOG.md | 4 ++++ 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/internal/site/biome.json b/internal/site/biome.json index 330e804..14bd3e8 100644 --- a/internal/site/biome.json +++ b/internal/site/biome.json @@ -17,7 +17,10 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "correctness": { + "useUniqueElementIds": "off" + } } }, "javascript": { @@ -35,4 +38,4 @@ } } } -} \ No newline at end of file +} diff --git a/internal/site/src/components/add-system.tsx b/internal/site/src/components/add-system.tsx index 9613830..9f1e3dc 100644 --- a/internal/site/src/components/add-system.tsx +++ b/internal/site/src/components/add-system.tsx @@ -22,7 +22,7 @@ import { memo, useEffect, useRef, useState } from "react" import { $router, basePath, Link, navigate } from "./router" import { SystemRecord } from "@/types" import { SystemStatus } from "@/lib/enums" -import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "./ui/icons" +import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "./ui/icons" import { InputCopy } from "./ui/input-copy" import { getPagePath } from "@nanostores/router" import { @@ -253,6 +253,12 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) => copyWindowsCommand(isUnixSocket ? hostValue : port.current?.value, publicKey, token), icons: [WindowsIcon], }, + { + text: t({ message: "FreeBSD command", context: "Button to copy install command" }), + onClick: async () => + copyLinuxCommand(isUnixSocket ? hostValue : port.current?.value, publicKey, token), + icons: [FreeBsdIcon], + }, { text: t`Manual setup instructions`, url: "https://beszel.dev/guide/agent-installation#binary", diff --git a/internal/site/src/components/routes/settings/tokens-fingerprints.tsx b/internal/site/src/components/routes/settings/tokens-fingerprints.tsx index 502525b..7a98894 100644 --- a/internal/site/src/components/routes/settings/tokens-fingerprints.tsx +++ b/internal/site/src/components/routes/settings/tokens-fingerprints.tsx @@ -9,6 +9,7 @@ import { RotateCwIcon, ServerIcon, Trash2Icon, + ExternalLinkIcon, } from "lucide-react" import { memo, useEffect, useMemo, useState } from "react" import { @@ -28,7 +29,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" -import { AppleIcon, DockerIcon, TuxIcon, WindowsIcon } from "@/components/ui/icons" +import { AppleIcon, DockerIcon, FreeBsdIcon, TuxIcon, WindowsIcon } from "@/components/ui/icons" import { Separator } from "@/components/ui/separator" import { Switch } from "@/components/ui/switch" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" @@ -150,6 +151,7 @@ const SectionUniversalToken = memo(() => { setIsLoading(false) } + // biome-ignore lint/correctness/useExhaustiveDependencies: only on mount useEffect(() => { updateToken() }, []) @@ -221,6 +223,16 @@ const ActionsButtonUniversalToken = memo(({ token, checked }: { token: string; c onClick: () => copyWindowsCommand(port, publicKey, token), icons: [WindowsIcon], }, + { + text: t({ message: "FreeBSD command", context: "Button to copy install command" }), + onClick: () => copyLinuxCommand(port, publicKey, token), + icons: [FreeBsdIcon], + }, + { + text: t`Manual setup instructions`, + url: "https://beszel.dev/guide/agent-installation#binary", + icons: [ExternalLinkIcon], + }, ] return (
@@ -291,8 +303,8 @@ const SectionTable = memo(({ fingerprints = [] }: { fingerprints: FingerprintRec - {fingerprints.map((fingerprint, i) => ( - + {fingerprints.map((fingerprint) => ( + {fingerprint.expand.system.name} @@ -317,10 +329,10 @@ async function updateFingerprint(fingerprint: FingerprintRecord, rotateToken = f fingerprint: "", token: rotateToken ? generateToken() : fingerprint.token, }) - } catch (error: any) { + } catch (error: unknown) { toast({ title: t`Error`, - description: error.message, + description: (error as Error).message, }) } } diff --git a/supplemental/CHANGELOG.md b/supplemental/CHANGELOG.md index 323f856..1fe5635 100644 --- a/supplemental/CHANGELOG.md +++ b/supplemental/CHANGELOG.md @@ -1,5 +1,9 @@ ## 0.12.8 +- Add setting for time format (12h / 24h). (#424) + +- Add experimental one-time password (OTP) support. + - Add `TRUSTED_AUTH_HEADER` environment variable for authentication forwarding. (#399) - Add `AUTO_LOGIN` environment variable for automatic login. (#399) From efa37b2312d34d2615fd2bc30429046b6f3c015e Mon Sep 17 00:00:00 2001 From: henrygd Date: Thu, 11 Sep 2025 15:37:11 -0400 Subject: [PATCH 4/6] web: extra check for valid system before adding (#1063) --- internal/site/src/lib/systemsManager.ts | 37 +++++++++++++++++++------ 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/internal/site/src/lib/systemsManager.ts b/internal/site/src/lib/systemsManager.ts index 39afb5d..7ff78af 100644 --- a/internal/site/src/lib/systemsManager.ts +++ b/internal/site/src/lib/systemsManager.ts @@ -1,3 +1,4 @@ +/** biome-ignore-all lint/suspicious/noAssignInExpressions: it's fine :) */ import type { PreinitializedMapStore } from "nanostores" import { pb, verifyAuth } from "@/lib/api" import { @@ -16,9 +17,10 @@ const COLLECTION = pb.collection("systems") const FIELDS_DEFAULT = "id,name,host,port,info,status" /** Maximum system name length for display purposes */ -const MAX_SYSTEM_NAME_LENGTH = 20 +const MAX_SYSTEM_NAME_LENGTH = 22 let initialized = false +// biome-ignore lint/suspicious/noConfusingVoidType: typescript rocks let unsub: (() => void) | undefined | void /** Initialize the systems manager and set up listeners */ @@ -104,20 +106,37 @@ async function fetchSystems(): Promise { } } +/** Makes sure the system has valid info object and throws if not */ +function validateSystemInfo(system: SystemRecord) { + if (!("cpu" in system.info)) { + throw new Error(`${system.name} has no CPU info`) + } +} + /** Add system to both name and ID stores */ export function add(system: SystemRecord) { - $allSystemsByName.setKey(system.name, system) - $allSystemsById.setKey(system.id, system) + try { + validateSystemInfo(system) + $allSystemsByName.setKey(system.name, system) + $allSystemsById.setKey(system.id, system) + } catch (error) { + console.error(error) + } } /** Update system in stores */ export function update(system: SystemRecord) { - // if name changed, make sure old name is removed from the name store - const oldName = $allSystemsById.get()[system.id]?.name - if (oldName !== system.name) { - $allSystemsByName.setKey(oldName, undefined as any) + try { + validateSystemInfo(system) + // if name changed, make sure old name is removed from the name store + const oldName = $allSystemsById.get()[system.id]?.name + if (oldName !== system.name) { + $allSystemsByName.setKey(oldName, undefined as unknown as SystemRecord) + } + add(system) + } catch (error) { + console.error(error) } - add(system) } /** Remove system from stores */ @@ -132,7 +151,7 @@ export function remove(system: SystemRecord) { /** Remove system from specific store */ function removeFromStore(system: SystemRecord, store: PreinitializedMapStore>) { const key = store === $allSystemsByName ? system.name : system.id - store.setKey(key, undefined as any) + store.setKey(key, undefined as unknown as SystemRecord) } /** Action functions for subscription */ From 8da1ded73ea6dd342cc2924f135b5e0c81283f88 Mon Sep 17 00:00:00 2001 From: henrygd Date: Fri, 12 Sep 2025 12:59:53 -0400 Subject: [PATCH 5/6] strip whitespace from `TOKEN_FILE` (#984) --- agent/client.go | 2 +- agent/client_test.go | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/agent/client.go b/agent/client.go index 19933c1..87449cf 100644 --- a/agent/client.go +++ b/agent/client.go @@ -85,7 +85,7 @@ func getToken() (string, error) { if err != nil { return "", err } - return string(tokenBytes), nil + return strings.TrimSpace(string(tokenBytes)), nil } // getOptions returns the WebSocket client options, creating them if necessary. diff --git a/agent/client_test.go b/agent/client_test.go index 2edc203..5741884 100644 --- a/agent/client_test.go +++ b/agent/client_test.go @@ -537,4 +537,25 @@ func TestGetToken(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "", token, "Empty file should return empty string") }) + + t.Run("strips whitespace from TOKEN_FILE", func(t *testing.T) { + unsetEnvVars() + + tokenWithWhitespace := " test-token-with-whitespace \n\t" + expectedToken := "test-token-with-whitespace" + tokenFile, err := os.CreateTemp("", "token-test-*.txt") + require.NoError(t, err) + defer os.Remove(tokenFile.Name()) + + _, err = tokenFile.WriteString(tokenWithWhitespace) + require.NoError(t, err) + tokenFile.Close() + + os.Setenv("TOKEN_FILE", tokenFile.Name()) + defer os.Unsetenv("TOKEN_FILE") + + token, err := getToken() + assert.NoError(t, err) + assert.Equal(t, expectedToken, token, "Whitespace should be stripped from token file content") + }) } From e149366451ccf374582b1f68e7bc4f95f3658767 Mon Sep 17 00:00:00 2001 From: Ryan W <60849886+twentybit@users.noreply.github.com> Date: Sat, 13 Sep 2025 17:09:36 +0100 Subject: [PATCH 6/6] Fixing service name in helm chart and making default values unopinionated (#1166) --- .../beszel-hub/charts/templates/service.yaml | 2 +- .../kubernetes/beszel-hub/charts/values.yaml | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/supplemental/kubernetes/beszel-hub/charts/templates/service.yaml b/supplemental/kubernetes/beszel-hub/charts/templates/service.yaml index 0f1c962..bfc0207 100644 --- a/supplemental/kubernetes/beszel-hub/charts/templates/service.yaml +++ b/supplemental/kubernetes/beszel-hub/charts/templates/service.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "beszel.fullname" . }}-web + name: {{ include "beszel.fullname" . }} labels: {{- include "beszel.labels" . | nindent 4 }} {{- if .Values.service.annotations }} diff --git a/supplemental/kubernetes/beszel-hub/charts/values.yaml b/supplemental/kubernetes/beszel-hub/charts/values.yaml index dcacc46..dabae71 100644 --- a/supplemental/kubernetes/beszel-hub/charts/values.yaml +++ b/supplemental/kubernetes/beszel-hub/charts/values.yaml @@ -30,14 +30,10 @@ securityContext: {} service: enabled: true - type: LoadBalancer - loadBalancerIP: "10.0.10.251" + annotations: {} + type: ClusterIP + loadBalancerIP: "" port: 8090 -# -- Annotations for the DHCP service - annotations: - metallb.universe.tf/address-pool: pool - metallb.universe.tf/allow-shared-ip: beszel-hub-web - # -- Labels for the DHCP service ingress: enabled: false @@ -96,7 +92,7 @@ persistentVolumeClaim: accessModes: - ReadWriteOnce - storageClass: "retain-local-path" + storageClass: "" # -- volume claim size size: "500Mi"