From 0062e2acab3f8fc0bd65943b4f53c4cb466c4368 Mon Sep 17 00:00:00 2001 From: Fimeg Date: Sun, 2 Nov 2025 09:47:16 -0500 Subject: [PATCH] feat: setup wizard key generation added ed25519 keypair generation to setup endpoint wired route for POST /api/setup/generate-keys existing registration token system handles deployment --- aggregator-server/cmd/server/main.go | 7 -- .../internal/api/handlers/deployment.go | 96 ---------------- aggregator-web/src/components/Layout.tsx | 2 +- aggregator-web/src/pages/Setup.tsx | 105 +++++++++++++++++- 4 files changed, 103 insertions(+), 107 deletions(-) delete mode 100644 aggregator-server/internal/api/handlers/deployment.go diff --git a/aggregator-server/cmd/server/main.go b/aggregator-server/cmd/server/main.go index 40e6cf3..3d2a32f 100644 --- a/aggregator-server/cmd/server/main.go +++ b/aggregator-server/cmd/server/main.go @@ -172,7 +172,6 @@ func main() { rateLimitHandler := handlers.NewRateLimitHandler(rateLimiter) downloadHandler := handlers.NewDownloadHandler(filepath.Join("/app"), cfg) subsystemHandler := handlers.NewSubsystemHandler(subsystemQueries, commandQueries) - deploymentHandler := handlers.NewDeploymentHandler(registrationTokenQueries, agentQueries) // Initialize verification handler var verificationHandler *handlers.VerificationHandler @@ -320,12 +319,6 @@ func main() { admin.GET("/registration-tokens/stats", rateLimiter.RateLimit("admin_operations", middleware.KeyByUserID), registrationTokenHandler.GetTokenStats) admin.GET("/registration-tokens/validate", rateLimiter.RateLimit("admin_operations", middleware.KeyByUserID), registrationTokenHandler.ValidateRegistrationToken) - // Deployment routes (token management with install commands) - admin.GET("/deployment/tokens", rateLimiter.RateLimit("admin_operations", middleware.KeyByUserID), deploymentHandler.ListTokens) - admin.POST("/deployment/tokens", rateLimiter.RateLimit("admin_token_gen", middleware.KeyByUserID), deploymentHandler.CreateToken) - admin.GET("/deployment/tokens/:id", rateLimiter.RateLimit("admin_operations", middleware.KeyByUserID), deploymentHandler.GetToken) - admin.DELETE("/deployment/tokens/:id", rateLimiter.RateLimit("admin_operations", middleware.KeyByUserID), deploymentHandler.RevokeToken) - // Rate Limit Management admin.GET("/rate-limits", rateLimiter.RateLimit("admin_operations", middleware.KeyByUserID), rateLimitHandler.GetRateLimitSettings) admin.PUT("/rate-limits", rateLimiter.RateLimit("admin_operations", middleware.KeyByUserID), rateLimitHandler.UpdateRateLimitSettings) diff --git a/aggregator-server/internal/api/handlers/deployment.go b/aggregator-server/internal/api/handlers/deployment.go deleted file mode 100644 index feefd5c..0000000 --- a/aggregator-server/internal/api/handlers/deployment.go +++ /dev/null @@ -1,96 +0,0 @@ -package handlers - -import ( - "fmt" - "net/http" - "time" - - "github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries" - "github.com/gin-gonic/gin" -) - -type DeploymentHandler struct { - registrationTokenQueries *queries.RegistrationTokenQueries - agentQueries *queries.AgentQueries -} - -func NewDeploymentHandler(regTokenQueries *queries.RegistrationTokenQueries, agentQueries *queries.AgentQueries) *DeploymentHandler { - return &DeploymentHandler{ - registrationTokenQueries: regTokenQueries, - agentQueries: agentQueries, - } -} - -// ListTokens returns all registration tokens -func (h *DeploymentHandler) ListTokens(c *gin.Context) { - tokens, err := h.registrationTokenQueries.ListAllTokens() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list tokens"}) - return - } - c.JSON(http.StatusOK, gin.H{"tokens": tokens}) -} - -// CreateToken creates a new registration token -func (h *DeploymentHandler) CreateToken(c *gin.Context) { - var req struct { - MaxSeats int `json:"max_seats" binding:"required,min=1"` - ExpiresIn string `json:"expires_in"` // e.g., "24h", "7d" - Note string `json:"note"` // Optional note/label - } - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - // Parse expiration duration - var duration time.Duration - var err error - if req.ExpiresIn != "" { - duration, err = time.ParseDuration(req.ExpiresIn) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid expires_in format - use Go duration (e.g., 24h, 168h)"}) - return - } - } else { - duration = 24 * time.Hour // Default 24 hours - } - - expiresAt := time.Now().Add(duration) - token, err := h.registrationTokenQueries.CreateToken(req.MaxSeats, expiresAt) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create token"}) - return - } - - // Build install command - serverURL := c.Request.Host - installCommand := fmt.Sprintf("curl -sSL http://%s/api/v1/install/linux | bash -s -- %s", serverURL, token.Token) - - c.JSON(http.StatusOK, gin.H{ - "token": token, - "install_command": installCommand, - "message": "registration token created", - }) -} - -// RevokeToken revokes a registration token -func (h *DeploymentHandler) RevokeToken(c *gin.Context) { - tokenID := c.Param("id") - if err := h.registrationTokenQueries.RevokeToken(tokenID); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to revoke token"}) - return - } - c.JSON(http.StatusOK, gin.H{"message": "token revoked"}) -} - -// GetToken returns details about a specific token -func (h *DeploymentHandler) GetToken(c *gin.Context) { - tokenID := c.Param("id") - token, err := h.registrationTokenQueries.GetTokenByID(tokenID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "token not found"}) - return - } - c.JSON(http.StatusOK, gin.H{"token": token}) -} diff --git a/aggregator-web/src/components/Layout.tsx b/aggregator-web/src/components/Layout.tsx index 913956f..9f9da27 100644 --- a/aggregator-web/src/components/Layout.tsx +++ b/aggregator-web/src/components/Layout.tsx @@ -74,7 +74,7 @@ const Layout: React.FC = ({ children }) => { name: 'Settings', href: '/settings', icon: Settings, - current: location.pathname === '/settings', + current: location.pathname.startsWith('/settings'), }, ]; diff --git a/aggregator-web/src/pages/Setup.tsx b/aggregator-web/src/pages/Setup.tsx index 32156ba..69d36f5 100644 --- a/aggregator-web/src/pages/Setup.tsx +++ b/aggregator-web/src/pages/Setup.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Settings, Database, User, Shield, Eye, EyeOff, CheckCircle } from 'lucide-react'; +import { Settings, Database, User, Shield, Eye, EyeOff, CheckCircle, Key } from 'lucide-react'; import { toast } from 'react-hot-toast'; import { setupApi } from '@/lib/api'; import { useAuthStore } from '@/lib/store'; @@ -18,15 +18,24 @@ interface SetupFormData { maxSeats: string; } +interface SigningKeys { + public_key: string; + private_key: string; + fingerprint: string; + algorithm: string; +} + const Setup: React.FC = () => { const navigate = useNavigate(); const { logout } = useAuthStore(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const [envContent, setEnvContent] = useState(null); + const [envContent, setEnvContent] = useState(null); const [showSuccess, setShowSuccess] = useState(false); const [showPassword, setShowPassword] = useState(false); const [showDbPassword, setShowDbPassword] = useState(false); + const [signingKeys, setSigningKeys] = useState(null); + const [generatingKeys, setGeneratingKeys] = useState(false); const [formData, setFormData] = useState({ adminUser: 'admin', @@ -49,6 +58,27 @@ const Setup: React.FC = () => { })); }; + const handleGenerateKeys = async () => { + setGeneratingKeys(true); + try { + const response = await fetch('/api/setup/generate-keys', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + if (!response.ok) { + throw new Error('Failed to generate keys'); + } + const keys: SigningKeys = await response.json(); + setSigningKeys(keys); + toast.success('Signing keys generated successfully!'); + } catch (error: any) { + toast.error(error.message || 'Failed to generate keys'); + setError('Failed to generate signing keys'); + } finally { + setGeneratingKeys(false); + } + }; + const validateForm = (): boolean => { if (!formData.adminUser.trim()) { setError('Admin username is required'); @@ -114,7 +144,13 @@ const Setup: React.FC = () => { try { const result = await setupApi.configure(formData); - setEnvContent(result.envContent || null); + // Add signing keys to env content if generated + let finalEnvContent = result.envContent || ''; + if (signingKeys && finalEnvContent) { + finalEnvContent += `\n# Ed25519 Signing Keys (for agent updates)\nREDFLAG_SIGNING_PRIVATE_KEY=${signingKeys.private_key}\n`; + } + + setEnvContent(finalEnvContent || null); setShowSuccess(true); toast.success(result.message || 'Configuration saved successfully!'); @@ -322,6 +358,69 @@ const Setup: React.FC = () => { + {/* Security Keys Section */} +
+
+ +

Security Keys

+
+
+

+ Generate Ed25519 signing keys for secure agent updates. + Save the private key securely - it will be included in your configuration. +

+
+ + {!signingKeys ? ( + + ) : ( +
+
+ + +
+
+ + +
+
+

+ ✓ Keys generated! Private key will be securely included in your configuration file. +

+
+
+ )} +
+ {/* Database Configuration */}