package handlers
import (
"crypto/ed25519"
"crypto/rand"
"database/sql"
"encoding/hex"
"fmt"
"net/http"
"strconv"
"github.com/Fimeg/RedFlag/aggregator-server/internal/config"
"github.com/Fimeg/RedFlag/aggregator-server/internal/services"
"github.com/gin-gonic/gin"
"github.com/lib/pq"
_ "github.com/lib/pq"
)
// SetupHandler handles server configuration
type SetupHandler struct {
configPath string
}
func NewSetupHandler(configPath string) *SetupHandler {
return &SetupHandler{
configPath: configPath,
}
}
// updatePostgresPassword updates the PostgreSQL user password
func updatePostgresPassword(dbHost, dbPort, dbUser, currentPassword, newPassword string) error {
// Connect to PostgreSQL with current credentials
connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/postgres?sslmode=disable", dbUser, currentPassword, dbHost, dbPort)
db, err := sql.Open("postgres", connStr)
if err != nil {
return fmt.Errorf("failed to connect to PostgreSQL: %v", err)
}
defer db.Close()
// Test connection
if err := db.Ping(); err != nil {
return fmt.Errorf("failed to ping PostgreSQL: %v", err)
}
// Update the password
_, err = db.Exec("ALTER USER "+pq.QuoteIdentifier(dbUser)+" PASSWORD '"+newPassword+"'")
if err != nil {
return fmt.Errorf("failed to update PostgreSQL password: %v", err)
}
fmt.Println("PostgreSQL password updated successfully")
return nil
}
// createSharedEnvContentForDisplay generates the .env file content for display
func createSharedEnvContentForDisplay(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"`
}, jwtSecret string, signingKeys map[string]string) (string, error) {
// Generate .env file content for user to copy
envContent := fmt.Sprintf(`# RedFlag Environment Configuration
# Generated by web setup on 2025-12-13
# [WARNING] SECURITY CRITICAL: Backup the signing key or you will lose access to all agents
# PostgreSQL Configuration (for PostgreSQL container)
POSTGRES_DB=%s
POSTGRES_USER=%s
POSTGRES_PASSWORD=%s
# RedFlag Security - Ed25519 Signing Keys
# These keys are used to cryptographically sign agent updates and commands
# BACKUP THE PRIVATE KEY IMMEDIATELY - Store it in a secure location like a password manager
REDFLAG_SIGNING_PRIVATE_KEY=%s
REDFLAG_SIGNING_PUBLIC_KEY=%s
# RedFlag Server Configuration
REDFLAG_SERVER_HOST=%s
REDFLAG_SERVER_PORT=%s
REDFLAG_DB_HOST=%s
REDFLAG_DB_PORT=%s
REDFLAG_DB_NAME=%s
REDFLAG_DB_USER=%s
REDFLAG_DB_PASSWORD=%s
REDFLAG_ADMIN_USER=%s
REDFLAG_ADMIN_PASSWORD=%s
REDFLAG_JWT_SECRET=%s
REDFLAG_TOKEN_EXPIRY=24h
REDFLAG_MAX_TOKENS=100
REDFLAG_MAX_SEATS=%s
# Security Settings
REDFLAG_SECURITY_COMMAND_SIGNING_ENFORCEMENT=strict
REDFLAG_SECURITY_NONCE_TIMEOUT=600
REDFLAG_SECURITY_LOG_LEVEL=warn
`,
req.DBName, req.DBUser, req.DBPassword,
signingKeys["private_key"], signingKeys["public_key"],
req.ServerHost, req.ServerPort,
req.DBHost, req.DBPort, req.DBName, req.DBUser, req.DBPassword,
req.AdminUser, req.AdminPass, jwtSecret, req.MaxSeats)
return envContent, nil
}
// ShowSetupPage displays the web setup interface
func (h *SetupHandler) ShowSetupPage(c *gin.Context) {
// Display setup page - configuration will be generated via web interface
fmt.Println("Showing setup page - configuration will be generated via web interface")
html := `
RedFlag - Server Configuration
Configuring your RedFlag server...
`
c.Data(http.StatusOK, "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
}
// Generate secure JWT secret (not derived from credentials for security)
jwtSecret, err := config.GenerateSecureToken()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate JWT secret"})
return
}
// SECURITY: Generate Ed25519 signing keypair (critical for v0.2.x)
fmt.Println("[START] Generating Ed25519 signing keypair for security...")
signingPublicKey, signingPrivateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
fmt.Printf("CRITICAL ERROR: Failed to generate signing keys: %v\n", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate signing keys. Security features cannot be enabled."})
return
}
signingKeys := map[string]string{
"public_key": hex.EncodeToString(signingPublicKey),
"private_key": hex.EncodeToString(signingPrivateKey),
}
fmt.Printf("[SUCCESS] Generated Ed25519 keypair - Fingerprint: %s\n", signingKeys["public_key"][:16])
fmt.Println("[WARNING] SECURITY WARNING: Backup the private key immediately or you will lose access to all agents!")
// Step 1: Update PostgreSQL password from bootstrap to user password
fmt.Println("Updating PostgreSQL password from bootstrap to user-provided password...")
bootstrapPassword := "redflag_bootstrap" // This matches our bootstrap .env
if err := updatePostgresPassword(req.DBHost, req.DBPort, req.DBUser, bootstrapPassword, req.DBPassword); err != nil {
fmt.Printf("CRITICAL ERROR: Failed to update PostgreSQL password: %v\n", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to update database password. Setup cannot continue.",
"details": err.Error(),
"help": "Ensure PostgreSQL is accessible and the bootstrap password is correct. Check Docker logs for details.",
})
return
}
fmt.Println("PostgreSQL password successfully updated from bootstrap to user-provided password")
// Step 2: Generate configuration content for manual update
fmt.Println("Generating configuration content for manual .env file update...")
// Generate the complete .env file content for the user to copy
newEnvContent, err := createSharedEnvContentForDisplay(req, jwtSecret, signingKeys)
if err != nil {
fmt.Printf("Failed to generate .env content: %v\n", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate configuration content"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Configuration generated successfully!",
"envContent": newEnvContent,
"restartMessage": "Please replace the bootstrap environment variables with the newly generated ones, then run: docker-compose down && docker-compose up -d",
"manualRestartRequired": true,
"manualRestartCommand": "docker-compose down && docker-compose up -d",
"configFilePath": "./config/.env",
"securityNotice": "[WARNING] A signing key has been generated. BACKUP THE PRIVATE KEY or you will lose access to all agents!",
"publicKeyFingerprint": signingKeys["public_key"][:16] + "...",
})
}
// GenerateSigningKeys generates Ed25519 keypair for agent update signing
func (h *SetupHandler) GenerateSigningKeys(c *gin.Context) {
// Prevent caching of generated keys (security critical)
c.Header("Cache-Control", "no-store, no-cache, must-revalidate, private")
c.Header("Pragma", "no-cache")
c.Header("Expires", "0")
// Load configuration to check for existing key
cfg, err := config.Load() // This will load from .env file
if err == nil && cfg.SigningPrivateKey != "" {
c.JSON(http.StatusConflict, gin.H{"error": "A signing key is already configured for this server."})
return
}
// Generate Ed25519 keypair
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate keypair"})
return
}
// Encode to hex
publicKeyHex := hex.EncodeToString(publicKey)
privateKeyHex := hex.EncodeToString(privateKey)
// Generate fingerprint (first 16 chars)
fingerprint := publicKeyHex[:16]
// Log key generation for security audit trail (only fingerprint, not full key)
fmt.Printf("Generated new Ed25519 keypair - Fingerprint: %s\n", fingerprint)
c.JSON(http.StatusOK, gin.H{
"public_key": publicKeyHex,
"private_key": privateKeyHex,
"fingerprint": fingerprint,
"algorithm": "ed25519",
"key_size": 32,
})
}
// ConfigureSecrets creates all Docker secrets automatically
func (h *SetupHandler) ConfigureSecrets(c *gin.Context) {
// Check if Docker API is available
if !services.IsDockerAvailable() {
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": "Docker API not available",
"message": "Docker socket is not mounted. Please ensure the server can access Docker daemon",
})
return
}
// Create Docker secrets service
dockerSecrets, err := services.NewDockerSecretsService()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to connect to Docker",
"details": err.Error(),
})
return
}
defer dockerSecrets.Close()
// Generate all required secrets
type SecretConfig struct {
Name string
Value string
}
secrets := []SecretConfig{
{"redflag_admin_password", config.GenerateSecurePassword()},
{"redflag_jwt_secret", generateSecureJWTSecret()},
{"redflag_db_password", config.GenerateSecurePassword()},
}
// Try to create each secret
createdSecrets := []string{}
failedSecrets := []string{}
for _, secret := range secrets {
if err := dockerSecrets.CreateSecret(secret.Name, secret.Value); err != nil {
failedSecrets = append(failedSecrets, fmt.Sprintf("%s: %v", secret.Name, err))
} else {
createdSecrets = append(createdSecrets, secret.Name)
}
}
// Generate signing keys
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to generate signing keys",
"details": err.Error(),
})
return
}
publicKeyHex := hex.EncodeToString(publicKey)
privateKeyHex := hex.EncodeToString(privateKey)
// Create signing key secret
if err := dockerSecrets.CreateSecret("redflag_signing_private_key", privateKeyHex); err != nil {
failedSecrets = append(failedSecrets, fmt.Sprintf("redflag_signing_private_key: %v", err))
} else {
createdSecrets = append(createdSecrets, "redflag_signing_private_key")
}
response := gin.H{
"created_secrets": createdSecrets,
"public_key": publicKeyHex,
"fingerprint": publicKeyHex[:16],
}
if len(failedSecrets) > 0 {
response["failed_secrets"] = failedSecrets
c.JSON(http.StatusMultiStatus, response)
return
}
c.JSON(http.StatusOK, response)
}
// GenerateSecurePassword generates a secure password for admin/db
func generateSecurePassword() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return hex.EncodeToString(bytes)[:16] // 16 character random password
}
// generateSecureJWTSecret generates a secure JWT secret
func generateSecureJWTSecret() string {
bytes := make([]byte, 32)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}