Browse Source

feat: add environment config to hide GDPR access policy settings #95

- Add VITE_HIDE_ORDERS_ACCESS_SETTINGS and VITE_HIDE_CUSTOMERS_ACCESS_SETTINGS
  environment variables to control visibility of PII access settings
- Update DataAccessSettings component to conditionally hide customer/order
  settings based on env vars
- Change default access policies for new stores:
  - customers_access_policy: 'api_only' (GDPR compliant - no local caching)
  - orders_access_policy: 'api_only' (GDPR compliant - no local caching)
  - products_access_policy: 'sync' (non-PII, full caching allowed)
- Update database trigger and helper functions for GDPR-compliant defaults
- Update .env.example and CLAUDE.md documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude 5 months ago
parent
commit
69c865f

+ 8 - 0
CLAUDE.md

@@ -557,10 +557,18 @@ VITE_API_URL=https://YOUR_PROJECT.supabase.co/functions/v1
 
 # Frontend URL (for OAuth callbacks)
 VITE_FRONTEND_URL=http://localhost:8080  # or https://yourdomain.com for production
+
+# GDPR Compliance Settings (optional)
+# Hide data access policy settings from store owners for stricter GDPR compliance
+# When set to 'true', the corresponding settings will be hidden in the UI
+VITE_HIDE_ORDERS_ACCESS_SETTINGS=true    # Hide orders access policy settings
+VITE_HIDE_CUSTOMERS_ACCESS_SETTINGS=true # Hide customers access policy settings
 ```
 
 **Note**: `VITE_API_URL` is derived from `VITE_SUPABASE_URL` by appending `/functions/v1`
 
+**GDPR Settings**: When `VITE_HIDE_ORDERS_ACCESS_SETTINGS` and `VITE_HIDE_CUSTOMERS_ACCESS_SETTINGS` are set to `true`, store owners won't see these settings in the UI, and new stores will default to `api_only` mode (direct API access, no local caching of PII).
+
 ### Supabase Edge Functions `.env` (supabase)
 
 Required environment variables:

+ 7 - 0
shopcall.ai-main/.env.example

@@ -13,3 +13,10 @@ VITE_API_URL=https://YOUR_PROJECT.supabase.co/functions/v1
 # For production: https://yourdomain.com
 # For local dev: http://localhost:8080
 VITE_FRONTEND_URL=http://localhost:8080
+
+# GDPR Compliance Settings
+# Hide data access policy settings from store owners for stricter GDPR compliance
+# When set to 'true', the corresponding settings will be hidden in the UI
+# and stores will use api_only mode by default (direct API access, no local caching)
+VITE_HIDE_ORDERS_ACCESS_SETTINGS=true
+VITE_HIDE_CUSTOMERS_ACCESS_SETTINGS=true

+ 70 - 30
shopcall.ai-main/src/components/DataAccessSettings.tsx

@@ -10,6 +10,10 @@ import { Shield, ShieldCheck, ShieldAlert, Info, Loader2, Database, Cloud, Ban }
 import { API_URL } from "@/lib/config";
 import { useToast } from "@/hooks/use-toast";
 
+// GDPR Compliance: Check if settings should be hidden
+const HIDE_ORDERS_ACCESS_SETTINGS = import.meta.env.VITE_HIDE_ORDERS_ACCESS_SETTINGS === 'true';
+const HIDE_CUSTOMERS_ACCESS_SETTINGS = import.meta.env.VITE_HIDE_CUSTOMERS_ACCESS_SETTINGS === 'true';
+
 type DataAccessPolicy = 'sync' | 'api_only' | 'not_allowed';
 
 interface DataAccessPermissions {
@@ -116,12 +120,27 @@ export function DataAccessSettings({
   };
 
   const getSecurityLevel = () => {
-    const notAllowedCount = Object.values(policies).filter(p => p === 'not_allowed').length;
-    const apiOnlyCount = Object.values(policies).filter(p => p === 'api_only').length;
+    // Only count policies for visible settings
+    const visiblePolicies = [
+      policies.products_access_policy,
+      ...(!HIDE_CUSTOMERS_ACCESS_SETTINGS ? [policies.customers_access_policy] : []),
+      ...(!HIDE_ORDERS_ACCESS_SETTINGS ? [policies.orders_access_policy] : [])
+    ];
+
+    const notAllowedCount = visiblePolicies.filter(p => p === 'not_allowed').length;
+    const apiOnlyCount = visiblePolicies.filter(p => p === 'api_only').length;
+    const totalVisible = visiblePolicies.length;
+
+    // When all settings are hidden except products, show based on products only
+    if (totalVisible === 1) {
+      if (policies.products_access_policy === 'not_allowed') return { level: "maximum", icon: ShieldCheck, color: "text-green-500", label: "Maximum Privacy" };
+      if (policies.products_access_policy === 'api_only') return { level: "high", icon: ShieldCheck, color: "text-green-400", label: "High Privacy" };
+      return { level: "full", icon: ShieldAlert, color: "text-orange-500", label: "Full Sync" };
+    }
 
-    if (notAllowedCount === 3) return { level: "maximum", icon: ShieldCheck, color: "text-green-500", label: "Maximum Privacy" };
-    if (notAllowedCount >= 2) return { level: "high", icon: ShieldCheck, color: "text-green-400", label: "High Privacy" };
-    if (notAllowedCount === 1 || apiOnlyCount >= 2) return { level: "medium", icon: Shield, color: "text-yellow-500", label: "Medium Privacy" };
+    if (notAllowedCount === totalVisible) return { level: "maximum", icon: ShieldCheck, color: "text-green-500", label: "Maximum Privacy" };
+    if (notAllowedCount >= totalVisible - 1) return { level: "high", icon: ShieldCheck, color: "text-green-400", label: "High Privacy" };
+    if (notAllowedCount >= 1 || apiOnlyCount >= totalVisible - 1) return { level: "medium", icon: Shield, color: "text-yellow-500", label: "Medium Privacy" };
     if (apiOnlyCount >= 1) return { level: "balanced", icon: Shield, color: "text-blue-500", label: "Balanced" };
     return { level: "full", icon: ShieldAlert, color: "text-orange-500", label: "Full Sync" };
   };
@@ -210,6 +229,21 @@ export function DataAccessSettings({
           </AlertDescription>
         </Alert>
 
+        {/* Notice when PII settings are hidden for GDPR compliance */}
+        {(HIDE_CUSTOMERS_ACCESS_SETTINGS || HIDE_ORDERS_ACCESS_SETTINGS) && (
+          <Alert className="bg-green-500/10 border-green-500/50">
+            <ShieldCheck className="h-4 w-4 text-green-400" />
+            <AlertDescription className="text-green-300 text-sm">
+              <strong>GDPR Protection Active:</strong> Personal data (
+              {[
+                HIDE_CUSTOMERS_ACCESS_SETTINGS && 'customer data',
+                HIDE_ORDERS_ACCESS_SETTINGS && 'order data'
+              ].filter(Boolean).join(' and ')}
+              ) is accessed directly from your store without local caching, ensuring maximum privacy compliance.
+            </AlertDescription>
+          </Alert>
+        )}
+
         {/* Policy Settings */}
         <div className="space-y-6">
           {/* Products Access */}
@@ -226,33 +260,37 @@ export function DataAccessSettings({
             {renderPolicyOptions('products', 'products_access_policy', false)}
           </div>
 
-          {/* Customers Access */}
-          <div className="p-4 bg-slate-700/50 rounded-lg border border-slate-600">
-            <div className="flex items-center gap-2 mb-2">
-              <h4 className="text-white font-medium">Customer Data</h4>
-              <Badge variant="outline" className="text-xs border-orange-500 text-orange-400">
-                Personal Data (PII)
-              </Badge>
+          {/* Customers Access - Hidden when VITE_HIDE_CUSTOMERS_ACCESS_SETTINGS is true */}
+          {!HIDE_CUSTOMERS_ACCESS_SETTINGS && (
+            <div className="p-4 bg-slate-700/50 rounded-lg border border-slate-600">
+              <div className="flex items-center gap-2 mb-2">
+                <h4 className="text-white font-medium">Customer Data</h4>
+                <Badge variant="outline" className="text-xs border-orange-500 text-orange-400">
+                  Personal Data (PII)
+                </Badge>
+              </div>
+              <p className="text-sm text-slate-400 mb-3">
+                Customer information (names, emails, addresses, purchase history)
+              </p>
+              {renderPolicyOptions('customers', 'customers_access_policy', true)}
             </div>
-            <p className="text-sm text-slate-400 mb-3">
-              Customer information (names, emails, addresses, purchase history)
-            </p>
-            {renderPolicyOptions('customers', 'customers_access_policy', true)}
-          </div>
+          )}
 
-          {/* Orders Access */}
-          <div className="p-4 bg-slate-700/50 rounded-lg border border-slate-600">
-            <div className="flex items-center gap-2 mb-2">
-              <h4 className="text-white font-medium">Order Data</h4>
-              <Badge variant="outline" className="text-xs border-orange-500 text-orange-400">
-                Personal Data (PII)
-              </Badge>
+          {/* Orders Access - Hidden when VITE_HIDE_ORDERS_ACCESS_SETTINGS is true */}
+          {!HIDE_ORDERS_ACCESS_SETTINGS && (
+            <div className="p-4 bg-slate-700/50 rounded-lg border border-slate-600">
+              <div className="flex items-center gap-2 mb-2">
+                <h4 className="text-white font-medium">Order Data</h4>
+                <Badge variant="outline" className="text-xs border-orange-500 text-orange-400">
+                  Personal Data (PII)
+                </Badge>
+              </div>
+              <p className="text-sm text-slate-400 mb-3">
+                Order information (order details, amounts, customer info, shipping addresses)
+              </p>
+              {renderPolicyOptions('orders', 'orders_access_policy', true)}
             </div>
-            <p className="text-sm text-slate-400 mb-3">
-              Order information (order details, amounts, customer info, shipping addresses)
-            </p>
-            {renderPolicyOptions('orders', 'orders_access_policy', true)}
-          </div>
+          )}
         </div>
 
         {/* Action Buttons */}
@@ -293,7 +331,9 @@ export function DataAccessSettings({
           </Alert>
         )}
 
-        {(policies.customers_access_policy === 'api_only' || policies.orders_access_policy === 'api_only') && (
+        {/* Only show api_only alert for visible settings */}
+        {((!HIDE_CUSTOMERS_ACCESS_SETTINGS && policies.customers_access_policy === 'api_only') ||
+          (!HIDE_ORDERS_ACCESS_SETTINGS && policies.orders_access_policy === 'api_only')) && (
           <Alert className="bg-blue-500/10 border-blue-500/50">
             <Info className="h-4 w-4 text-blue-400" />
             <AlertDescription className="text-blue-300 text-sm">

+ 113 - 0
supabase/migrations/20251119_gdpr_default_api_only.sql

@@ -0,0 +1,113 @@
+-- Migration: GDPR compliance - Set default access policy to api_only for customers and orders
+-- Date: 2025-11-19
+-- Description: Changes default access policies for GDPR compliance:
+--   - customers_access_policy: default 'api_only' (no local caching of PII)
+--   - orders_access_policy: default 'api_only' (no local caching of PII)
+--   - products_access_policy: remains 'sync' (non-PII data)
+
+-- ============================================================================
+-- Update column defaults for new stores
+-- ============================================================================
+
+-- Change default for customers_access_policy to 'api_only'
+ALTER TABLE store_sync_config
+  ALTER COLUMN customers_access_policy SET DEFAULT 'api_only'::data_access_policy;
+
+-- Change default for orders_access_policy to 'api_only'
+ALTER TABLE store_sync_config
+  ALTER COLUMN orders_access_policy SET DEFAULT 'api_only'::data_access_policy;
+
+-- Update comments to reflect GDPR-compliant defaults
+COMMENT ON COLUMN store_sync_config.customers_access_policy IS 'Customer data access policy: sync (cache + Qdrant), api_only (direct API only - GDPR default), not_allowed (no access)';
+COMMENT ON COLUMN store_sync_config.orders_access_policy IS 'Order data access policy: sync (cache + Qdrant), api_only (direct API only - GDPR default), not_allowed (no access)';
+
+-- ============================================================================
+-- Update the auto_create_store_sync_config trigger function
+-- ============================================================================
+
+CREATE OR REPLACE FUNCTION auto_create_store_sync_config()
+RETURNS TRIGGER AS $$
+BEGIN
+  -- Only create config for platforms that support scheduled sync
+  IF NEW.platform_name IN ('woocommerce', 'shoprenter', 'shopify') THEN
+    INSERT INTO store_sync_config (
+      store_id,
+      enabled,
+      sync_frequency,
+      products_access_policy,
+      customers_access_policy,
+      orders_access_policy
+    )
+    VALUES (
+      NEW.id,
+      true,
+      'hourly',
+      'sync'::data_access_policy,      -- Products: allow full sync (non-PII)
+      'api_only'::data_access_policy,  -- Customers: API only (GDPR compliant)
+      'api_only'::data_access_policy   -- Orders: API only (GDPR compliant)
+    )
+    ON CONFLICT (store_id) DO NOTHING;
+  END IF;
+
+  RETURN NEW;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+-- ============================================================================
+-- Update the set_data_access_policy function to use GDPR-compliant defaults
+-- ============================================================================
+
+CREATE OR REPLACE FUNCTION set_data_access_policy(
+  p_store_id UUID,
+  p_data_type TEXT,
+  p_policy TEXT
+)
+RETURNS VOID
+LANGUAGE plpgsql
+SECURITY DEFINER
+AS $$
+BEGIN
+  -- Validate data type
+  IF p_data_type NOT IN ('products', 'customers', 'orders') THEN
+    RAISE EXCEPTION 'Invalid data type: %. Must be products, customers, or orders', p_data_type;
+  END IF;
+
+  -- Validate policy
+  IF p_policy NOT IN ('sync', 'api_only', 'not_allowed') THEN
+    RAISE EXCEPTION 'Invalid policy: %. Must be sync, api_only, or not_allowed', p_policy;
+  END IF;
+
+  -- Update the appropriate policy based on data type
+  EXECUTE format(
+    'UPDATE store_sync_config SET %I = $1::data_access_policy, updated_at = now() WHERE store_id = $2',
+    p_data_type || '_access_policy'
+  )
+  USING p_policy, p_store_id;
+
+  -- If no rows were updated, insert a new config row with GDPR-compliant defaults
+  IF NOT FOUND THEN
+    -- Insert with the specified policy and GDPR-compliant defaults for others
+    INSERT INTO store_sync_config (store_id, products_access_policy, customers_access_policy, orders_access_policy)
+    VALUES (
+      p_store_id,
+      CASE WHEN p_data_type = 'products' THEN p_policy::data_access_policy ELSE 'sync'::data_access_policy END,
+      CASE WHEN p_data_type = 'customers' THEN p_policy::data_access_policy ELSE 'api_only'::data_access_policy END,
+      CASE WHEN p_data_type = 'orders' THEN p_policy::data_access_policy ELSE 'api_only'::data_access_policy END
+    );
+  END IF;
+END;
+$$;
+
+COMMENT ON FUNCTION set_data_access_policy IS 'Set data access policy for a specific store and data type (products, customers, orders). New stores default to api_only for customers and orders (GDPR compliant).';
+
+-- ============================================================================
+-- Log migration completion
+-- ============================================================================
+
+DO $$
+BEGIN
+  RAISE NOTICE 'Updated default access policies for GDPR compliance:';
+  RAISE NOTICE '  - customers_access_policy: default api_only';
+  RAISE NOTICE '  - orders_access_policy: default api_only';
+  RAISE NOTICE '  - products_access_policy: remains sync';
+END $$;