Browse Source

feat: improve WooCommerce authentication implementation #17

- Fixed OAuth 1.0a implementation by removing oauth_version parameter
  (per WooCommerce docs, oauth_version should be excluded)
- Added manual API key authentication method as alternative to OAuth
- Updated UI with tabbed interface for OAuth vs API Keys
- Both methods test connection before storing credentials
- Added authMethod tracking in alt_data to distinguish connection types

Changes:
- oauth-woocommerce/index.ts: Added connect_manual action handler
- woocommerce-client.ts: Removed oauth_version from OAuth parameters
- WooCommerceConnect.tsx: Added tabs for OAuth and manual API key input
Claude 5 months ago
parent
commit
1b2340bf5c

+ 268 - 68
shopcall.ai-main/src/components/WooCommerceConnect.tsx

@@ -4,7 +4,8 @@ import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Alert, AlertDescription } from "@/components/ui/alert";
-import { Loader2, ShoppingBag, ExternalLink, CheckCircle2, AlertCircle } from "lucide-react";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Loader2, ShoppingBag, ExternalLink, CheckCircle2, AlertCircle, Key } from "lucide-react";
 import { API_URL } from "@/lib/config";
 
 interface WooCommerceConnectProps {
@@ -13,11 +14,14 @@ interface WooCommerceConnectProps {
 
 export function WooCommerceConnect({ onClose }: WooCommerceConnectProps) {
   const [storeUrl, setStoreUrl] = useState("");
+  const [consumerKey, setConsumerKey] = useState("");
+  const [consumerSecret, setConsumerSecret] = useState("");
   const [isConnecting, setIsConnecting] = useState(false);
   const [error, setError] = useState("");
   const [success, setSuccess] = useState(false);
+  const [successMessage, setSuccessMessage] = useState("");
 
-  const handleConnect = async () => {
+  const handleOAuthConnect = async () => {
     setError("");
     setSuccess(false);
 
@@ -85,6 +89,7 @@ export function WooCommerceConnect({ onClose }: WooCommerceConnectProps) {
       if (data.success && data.authUrl) {
         // Redirect to WooCommerce for authorization
         setSuccess(true);
+        setSuccessMessage("Redirecting to WooCommerce for authorization...");
         setTimeout(() => {
           window.location.href = data.authUrl;
         }, 1000);
@@ -99,9 +104,105 @@ export function WooCommerceConnect({ onClose }: WooCommerceConnectProps) {
     }
   };
 
-  const handleKeyPress = (e: React.KeyboardEvent) => {
-    if (e.key === "Enter" && !isConnecting) {
-      handleConnect();
+  const handleManualConnect = async () => {
+    setError("");
+    setSuccess(false);
+
+    // Validate inputs
+    if (!storeUrl.trim()) {
+      setError("Please enter your WooCommerce store URL");
+      return;
+    }
+
+    if (!consumerKey.trim()) {
+      setError("Please enter your Consumer Key");
+      return;
+    }
+
+    if (!consumerSecret.trim()) {
+      setError("Please enter your Consumer Secret");
+      return;
+    }
+
+    // Normalize URL
+    let normalizedUrl = storeUrl.trim();
+
+    // Remove trailing slash
+    normalizedUrl = normalizedUrl.replace(/\/$/, "");
+
+    // Add https:// if missing
+    if (!normalizedUrl.startsWith('http://') && !normalizedUrl.startsWith('https://')) {
+      normalizedUrl = `https://${normalizedUrl}`;
+    }
+
+    // Validate URL format
+    try {
+      const url = new URL(normalizedUrl);
+      if (url.protocol !== 'https:') {
+        setError("Store URL must use HTTPS for security");
+        return;
+      }
+    } catch (e) {
+      setError("Please enter a valid store URL (e.g., https://yourstore.com)");
+      return;
+    }
+
+    setIsConnecting(true);
+
+    try {
+      // Get auth token
+      const sessionData = localStorage.getItem('session_data');
+      if (!sessionData) {
+        setError("Authentication required. Please log in again.");
+        setIsConnecting(false);
+        return;
+      }
+
+      const session = JSON.parse(sessionData);
+
+      // Call the manual connection Edge Function
+      const response = await fetch(
+        `${API_URL}/oauth-woocommerce?action=connect_manual`,
+        {
+          method: 'POST',
+          headers: {
+            'Authorization': `Bearer ${session.session.access_token}`,
+            'Content-Type': 'application/json'
+          },
+          body: JSON.stringify({
+            storeUrl: normalizedUrl,
+            consumerKey: consumerKey.trim(),
+            consumerSecret: consumerSecret.trim()
+          })
+        }
+      );
+
+      if (!response.ok) {
+        const errorData = await response.json();
+        throw new Error(errorData.error || 'Failed to connect to WooCommerce');
+      }
+
+      const data = await response.json();
+
+      if (data.success) {
+        setSuccess(true);
+        setSuccessMessage(`Store "${data.storeName}" connected successfully!`);
+
+        // Redirect to webshops page after a short delay
+        setTimeout(() => {
+          if (onClose) {
+            onClose();
+          }
+          window.location.href = '/webshops?wc_connected=true';
+        }, 2000);
+      } else {
+        throw new Error('Invalid response from server');
+      }
+
+    } catch (err) {
+      console.error("Connection error:", err);
+      setError(err instanceof Error ? err.message : "Failed to connect to WooCommerce. Please try again.");
+      setIsConnecting(false);
     }
   };
 
@@ -124,7 +225,7 @@ export function WooCommerceConnect({ onClose }: WooCommerceConnectProps) {
           <Alert className="bg-green-500/10 border-green-500/50">
             <CheckCircle2 className="h-4 w-4 text-green-500" />
             <AlertDescription className="text-green-500">
-              Redirecting to WooCommerce for authorization...
+              {successMessage}
             </AlertDescription>
           </Alert>
         )}
@@ -139,71 +240,170 @@ export function WooCommerceConnect({ onClose }: WooCommerceConnectProps) {
           </Alert>
         )}
 
-        {/* Connection Form */}
-        <div className="space-y-4">
-          <div className="space-y-2">
-            <Label htmlFor="storeUrl" className="text-white">
-              WooCommerce Store URL
-            </Label>
-            <Input
-              id="storeUrl"
-              type="text"
-              placeholder="https://yourstore.com"
-              value={storeUrl}
-              onChange={(e) => setStoreUrl(e.target.value)}
-              onKeyPress={handleKeyPress}
-              className="bg-slate-700 border-slate-600 text-white placeholder:text-slate-400"
+        {/* Connection Methods */}
+        <Tabs defaultValue="oauth" className="w-full">
+          <TabsList className="grid w-full grid-cols-2 bg-slate-700">
+            <TabsTrigger value="oauth" className="data-[state=active]:bg-slate-600">
+              OAuth (Recommended)
+            </TabsTrigger>
+            <TabsTrigger value="manual" className="data-[state=active]:bg-slate-600">
+              <Key className="w-4 h-4 mr-2" />
+              API Keys
+            </TabsTrigger>
+          </TabsList>
+
+          {/* OAuth Method */}
+          <TabsContent value="oauth" className="space-y-4 mt-4">
+            <div className="space-y-2">
+              <Label htmlFor="storeUrl" className="text-white">
+                WooCommerce Store URL
+              </Label>
+              <Input
+                id="storeUrl"
+                type="text"
+                placeholder="https://yourstore.com"
+                value={storeUrl}
+                onChange={(e) => setStoreUrl(e.target.value)}
+                onKeyPress={(e) => e.key === "Enter" && !isConnecting && handleOAuthConnect()}
+                className="bg-slate-700 border-slate-600 text-white placeholder:text-slate-400"
+                disabled={isConnecting}
+              />
+              <p className="text-sm text-slate-400">
+                Enter your WooCommerce store URL (must use HTTPS)
+              </p>
+            </div>
+
+            <Button
+              onClick={handleOAuthConnect}
               disabled={isConnecting}
-            />
-            <p className="text-sm text-slate-400">
-              Enter your WooCommerce store URL (must use HTTPS)
-            </p>
-          </div>
+              className="w-full bg-purple-500 hover:bg-purple-600 text-white"
+            >
+              {isConnecting ? (
+                <>
+                  <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+                  Connecting...
+                </>
+              ) : (
+                <>
+                  <ShoppingBag className="w-4 h-4 mr-2" />
+                  Connect via OAuth
+                </>
+              )}
+            </Button>
 
-          <Button
-            onClick={handleConnect}
-            disabled={isConnecting}
-            className="w-full bg-purple-500 hover:bg-purple-600 text-white"
-          >
-            {isConnecting ? (
-              <>
-                <Loader2 className="w-4 h-4 mr-2 animate-spin" />
-                Connecting...
-              </>
-            ) : (
-              <>
-                <ShoppingBag className="w-4 h-4 mr-2" />
-                Connect to WooCommerce
-              </>
-            )}
-          </Button>
-        </div>
+            <div className="bg-slate-700/50 rounded-lg p-4 space-y-3">
+              <h4 className="text-white font-medium flex items-center gap-2">
+                <ExternalLink className="w-4 h-4 text-purple-500" />
+                What happens next?
+              </h4>
+              <ul className="space-y-2 text-sm text-slate-300">
+                <li className="flex items-start gap-2">
+                  <span className="text-purple-500 mt-0.5">1.</span>
+                  <span>You'll be redirected to your WooCommerce admin panel</span>
+                </li>
+                <li className="flex items-start gap-2">
+                  <span className="text-purple-500 mt-0.5">2.</span>
+                  <span>Review and approve the connection request</span>
+                </li>
+                <li className="flex items-start gap-2">
+                  <span className="text-purple-500 mt-0.5">3.</span>
+                  <span>WooCommerce will generate secure API credentials</span>
+                </li>
+                <li className="flex items-start gap-2">
+                  <span className="text-purple-500 mt-0.5">4.</span>
+                  <span>Return to ShopCall.ai to complete your setup</span>
+                </li>
+              </ul>
+            </div>
+          </TabsContent>
 
-        {/* Information Section */}
-        <div className="bg-slate-700/50 rounded-lg p-4 space-y-3">
-          <h4 className="text-white font-medium flex items-center gap-2">
-            <ExternalLink className="w-4 h-4 text-purple-500" />
-            What happens next?
-          </h4>
-          <ul className="space-y-2 text-sm text-slate-300">
-            <li className="flex items-start gap-2">
-              <span className="text-purple-500 mt-0.5">1.</span>
-              <span>You'll be redirected to your WooCommerce admin panel</span>
-            </li>
-            <li className="flex items-start gap-2">
-              <span className="text-purple-500 mt-0.5">2.</span>
-              <span>Review and approve the connection request</span>
-            </li>
-            <li className="flex items-start gap-2">
-              <span className="text-purple-500 mt-0.5">3.</span>
-              <span>WooCommerce will generate secure API credentials</span>
-            </li>
-            <li className="flex items-start gap-2">
-              <span className="text-purple-500 mt-0.5">4.</span>
-              <span>Return to ShopCall.ai to complete your setup</span>
-            </li>
-          </ul>
-        </div>
+          {/* Manual API Keys Method */}
+          <TabsContent value="manual" className="space-y-4 mt-4">
+            <div className="space-y-4">
+              <div className="space-y-2">
+                <Label htmlFor="manualStoreUrl" className="text-white">
+                  WooCommerce Store URL
+                </Label>
+                <Input
+                  id="manualStoreUrl"
+                  type="text"
+                  placeholder="https://yourstore.com"
+                  value={storeUrl}
+                  onChange={(e) => setStoreUrl(e.target.value)}
+                  className="bg-slate-700 border-slate-600 text-white placeholder:text-slate-400"
+                  disabled={isConnecting}
+                />
+              </div>
+
+              <div className="space-y-2">
+                <Label htmlFor="consumerKey" className="text-white">
+                  Consumer Key
+                </Label>
+                <Input
+                  id="consumerKey"
+                  type="text"
+                  placeholder="ck_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+                  value={consumerKey}
+                  onChange={(e) => setConsumerKey(e.target.value)}
+                  className="bg-slate-700 border-slate-600 text-white placeholder:text-slate-400 font-mono text-sm"
+                  disabled={isConnecting}
+                />
+              </div>
+
+              <div className="space-y-2">
+                <Label htmlFor="consumerSecret" className="text-white">
+                  Consumer Secret
+                </Label>
+                <Input
+                  id="consumerSecret"
+                  type="password"
+                  placeholder="cs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+                  value={consumerSecret}
+                  onChange={(e) => setConsumerSecret(e.target.value)}
+                  onKeyPress={(e) => e.key === "Enter" && !isConnecting && handleManualConnect()}
+                  className="bg-slate-700 border-slate-600 text-white placeholder:text-slate-400 font-mono text-sm"
+                  disabled={isConnecting}
+                />
+              </div>
+            </div>
+
+            <Button
+              onClick={handleManualConnect}
+              disabled={isConnecting}
+              className="w-full bg-purple-500 hover:bg-purple-600 text-white"
+            >
+              {isConnecting ? (
+                <>
+                  <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+                  Connecting...
+                </>
+              ) : (
+                <>
+                  <Key className="w-4 h-4 mr-2" />
+                  Connect with API Keys
+                </>
+              )}
+            </Button>
+
+            <div className="bg-blue-500/10 border border-blue-500/50 rounded-lg p-4 space-y-2">
+              <h4 className="text-white font-medium flex items-center gap-2">
+                <ExternalLink className="w-4 h-4 text-blue-500" />
+                How to generate API keys
+              </h4>
+              <ol className="space-y-2 text-sm text-slate-300 list-decimal list-inside">
+                <li>Go to WooCommerce → Settings → Advanced → REST API</li>
+                <li>Click "Add key"</li>
+                <li>Set description: "ShopCall.ai"</li>
+                <li>Set permissions: "Read"</li>
+                <li>Click "Generate API key"</li>
+                <li>Copy the Consumer Key and Consumer Secret</li>
+              </ol>
+              <p className="text-xs text-slate-400 mt-2">
+                Make sure to copy both keys immediately - WooCommerce shows the Consumer Secret only once!
+              </p>
+            </div>
+          </TabsContent>
+        </Tabs>
 
         {/* Security Notice */}
         <div className="bg-blue-500/10 border border-blue-500/50 rounded-lg p-4 space-y-2">

+ 1 - 2
supabase/functions/_shared/woocommerce-client.ts

@@ -155,8 +155,7 @@ export async function wooCommerceApiRequest(
     oauth_consumer_key: store.api_key,
     oauth_timestamp: Math.floor(Date.now() / 1000).toString(),
     oauth_nonce: crypto.randomUUID().replace(/-/g, ''),
-    oauth_signature_method: 'HMAC-SHA256',
-    oauth_version: '1.0'
+    oauth_signature_method: 'HMAC-SHA256'
   }
 
   // Generate signature

+ 109 - 3
supabase/functions/oauth-woocommerce/index.ts

@@ -78,8 +78,7 @@ async function testWooCommerceConnection(
       oauth_consumer_key: consumerKey,
       oauth_timestamp: Math.floor(Date.now() / 1000).toString(),
       oauth_nonce: crypto.randomUUID().replace(/-/g, ''),
-      oauth_signature_method: 'HMAC-SHA256',
-      oauth_version: '1.0'
+      oauth_signature_method: 'HMAC-SHA256'
     }
 
     // Generate signature
@@ -220,6 +219,112 @@ serve(async (req) => {
       )
     }
 
+    // Handle manual API key connection
+    if (action === 'connect_manual') {
+      const { storeUrl: storeUrlParam, consumerKey, consumerSecret } = await req.json()
+
+      if (!storeUrlParam || !consumerKey || !consumerSecret) {
+        return new Response(
+          JSON.stringify({ error: 'storeUrl, consumerKey, and consumerSecret are required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Validate store URL
+      const validation = validateStoreUrl(storeUrlParam)
+      if (!validation.valid) {
+        return new Response(
+          JSON.stringify({ error: validation.error }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Get user from authorization header
+      const authHeader = req.headers.get('authorization')
+      if (!authHeader) {
+        return new Response(
+          JSON.stringify({ error: 'No authorization header' }),
+          { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      const token = authHeader.replace('Bearer ', '')
+      const supabase = createClient(supabaseUrl, supabaseKey)
+      const { data: { user }, error: userError } = await supabase.auth.getUser(token)
+
+      if (userError || !user) {
+        return new Response(
+          JSON.stringify({ error: 'Invalid token' }),
+          { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Test API connection
+      const testResult = await testWooCommerceConnection(
+        validation.normalized!,
+        consumerKey,
+        consumerSecret
+      )
+
+      if (!testResult.success) {
+        console.error('[WooCommerce] API connection test failed:', testResult.error)
+        return new Response(
+          JSON.stringify({
+            error: testResult.error || 'Failed to connect to WooCommerce store'
+          }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Extract store info from API response
+      const systemStatus = testResult.data
+      const wcVersion = systemStatus?.environment?.version || 'unknown'
+      const wpVersion = systemStatus?.environment?.wp_version || 'unknown'
+      const storeName = systemStatus?.settings?.site_title?.value || new URL(validation.normalized!).hostname
+
+      // Create Supabase admin client
+      const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey)
+
+      // Store credentials in database
+      const { error: insertError } = await supabaseAdmin
+        .from('stores')
+        .insert({
+          user_id: user.id,
+          platform_name: 'woocommerce',
+          store_name: storeName,
+          store_url: validation.normalized,
+          api_key: consumerKey,
+          api_secret: consumerSecret,
+          scopes: ['read'],
+          alt_data: {
+            wcVersion,
+            wpVersion,
+            apiVersion: 'wc/v3',
+            connectedAt: new Date().toISOString(),
+            authMethod: 'manual'
+          }
+        })
+
+      if (insertError) {
+        console.error('[WooCommerce] Error storing credentials:', insertError)
+        return new Response(
+          JSON.stringify({ error: 'Failed to save store credentials' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      console.log(`[WooCommerce] Store connected successfully (manual): ${storeName}`)
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          message: 'Store connected successfully',
+          storeName
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
     // Handle OAuth callback
     if (action === 'callback') {
       const success = url.searchParams.get('success')
@@ -306,7 +411,8 @@ serve(async (req) => {
             wcVersion,
             wpVersion,
             apiVersion: 'wc/v3',
-            connectedAt: new Date().toISOString()
+            connectedAt: new Date().toISOString(),
+            authMethod: 'oauth'
           }
         })