diff --git a/backend/internal/api/handlers/credentials_handler.go b/backend/internal/api/handlers/credentials_handler.go
index faf9384..9dc80fa 100644
--- a/backend/internal/api/handlers/credentials_handler.go
+++ b/backend/internal/api/handlers/credentials_handler.go
@@ -93,3 +93,29 @@ func (h *CredentialsHandler) DeleteCredential(w http.ResponseWriter, r *http.Req
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"message": "Credentials deleted successfully"})
}
+
+// TestCredential runs a connection test for the provided service
+// POST /api/v1/system/credentials/{service}/test
+func (h *CredentialsHandler) TestCredential(w http.ResponseWriter, r *http.Request) {
+ serviceName := r.PathValue("service")
+ if serviceName == "" {
+ http.Error(w, "Service name is required", http.StatusBadRequest)
+ return
+ }
+
+ // Handle specific existing endpoints independently if they don't map to the new CredentialsService logic
+ if serviceName == "storage" {
+ http.Error(w, "Please use the /api/v1/admin/storage/test-connection endpoint for storage", http.StatusBadRequest)
+ return
+ }
+
+ if err := h.credentialsService.TestConnection(r.Context(), serviceName); err != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Test failed: " + err.Error()})
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(map[string]string{"message": "Connection test successful"})
+}
diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go
index 2cc0b73..003a2b5 100755
--- a/backend/internal/router/router.go
+++ b/backend/internal/router/router.go
@@ -246,6 +246,7 @@ func NewRouter() http.Handler {
mux.Handle("GET /api/v1/system/credentials", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(credentialsHandler.ListCredentials))))
mux.Handle("POST /api/v1/system/credentials", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(credentialsHandler.SaveCredential))))
mux.Handle("DELETE /api/v1/system/credentials/{service}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(credentialsHandler.DeleteCredential))))
+ mux.Handle("POST /api/v1/system/credentials/{service}/test", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(credentialsHandler.TestCredential))))
// Storage (Presigned URL)
mux.Handle("GET /api/v1/storage/upload-url", authMiddleware.OptionalHeaderAuthGuard(http.HandlerFunc(storageHandler.GetUploadURL)))
diff --git a/backend/internal/services/credentials_testing.go b/backend/internal/services/credentials_testing.go
new file mode 100644
index 0000000..6649a68
--- /dev/null
+++ b/backend/internal/services/credentials_testing.go
@@ -0,0 +1,272 @@
+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
+}
diff --git a/frontend/src/app/dashboard/settings/page.tsx b/frontend/src/app/dashboard/settings/page.tsx
index 6134fca..68bc9df 100644
--- a/frontend/src/app/dashboard/settings/page.tsx
+++ b/frontend/src/app/dashboard/settings/page.tsx
@@ -72,20 +72,7 @@ export default function SettingsPage() {
const [selectedService, setSelectedService] = useState