v0.1.17: UI fixes, Linux improvements, documentation overhaul
UI/UX: - Fix heartbeat auto-refresh and rate-limiting page - Add navigation breadcrumbs to settings pages - New screenshots added Linux Agent v0.1.17: - Fix disk detection for multiple mount points - Improve installer idempotency - Prevent duplicate registrations Documentation: - README rewrite: 538→229 lines, homelab-focused - Split docs: API.md, CONFIGURATION.md, DEVELOPMENT.md - Add NOTICE for Apache 2.0 attribution
This commit is contained in:
@@ -2,16 +2,15 @@ package handlers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lib/pq"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
// SetupHandler handles server configuration
|
||||
@@ -25,8 +24,81 @@ func NewSetupHandler(configPath string) *SetupHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) (string, error) {
|
||||
// Generate .env file content for user to copy
|
||||
envContent := fmt.Sprintf(`# RedFlag Environment Configuration
|
||||
# Generated by web setup - Save this content to ./config/.env
|
||||
|
||||
# PostgreSQL Configuration (for PostgreSQL container)
|
||||
POSTGRES_DB=%s
|
||||
POSTGRES_USER=%s
|
||||
POSTGRES_PASSWORD=%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`,
|
||||
req.DBName, req.DBUser, req.DBPassword,
|
||||
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 := `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -46,19 +118,16 @@ func (h *SetupHandler) ShowSetupPage(c *gin.Context) {
|
||||
.form-section h3 { color: #4f46e5; margin-bottom: 15px; font-size: 1.2rem; }
|
||||
.form-group { margin-bottom: 20px; }
|
||||
label { display: block; margin-bottom: 5px; font-weight: 500; color: #374151; }
|
||||
input, select { width: 100%; padding: 12px; border: 2px solid #e5e7eb; border-radius: 6px; font-size: 1rem; transition: border-color 0.3s; }
|
||||
input, select { width: 100%%; padding: 12px; border: 2px solid #e5e7eb; border-radius: 6px; font-size: 1rem; transition: border-color 0.3s; }
|
||||
input:focus, select:focus { outline: none; border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); }
|
||||
input[type="password"] { font-family: monospace; }
|
||||
.button { background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); color: white; border: none; padding: 14px 28px; border-radius: 6px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: transform 0.2s; }
|
||||
.button:hover { transform: translateY(-1px); }
|
||||
.button:active { transform: translateY(0); }
|
||||
.progress { background: #f3f4f6; border-radius: 6px; height: 8px; overflow: hidden; margin: 20px 0; }
|
||||
.progress-bar { background: linear-gradient(90deg, #4f46e5, #7c3aed); height: 100%; width: 0%; transition: width 0.3s; }
|
||||
.status { text-align: center; padding: 20px; display: none; }
|
||||
.error { background: #fef2f2; color: #dc2626; padding: 15px; border-radius: 6px; margin: 20px 0; border: 1px solid #fecaca; }
|
||||
.success { background: #f0fdf4; color: #16a34a; padding: 15px; border-radius: 6px; margin: 20px 0; border: 1px solid #bbf7d0; }
|
||||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
||||
@media (max-width: 768px) { .grid { grid-template-columns: 1fr; } }
|
||||
.btn { background: linear-gradient(135deg, #4f46e5 0%%, #7c3aed 100%%); color: white; border: none; padding: 14px 28px; border-radius: 6px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: transform 0.2s; }
|
||||
.btn:hover { transform: translateY(-2px); }
|
||||
.btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
|
||||
.success { color: #10b981; background: #ecfdf5; padding: 12px; border-radius: 6px; border: 1px solid #10b981; }
|
||||
.error { color: #ef4444; background: #fef2f2; padding: 12px; border-radius: 6px; border: 1px solid #ef4444; }
|
||||
.loading { display: none; text-align: center; margin: 20px 0; }
|
||||
.spinner { border: 3px solid #f3f3f3; border-top: 3px solid #4f46e5; border-radius: 50%%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 0 auto; }
|
||||
@keyframes spin { 0%% { transform: rotate(0deg); } 100%% { transform: rotate(360deg); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -66,76 +135,78 @@ func (h *SetupHandler) ShowSetupPage(c *gin.Context) {
|
||||
<div class="card">
|
||||
<div class="header">
|
||||
<h1>🚀 RedFlag Server Setup</h1>
|
||||
<p class="subtitle">Configure your update management server</p>
|
||||
<p class="subtitle">Configure your RedFlag deployment</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<form id="setupForm">
|
||||
<div class="form-section">
|
||||
<h3>🔐 Admin Account</h3>
|
||||
<div class="grid">
|
||||
<div class="form-group">
|
||||
<label for="adminUser">Admin Username</label>
|
||||
<input type="text" id="adminUser" name="adminUser" value="admin" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="adminPassword">Admin Password</label>
|
||||
<input type="password" id="adminPassword" name="adminPassword" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h3>💾 Database Configuration</h3>
|
||||
<div class="grid">
|
||||
<div class="form-group">
|
||||
<label for="dbHost">Database Host</label>
|
||||
<input type="text" id="dbHost" name="dbHost" value="postgres" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbPort">Database Port</label>
|
||||
<input type="number" id="dbPort" name="dbPort" value="5432" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbName">Database Name</label>
|
||||
<input type="text" id="dbName" name="dbName" value="redflag" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbUser">Database User</label>
|
||||
<input type="text" id="dbUser" name="dbUser" value="redflag" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbPassword">Database Password</label>
|
||||
<input type="password" id="dbPassword" name="dbPassword" value="redflag" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h3>🌐 Server Configuration</h3>
|
||||
<div class="grid">
|
||||
<div class="form-group">
|
||||
<label for="serverHost">Server Host</label>
|
||||
<input type="text" id="serverHost" name="serverHost" value="0.0.0.0" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="serverPort">Server Port</label>
|
||||
<input type="number" id="serverPort" name="serverPort" value="8080" required>
|
||||
</div>
|
||||
<h3>📊 Server Configuration</h3>
|
||||
<div class="form-group">
|
||||
<label for="serverHost">Server Host</label>
|
||||
<input type="text" id="serverHost" name="serverHost" value="0.0.0.0" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="maxSeats">Maximum Agent Seats</label>
|
||||
<input type="number" id="maxSeats" name="maxSeats" value="50" min="1" max="1000">
|
||||
<label for="serverPort">Server Port</label>
|
||||
<input type="number" id="serverPort" name="serverPort" value="8080" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress" id="progress" style="display: none;">
|
||||
<div class="progress-bar" id="progressBar"></div>
|
||||
<div class="form-section">
|
||||
<h3>🗄️ Database Configuration</h3>
|
||||
<div class="form-group">
|
||||
<label for="dbHost">Database Host</label>
|
||||
<input type="text" id="dbHost" name="dbHost" value="postgres" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbPort">Database Port</label>
|
||||
<input type="number" id="dbPort" name="dbPort" value="5432" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbName">Database Name</label>
|
||||
<input type="text" id="dbName" name="dbName" value="redflag" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbUser">Database User</label>
|
||||
<input type="text" id="dbUser" name="dbUser" value="redflag" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dbPassword">Database Password</label>
|
||||
<input type="password" id="dbPassword" name="dbPassword" placeholder="Enter a secure database password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status" class="status"></div>
|
||||
<div class="form-section">
|
||||
<h3>👤 Administrator Account</h3>
|
||||
<div class="form-group">
|
||||
<label for="adminUser">Admin Username</label>
|
||||
<input type="text" id="adminUser" name="adminUser" value="admin" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="adminPassword">Admin Password</label>
|
||||
<input type="password" id="adminPassword" name="adminPassword" placeholder="Enter a secure admin password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="button">Configure Server</button>
|
||||
<div class="form-section">
|
||||
<h3>🔧 Agent Settings</h3>
|
||||
<div class="form-group">
|
||||
<label for="maxSeats">Maximum Agent Seats</label>
|
||||
<input type="number" id="maxSeats" name="maxSeats" value="50" min="1" max="1000" required>
|
||||
<small style="color: #6b7280; font-size: 0.875rem;">Maximum number of agents that can register</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" id="submitBtn">
|
||||
🚀 Configure RedFlag Server
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Configuring your RedFlag server...</p>
|
||||
</div>
|
||||
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -144,56 +215,113 @@ func (h *SetupHandler) ShowSetupPage(c *gin.Context) {
|
||||
document.getElementById('setupForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const loading = document.getElementById('loading');
|
||||
const result = document.getElementById('result');
|
||||
|
||||
const progress = document.getElementById('progress');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const status = document.getElementById('status');
|
||||
const submitButton = e.target.querySelector('button[type="submit"]');
|
||||
// Get form values
|
||||
const formData = {
|
||||
serverHost: document.getElementById('serverHost').value,
|
||||
serverPort: document.getElementById('serverPort').value,
|
||||
dbHost: document.getElementById('dbHost').value,
|
||||
dbPort: document.getElementById('dbPort').value,
|
||||
dbName: document.getElementById('dbName').value,
|
||||
dbUser: document.getElementById('dbUser').value,
|
||||
dbPassword: document.getElementById('dbPassword').value,
|
||||
adminUser: document.getElementById('adminUser').value,
|
||||
adminPassword: document.getElementById('adminPassword').value,
|
||||
maxSeats: document.getElementById('maxSeats').value
|
||||
};
|
||||
|
||||
// Show progress and disable button
|
||||
progress.style.display = 'block';
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = 'Configuring...';
|
||||
// Validate inputs
|
||||
if (!formData.adminUser || !formData.adminPassword) {
|
||||
result.innerHTML = '<div class="error">❌ Admin username and password are required</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.dbHost || !formData.dbPort || !formData.dbName || !formData.dbUser || !formData.dbPassword) {
|
||||
result.innerHTML = '<div class="error">❌ All database fields are required</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading
|
||||
submitBtn.disabled = true;
|
||||
loading.style.display = 'block';
|
||||
result.innerHTML = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/setup', {
|
||||
const response = await fetch('/api/setup/configure', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const resultData = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Success
|
||||
progressBar.style.width = '100%';
|
||||
status.innerHTML = '<div class="success">✅ ' + result.message + '</div>';
|
||||
submitButton.textContent = 'Configuration Complete';
|
||||
let resultHtml = '<div class="success">';
|
||||
resultHtml += '<h3>✅ Configuration Generated Successfully!</h3>';
|
||||
resultHtml += '<p><strong>Your JWT Secret:</strong> <code style="background: #f3f4f6; padding: 2px 6px; border-radius: 3px;">' + resultData.jwtSecret + '</code> ';
|
||||
resultHtml += '<button onclick="copyJWT(\'' + resultData.jwtSecret + '\')" style="background: #4f46e5; color: white; border: none; padding: 4px 8px; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">📋 Copy</button></p>';
|
||||
resultHtml += '<p><strong>⚠️ Important Next Steps:</strong></p>';
|
||||
resultHtml += '<div style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 6px; padding: 15px; margin: 15px 0;">';
|
||||
resultHtml += '<p style="margin: 0; color: #92400e;"><strong>🔧 Complete Setup Required:</strong></p>';
|
||||
resultHtml += '<ol style="margin: 10px 0 0 0; color: #92400e;">';
|
||||
resultHtml += '<li>Replace the bootstrap environment variables with the newly generated ones below</li>';
|
||||
resultHtml += '<li>Run: <code style="background: #fef3c7; padding: 2px 6px; border-radius: 3px;">' + resultData.manualRestartCommand + '</code></li>';
|
||||
resultHtml += '</ol>';
|
||||
resultHtml += '<p style="margin: 10px 0 0 0; color: #92400e; font-size: 0.9rem;"><strong>This step is required to apply your configuration and run database migrations.</strong></p>';
|
||||
resultHtml += '</div>';
|
||||
resultHtml += '</div>';
|
||||
|
||||
resultHtml += '<div style="margin-top: 20px;">';
|
||||
resultHtml += '<h4>📄 Configuration Content:</h4>';
|
||||
resultHtml += '<textarea readonly style="width: 100%%; height: 300px; font-family: monospace; font-size: 0.85rem; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; background: #f9fafb;">' + resultData.envContent + '</textarea>';
|
||||
resultHtml += '<button onclick="copyConfig()" style="background: #10b981; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; margin-top: 10px;">📋 Copy All Configuration</button>';
|
||||
resultHtml += '</div>';
|
||||
|
||||
result.innerHTML = resultHtml;
|
||||
loading.style.display = 'none';
|
||||
|
||||
// Store JWT for copy function
|
||||
window.jwtSecret = resultData.jwtSecret;
|
||||
window.envContent = resultData.envContent;
|
||||
|
||||
// Redirect to admin interface after delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/admin';
|
||||
}, 3000);
|
||||
} else {
|
||||
// Error
|
||||
status.innerHTML = '<div class="error">❌ ' + result.error + '</div>';
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Configure Server';
|
||||
result.innerHTML = '<div class="error">❌ Error: ' + resultData.error + '</div>';
|
||||
submitBtn.disabled = false;
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
status.innerHTML = '<div class="error">❌ Network error: ' + error.message + '</div>';
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Configure Server';
|
||||
result.innerHTML = '<div class="error">❌ Network error: ' + error.message + '</div>';
|
||||
submitBtn.disabled = false;
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
function copyJWT(jwt) {
|
||||
navigator.clipboard.writeText(jwt).then(() => {
|
||||
alert('JWT secret copied to clipboard!');
|
||||
}).catch(() => {
|
||||
prompt('Copy this JWT secret:', jwt);
|
||||
});
|
||||
}
|
||||
|
||||
function copyConfig() {
|
||||
if (window.envContent) {
|
||||
navigator.clipboard.writeText(window.envContent).then(() => {
|
||||
alert('Configuration copied to clipboard!');
|
||||
}).catch(() => {
|
||||
prompt('Copy this configuration:', window.envContent);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
c.Data(200, "text/html; charset=utf-8", []byte(html))
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html))
|
||||
}
|
||||
|
||||
// ConfigureServer handles the configuration submission
|
||||
@@ -246,95 +374,36 @@ func (h *SetupHandler) ConfigureServer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create configuration content
|
||||
envContent := fmt.Sprintf(`# RedFlag Server Configuration
|
||||
# Generated by web setup
|
||||
// Generate JWT secret for display (not logged for security)
|
||||
jwtSecret := deriveJWTSecret(req.AdminUser, req.AdminPass)
|
||||
|
||||
# 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 {
|
||||
fmt.Printf("Failed to create config directory: %v\n", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create config directory: %v", err)})
|
||||
return
|
||||
// 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("Warning: Failed to update PostgreSQL password: %v\n", err)
|
||||
fmt.Println("Will proceed with configuration anyway...")
|
||||
}
|
||||
|
||||
envPath := filepath.Join(configDir, ".env")
|
||||
if err := os.WriteFile(envPath, []byte(envContent), 0600); err != nil {
|
||||
fmt.Printf("Failed to save configuration: %v\n", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save configuration: %v", err)})
|
||||
// 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)
|
||||
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
|
||||
}
|
||||
|
||||
// Trigger graceful server restart after configuration
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second) // Give response time to reach client
|
||||
|
||||
// Get the current executable path
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to get executable path: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Restart the server with the same executable
|
||||
cmd := exec.Command(execPath)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
// Start the new process
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Printf("Failed to start new server process: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Exit the current process gracefully
|
||||
fmt.Printf("Server restarting... PID: %d\n", cmd.Process.Pid)
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Configuration saved successfully! Server will restart automatically.",
|
||||
"configPath": envPath,
|
||||
"restart": true,
|
||||
"message": "Configuration generated successfully!",
|
||||
"jwtSecret": jwtSecret,
|
||||
"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",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user