Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cmd/server/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down
17 changes: 16 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package config

import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
39 changes: 36 additions & 3 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"encoding/hex"
"os"
"path/filepath"
"reflect"
Expand All @@ -21,7 +22,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)
}
Expand All @@ -47,7 +48,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")
}
Expand All @@ -63,7 +64,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")
}
Expand Down Expand Up @@ -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)
}

}
9 changes: 4 additions & 5 deletions env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# =============================================================================
# marchat Server Environment Configuration
# =============================================================================
#
#
# Copy this file to .env and customize the values for your deployment:
# cp env.example .env
#
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
# =============================================================================
# =============================================================================
9 changes: 9 additions & 0 deletions server/config_ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -49,6 +50,7 @@ type ServerConfig struct {
AdminKey string
AdminUsers string
Port string
JWTSecret string
}

func NewServerConfigUI() ServerConfigModel {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions server/config_ui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}