diff --git a/aggregator-server/cmd/server/main.go b/aggregator-server/cmd/server/main.go index 8d88203..b127653 100644 --- a/aggregator-server/cmd/server/main.go +++ b/aggregator-server/cmd/server/main.go @@ -142,7 +142,6 @@ 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 index 4e7d1a0..9a7174e 100644 --- a/aggregator-server/internal/api/handlers/setup.go +++ b/aggregator-server/internal/api/handlers/setup.go @@ -6,10 +6,11 @@ import ( "fmt" "net/http" "os" + "os/exec" "path/filepath" "strconv" + "time" - "github.com/Fimeg/RedFlag/aggregator-server/internal/config" "github.com/gin-gonic/gin" ) @@ -300,9 +301,38 @@ LATEST_AGENT_VERSION=0.1.16`, 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, }) } diff --git a/aggregator-web/src/App.tsx b/aggregator-web/src/App.tsx index 83364f3..c2de34c 100644 --- a/aggregator-web/src/App.tsx +++ b/aggregator-web/src/App.tsx @@ -13,6 +13,8 @@ import Settings from '@/pages/Settings'; import TokenManagement from '@/pages/TokenManagement'; import RateLimiting from '@/pages/RateLimiting'; import Login from '@/pages/Login'; +import Setup from '@/pages/Setup'; +import { WelcomeChecker } from '@/components/WelcomeChecker'; // Protected route component const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -75,8 +77,11 @@ const App: React.FC = () => { /> - {/* App routes */} + {/* App routes */} + {/* Setup route - shown when server needs configuration */} + } /> + {/* Login route */} { - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - + + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } /> diff --git a/aggregator-web/src/components/WelcomeChecker.tsx b/aggregator-web/src/components/WelcomeChecker.tsx new file mode 100644 index 0000000..7a023f5 --- /dev/null +++ b/aggregator-web/src/components/WelcomeChecker.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useState } from 'react'; +import { Navigate } from 'react-router-dom'; +import { setupApi } from '@/lib/api'; + +interface WelcomeCheckerProps { + children: React.ReactNode; +} + +export const WelcomeChecker: React.FC = ({ children }) => { + const [isWelcomeMode, setIsWelcomeMode] = useState(null); + + useEffect(() => { + const checkWelcomeMode = async () => { + try { + const data = await setupApi.checkHealth(); + + if (data.status === 'waiting for configuration') { + setIsWelcomeMode(true); + } else { + setIsWelcomeMode(false); + } + } catch (error) { + // If we can't reach the health endpoint, assume normal mode + setIsWelcomeMode(false); + } + }; + + checkWelcomeMode(); + + // Check periodically for configuration changes + const interval = setInterval(checkWelcomeMode, 5000); + + return () => clearInterval(interval); + }, []); + + if (isWelcomeMode === null) { + // Loading state + return ( + + + + Checking server status... + + + ); + } + + if (isWelcomeMode) { + // Redirect to setup page + return ; + } + + // Normal mode - render children + return <>{children}>; +}; \ No newline at end of file diff --git a/aggregator-web/src/lib/api.ts b/aggregator-web/src/lib/api.ts index 3f96e79..b5e0194 100644 --- a/aggregator-web/src/lib/api.ts +++ b/aggregator-web/src/lib/api.ts @@ -255,6 +255,40 @@ export const authApi = { }, }; +// Setup API for server configuration (uses base API without auth) +const setupApiInstance = axios.create({ + baseURL: API_BASE_URL, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +export const setupApi = { + // Check server health and status + checkHealth: async (): Promise<{ status: string }> => { + const response = await setupApiInstance.get('/health'); + return response.data; + }, + + // Submit server configuration + configure: async (config: { + adminUser: string; + adminPassword: string; + dbHost: string; + dbPort: string; + dbName: string; + dbUser: string; + dbPassword: string; + serverHost: string; + serverPort: string; + maxSeats: string; + }): Promise<{ message: string; configPath?: string; restart?: boolean }> => { + const response = await setupApiInstance.post('/setup', config); + return response.data; + }, +}; + // Utility functions export const createQueryString = (params: Record): string => { const searchParams = new URLSearchParams(); diff --git a/aggregator-web/src/pages/Setup.tsx b/aggregator-web/src/pages/Setup.tsx new file mode 100644 index 0000000..24fb218 --- /dev/null +++ b/aggregator-web/src/pages/Setup.tsx @@ -0,0 +1,349 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { XCircle } from 'lucide-react'; +import { toast } from 'react-hot-toast'; +import { setupApi } from '@/lib/api'; + +interface SetupFormData { + adminUser: string; + adminPassword: string; + dbHost: string; + dbPort: string; + dbName: string; + dbUser: string; + dbPassword: string; + serverHost: string; + serverPort: string; + maxSeats: string; +} + + +const Setup: React.FC = () => { + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const [formData, setFormData] = useState({ + adminUser: 'admin', + adminPassword: '', + dbHost: 'postgres', + dbPort: '5432', + dbName: 'redflag', + dbUser: 'redflag', + dbPassword: 'redflag', + serverHost: '0.0.0.0', + serverPort: '8080', + maxSeats: '50', + }); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + const validateForm = (): boolean => { + if (!formData.adminUser.trim()) { + setError('Admin username is required'); + return false; + } + if (!formData.adminPassword.trim()) { + setError('Admin password is required'); + return false; + } + if (!formData.dbHost.trim()) { + setError('Database host is required'); + return false; + } + if (!formData.dbPort.trim()) { + setError('Database port is required'); + return false; + } + const dbPort = parseInt(formData.dbPort); + if (isNaN(dbPort) || dbPort <= 0 || dbPort > 65535) { + setError('Database port must be between 1 and 65535'); + return false; + } + if (!formData.dbName.trim()) { + setError('Database name is required'); + return false; + } + if (!formData.dbUser.trim()) { + setError('Database user is required'); + return false; + } + if (!formData.dbPassword.trim()) { + setError('Database password is required'); + return false; + } + if (!formData.serverPort.trim()) { + setError('Server port is required'); + return false; + } + const serverPort = parseInt(formData.serverPort); + if (isNaN(serverPort) || serverPort <= 0 || serverPort > 65535) { + setError('Server port must be between 1 and 65535'); + return false; + } + const maxSeats = parseInt(formData.maxSeats); + if (isNaN(maxSeats) || maxSeats <= 0) { + setError('Maximum agent seats must be greater than 0'); + return false; + } + return true; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!validateForm()) { + return; + } + + setIsLoading(true); + + try { + const result = await setupApi.configure(formData); + + toast.success(result.message || 'Configuration saved successfully!'); + + if (result.restart) { + // Server is restarting, wait for it to come back online + setTimeout(() => { + navigate('/login'); + }, 5000); // Give server time to restart + } else { + // No restart, redirect immediately + setTimeout(() => { + navigate('/login'); + }, 2000); + } + + } catch (error: any) { + console.error('Setup error:', error); + const errorMessage = error.response?.data?.error || error.message || 'Setup failed'; + setError(errorMessage); + toast.error(errorMessage); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + Server Setup + + Configure your update management server + + + + + + {/* Error Display */} + {error && ( + + + + + + + {error} + + + + )} + + {/* Admin Account */} + + Admin Account + + + + Admin Username + + + + + + Admin Password + + + + + + + {/* Database Configuration */} + + Database Configuration + + + + Database Host + + + + + + Database Port + + + + + + Database Name + + + + + + Database User + + + + + + Database Password + + + + + + + {/* Server Configuration */} + + Server Configuration + + + + Server Host + + + + + + Server Port + + + + + + Maximum Agent Seats + + + Security limit for agent registration + + + + + {/* Submit Button */} + + + {isLoading ? ( + + + Configuring... + + ) : ( + 'Configure Server' + )} + + + + + + + ); +}; + +export default Setup; \ No newline at end of file diff --git a/aggregator-web/vite.config.ts b/aggregator-web/vite.config.ts index 791139e..5e096bc 100644 --- a/aggregator-web/vite.config.ts +++ b/aggregator-web/vite.config.ts @@ -16,6 +16,10 @@ export default defineConfig({ '/api': { target: 'http://localhost:8080', changeOrigin: true, + }, + '/health': { + target: 'http://localhost:8080', + changeOrigin: true, } } }
Checking server status...
+ Configure your update management server +
Security limit for agent registration