diff --git a/beszel/internal/hub/hub_test.go b/beszel/internal/hub/hub_test.go index 1c0d2d1..b686f69 100644 --- a/beszel/internal/hub/hub_test.go +++ b/beszel/internal/hub/hub_test.go @@ -5,6 +5,7 @@ package hub_test import ( beszelTests "beszel/internal/tests" + "beszel/migrations" "testing" "bytes" @@ -534,6 +535,115 @@ func TestApiRoutesAuthentication(t *testing.T) { } } +func TestFirstUserCreation(t *testing.T) { + t.Run("CreateUserEndpoint available when no users exist", func(t *testing.T) { + hub, _ := beszelTests.NewTestHub(t.TempDir()) + defer hub.Cleanup() + + hub.StartHub() + + testAppFactoryExisting := func(t testing.TB) *pbTests.TestApp { + return hub.TestApp + } + + scenarios := []beszelTests.ApiScenario{ + { + Name: "POST /create-user - should be available when no users exist", + Method: http.MethodPost, + URL: "/api/beszel/create-user", + Body: jsonReader(map[string]any{ + "email": "firstuser@example.com", + "password": "password123", + }), + ExpectedStatus: 200, + ExpectedContent: []string{"User created"}, + TestAppFactory: testAppFactoryExisting, + BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) { + userCount, err := hub.CountRecords("users") + require.NoError(t, err) + require.Zero(t, userCount, "Should start with no users") + superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers) + require.NoError(t, err) + require.EqualValues(t, 1, len(superusers), "Should start with one temporary superuser") + require.EqualValues(t, migrations.TempAdminEmail, superusers[0].GetString("email"), "Should have created one temporary superuser") + }, + AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) { + userCount, err := hub.CountRecords("users") + require.NoError(t, err) + require.EqualValues(t, 1, userCount, "Should have created one user") + superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers) + require.NoError(t, err) + require.EqualValues(t, 1, len(superusers), "Should have created one superuser") + require.EqualValues(t, "firstuser@example.com", superusers[0].GetString("email"), "Should have created one superuser") + }, + }, + { + Name: "POST /create-user - should not be available when users exist", + Method: http.MethodPost, + URL: "/api/beszel/create-user", + Body: jsonReader(map[string]any{ + "email": "firstuser@example.com", + "password": "password123", + }), + ExpectedStatus: 404, + ExpectedContent: []string{"wasn't found"}, + TestAppFactory: testAppFactoryExisting, + }, + } + + for _, scenario := range scenarios { + scenario.Test(t) + } + }) + + t.Run("CreateUserEndpoint not available when USER_EMAIL, USER_PASSWORD are set", func(t *testing.T) { + os.Setenv("BESZEL_HUB_USER_EMAIL", "me@example.com") + os.Setenv("BESZEL_HUB_USER_PASSWORD", "password123") + defer os.Unsetenv("BESZEL_HUB_USER_EMAIL") + defer os.Unsetenv("BESZEL_HUB_USER_PASSWORD") + + hub, _ := beszelTests.NewTestHub(t.TempDir()) + defer hub.Cleanup() + + hub.StartHub() + + testAppFactory := func(t testing.TB) *pbTests.TestApp { + return hub.TestApp + } + + scenario := beszelTests.ApiScenario{ + Name: "POST /create-user - should not be available when USER_EMAIL, USER_PASSWORD are set", + Method: http.MethodPost, + URL: "/api/beszel/create-user", + ExpectedStatus: 404, + ExpectedContent: []string{"wasn't found"}, + TestAppFactory: testAppFactory, + BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) { + users, err := hub.FindAllRecords("users") + require.NoError(t, err) + require.EqualValues(t, 1, len(users), "Should start with one user") + require.EqualValues(t, "me@example.com", users[0].GetString("email"), "Should have created one user") + superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers) + require.NoError(t, err) + require.EqualValues(t, 1, len(superusers), "Should start with one superuser") + require.EqualValues(t, "me@example.com", superusers[0].GetString("email"), "Should have created one superuser") + }, + AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) { + users, err := hub.FindAllRecords("users") + require.NoError(t, err) + require.EqualValues(t, 1, len(users), "Should still have one user") + require.EqualValues(t, "me@example.com", users[0].GetString("email"), "Should have created one user") + superusers, err := hub.FindAllRecords(core.CollectionNameSuperusers) + require.NoError(t, err) + require.EqualValues(t, 1, len(superusers), "Should still have one superuser") + require.EqualValues(t, "me@example.com", superusers[0].GetString("email"), "Should have created one superuser") + }, + } + + scenario.Test(t) + }) +} + func TestCreateUserEndpointAvailability(t *testing.T) { t.Run("CreateUserEndpoint available when no users exist", func(t *testing.T) { hub, _ := beszelTests.NewTestHub(t.TempDir()) diff --git a/beszel/migrations/initial-settings.go b/beszel/migrations/initial-settings.go index 8dd1269..98633f2 100644 --- a/beszel/migrations/initial-settings.go +++ b/beszel/migrations/initial-settings.go @@ -1,6 +1,8 @@ package migrations import ( + "os" + "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" ) @@ -19,11 +21,50 @@ func init() { if err := app.Save(settings); err != nil { return err } + // create superuser - collection, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) - user := core.NewRecord(collection) - user.SetEmail(TempAdminEmail) - user.SetRandomPassword() - return app.Save(user) + superuserCollection, _ := app.FindCollectionByNameOrId(core.CollectionNameSuperusers) + superUser := core.NewRecord(superuserCollection) + + // set email + email, _ := GetEnv("USER_EMAIL") + password, _ := GetEnv("USER_PASSWORD") + didProvideUserDetails := email != "" && password != "" + + // set superuser email + if email == "" { + email = TempAdminEmail + } + superUser.SetEmail(email) + + // set superuser password + if password != "" { + superUser.SetPassword(password) + } else { + superUser.SetRandomPassword() + } + + // if user details are provided, we create a regular user as well + if didProvideUserDetails { + usersCollection, _ := app.FindCollectionByNameOrId("users") + user := core.NewRecord(usersCollection) + user.SetEmail(email) + user.SetPassword(password) + err := app.Save(user) + if err != nil { + return err + } + } + + return app.Save(superUser) }, nil) } + +// GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key. +func GetEnv(key string) (value string, exists bool) { + if value, exists = os.LookupEnv("BESZEL_HUB_" + key); exists { + return value, exists + } + // Fallback to the old unprefixed key + return os.LookupEnv(key) +} diff --git a/supplemental/CHANGELOG.md b/supplemental/CHANGELOG.md index 02b9cfd..7246d8f 100644 --- a/supplemental/CHANGELOG.md +++ b/supplemental/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.12.7 + +- Make LibreHardwareMonitor opt-in with `LHM=true` environment variable. (#1130) + +- Fix bug where token was not refreshed when adding a new system. (#1141) + +- Add `USER_EMAIL` and `USER_PASSWORD` environment variables to set the email and password of the initial user. (#1137) + ## 0.12.6 - Add maximum 1 minute memory usage.