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
+
+
+
+
+
+
+
+
+
+`
+ 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: