From 73fb8d49b0b89c6b80569e7277f2b5f25a0c425d Mon Sep 17 00:00:00 2001 From: Fimeg Date: Wed, 29 Oct 2025 13:16:17 -0400 Subject: [PATCH] Implement web-based welcome mode configuration --- aggregator-server/cmd/server/main.go | 52 +-- .../internal/api/handlers/setup.go | 313 ++++++++++++++++++ aggregator-server/internal/config/config.go | 22 +- docker-compose.yml | 1 + 4 files changed, 337 insertions(+), 51 deletions(-) create mode 100644 aggregator-server/internal/api/handlers/setup.go diff --git a/aggregator-server/cmd/server/main.go b/aggregator-server/cmd/server/main.go index aa11919..8d88203 100644 --- a/aggregator-server/cmd/server/main.go +++ b/aggregator-server/cmd/server/main.go @@ -17,6 +17,7 @@ import ( ) func startWelcomeModeServer() { + setupHandler := handlers.NewSetupHandler("/app/config") router := gin.Default() // Add CORS middleware @@ -28,54 +29,10 @@ func startWelcomeModeServer() { }) // Welcome page with setup instructions - router.GET("/", func(c *gin.Context) { - html := ` - - - - RedFlag - Setup Required - - - -
-

🚀 RedFlag Server

-
-

⏳ Server is waiting for configuration

-

Choose one of the setup methods below:

-
+ router.GET("/", setupHandler.ShowSetupPage) -
-
-

Option 1: Command Line Setup

-

Run this command in your terminal:

-
docker-compose exec server ./redflag-server --setup
-

Follow the interactive prompts to configure your server.

-
- -
-

Option 2: Web Setup (Coming Soon)

-

Web-based configuration wizard will be available in a future release.

-

For now, use the command line setup above.

-
-
- -
-

After configuration, the server will automatically restart.

-

Refresh this page to see the admin interface.

-
-
- -` - c.Data(200, "text/html; charset=utf-8", []byte(html)) - }) + // Setup endpoint for web configuration + router.POST("/api/v1/setup", setupHandler.ConfigureServer) // Setup endpoint for web configuration (future) router.GET("/setup", func(c *gin.Context) { @@ -185,6 +142,7 @@ func main() { registrationTokenHandler := handlers.NewRegistrationTokenHandler(registrationTokenQueries, agentQueries, cfg) rateLimitHandler := handlers.NewRateLimitHandler(rateLimiter) downloadHandler := handlers.NewDownloadHandler(filepath.Join(".", "redflag-agent")) + setupHandler := handlers.NewSetupHandler("/app/config") // Setup router router := gin.Default() diff --git a/aggregator-server/internal/api/handlers/setup.go b/aggregator-server/internal/api/handlers/setup.go new file mode 100644 index 0000000..4e7d1a0 --- /dev/null +++ b/aggregator-server/internal/api/handlers/setup.go @@ -0,0 +1,313 @@ +package handlers + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "net/http" + "os" + "path/filepath" + "strconv" + + "github.com/Fimeg/RedFlag/aggregator-server/internal/config" + "github.com/gin-gonic/gin" +) + +// SetupHandler handles server configuration +type SetupHandler struct { + configPath string +} + +func NewSetupHandler(configPath string) *SetupHandler { + return &SetupHandler{ + configPath: configPath, + } +} + +// ShowSetupPage displays the web setup interface +func (h *SetupHandler) ShowSetupPage(c *gin.Context) { + html := ` + + + + RedFlag - Server Configuration + + + + + +
+
+
+

🚀 RedFlag Server Setup

+

Configure your update management server

+
+
+
+
+

🔐 Admin Account

+
+
+ + +
+
+ + +
+
+
+ +
+

💾 Database Configuration

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

🌐 Server Configuration

+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + +
+ + +
+
+
+
+ + + +` + c.Data(200, "text/html; charset=utf-8", []byte(html)) +} + +// ConfigureServer handles the configuration submission +func (h *SetupHandler) ConfigureServer(c *gin.Context) { + var req struct { + AdminUser string `json:"adminUser"` + AdminPass string `json:"adminPassword"` + DBHost string `json:"dbHost"` + DBPort string `json:"dbPort"` + DBName string `json:"dbName"` + DBUser string `json:"dbUser"` + DBPassword string `json:"dbPassword"` + ServerHost string `json:"serverHost"` + ServerPort string `json:"serverPort"` + MaxSeats string `json:"maxSeats"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"}) + return + } + + // Validate inputs + if req.AdminUser == "" || req.AdminPass == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Admin username and password are required"}) + return + } + + if req.DBHost == "" || req.DBPort == "" || req.DBName == "" || req.DBUser == "" || req.DBPassword == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "All database fields are required"}) + return + } + + // Parse numeric values + dbPort, err := strconv.Atoi(req.DBPort) + if err != nil || dbPort <= 0 || dbPort > 65535 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid database port"}) + return + } + + serverPort, err := strconv.Atoi(req.ServerPort) + if err != nil || serverPort <= 0 || serverPort > 65535 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid server port"}) + return + } + + maxSeats, err := strconv.Atoi(req.MaxSeats) + if err != nil || maxSeats <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid maximum agent seats"}) + return + } + + // Create configuration content + envContent := fmt.Sprintf(`# RedFlag Server Configuration +# Generated by web setup + +# Server Configuration +REDFLAG_SERVER_HOST=%s +REDFLAG_SERVER_PORT=%d +REDFLAG_TLS_ENABLED=false +# REDFLAG_TLS_CERT_FILE= +# REDFLAG_TLS_KEY_FILE= + +# Database Configuration +REDFLAG_DB_HOST=%s +REDFLAG_DB_PORT=%d +REDFLAG_DB_NAME=%s +REDFLAG_DB_USER=%s +REDFLAG_DB_PASSWORD=%s + +# Admin Configuration +REDFLAG_ADMIN_USER=%s +REDFLAG_ADMIN_PASSWORD=%s +REDFLAG_JWT_SECRET=%s + +# Agent Registration +REDFLAG_TOKEN_EXPIRY=24h +REDFLAG_MAX_TOKENS=100 +REDFLAG_MAX_SEATS=%d + +# Legacy Configuration (for backwards compatibility) +SERVER_PORT=%d +DATABASE_URL=postgres://%s:%s@%s:%d/%s?sslmode=disable +JWT_SECRET=%s +CHECK_IN_INTERVAL=300 +OFFLINE_THRESHOLD=600 +TIMEZONE=UTC +LATEST_AGENT_VERSION=0.1.16`, + req.ServerHost, serverPort, + req.DBHost, dbPort, req.DBName, req.DBUser, req.DBPassword, + req.AdminUser, req.AdminPass, deriveJWTSecret(req.AdminUser, req.AdminPass), + maxSeats, + serverPort, req.DBUser, req.DBPassword, req.DBHost, dbPort, req.DBName, deriveJWTSecret(req.AdminUser, req.AdminPass)) + + // Write configuration to persistent location + configDir := "/app/config" + if err := os.MkdirAll(configDir, 0755); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create config directory"}) + return + } + + envPath := filepath.Join(configDir, ".env") + if err := os.WriteFile(envPath, []byte(envContent), 0600); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save configuration"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Configuration saved successfully! Server will restart automatically.", + "configPath": envPath, + }) +} + +// deriveJWTSecret generates a JWT secret from admin credentials +func deriveJWTSecret(username, password string) string { + hash := sha256.Sum256([]byte(username + password + "redflag-jwt-2024")) + return hex.EncodeToString(hash[:]) +} \ No newline at end of file diff --git a/aggregator-server/internal/config/config.go b/aggregator-server/internal/config/config.go index 730c630..bcc4443 100644 --- a/aggregator-server/internal/config/config.go +++ b/aggregator-server/internal/config/config.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" "os" + "path/filepath" "strconv" "strings" "time" @@ -50,8 +51,15 @@ type Config struct { // Load reads configuration from environment variables func Load() (*Config, error) { - // Load .env file if it exists (for development) - _ = godotenv.Load() + // Load .env file from persistent config directory + configPaths := []string{"/app/config/.env", ".env"} + + for _, path := range configPaths { + if _, err := os.Stat(path); err == nil { + _ = godotenv.Load(path) + break + } + } cfg := &Config{} @@ -182,8 +190,14 @@ LATEST_AGENT_VERSION=0.1.8 username, password, jwtSecret, maxSeats, serverPort, dbUser, dbPassword, dbHost, dbPort, dbName, jwtSecret) - // Write .env file - if err := os.WriteFile(".env", []byte(envContent), 0600); err != nil { + // Write .env file to persistent location + configDir := "/app/config" + if err := os.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + envPath := filepath.Join(configDir, ".env") + if err := os.WriteFile(envPath, []byte(envContent), 0600); err != nil { return fmt.Errorf("failed to write .env file: %w", err) } diff --git a/docker-compose.yml b/docker-compose.yml index ebb78ca..03229a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,7 @@ services: container_name: redflag-server volumes: - ./aggregator-agent/redflag-agent:/app/redflag-agent:ro + - ./aggregator-server/config:/app/config - server-data:/app/data depends_on: postgres: