diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd161d7..02d5164 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,6 +46,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: lts/* + cache: 'npm' - name: Setup Rust uses: dtolnay/rust-toolchain@stable @@ -59,6 +60,11 @@ jobs: - name: Install frontend dependencies run: npm ci + + - name: Verify dependencies installed + run: | + echo "Checking if @vitejs/plugin-react-swc is installed..." + npm list @vitejs/plugin-react-swc || echo "Warning: @vitejs/plugin-react-swc not found in node_modules" - name: Build Tauri app uses: tauri-apps/tauri-action@v0 diff --git a/package-lock.json b/package-lock.json index 088b995..aa796db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,6 +78,7 @@ "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", + "@vitejs/plugin-react-swc": "^3.11.0", "eslint": "^9.36.0", "eslint-plugin-react-hooks": "^6.1.0", "eslint-plugin-react-refresh": "^0.4.23", @@ -3036,6 +3037,232 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@swc/core": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", + "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.24" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.13.5", + "@swc/core-darwin-x64": "1.13.5", + "@swc/core-linux-arm-gnueabihf": "1.13.5", + "@swc/core-linux-arm64-gnu": "1.13.5", + "@swc/core-linux-arm64-musl": "1.13.5", + "@swc/core-linux-x64-gnu": "1.13.5", + "@swc/core-linux-x64-musl": "1.13.5", + "@swc/core-win32-arm64-msvc": "1.13.5", + "@swc/core-win32-ia32-msvc": "1.13.5", + "@swc/core-win32-x64-msvc": "1.13.5" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz", + "integrity": "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz", + "integrity": "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz", + "integrity": "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz", + "integrity": "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz", + "integrity": "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz", + "integrity": "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz", + "integrity": "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz", + "integrity": "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz", + "integrity": "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz", + "integrity": "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", + "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tailwindcss/node": { "version": "4.1.14", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", @@ -4547,6 +4774,20 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, "node_modules/@xobotyi/scrollbar-width": { "version": "1.9.5", "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", diff --git a/package.json b/package.json index 899a77b..f0289e2 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "vite build", + "build:check": "tsc && vite build", "preview": "vite preview", "tauri": "tauri" }, @@ -41,6 +42,8 @@ "@tanstack/react-query": "^5.90.2", "@tanstack/react-router": "^1.132.33", "@tanstack/react-table": "^8.21.3", + "@tauri-apps/api": "^2", + "@tauri-apps/plugin-opener": "^2", "axios": "^1.12.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -66,26 +69,25 @@ "tw-animate-css": "^1.4.0", "vaul": "^1.1.2", "zod": "^4.1.11", - "zustand": "^5.0.8", - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-opener": "^2" + "zustand": "^5.0.8" }, "devDependencies": { "@eslint/js": "^9.36.0", "@tailwindcss/vite": "^4.1.14", "@tanstack/react-router-devtools": "^1.132.33", "@tanstack/router-plugin": "^1.132.33", + "@tauri-apps/cli": "^2", "@types/node": "^24.6.2", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", + "@vitejs/plugin-react-swc": "^3.11.0", "eslint": "^9.36.0", "eslint-plugin-react-hooks": "^6.1.0", "eslint-plugin-react-refresh": "^0.4.23", "tailwindcss": "^4.1.14", "typescript": "^5.9.3", "typescript-eslint": "^8.45.0", - "vite": "^5.4.10", - "@tauri-apps/cli": "^2" + "vite": "^5.4.10" } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e91e54c..655258b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Nginx WAF", - "version": "0.1.0", + "version": "0.1.1", "identifier": "com.tinyactive.nginx-waf", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/access-lists/AccessListCard.tsx b/src/components/access-lists/AccessListCard.tsx new file mode 100644 index 0000000..41711cc --- /dev/null +++ b/src/components/access-lists/AccessListCard.tsx @@ -0,0 +1,242 @@ +import { useState } from 'react'; +import { Edit, Trash2, Globe, Shield, ShieldCheck, Power } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Switch } from '@/components/ui/switch'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; +import { useToast } from '@/hooks/use-toast'; +import { useDeleteAccessList, useToggleAccessList } from '@/queries/access-lists.query-options'; +import { AccessListFormDialog } from './AccessListFormDialog'; +import type { AccessList } from '@/services/access-lists.service'; + +interface AccessListCardProps { + accessList: AccessList; +} + +export function AccessListCard({ accessList }: AccessListCardProps) { + const { toast } = useToast(); + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + + const deleteMutation = useDeleteAccessList(); + const toggleMutation = useToggleAccessList(); + + const handleDelete = async () => { + try { + await deleteMutation.mutateAsync(accessList.id); + toast({ + title: 'Success', + description: 'Access list deleted successfully', + }); + setIsDeleteDialogOpen(false); + } catch (error: any) { + toast({ + title: 'Error', + description: error.response?.data?.message || 'Failed to delete access list', + variant: 'destructive', + }); + } + }; + + const handleToggle = async (enabled: boolean) => { + try { + await toggleMutation.mutateAsync({ id: accessList.id, enabled }); + toast({ + title: 'Success', + description: `Access list ${enabled ? 'enabled' : 'disabled'} successfully`, + }); + } catch (error: any) { + toast({ + title: 'Error', + description: error.response?.data?.message || 'Failed to toggle access list', + variant: 'destructive', + }); + } + }; + + const getTypeIcon = () => { + switch (accessList.type) { + case 'ip_whitelist': + return ; + case 'http_basic_auth': + return ; + case 'combined': + return ; + default: + return ; + } + }; + + const getTypeLabel = () => { + switch (accessList.type) { + case 'ip_whitelist': + return 'IP Whitelist'; + case 'http_basic_auth': + return 'HTTP Basic Auth'; + case 'combined': + return 'Combined'; + default: + return accessList.type; + } + }; + + return ( + <> + + +
+
+
+ {accessList.name} + + {accessList.enabled ? 'Enabled' : 'Disabled'} + + + {getTypeIcon()} + {getTypeLabel()} + +
+ {accessList.description && ( +

{accessList.description}

+ )} +
+ +
+ + + +
+
+
+ + +
+ {/* IP Whitelist Info */} + {(accessList.type === 'ip_whitelist' || accessList.type === 'combined') && ( +
+

Allowed IPs

+
+ {accessList.allowedIps && accessList.allowedIps.length > 0 ? ( + accessList.allowedIps.slice(0, 3).map((ip, index) => ( + + {ip} + + )) + ) : ( +

No IPs configured

+ )} + {accessList.allowedIps && accessList.allowedIps.length > 3 && ( + + +{accessList.allowedIps.length - 3} more + + )} +
+
+ )} + + {/* Auth Users Info */} + {(accessList.type === 'http_basic_auth' || accessList.type === 'combined') && ( +
+

Auth Users

+
+ {accessList.authUsers && accessList.authUsers.length > 0 ? ( + accessList.authUsers.slice(0, 3).map((user) => ( + + {user.username} + + )) + ) : ( +

No users configured

+ )} + {accessList.authUsers && accessList.authUsers.length > 3 && ( + + +{accessList.authUsers.length - 3} more + + )} +
+
+ )} + + {/* Assigned Domains */} +
+

+ + Assigned Domains +

+
+ {accessList.domains && accessList.domains.length > 0 ? ( + accessList.domains.slice(0, 3).map((domainLink) => ( + + {domainLink.domain.name} + + )) + ) : ( +

No domains assigned

+ )} + {accessList.domains && accessList.domains.length > 3 && ( + + +{accessList.domains.length - 3} more + + )} +
+
+
+
+
+ + {/* Edit Dialog */} + + + {/* Delete Confirmation Dialog */} + + + + Delete Access List + + Are you sure you want to delete "{accessList.name}"? This action cannot be undone. + + + + Cancel + + {deleteMutation.isPending ? 'Deleting...' : 'Delete'} + + + + + + ); +} diff --git a/src/components/access-lists/AccessListFormDialog.tsx b/src/components/access-lists/AccessListFormDialog.tsx new file mode 100644 index 0000000..576b059 --- /dev/null +++ b/src/components/access-lists/AccessListFormDialog.tsx @@ -0,0 +1,628 @@ +import { useState, useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { Plus, Trash2, Eye, EyeOff } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Switch } from '@/components/ui/switch'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Badge } from '@/components/ui/badge'; +import { useToast } from '@/hooks/use-toast'; +import { + useCreateAccessList, + useUpdateAccessList, + useRemoveFromDomain, +} from '@/queries/access-lists.query-options'; +import { domainQueryOptions } from '@/queries/domain.query-options'; +import type { AccessList } from '@/services/access-lists.service'; + +interface AccessListFormDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + accessList?: AccessList; +} + +interface AuthUserFormData { + username: string; + password: string; + description?: string; + showPassword?: boolean; +} + +export function AccessListFormDialog({ + open, + onOpenChange, + accessList, +}: AccessListFormDialogProps) { + const { toast } = useToast(); + const isEditMode = !!accessList; + + const createMutation = useCreateAccessList(); + const updateMutation = useUpdateAccessList(); + const removeFromDomainMutation = useRemoveFromDomain(); + + // Fetch domains for selection + const { data: domainsData } = useQuery(domainQueryOptions.all({ page: 1, limit: 100 })); + const domains = domainsData?.data || []; + + const [formData, setFormData] = useState({ + name: '', + description: '', + type: 'ip_whitelist' as 'ip_whitelist' | 'http_basic_auth' | 'combined', + enabled: true, + }); + + const [allowedIps, setAllowedIps] = useState(['']); + const [authUsers, setAuthUsers] = useState([ + { username: '', password: '', description: '', showPassword: false }, + ]); + const [selectedDomains, setSelectedDomains] = useState([]); + const [originalDomainIds, setOriginalDomainIds] = useState([]); // Track original domains for edit mode + + // Reset form when dialog opens or access list changes + useEffect(() => { + if (open) { + if (accessList) { + // Edit mode + setFormData({ + name: accessList.name, + description: accessList.description || '', + type: accessList.type, + enabled: accessList.enabled, + }); + + setAllowedIps( + accessList.allowedIps && accessList.allowedIps.length > 0 + ? accessList.allowedIps + : [''] + ); + + setAuthUsers( + accessList.authUsers && accessList.authUsers.length > 0 + ? accessList.authUsers.map((u) => ({ + username: u.username, + password: '', // Don't populate password for security + description: u.description || '', + showPassword: false, + })) + : [{ username: '', password: '', description: '', showPassword: false }] + ); + + const domainIds = accessList.domains?.map((d) => d.domainId) || []; + setSelectedDomains(domainIds); + setOriginalDomainIds(domainIds); // Store original for comparison + } else { + // Create mode - reset form + setFormData({ + name: '', + description: '', + type: 'ip_whitelist', + enabled: true, + }); + setAllowedIps(['']); + setAuthUsers([{ username: '', password: '', description: '', showPassword: false }]); + setSelectedDomains([]); + setOriginalDomainIds([]); // Reset original domains + } + } + }, [open, accessList]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // Validation + if (!formData.name.trim()) { + toast({ + title: 'Error', + description: 'Access list name is required', + variant: 'destructive', + }); + return; + } + + // Validate based on type + if (formData.type === 'ip_whitelist' || formData.type === 'combined') { + const validIps = allowedIps.filter((ip) => ip.trim()); + if (validIps.length === 0) { + toast({ + title: 'Error', + description: 'At least one IP address is required for IP whitelist', + variant: 'destructive', + }); + return; + } + } + + if (formData.type === 'http_basic_auth' || formData.type === 'combined') { + // In edit mode, password is optional (empty = keep existing) + // In create mode, password is required + const validUsers = authUsers.filter((u) => { + if (isEditMode) { + return u.username.trim(); // Only username required in edit mode + } + return u.username.trim() && u.password.trim(); // Both required in create mode + }); + + if (validUsers.length === 0) { + toast({ + title: 'Error', + description: 'At least one auth user is required for HTTP Basic Auth', + variant: 'destructive', + }); + return; + } + + // Validate username and password length + for (const user of validUsers) { + if (!user.username.trim()) { + toast({ + title: 'Error', + description: 'Username is required for all auth users', + variant: 'destructive', + }); + return; + } + // In create mode, password is required + // In edit mode, empty password means keep existing password + if (!isEditMode && !user.password.trim()) { + toast({ + title: 'Error', + description: 'Password is required for new auth users', + variant: 'destructive', + }); + return; + } + // If password is provided, validate minimum length + if (user.password.trim() && user.password.length < 4) { + toast({ + title: 'Error', + description: 'Password must be at least 4 characters', + variant: 'destructive', + }); + return; + } + } + } + + const payload = { + ...formData, + allowedIps: + formData.type === 'ip_whitelist' || formData.type === 'combined' + ? allowedIps.filter((ip) => ip.trim()) + : undefined, + authUsers: + formData.type === 'http_basic_auth' || formData.type === 'combined' + ? authUsers + .filter((u) => { + // In create mode, require both username and password + // In edit mode, only require username (empty password = keep existing) + if (isEditMode) { + return u.username.trim(); + } + return u.username.trim() && u.password.trim(); + }) + .map(({ username, password, description }) => ({ + username, + password, // In edit mode, empty password will be handled by backend + description, + })) + : undefined, + domainIds: selectedDomains.length > 0 ? selectedDomains : undefined, + }; + + try { + if (isEditMode) { + // Detect removed domains (domains that were assigned but now unchecked) + const removedDomainIds = originalDomainIds.filter( + (domainId) => !selectedDomains.includes(domainId) + ); + + // Remove domains first if any + if (removedDomainIds.length > 0) { + await Promise.all( + removedDomainIds.map((domainId) => + removeFromDomainMutation.mutateAsync({ + accessListId: accessList.id, + domainId, + }) + ) + ); + } + + // Then update the access list + await updateMutation.mutateAsync({ id: accessList.id, data: payload }); + toast({ + title: 'Success', + description: 'Access list updated successfully', + }); + } else { + await createMutation.mutateAsync(payload); + toast({ + title: 'Success', + description: 'Access list created successfully', + }); + } + onOpenChange(false); + } catch (error: any) { + toast({ + title: 'Error', + description: error.response?.data?.message || 'Failed to save access list', + variant: 'destructive', + }); + } + }; + + const addIpField = () => { + setAllowedIps([...allowedIps, '']); + }; + + const removeIpField = (index: number) => { + setAllowedIps(allowedIps.filter((_, i) => i !== index)); + }; + + const updateIpField = (index: number, value: string) => { + const newIps = [...allowedIps]; + newIps[index] = value; + setAllowedIps(newIps); + }; + + const addAuthUser = () => { + setAuthUsers([ + ...authUsers, + { username: '', password: '', description: '', showPassword: false }, + ]); + }; + + const removeAuthUser = (index: number) => { + setAuthUsers(authUsers.filter((_, i) => i !== index)); + }; + + const updateAuthUser = ( + index: number, + field: keyof AuthUserFormData, + value: string | boolean + ) => { + const newUsers = [...authUsers]; + (newUsers[index] as any)[field] = value; + setAuthUsers(newUsers); + }; + + const toggleDomainSelection = (domainId: string) => { + console.log('Toggling domain:', domainId); + setSelectedDomains((prev) => { + const isSelected = prev.includes(domainId); + console.log('Current selected:', prev); + console.log('Is selected:', isSelected); + const newSelection = isSelected + ? prev.filter((id) => id !== domainId) + : [...prev, domainId]; + console.log('New selection:', newSelection); + return newSelection; + }); + }; + + const isPending = createMutation.isPending || updateMutation.isPending; + + return ( + + + + + {isEditMode ? 'Edit Access List' : 'Create Access List'} + + + {isEditMode + ? 'Update access list configuration' + : 'Create a new access list to restrict access to your domains'} + + + +
+ {/* Basic Information */} +
+
+ + + setFormData({ ...formData, name: e.target.value }) + } + placeholder="e.g., admin-panel-access" + disabled={isPending} + required + /> +
+ +
+ +