gohorsejobs/backend/internal/services/credentials_testing.go
Redbull Deployer 2bfe3b7173 feat: add test connection feature for all external services
# Conflicts:
#	frontend/src/lib/api.ts
2026-03-07 12:12:05 -06:00

272 lines
7 KiB
Go

package services
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"net/smtp"
"time"
firebase "firebase.google.com/go/v4"
"github.com/appwrite/sdk-for-go/appwrite"
amqp "github.com/rabbitmq/amqp091-go"
"github.com/stripe/stripe-go/v76"
"github.com/stripe/stripe-go/v76/balance"
"google.golang.org/api/option"
)
// TestConnection attempts to use the saved credentials for a specific service to verify they work.
func (s *CredentialsService) TestConnection(ctx context.Context, serviceName string) error {
// First, check if configured
configured, err := s.isServiceConfigured(ctx, serviceName)
if err != nil {
return fmt.Errorf("failed to check if service is configured: %w", err)
}
if !configured {
return fmt.Errorf("service %s is not configured", serviceName)
}
// 2. Fetch the credentials payload
decrypted, err := s.GetDecryptedKey(ctx, serviceName)
if err != nil {
return fmt.Errorf("failed to retrieve credentials: %w", err)
}
var creds map[string]string
if err := json.Unmarshal([]byte(decrypted), &creds); err != nil {
return fmt.Errorf("failed to parse credentials payload: %w", err)
}
// 3. Test based on service type
switch serviceName {
case "stripe":
return s.testStripe(creds)
case "cloudflare_config":
return s.testCloudflare(ctx, creds)
case "cpanel":
return s.testCPanel(ctx, creds)
case "lavinmq":
return s.testLavinMQ(creds)
case "appwrite":
return s.testAppwrite(creds)
case "fcm_service_account":
return s.testFCM(ctx, creds)
case "smtp":
return s.testSMTP(creds)
default:
return fmt.Errorf("testing for service %s is not implemented", serviceName)
}
}
func (s *CredentialsService) testStripe(creds map[string]string) error {
secretKey := creds["secretKey"]
if secretKey == "" {
return fmt.Errorf("missing secretKey in credentials")
}
stripe.Key = secretKey
_, err := balance.Get(nil)
if err != nil {
return fmt.Errorf("stripe connection test failed: %w", err)
}
return nil
}
func (s *CredentialsService) testCloudflare(ctx context.Context, creds map[string]string) error {
apiToken := creds["apiToken"]
if apiToken == "" {
return fmt.Errorf("missing apiToken in credentials")
}
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cloudflare.com/client/v4/user/tokens/verify", nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+apiToken)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("cloudflare connection failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("cloudflare API returned status: %d", resp.StatusCode)
}
var result struct {
Success bool `json:"success"`
Messages []struct {
Message string `json:"message"`
} `json:"messages"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("failed to parse cloudflare response: %w", err)
}
if !result.Success {
return fmt.Errorf("cloudflare verification failed")
}
return nil
}
func (s *CredentialsService) testLavinMQ(creds map[string]string) error {
amqpUrl := creds["amqpUrl"]
if amqpUrl == "" {
return fmt.Errorf("missing amqpUrl in credentials")
}
conn, err := amqp.Dial(amqpUrl)
if err != nil {
return fmt.Errorf("lavinmq connection failed: %w", err)
}
defer conn.Close()
return nil
}
func (s *CredentialsService) testAppwrite(creds map[string]string) error {
endpoint := creds["endpoint"]
projectId := creds["projectId"]
apiKey := creds["apiKey"]
if endpoint == "" || projectId == "" || apiKey == "" {
return fmt.Errorf("missing required Appwrite credentials")
}
client := appwrite.NewClient(
appwrite.WithEndpoint(endpoint),
appwrite.WithProject(projectId),
appwrite.WithKey(apiKey),
)
health := appwrite.NewHealth(client)
_, err := health.Get()
if err != nil {
return fmt.Errorf("appwrite connection failed: %w", err)
}
return nil
}
func (s *CredentialsService) testCPanel(ctx context.Context, creds map[string]string) error {
host := creds["host"]
username := creds["username"]
apiToken := creds["apiToken"]
if host == "" || username == "" || apiToken == "" {
return fmt.Errorf("missing required cpanel credentials")
}
// Just checking the base server reachability since testing cPanel API token accurately can be complex.
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(ctx, "GET", host, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", fmt.Sprintf("cpanel %s:%s", username, apiToken))
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("cpanel connection failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
return fmt.Errorf("cpanel authentication failed (status %d)", resp.StatusCode)
}
return nil
}
func (s *CredentialsService) testFCM(ctx context.Context, creds map[string]string) error {
serviceAccountJson := creds["serviceAccountJson"]
if serviceAccountJson == "" {
return fmt.Errorf("missing serviceAccountJson in credentials")
}
opt := option.WithCredentialsJSON([]byte(serviceAccountJson))
app, err := firebase.NewApp(ctx, nil, opt)
if err != nil {
return fmt.Errorf("failed to initialize FCM app: %w", err)
}
client, err := app.Messaging(ctx)
if err != nil {
return fmt.Errorf("failed to get FCM messaging client: %w", err)
}
// We don't actually send a notification, we just check if we can instance the client
// without credential errors.
if client == nil {
return fmt.Errorf("FCM client is nil")
}
return nil
}
func (s *CredentialsService) testSMTP(creds map[string]string) error {
host := creds["host"]
port := creds["port"]
username := creds["username"]
password := creds["password"]
secure := creds["secure"]
if host == "" || port == "" {
return fmt.Errorf("missing host or port in smtp credentials")
}
serverName := host + ":" + port
// Set 10s timeout
conn, err := tls.DialWithDialer(
&net.Dialer{Timeout: 10 * time.Second},
"tcp",
serverName,
&tls.Config{InsecureSkipVerify: true},
)
if err != nil {
if secure == "true" {
return fmt.Errorf("secure SMTP connection failed: %w", err)
}
// Try insecure
client, err := smtp.Dial(serverName)
if err != nil {
return fmt.Errorf("insecure SMTP connection failed: %w", err)
}
defer client.Close()
if username != "" && password != "" {
auth := smtp.PlainAuth("", username, password, host)
if err = client.Auth(auth); err != nil {
return fmt.Errorf("SMTP authentication failed: %w", err)
}
}
return nil
}
defer conn.Close()
client, err := smtp.NewClient(conn, host)
if err != nil {
return fmt.Errorf("failed to create SMTP client: %w", err)
}
defer client.Close()
if username != "" && password != "" {
auth := smtp.PlainAuth("", username, password, host)
if err = client.Auth(auth); err != nil {
return fmt.Errorf("SMTP authentication failed: %w", err)
}
}
return nil
}