From 90ee054a7d7b4137d0cfdfd4ef36f0de970c449a Mon Sep 17 00:00:00 2001 From: Alexander Stevenson Date: Thu, 12 Feb 2026 13:10:41 +0100 Subject: [PATCH 1/3] Tests use tempdir, so doesn't interfere with existing config --- cmd/server/main_test.go | 6 +++--- config/config_test.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/server/main_test.go b/cmd/server/main_test.go index d204e54..4e5d425 100644 --- a/cmd/server/main_test.go +++ b/cmd/server/main_test.go @@ -431,7 +431,7 @@ func TestConfigurationValidation(t *testing.T) { }() // Test configuration loading - _, err := config.LoadConfig("") + _, err := config.LoadConfig(t.TempDir()) if tt.shouldError { if err == nil { t.Errorf("Expected error but got none") @@ -548,7 +548,7 @@ func TestTLSConfiguration(t *testing.T) { }() // Load configuration - cfg, err := config.LoadConfig("") + cfg, err := config.LoadConfig(t.TempDir()) if err != nil { t.Fatalf("Failed to load config: %v", err) } @@ -662,7 +662,7 @@ func TestMainFunctionIntegration(t *testing.T) { }() // Test that configuration loads successfully - cfg, err := config.LoadConfig("") + cfg, err := config.LoadConfig(t.TempDir()) if err != nil { t.Fatalf("Configuration should load successfully: %v", err) } diff --git a/config/config_test.go b/config/config_test.go index 556804a..0b41697 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -21,7 +21,7 @@ func TestLoadConfig(t *testing.T) { os.Unsetenv("MARCHAT_USERS") }() - cfg, err := LoadConfig("") + cfg, err := LoadConfig(t.TempDir()) if err != nil { t.Fatalf("LoadConfig failed: %v", err) } @@ -47,7 +47,7 @@ func TestLoadConfig(t *testing.T) { os.Unsetenv("MARCHAT_ADMIN_KEY") os.Unsetenv("MARCHAT_USERS") - _, err := LoadConfig("") + _, err := LoadConfig(t.TempDir()) if err == nil { t.Error("Expected error when required environment variables are missing") } @@ -63,7 +63,7 @@ func TestLoadConfig(t *testing.T) { os.Unsetenv("MARCHAT_USERS") }() - _, err := LoadConfig("") + _, err := LoadConfig(t.TempDir()) if err == nil { t.Error("Expected error for invalid port") } From 98f84e9b6d12afe421e46627ad26fc277c25c348 Mon Sep 17 00:00:00 2001 From: Alexander Stevenson Date: Thu, 12 Feb 2026 13:27:48 +0100 Subject: [PATCH 2/3] Generate a JWTSecret if not provided --- config/config.go | 17 ++++++++++++++++- config/config_test.go | 33 +++++++++++++++++++++++++++++++++ env.example | 9 ++++----- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/config/config.go b/config/config.go index e19e809..396e86b 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,8 @@ package config import ( + "crypto/rand" + "encoding/hex" "fmt" "os" "path/filepath" @@ -138,7 +140,11 @@ func (c *Config) loadFromEnv() error { if jwtSecret := os.Getenv("MARCHAT_JWT_SECRET"); jwtSecret != "" { c.JWTSecret = jwtSecret } else { - c.JWTSecret = "marchat-default-secret-change-in-production" + jwtSecret, err := generateJWTSecret() + if err != nil { + return fmt.Errorf("when generating JWT secret: %w", err) + } + c.JWTSecret = jwtSecret } // TLS configuration @@ -302,3 +308,12 @@ func (c *Config) GetWebSocketScheme() string { } return "ws" } + +func generateJWTSecret() (string, error) { + jwtSecret := make([]byte, 32) + _, err := rand.Read(jwtSecret) + if err != nil { + return "", fmt.Errorf("when reading random bytes: %w", err) + } + return hex.EncodeToString(jwtSecret), nil +} diff --git a/config/config_test.go b/config/config_test.go index 0b41697..2c92ff0 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "encoding/hex" "os" "path/filepath" "reflect" @@ -347,3 +348,35 @@ func TestGetEnvIntWithDefault(t *testing.T) { t.Errorf("Expected 789 (default), got %d", result) } } + +// TestJWTSecretGeneratedRandomly checks that JWTSecret is a 64 char long generated hex when not provided +func TestJWTSecretGeneratedRandomly(t *testing.T) { + os.Setenv("MARCHAT_PORT", "8080") + os.Setenv("MARCHAT_ADMIN_KEY", "test-key") + os.Setenv("MARCHAT_USERS", "user1,user2") + os.Unsetenv("MARCHAT_JWT_SECRET") + defer func() { + os.Unsetenv("MARCHAT_PORT") + os.Unsetenv("MARCHAT_ADMIN_KEY") + os.Unsetenv("MARCHAT_USERS") + }() + + cfg, err := LoadConfig(t.TempDir()) + if err != nil { + t.Fatalf("LoadConfig failed: %v", err) + } + + if len(cfg.JWTSecret) != 64 { + t.Fatalf("invalid JWTSecret length: %s (%d)", cfg.JWTSecret, len(cfg.JWTSecret)) + } + + dest := make([]byte, 32) + n, err := hex.Decode(dest, []byte(cfg.JWTSecret)) + if err != nil { + t.Fatalf("JWT Secret is not made of hex: %s", err.Error()) + } + if n != 32 { + t.Errorf("unexpected number of bytes decoded: %d", n) + } + +} diff --git a/env.example b/env.example index 14d2fcc..a4bd408 100644 --- a/env.example +++ b/env.example @@ -1,7 +1,7 @@ # ============================================================================= # marchat Server Environment Configuration # ============================================================================= -# +# # Copy this file to .env and customize the values for your deployment: # cp env.example .env # @@ -52,10 +52,9 @@ MARCHAT_LOG_LEVEL=info # JWT Configuration (Future Use) # ============================================================================= -# JWT secret for authentication (auto-generated if not set) +# JWT secret for authentication (auto-generated randomly if not set) # This will be used for enhanced authentication features in future releases -# IMPORTANT: Change this to a secure value in production! -MARCHAT_JWT_SECRET=your-jwt-secret-change-in-production +MARCHAT_JWT_SECRET=your-jwt-secret # ============================================================================= # Advanced Configuration (Optional) @@ -91,4 +90,4 @@ MARCHAT_JWT_SECRET=your-jwt-secret-change-in-production # - Setting MARCHAT_LOG_LEVEL=warn or error # - Using absolute paths for MARCHAT_DB_PATH # - Changing all default secrets to secure values -# ============================================================================= \ No newline at end of file +# ============================================================================= From 368c8a08065653fc3782f5f167afc5484898af1e Mon Sep 17 00:00:00 2001 From: Alexander Stevenson Date: Sat, 14 Feb 2026 11:32:15 +0100 Subject: [PATCH 3/3] Generating and storing the JWT secret in the config UI --- config/config.go | 4 ++-- server/config_ui.go | 9 +++++++++ server/config_ui_test.go | 11 +++++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index 396e86b..7e51b31 100644 --- a/config/config.go +++ b/config/config.go @@ -140,7 +140,7 @@ func (c *Config) loadFromEnv() error { if jwtSecret := os.Getenv("MARCHAT_JWT_SECRET"); jwtSecret != "" { c.JWTSecret = jwtSecret } else { - jwtSecret, err := generateJWTSecret() + jwtSecret, err := GenerateJWTSecret() if err != nil { return fmt.Errorf("when generating JWT secret: %w", err) } @@ -309,7 +309,7 @@ func (c *Config) GetWebSocketScheme() string { return "ws" } -func generateJWTSecret() (string, error) { +func GenerateJWTSecret() (string, error) { jwtSecret := make([]byte, 32) _, err := rand.Read(jwtSecret) if err != nil { diff --git a/server/config_ui.go b/server/config_ui.go index 5728c50..37e64cd 100644 --- a/server/config_ui.go +++ b/server/config_ui.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/Cod-e-Codes/marchat/config" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -49,6 +50,7 @@ type ServerConfig struct { AdminKey string AdminUsers string Port string + JWTSecret string } func NewServerConfigUI() ServerConfigModel { @@ -138,6 +140,7 @@ func (m *ServerConfigModel) saveConfigToEnv() error { envContent.WriteString(fmt.Sprintf("MARCHAT_PORT=%s\n", m.config.Port)) envContent.WriteString(fmt.Sprintf("MARCHAT_ADMIN_KEY=%s\n", m.config.AdminKey)) envContent.WriteString(fmt.Sprintf("MARCHAT_USERS=%s\n", m.config.AdminUsers)) + envContent.WriteString(fmt.Sprintf("MARCHAT_JWT_SECRET=%s\n", m.config.JWTSecret)) // Write to file if err := os.WriteFile(envPath, []byte(envContent.String()), 0600); err != nil { @@ -288,11 +291,17 @@ func (m *ServerConfigModel) validateAndBuildConfig() error { return fmt.Errorf("port must be a number between 1 and 65535") } + jwtSecret, err := config.GenerateJWTSecret() + if err != nil { + return fmt.Errorf("failed at generating JWT secret: %w", err) + } + // Build config m.config = &ServerConfig{ AdminKey: adminKey, AdminUsers: adminUsers, Port: port, + JWTSecret: jwtSecret, } // Save configuration to .env file diff --git a/server/config_ui_test.go b/server/config_ui_test.go index 3238999..da97534 100644 --- a/server/config_ui_test.go +++ b/server/config_ui_test.go @@ -31,7 +31,14 @@ func TestServerConfigUISavesEnv(t *testing.T) { // Basic content checks b, _ := os.ReadFile(envPath) content := string(b) - if !strings.Contains(content, "MARCHAT_PORT=8123") || !strings.Contains(content, "MARCHAT_ADMIN_KEY=adminkey123") || !strings.Contains(content, "MARCHAT_USERS=alice,bob") { - t.Fatalf("unexpected .env content: %s", content) + for _, needle := range []string{ + "MARCHAT_PORT=8123", + "MARCHAT_ADMIN_KEY=adminkey123", + "MARCHAT_USERS=alice,bob", + "MARCHAT_JWT_SECRET=", // The value is random, so we only check for the presence of the env var + } { + if !strings.Contains(content, needle) { + t.Fatalf("missing .env content: %s", needle) + } } }