package handlers import ( "fmt" "net/http" "strconv" "time" "github.com/Fimeg/RedFlag/aggregator-server/internal/config" "github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type RegistrationTokenHandler struct { tokenQueries *queries.RegistrationTokenQueries agentQueries *queries.AgentQueries config *config.Config } func NewRegistrationTokenHandler(tokenQueries *queries.RegistrationTokenQueries, agentQueries *queries.AgentQueries, config *config.Config) *RegistrationTokenHandler { return &RegistrationTokenHandler{ tokenQueries: tokenQueries, agentQueries: agentQueries, config: config, } } // GenerateRegistrationToken creates a new registration token func (h *RegistrationTokenHandler) GenerateRegistrationToken(c *gin.Context) { var request struct { Label string `json:"label" binding:"required"` ExpiresIn string `json:"expires_in"` // e.g., "24h", "7d", "168h" MaxSeats int `json:"max_seats"` // Number of agents that can use this token Metadata map[string]interface{} `json:"metadata"` } if err := c.ShouldBindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format: " + err.Error()}) return } // Check agent seat limit (security, not licensing) activeAgents, err := h.agentQueries.GetActiveAgentCount() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check agent count"}) return } if activeAgents >= h.config.AgentRegistration.MaxSeats { c.JSON(http.StatusForbidden, gin.H{ "error": "Maximum agent seats reached", "limit": h.config.AgentRegistration.MaxSeats, "current": activeAgents, }) return } // Parse expiration duration expiresIn := request.ExpiresIn if expiresIn == "" { expiresIn = h.config.AgentRegistration.TokenExpiry } duration, err := time.ParseDuration(expiresIn) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid expiration format. Use formats like '24h', '7d', '168h'"}) return } expiresAt := time.Now().Add(duration) if duration > 168*time.Hour { // Max 7 days c.JSON(http.StatusBadRequest, gin.H{"error": "Token expiration cannot exceed 7 days"}) return } // Generate secure token token, err := config.GenerateSecureToken() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) return } // Create metadata with default values metadata := request.Metadata if metadata == nil { metadata = make(map[string]interface{}) } metadata["server_url"] = c.Request.Host metadata["expires_in"] = expiresIn // Default max_seats to 1 if not provided or invalid maxSeats := request.MaxSeats if maxSeats < 1 { maxSeats = 1 } // Store token in database err = h.tokenQueries.CreateRegistrationToken(token, request.Label, expiresAt, maxSeats, metadata) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create token"}) return } // Build install command serverURL := c.Request.Host if serverURL == "" { serverURL = "localhost:8080" // Fallback for development } // Use http:// for localhost, correct API endpoint, and query parameter for token protocol := "http://" if serverURL != "localhost:8080" { protocol = "https://" } installCommand := fmt.Sprintf("curl -sfL \"%s%s/api/v1/install/linux?token=%s\" | sudo bash", protocol, serverURL, token) response := gin.H{ "token": token, "label": request.Label, "expires_at": expiresAt, "install_command": installCommand, "metadata": metadata, } c.JSON(http.StatusCreated, response) } // ListRegistrationTokens returns all registration tokens with pagination func (h *RegistrationTokenHandler) ListRegistrationTokens(c *gin.Context) { // Parse pagination parameters page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) status := c.Query("status") isActive := c.Query("is_active") == "true" // Validate pagination if limit > 100 { limit = 100 } if page < 1 { page = 1 } offset := (page - 1) * limit var tokens []queries.RegistrationToken var err error // Handle filtering by active status if isActive || status == "active" { // Get only active tokens (no pagination for active-only queries) tokens, err = h.tokenQueries.GetActiveRegistrationTokens() // Apply manual pagination to active tokens if needed if err == nil && len(tokens) > 0 { start := offset end := offset + limit if start >= len(tokens) { tokens = []queries.RegistrationToken{} } else { if end > len(tokens) { end = len(tokens) } tokens = tokens[start:end] } } } else { // Get all tokens with database-level pagination tokens, err = h.tokenQueries.GetAllRegistrationTokens(limit, offset) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list tokens"}) return } // Get token usage stats stats, err := h.tokenQueries.GetTokenUsageStats() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get token stats"}) return } response := gin.H{ "tokens": tokens, "pagination": gin.H{ "page": page, "limit": limit, "offset": offset, }, "stats": stats, "seat_usage": gin.H{ "current": func() int { count, _ := h.agentQueries.GetActiveAgentCount() return count }(), "max": h.config.AgentRegistration.MaxSeats, }, } c.JSON(http.StatusOK, response) } // GetActiveRegistrationTokens returns only active tokens func (h *RegistrationTokenHandler) GetActiveRegistrationTokens(c *gin.Context) { tokens, err := h.tokenQueries.GetActiveRegistrationTokens() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get active tokens"}) return } c.JSON(http.StatusOK, gin.H{"tokens": tokens}) } // RevokeRegistrationToken revokes a registration token func (h *RegistrationTokenHandler) RevokeRegistrationToken(c *gin.Context) { token := c.Param("token") if token == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Token is required"}) return } var request struct { Reason string `json:"reason"` } c.ShouldBindJSON(&request) // Reason is optional reason := request.Reason if reason == "" { reason = "Revoked via API" } err := h.tokenQueries.RevokeRegistrationToken(token, reason) if err != nil { if err.Error() == "token not found or already used/revoked" { c.JSON(http.StatusNotFound, gin.H{"error": "Token not found or already used/revoked"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to revoke token"}) } return } c.JSON(http.StatusOK, gin.H{"message": "Token revoked successfully"}) } // DeleteRegistrationToken permanently deletes a registration token func (h *RegistrationTokenHandler) DeleteRegistrationToken(c *gin.Context) { tokenID := c.Param("id") if tokenID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Token ID is required"}) return } // Parse UUID id, err := uuid.Parse(tokenID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid token ID format"}) return } err = h.tokenQueries.DeleteRegistrationToken(id) if err != nil { if err.Error() == "token not found" { c.JSON(http.StatusNotFound, gin.H{"error": "Token not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete token"}) } return } c.JSON(http.StatusOK, gin.H{"message": "Token deleted successfully"}) } // ValidateRegistrationToken checks if a token is valid (for testing/debugging) func (h *RegistrationTokenHandler) ValidateRegistrationToken(c *gin.Context) { token := c.Query("token") if token == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Token query parameter is required"}) return } tokenInfo, err := h.tokenQueries.ValidateRegistrationToken(token) if err != nil { c.JSON(http.StatusNotFound, gin.H{ "valid": false, "error": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "valid": true, "token": tokenInfo, }) } // CleanupExpiredTokens performs cleanup of expired tokens func (h *RegistrationTokenHandler) CleanupExpiredTokens(c *gin.Context) { count, err := h.tokenQueries.CleanupExpiredTokens() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cleanup expired tokens"}) return } c.JSON(http.StatusOK, gin.H{ "message": "Cleanup completed", "cleaned": count, }) } // GetTokenStats returns comprehensive token usage statistics func (h *RegistrationTokenHandler) GetTokenStats(c *gin.Context) { // Get token stats tokenStats, err := h.tokenQueries.GetTokenUsageStats() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get token stats"}) return } // Get agent count activeAgentCount, err := h.agentQueries.GetActiveAgentCount() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get agent count"}) return } response := gin.H{ "token_stats": tokenStats, "agent_usage": gin.H{ "active_agents": activeAgentCount, "max_seats": h.config.AgentRegistration.MaxSeats, "available": h.config.AgentRegistration.MaxSeats - activeAgentCount, }, "security_limits": gin.H{ "max_tokens_per_request": h.config.AgentRegistration.MaxTokens, "max_token_duration": "7 days", "token_expiry_default": h.config.AgentRegistration.TokenExpiry, }, } c.JSON(http.StatusOK, response) }