Compare commits

..

6 Commits

45 changed files with 1915 additions and 26414 deletions

View File

@@ -20,9 +20,8 @@ func HasReadableBattery() bool {
}
haveCheckedBattery = true
bat, err := battery.Get(0)
if err == nil && bat != nil {
systemHasBattery = true
} else {
systemHasBattery = err == nil && bat != nil && bat.Design != 0 && bat.Full != 0
if !systemHasBattery {
slog.Debug("No battery found", "err", err)
}
return systemHasBattery

View File

@@ -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.

View File

@@ -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")
})
}

View File

@@ -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))
})
}
}

View File

@@ -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

View File

@@ -17,7 +17,10 @@
"linter": {
"enabled": true,
"rules": {
"recommended": true
"recommended": true,
"correctness": {
"useUniqueElementIds": "off"
}
}
},
"javascript": {
@@ -35,4 +38,4 @@
}
}
}
}
}

View File

@@ -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",

View File

@@ -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 (
<div className="flex items-center gap-2">
@@ -291,8 +303,8 @@ const SectionTable = memo(({ fingerprints = [] }: { fingerprints: FingerprintRec
</tr>
</TableHeader>
<TableBody className="whitespace-pre">
{fingerprints.map((fingerprint, i) => (
<TableRow key={i}>
{fingerprints.map((fingerprint) => (
<TableRow key={fingerprint.id}>
<TableCell className="font-medium ps-5 py-2 max-w-60 truncate">
{fingerprint.expand.system.name}
</TableCell>
@@ -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,
})
}
}

View File

@@ -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<SystemRecord>("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<SystemRecord[]> {
}
}
/** 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<Record<string, SystemRecord>>) {
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 */

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,15 +8,15 @@ msgstr ""
"Language: is\n"
"Project-Id-Version: beszel\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-09-22 23:10\n"
"PO-Revision-Date: 2025-08-28 23:21\n"
"Last-Translator: \n"
"Language-Team: Icelandic\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: beszel\n"
"X-Crowdin-Project-ID: 733311\n"
"X-Crowdin-Language: is\n"
"X-Crowdin-File: /main/internal/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 32\n"
"X-Crowdin-File: /main/beszel/site/src/locales/en/en.po\n"
"X-Crowdin-File-ID: 16\n"
#. placeholder {0}: Math.trunc(system.info?.u / 86400)
#: src/components/routes/system.tsx
@@ -112,7 +112,7 @@ msgstr ""
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
msgid "Admin"
msgstr ""
msgstr "Admin"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Agent"
@@ -173,10 +173,6 @@ msgstr "Meðal nýting örgjörva yfir allt kerfið"
msgid "Average utilization of {0}"
msgstr "Meðal notkun af {0}"
#: src/components/routes/system.tsx
msgid "Average utilization of GPU engines"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/navbar.tsx
msgid "Backups"
@@ -201,7 +197,7 @@ msgstr "Beszel notar <0>Shoutrrr</0> til að tengjast vinsælum tilkynningaþjó
#: src/components/add-system.tsx
msgid "Binary"
msgstr ""
msgstr "Binary"
#: src/components/routes/settings/general.tsx
#: src/components/routes/settings/general.tsx
@@ -367,14 +363,6 @@ msgstr ""
msgid "Critical (%)"
msgstr "Kritískt (%)"
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Download"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Cumulative Upload"
msgstr ""
#. Context: Battery state
#: src/components/routes/system.tsx
msgid "Current state"
@@ -453,10 +441,6 @@ msgstr ""
msgid "Down ({downSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Download"
msgstr ""
#: src/components/alerts-history-columns.tsx
msgid "Duration"
msgstr ""
@@ -468,7 +452,6 @@ msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/otp-forms.tsx
msgid "Email"
msgstr "Netfang"
@@ -489,10 +472,6 @@ msgstr "Settu netfang til að endursetja lykilorð"
msgid "Enter email address..."
msgstr "Settu inn Netfang..."
#: src/components/login/otp-forms.tsx
msgid "Enter your one-time password."
msgstr ""
#: src/components/login/auth-form.tsx
#: src/components/routes/settings/alerts-history-data-table.tsx
#: src/components/routes/settings/config-yaml.tsx
@@ -563,16 +542,10 @@ msgstr ""
msgid "Forgot password?"
msgstr "Gleymt lykilorð?"
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgctxt "Button to copy install command"
msgid "FreeBSD command"
msgstr ""
#. Context: Battery state
#: src/lib/i18n.ts
msgid "Full"
msgstr ""
msgstr "Full"
#. Context: General settings
#: src/components/routes/settings/general.tsx
@@ -580,10 +553,6 @@ msgstr ""
msgid "General"
msgstr "Almennt"
#: src/components/routes/system.tsx
msgid "GPU Engines"
msgstr ""
#: src/components/routes/system.tsx
msgid "GPU Power Draw"
msgstr "Skjákorts rafmagnsnotkun"
@@ -600,7 +569,7 @@ msgstr "Homebrew skipun"
#: src/components/add-system.tsx
msgid "Host / IP"
msgstr ""
msgstr "Host / IP"
#. Context: Battery state
#: src/lib/i18n.ts
@@ -676,7 +645,6 @@ msgid "Manage display and notification preferences."
msgstr ""
#: src/components/add-system.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
msgid "Manual setup instructions"
msgstr ""
@@ -705,15 +673,13 @@ msgstr "Nafn"
#: src/components/systems-table/systems-table-columns.tsx
msgid "Net"
msgstr ""
msgstr "Net"
#: src/components/routes/system.tsx
msgid "Network traffic of docker containers"
msgstr "Net traffík docker kerfa"
#: src/components/routes/system.tsx
#: src/components/routes/system/network-sheet.tsx
#: src/components/routes/system/network-sheet.tsx
msgid "Network traffic of public interfaces"
msgstr ""
@@ -749,10 +715,6 @@ msgstr "OAuth 2 / OIDC stuðningur"
msgid "On each restart, systems in the database will be updated to match the systems defined in the file."
msgstr ""
#: src/components/login/auth-form.tsx
msgid "One-time password"
msgstr ""
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/routes/settings/tokens-fingerprints.tsx
#: src/components/systems-table/systems-table-columns.tsx
@@ -845,7 +807,7 @@ msgstr "Vinsamlegast skráðu þig inn á aðganginn þinn"
#: src/components/add-system.tsx
msgid "Port"
msgstr ""
msgstr "Port"
#: src/components/routes/system.tsx
#: src/components/routes/system.tsx
@@ -871,14 +833,6 @@ msgstr "Lesa"
msgid "Received"
msgstr "Móttekið"
#: src/components/login/login.tsx
msgid "Request a one-time password"
msgstr ""
#: src/components/login/otp-forms.tsx
msgid "Request OTP"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
msgid "Reset Password"
msgstr "Endurstilla lykilorð"
@@ -928,12 +882,16 @@ msgstr ""
#: src/components/routes/system.tsx
msgid "Sent"
msgstr ""
msgstr "Sent"
#: src/components/routes/settings/general.tsx
msgid "Set percentage thresholds for meter colors."
msgstr "Stilltu prósentuþröskuld fyrir mælaliti."
#: src/components/routes/settings/general.tsx
msgid "Sets the default time range for charts when a system is viewed."
msgstr ""
#: src/components/command-palette.tsx
#: src/components/command-palette.tsx
#: src/components/routes/settings/layout.tsx
@@ -1044,10 +1002,6 @@ msgstr ""
msgid "Throughput of root filesystem"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "Time format"
msgstr ""
#: src/components/routes/settings/notifications.tsx
msgid "To email(s)"
msgstr "Til tölvupósta"
@@ -1080,14 +1034,6 @@ msgstr ""
msgid "Tokens and fingerprints are used to authenticate WebSocket connections to the hub."
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Total data received for each interface"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Total data sent for each interface"
msgstr ""
#: src/lib/alerts.ts
msgid "Triggers when 1 minute load average exceeds a threshold"
msgstr ""
@@ -1149,10 +1095,6 @@ msgstr ""
msgid "Up ({upSystemsLength})"
msgstr ""
#: src/components/routes/system/network-sheet.tsx
msgid "Upload"
msgstr ""
#: src/components/routes/system.tsx
msgid "Uptime"
msgstr ""
@@ -1186,10 +1128,6 @@ msgstr ""
msgid "View"
msgstr "Skoða"
#: src/components/routes/system/network-sheet.tsx
msgid "View more"
msgstr ""
#: src/components/routes/settings/alerts-history-data-table.tsx
msgid "View your 200 most recent alerts."
msgstr ""
@@ -1245,4 +1183,3 @@ msgstr ""
#: src/components/routes/settings/layout.tsx
msgid "Your user settings have been updated."
msgstr "Notenda stillingar vistaðar."

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,13 @@
## 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)
- Add FreeBSD support for agent install script and update command.
## 0.12.7

View File

@@ -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 }}

View File

@@ -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"