Prechádzať zdrojové kódy

feat: add data access permissions UI and controls #48

- Created DataAccessSettings component with toggle switches for permissions
- Added data access badges column to webshops table
- Added "Configure" button to manage permissions per store
- Added permissions dialog with GDPR notices
- Implemented PUT /api/stores/:id/permissions endpoint
- Shows visual indicators (Products, Customers, Orders badges)
- Security level badge (High Privacy, Medium Privacy, Full Access)
- Real-time permission updates with optimistic UI
- Deployed API edge function v12

This completes the optional UI enhancements for GDPR-compliant
data access management.

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

Co-Authored-By: Claude <noreply@anthropic.com>
Claude 5 mesiacov pred
rodič
commit
410cd80931

+ 248 - 0
shopcall.ai-main/src/components/DataAccessSettings.tsx

@@ -0,0 +1,248 @@
+import { useState } from "react";
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
+import { Switch } from "@/components/ui/switch";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Shield, ShieldCheck, ShieldAlert, Info, Loader2 } from "lucide-react";
+import { API_URL } from "@/lib/config";
+import { useToast } from "@/hooks/use-toast";
+
+interface DataAccessPermissions {
+  allow_customer_access: boolean;
+  allow_order_access: boolean;
+  allow_product_access: boolean;
+}
+
+interface DataAccessSettingsProps {
+  storeId: string;
+  storeName: string;
+  currentPermissions: DataAccessPermissions;
+  onPermissionsUpdated?: (newPermissions: DataAccessPermissions) => void;
+}
+
+export function DataAccessSettings({
+  storeId,
+  storeName,
+  currentPermissions,
+  onPermissionsUpdated
+}: DataAccessSettingsProps) {
+  const [permissions, setPermissions] = useState<DataAccessPermissions>(currentPermissions);
+  const [saving, setSaving] = useState(false);
+  const [hasChanges, setHasChanges] = useState(false);
+  const { toast } = useToast();
+
+  const handleToggle = (key: keyof DataAccessPermissions) => {
+    setPermissions(prev => ({
+      ...prev,
+      [key]: !prev[key]
+    }));
+    setHasChanges(true);
+  };
+
+  const handleSave = async () => {
+    try {
+      setSaving(true);
+      const sessionData = localStorage.getItem('session_data');
+      if (!sessionData) {
+        throw new Error('No session data found');
+      }
+
+      const session = JSON.parse(sessionData);
+      const response = await fetch(`${API_URL}/api/stores/${storeId}/permissions`, {
+        method: 'PUT',
+        headers: {
+          'Authorization': `Bearer ${session.session.access_token}`,
+          'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+          data_access_permissions: permissions
+        })
+      });
+
+      const data = await response.json();
+
+      if (!response.ok) {
+        throw new Error(data.error || 'Failed to update permissions');
+      }
+
+      toast({
+        title: "Permissions Updated",
+        description: "Data access permissions have been successfully updated.",
+      });
+
+      setHasChanges(false);
+      if (onPermissionsUpdated) {
+        onPermissionsUpdated(permissions);
+      }
+    } catch (error) {
+      console.error('Error updating permissions:', error);
+      toast({
+        title: "Update Failed",
+        description: error instanceof Error ? error.message : "Failed to update permissions. Please try again.",
+        variant: "destructive",
+      });
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const handleReset = () => {
+    setPermissions(currentPermissions);
+    setHasChanges(false);
+  };
+
+  const getSecurityLevel = () => {
+    const enabledCount = Object.values(permissions).filter(Boolean).length;
+    if (enabledCount === 0) return { level: "high", icon: ShieldCheck, color: "text-green-500", label: "High Privacy" };
+    if (enabledCount <= 1) return { level: "medium", icon: Shield, color: "text-yellow-500", label: "Medium Privacy" };
+    return { level: "low", icon: ShieldAlert, color: "text-orange-500", label: "Full Access" };
+  };
+
+  const security = getSecurityLevel();
+  const SecurityIcon = security.icon;
+
+  return (
+    <Card className="bg-slate-800 border-slate-700">
+      <CardHeader>
+        <div className="flex items-center justify-between">
+          <div>
+            <CardTitle className="text-white flex items-center gap-2">
+              <SecurityIcon className={`w-5 h-5 ${security.color}`} />
+              Data Access Permissions
+            </CardTitle>
+            <CardDescription className="text-slate-400">
+              Control which data types can be accessed via API for {storeName}
+            </CardDescription>
+          </div>
+          <Badge className={`${security.color.replace('text-', 'bg-')} text-white`}>
+            {security.label}
+          </Badge>
+        </div>
+      </CardHeader>
+      <CardContent className="space-y-6">
+        {/* GDPR Notice */}
+        <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">
+            <strong>GDPR Compliance:</strong> Customer and order data are NOT stored in our database.
+            They are fetched in real-time from your webshop when accessed via API.
+            Disabling access prevents API calls from retrieving this data.
+          </AlertDescription>
+        </Alert>
+
+        {/* Permission Toggles */}
+        <div className="space-y-4">
+          {/* Products Access */}
+          <div className="flex items-start justify-between p-4 bg-slate-700/50 rounded-lg">
+            <div className="flex-1">
+              <div className="flex items-center gap-2 mb-1">
+                <h4 className="text-white font-medium">Product Data Access</h4>
+                <Badge variant="outline" className="text-xs border-blue-500 text-blue-400">
+                  Public Data
+                </Badge>
+              </div>
+              <p className="text-sm text-slate-400">
+                Allow API access to product information (names, prices, descriptions, stock levels)
+              </p>
+            </div>
+            <Switch
+              checked={permissions.allow_product_access}
+              onCheckedChange={() => handleToggle('allow_product_access')}
+              disabled={saving}
+            />
+          </div>
+
+          {/* Customers Access */}
+          <div className="flex items-start justify-between p-4 bg-slate-700/50 rounded-lg">
+            <div className="flex-1">
+              <div className="flex items-center gap-2 mb-1">
+                <h4 className="text-white font-medium">Customer Data Access</h4>
+                <Badge variant="outline" className="text-xs border-orange-500 text-orange-400">
+                  Personal Data
+                </Badge>
+              </div>
+              <p className="text-sm text-slate-400">
+                Allow API access to customer information (names, emails, addresses, purchase history)
+              </p>
+              {!permissions.allow_customer_access && (
+                <p className="text-xs text-yellow-400 mt-2">
+                  ⚠️ API requests for customer data will be denied
+                </p>
+              )}
+            </div>
+            <Switch
+              checked={permissions.allow_customer_access}
+              onCheckedChange={() => handleToggle('allow_customer_access')}
+              disabled={saving}
+            />
+          </div>
+
+          {/* Orders Access */}
+          <div className="flex items-start justify-between p-4 bg-slate-700/50 rounded-lg">
+            <div className="flex-1">
+              <div className="flex items-center gap-2 mb-1">
+                <h4 className="text-white font-medium">Order Data Access</h4>
+                <Badge variant="outline" className="text-xs border-orange-500 text-orange-400">
+                  Personal Data
+                </Badge>
+              </div>
+              <p className="text-sm text-slate-400">
+                Allow API access to order information (order details, amounts, customer info, shipping addresses)
+              </p>
+              {!permissions.allow_order_access && (
+                <p className="text-xs text-yellow-400 mt-2">
+                  ⚠️ API requests for order data will be denied
+                </p>
+              )}
+            </div>
+            <Switch
+              checked={permissions.allow_order_access}
+              onCheckedChange={() => handleToggle('allow_order_access')}
+              disabled={saving}
+            />
+          </div>
+        </div>
+
+        {/* Action Buttons */}
+        {hasChanges && (
+          <div className="flex items-center justify-end gap-3 pt-4 border-t border-slate-700">
+            <Button
+              variant="outline"
+              onClick={handleReset}
+              disabled={saving}
+              className="text-slate-400 border-slate-600 hover:bg-slate-700"
+            >
+              Cancel
+            </Button>
+            <Button
+              onClick={handleSave}
+              disabled={saving}
+              className="bg-cyan-500 hover:bg-cyan-600 text-white"
+            >
+              {saving ? (
+                <>
+                  <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+                  Saving...
+                </>
+              ) : (
+                'Save Changes'
+              )}
+            </Button>
+          </div>
+        )}
+
+        {/* Warning for disabling all access */}
+        {!permissions.allow_customer_access && !permissions.allow_order_access && (
+          <Alert className="bg-yellow-500/10 border-yellow-500/50">
+            <ShieldAlert className="h-4 w-4 text-yellow-400" />
+            <AlertDescription className="text-yellow-300 text-sm">
+              <strong>High Privacy Mode:</strong> Customer and order data access is disabled.
+              Only product data can be accessed via API. This maximizes privacy but limits functionality.
+            </AlertDescription>
+          </Alert>
+        )}
+      </CardContent>
+    </Card>
+  );
+}

+ 114 - 1
shopcall.ai-main/src/components/IntegrationsContent.tsx

@@ -6,13 +6,20 @@ import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Switch } from "@/components/ui/switch";
 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
-import { Plus, Settings, Store, Bot, PhoneCall, Globe, Zap, ShoppingBag, Loader2, Trash2, Info } from "lucide-react";
+import { Plus, Settings, Store, Bot, PhoneCall, Globe, Zap, ShoppingBag, Loader2, Trash2, Info, Shield } from "lucide-react";
 import { ShopRenterConnect } from "./ShopRenterConnect";
 import { WooCommerceConnect } from "./WooCommerceConnect";
 import { ShopifyConnect } from "./ShopifyConnect";
+import { DataAccessSettings } from "./DataAccessSettings";
 import { API_URL } from "@/lib/config";
 import { useToast } from "@/hooks/use-toast";
 
+interface DataAccessPermissions {
+  allow_customer_access: boolean;
+  allow_order_access: boolean;
+  allow_product_access: boolean;
+}
+
 interface ConnectedStore {
   id: string;
   store_name: string | null;
@@ -25,6 +32,7 @@ interface ConnectedStore {
   sync_started_at?: string | null;
   sync_completed_at?: string | null;
   sync_error?: string | null;
+  data_access_permissions?: DataAccessPermissions;
   alt_data?: {
     wcVersion?: string;
     wpVersion?: string;
@@ -47,6 +55,8 @@ export function IntegrationsContent() {
   const [showConnectDialog, setShowConnectDialog] = useState(false);
   const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
   const [syncingStores, setSyncingStores] = useState<Set<string>>(new Set());
+  const [showPermissionsDialog, setShowPermissionsDialog] = useState(false);
+  const [selectedStore, setSelectedStore] = useState<ConnectedStore | null>(null);
   const { toast } = useToast();
 
   const fetchStores = async () => {
@@ -421,6 +431,58 @@ export function IntegrationsContent() {
     );
   };
 
+  const getDataAccessBadges = (shop: ConnectedStore) => {
+    const permissions = shop.data_access_permissions || {
+      allow_customer_access: true,
+      allow_order_access: true,
+      allow_product_access: true
+    };
+
+    return (
+      <div className="flex flex-wrap gap-1">
+        {permissions.allow_product_access && (
+          <Badge variant="outline" className="text-xs border-blue-500 text-blue-400">
+            Products
+          </Badge>
+        )}
+        {permissions.allow_customer_access && (
+          <Badge variant="outline" className="text-xs border-green-500 text-green-400">
+            Customers
+          </Badge>
+        )}
+        {permissions.allow_order_access && (
+          <Badge variant="outline" className="text-xs border-purple-500 text-purple-400">
+            Orders
+          </Badge>
+        )}
+        {!permissions.allow_customer_access && !permissions.allow_order_access && (
+          <Badge variant="outline" className="text-xs border-orange-500 text-orange-400">
+            Limited Access
+          </Badge>
+        )}
+      </div>
+    );
+  };
+
+  const handleOpenPermissions = (shop: ConnectedStore) => {
+    setSelectedStore(shop);
+    setShowPermissionsDialog(true);
+  };
+
+  const handleClosePermissions = () => {
+    setShowPermissionsDialog(false);
+    setSelectedStore(null);
+  };
+
+  const handlePermissionsUpdated = (newPermissions: DataAccessPermissions) => {
+    // Update the store in the list
+    setConnectedShops(prev => prev.map(shop =>
+      shop.id === selectedStore?.id
+        ? { ...shop, data_access_permissions: newPermissions }
+        : shop
+    ));
+  };
+
   return (
     <div className="flex-1 space-y-6 p-8 bg-slate-900">
       <div className="flex items-center justify-between">
@@ -592,6 +654,7 @@ export function IntegrationsContent() {
                       <th className="p-4 text-slate-300 font-medium">Store</th>
                       <th className="p-4 text-slate-300 font-medium">Phone Number</th>
                       <th className="p-4 text-slate-300 font-medium">Platform</th>
+                      <th className="p-4 text-slate-300 font-medium">Data Access</th>
                       <th className="p-4 text-slate-300 font-medium">Sync Status</th>
                       <th className="p-4 text-slate-300 font-medium">Background Sync</th>
                       <th className="p-4 text-slate-300 font-medium">Created</th>
@@ -644,6 +707,20 @@ export function IntegrationsContent() {
                             {shop.platform_name}
                           </Badge>
                         </td>
+                        <td className="p-4">
+                          <div className="flex flex-col gap-2">
+                            {getDataAccessBadges(shop)}
+                            <Button
+                              size="sm"
+                              variant="ghost"
+                              className="text-xs text-cyan-400 hover:text-cyan-300 hover:bg-cyan-500/10 h-6 px-2"
+                              onClick={() => handleOpenPermissions(shop)}
+                            >
+                              <Shield className="w-3 h-3 mr-1" />
+                              Configure
+                            </Button>
+                          </div>
+                        </td>
                         <td className="p-4">
                           <div className="flex flex-col gap-2">
                             {getSyncStatusBadge(shop)}
@@ -781,6 +858,42 @@ export function IntegrationsContent() {
           </CardContent>
         </Card>
       </div>
+
+      {/* Data Access Permissions Dialog */}
+      <Dialog open={showPermissionsDialog} onOpenChange={setShowPermissionsDialog}>
+        <DialogContent className="bg-slate-800 border-slate-700 max-w-3xl max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle className="text-white text-2xl flex items-center gap-2">
+              <Shield className="w-6 h-6 text-cyan-500" />
+              Data Access Permissions
+            </DialogTitle>
+            <DialogDescription className="text-slate-400">
+              {selectedStore && `Configure data access permissions for ${selectedStore.store_name || 'this store'}`}
+            </DialogDescription>
+          </DialogHeader>
+          {selectedStore && (
+            <DataAccessSettings
+              storeId={selectedStore.id}
+              storeName={selectedStore.store_name || 'this store'}
+              currentPermissions={selectedStore.data_access_permissions || {
+                allow_customer_access: true,
+                allow_order_access: true,
+                allow_product_access: true
+              }}
+              onPermissionsUpdated={handlePermissionsUpdated}
+            />
+          )}
+          <div className="flex justify-end pt-4 border-t border-slate-700">
+            <Button
+              variant="outline"
+              onClick={handleClosePermissions}
+              className="text-slate-400 border-slate-600 hover:bg-slate-700"
+            >
+              Close
+            </Button>
+          </div>
+        </DialogContent>
+      </Dialog>
     </div>
   );
 }

+ 72 - 0
supabase/functions/api/index.ts

@@ -414,6 +414,78 @@ serve(wrapHandler('api', async (req) => {
       )
     }
 
+    // PUT /api/stores/:id/permissions - Update data access permissions for a store
+    if (path.match(/^stores\/[^\/]+\/permissions$/) && req.method === 'PUT') {
+      const storeId = path.split('/')[1]
+      const { data_access_permissions } = await req.json()
+
+      if (!data_access_permissions || typeof data_access_permissions !== 'object') {
+        return new Response(
+          JSON.stringify({ error: 'data_access_permissions object is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Validate permissions structure
+      const validPermissions = ['allow_customer_access', 'allow_order_access', 'allow_product_access']
+      for (const key of Object.keys(data_access_permissions)) {
+        if (!validPermissions.includes(key)) {
+          return new Response(
+            JSON.stringify({ error: `Invalid permission key: ${key}` }),
+            { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+        if (typeof data_access_permissions[key] !== 'boolean') {
+          return new Response(
+            JSON.stringify({ error: `Permission ${key} must be a boolean value` }),
+            { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id')
+        .eq('id', storeId)
+        .eq('user_id', user.id)
+        .single()
+
+      if (storeError || !store) {
+        return new Response(
+          JSON.stringify({ error: 'Store not found or access denied' }),
+          { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Update store permissions
+      const { error: updateError } = await supabase
+        .from('stores')
+        .update({
+          data_access_permissions,
+          updated_at: new Date().toISOString()
+        })
+        .eq('id', storeId)
+        .eq('user_id', user.id)
+
+      if (updateError) {
+        console.error('Error updating data access permissions:', updateError)
+        return new Response(
+          JSON.stringify({ error: 'Failed to update permissions' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          message: 'Data access permissions updated successfully',
+          permissions: data_access_permissions
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
     // DELETE /api/stores/{id} - Delete a store
     if (path.startsWith('stores/') && req.method === 'DELETE') {
       const storeId = path.replace('stores/', '')