-
Notifications
You must be signed in to change notification settings - Fork 0
Implement comprehensive DHCP backend and frontend functionality #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…ontroller with config generation, routes with authentication, service management integration, validation and backup functionality
…PI client with auth support, complete DHCP configuration component with tabs, forms, validation, service control, and responsive design
…ered with comprehensive functionality including shared package (types/validators/transformers), backend (controller/routes/service management), frontend (UI with tabs/forms/validation), and integration testing. IP conflict detection and authentication working correctly.
…proach and eliminated redundancy
WalkthroughThis update introduces a complete DHCP configuration management system across backend, shared, and frontend codebases. It adds data types, validation schemas, transformation utilities, backend controllers and routes, frontend API clients, and a comprehensive React UI for DHCP settings. Extensive tests and documentation are also included. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as DHCPConfig (React)
participant API as dhcp.ts (API client)
participant BE as Backend Express API
participant Controller as dhcpController
participant Service as DHCP Service Manager
UI->>API: getDhcpConfigurationAPI()
API->>BE: GET /api/dhcp/config
BE->>Controller: getCurrentDhcpConfiguration()
Controller-->>BE: DHCP config + status
BE-->>API: Response
API-->>UI: DHCP config + status
UI->>API: updateDhcpConfigurationAPI(formData)
API->>BE: PUT /api/dhcp/config
BE->>Controller: updateDhcpConfiguration()
Controller->>Controller: Validate & Write config
Controller->>Service: Reload DHCP Service
Service-->>Controller: Status
Controller-->>BE: Update response
BE-->>API: Response
API-->>UI: Update response
UI->>API: controlDhcpServiceAPI('restart')
API->>BE: POST /api/dhcp/service/restart
BE->>Controller: controlDhcpService()
Controller->>Service: Restart DHCP Service
Service-->>Controller: Status
Controller-->>BE: Status response
BE-->>API: Status response
API-->>UI: Status response
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (3)
💤 Files with no reviewable changes (1)
✅ Files skipped from review due to trivial changes (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🧹 Nitpick comments (7)
packages/shared/src/validators/__tests__/dhcpValidators.test.ts (2)
243-243: Consider if empty subnets should always be invalid.The test marks an empty subnets array as invalid, but there might be legitimate scenarios where a DHCP configuration could temporarily have no subnets defined. Consider whether this validation rule aligns with real-world use cases.
106-140: Consider adding more edge case tests for utility functions.While the current tests are good, consider adding more edge cases:
suggestIpRange: Test with different subnet sizes (/16, /8, etc.)calculateBroadcastAddress: Test with non-standard subnet masksgenerateDefaultDhcpConfig: Consider testing the immutability of returned objectsapps/ui/src/features/configuration/dhcp/DHCPConfig.tsx (1)
111-121: Consider making default subnet values configurable.The hardcoded default values for new subnets might not be suitable for all network environments. Consider making these configurable or deriving them from existing network configuration.
+ // Consider moving to configuration or calculating from existing subnets + const getDefaultSubnetValues = () => ({ + network: '192.168.1.0', + netmask: '255.255.255.0', + rangeStart: '192.168.1.100', + rangeEnd: '192.168.1.200', + defaultGateway: '192.168.1.1', + domainNameServers: '8.8.8.8, 8.8.4.4', + broadcastAddress: '192.168.1.255', + subnetMask: '255.255.255.0' + }); + const addSubnet = () => { appendSubnet({ id: uuidv4(), - network: '192.168.1.0', - netmask: '255.255.255.0', - rangeStart: '192.168.1.100', - rangeEnd: '192.168.1.200', - defaultGateway: '192.168.1.1', - domainNameServers: '8.8.8.8, 8.8.4.4', - broadcastAddress: '192.168.1.255', - subnetMask: '255.255.255.0' + ...getDefaultSubnetValues() }); };packages/shared/src/validators/dhcpFormValidator.ts (1)
87-87: Consider validating against reserved DHCP option codes.The current validation allows codes 1-254, but several DHCP option codes are reserved and shouldn't be user-configurable. For example:
- Code 0: Pad Option
- Code 255: End Option
- Codes 1-12: Basic network parameters (subnet mask, routers, etc.)
Consider maintaining a list of reserved codes and validating against them.
packages/shared/src/validators/dhcpTransformers.ts (1)
208-209: Verify subnet mask calculation for edge cases.The host bits calculation using
Math.log2(256 - maskOctets[rangeOctetIndex])assumes standard subnet mask patterns. While the input should be validated byisValidNetmask, consider adding a safety check or comment explaining this assumption.docs/implementation-plan/dhcp-config-implementation-plan.md (2)
7-7: Replace "exact same" for brevity.Consider shortening the phrasing to improve readability. For example:
- The DHCP implementation will follow the exact same pattern as your existing DNS and HTTP configurations: + The DHCP implementation will follow the same pattern as your existing DNS and HTTP configurations:🧰 Tools
🪛 LanguageTool
[style] ~7-~7: ‘exact same’ might be wordy. Consider a shorter alternative.
Context: ...The DHCP implementation will follow the exact same pattern as your existing DNS and HTTP c...(EN_WORDINESS_PREMIUM_EXACT_SAME)
1214-1214: Replace "exact same" for consistency.Similarly, update the closing sentence:
- This plan follows the exact same patterns as your existing DNS and HTTP implementations, ensuring consistency and maintainability across your codebase. + This plan follows the same patterns as your existing DNS and HTTP implementations, ensuring consistency and maintainability across your codebase.🧰 Tools
🪛 LanguageTool
[style] ~1214-~1214: ‘exact same’ might be wordy. Consider a shorter alternative.
Context: ... for development This plan follows the exact same patterns as your existing DNS and HTTP ...(EN_WORDINESS_PREMIUM_EXACT_SAME)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
apps/backend/src/app.ts(2 hunks)apps/backend/src/controllers/dhcpController.ts(1 hunks)apps/backend/src/routes/dhcpRoutes.ts(1 hunks)apps/ui/src/features/configuration/dhcp/DHCPConfig.tsx(1 hunks)apps/ui/src/lib/api/dhcp.ts(1 hunks)apps/ui/src/lib/api/index.ts(1 hunks)docs/implementation-plan/dhcp-config-implementation-plan.md(1 hunks)packages/shared/src/index.ts(1 hunks)packages/shared/src/types/dhcp.ts(1 hunks)packages/shared/src/types/index.ts(1 hunks)packages/shared/src/validators/__tests__/dhcpValidators.test.ts(1 hunks)packages/shared/src/validators/dhcpFormValidator.ts(1 hunks)packages/shared/src/validators/dhcpTransformers.ts(1 hunks)packages/shared/src/validators/index.ts(2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
apps/backend/src/routes/dhcpRoutes.ts (2)
apps/backend/src/controllers/dhcpController.ts (5)
getCurrentDhcpConfiguration(334-400)updateDhcpConfiguration(263-331)validateDhcpConfiguration(403-455)getDhcpServiceStatus(458-493)controlDhcpService(496-563)apps/backend/src/middlewares/authMiddleware.ts (1)
protect(51-51)
apps/ui/src/lib/api/dhcp.ts (1)
packages/shared/src/types/dhcp.ts (3)
DhcpConfiguration(55-59)DhcpUpdateResponse(98-106)DhcpServiceResponse(108-116)
🪛 LanguageTool
docs/implementation-plan/dhcp-config-implementation-plan.md
[style] ~7-~7: ‘exact same’ might be wordy. Consider a shorter alternative.
Context: ...The DHCP implementation will follow the exact same pattern as your existing DNS and HTTP c...
(EN_WORDINESS_PREMIUM_EXACT_SAME)
[style] ~1214-~1214: ‘exact same’ might be wordy. Consider a shorter alternative.
Context: ... for development This plan follows the exact same patterns as your existing DNS and HTTP ...
(EN_WORDINESS_PREMIUM_EXACT_SAME)
🔇 Additional comments (23)
apps/ui/src/lib/api/index.ts (1)
27-27: LGTM! Clean integration following established patterns.The DHCP API export follows the same pattern as other API modules and maintains alphabetical ordering.
apps/backend/src/app.ts (2)
11-11: LGTM! Import follows established patterns.The DHCP routes import is properly placed with other route imports.
38-38: LGTM! Route mounting follows API conventions.The DHCP routes are properly mounted under
/api/dhcp, maintaining consistency with other service routes.packages/shared/src/types/index.ts (1)
3-3: LGTM! Consistent with established export patterns.The DHCP types export maintains alphabetical ordering and follows the same pattern as other type exports.
apps/backend/src/routes/dhcpRoutes.ts (1)
1-28: Excellent route structure with appropriate security controls.The DHCP routes are well-organized with a sensible security model:
- Read operations (
GET /config,GET /status) are publicly accessible- Write/control operations (
PUT /config,POST /validate,POST /service/:action) are properly protected with authentication middleware- The route structure follows RESTful conventions and aligns well with the controller functions
The imports and route definitions are clean and consistent with the existing codebase patterns.
packages/shared/src/validators/__tests__/dhcpValidators.test.ts (2)
25-104: Excellent validator test coverage with comprehensive edge cases.The validation function tests are thorough and well-organized:
- IP address validation covers all major scenarios including edge cases
- MAC address validation tests multiple formats (colon and dash separated)
- Netmask validation properly tests contiguous bit requirements
- Hostname validation covers RFC compliance rules
- Network membership tests validate subnet calculations
The test structure is clean and easy to understand.
141-202: Robust round-trip transformation testing ensures data integrity.The round-trip transformation test is particularly valuable as it verifies that data can be converted from form format to API format and back without loss of information. This is critical for maintaining data consistency across the frontend and backend.
The test covers all major configuration elements including subnets, host reservations, and global options.
packages/shared/src/index.ts (1)
28-50: LGTM! Well-organized DHCP exports.The DHCP-related exports are comprehensive and well-structured. The naming convention with prefixes (e.g.,
dhcpIsValidIpAddress) effectively prevents naming conflicts with existing validators.packages/shared/src/validators/index.ts (2)
10-11: LGTM! Consistent import pattern.The DHCP validator imports follow the established pattern used for other validator modules in this file.
54-81: LGTM! Well-structured DHCP re-exports.The DHCP validator and transformer re-exports are comprehensive and follow the established pattern. The aliasing for conflicting names (e.g.,
parseStringToArray as dhcpParseStringToArray) and type exports are properly handled.apps/ui/src/features/configuration/dhcp/DHCPConfig.tsx (1)
1-16: LGTM! Comprehensive imports for DHCP component.The imports are well-organized and include all necessary dependencies for a feature-rich DHCP configuration component.
apps/ui/src/lib/api/dhcp.ts (6)
21-36: LGTM! Consistent error handling pattern.The centralized error handling function provides consistent error formatting and proper error propagation throughout the API client.
39-57: LGTM! Well-structured GET configuration endpoint.The function properly handles authentication, error cases, and response parsing with appropriate TypeScript typing.
59-79: LGTM! Proper PUT endpoint implementation.The update function correctly sends form data and handles responses with proper error handling.
81-101: LGTM! Validation endpoint follows consistent pattern.The validation endpoint maintains consistency with other API functions and provides proper error handling.
103-119: LGTM! Service status endpoint with proper error handling.The status endpoint correctly handles authentication and provides appropriate error messages.
121-137: LGTM! Service control endpoint with dynamic actions.The control endpoint properly parameterizes actions and maintains consistent error handling patterns.
packages/shared/src/types/dhcp.ts (3)
1-59: LGTM! Comprehensive DHCP configuration types.The type definitions are well-structured with clear separation of concerns:
- Core configuration interfaces (
DhcpServerConfig,DhcpSubnet, etc.)- Proper use of optional properties
- Logical composition with
DhcpConfigurationextending base config
61-95: LGTM! Well-designed form types for UI compatibility.The form types appropriately use string representations for numeric values (lease times) and flattened structures for easier form handling with react-hook-form. This separation from API types is a good design decision.
97-126: LGTM! Comprehensive API response types.The response types provide good coverage for all API operations with:
- Proper error handling structures
- Service status definitions
- Clear success/failure indicators
packages/shared/src/validators/dhcpFormValidator.ts (2)
4-13: LGTM!The IPv4 address validation is correctly implemented with both format and range checks.
15-18: LGTM!The MAC address validation correctly handles both colon and hyphen separators.
apps/backend/src/controllers/dhcpController.ts (1)
23-26:Details
❌ Incorrect review comment
Ensure test directories exist in development environment.
The non-production paths use
'./test/dhcp/config'which might not exist. Consider creating these directories on startup or using a configuration-based approach.Run this script to check if the test directories are properly created:
🏁 Script executed:
#!/bin/bash # Check if test directories exist or are created by the application fd -t d "test" -x echo "Found test directory: {}" rg -A 5 "ensureDirectoryExists.*test"Length of output: 376
🏁 Script executed:
#!/bin/bash set -e # Show fs operations in dhcpController echo "---- fs operations in dhcpController.ts ----" rg -n "fs\." -C2 apps/backend/src/controllers/dhcpController.ts || true # Search for mkdir usage anywhere in backend sources echo "---- mkdir invocations in apps/backend/src ----" rg -n "mkdir" -C2 apps/backend/src || true # Search for ensureDir (fs-extra) usage echo "---- ensureDir invocations in apps/backend/src ----" rg -n "ensureDir" -C2 apps/backend/src || trueLength of output: 10764
Directories auto-created by existing helper
Inapps/backend/src/controllers/dhcpController.ts, the calls
await ensureDirectoryExists(DHCPD_CONF_DIR)(line 278)await ensureDirectoryExists(DHCPD_BACKUP_DIR)(line 279)
automatically create./test/dhcp/...directories at runtime. No manual setup is required.Likely an incorrect or invalid review comment.
| export function DHCPConfig() { | ||
| return ( | ||
| <div className="space-y-6"> | ||
| <div className="flex items-center justify-between"> | ||
| <div className="space-y-1"> | ||
| <h4 className="text-sm font-medium leading-none">DHCP Server Status</h4> | ||
| <p className="text-sm text-muted-foreground">Enable or disable the DHCP server</p> | ||
| const [isLoading, setIsLoading] = React.useState(true); | ||
| const [isSaving, setIsSaving] = React.useState(false); | ||
| const [serviceStatus, setServiceStatus] = React.useState<'running' | 'stopped' | 'unknown'>('unknown'); | ||
|
|
||
| const form = useForm<DhcpConfigFormValues>({ | ||
| resolver: zodResolver(dhcpConfigSchema), | ||
| defaultValues: { | ||
| dhcpServerStatus: false, | ||
| domainName: 'local', | ||
| domainNameServers: '8.8.8.8, 8.8.4.4', | ||
| defaultLeaseTime: '86400', | ||
| maxLeaseTime: '604800', | ||
| authoritative: true, | ||
| ddnsUpdateStyle: 'none' as const, | ||
| subnets: [], | ||
| hostReservations: [], | ||
| globalOptions: [] | ||
| } | ||
| }); | ||
|
|
||
| const { fields: subnetFields, append: appendSubnet, remove: removeSubnet } = useFieldArray({ | ||
| control: form.control, | ||
| name: 'subnets' | ||
| }); | ||
|
|
||
| const { fields: hostFields, append: appendHost, remove: removeHost } = useFieldArray({ | ||
| control: form.control, | ||
| name: 'hostReservations' | ||
| }); | ||
|
|
||
| const { fields: optionFields, append: appendOption, remove: removeOption } = useFieldArray({ | ||
| control: form.control, | ||
| name: 'globalOptions' | ||
| }); | ||
|
|
||
| // Load existing configuration and service status | ||
| React.useEffect(() => { | ||
| const loadConfiguration = async () => { | ||
| try { | ||
| const [configResponse, statusResponse] = await Promise.all([ | ||
| getDhcpConfigurationAPI(), | ||
| getDhcpServiceStatusAPI().catch(() => ({ data: { status: 'unknown' } })) | ||
| ]); | ||
|
|
||
| const formData = transformDhcpApiToForm(configResponse.data); | ||
| form.reset(formData as any); // Type coercion for form reset | ||
| setServiceStatus(statusResponse.data.status as any); | ||
| } catch (error) { | ||
| console.error('Failed to load DHCP configuration:', error); | ||
| toast({ | ||
| title: "Error", | ||
| description: "Failed to load DHCP configuration", | ||
| variant: "destructive", | ||
| }); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| loadConfiguration(); | ||
| }, [form]); | ||
|
|
||
| // Submit handler | ||
| const onSubmit = async (data: DhcpConfigFormValues) => { | ||
| setIsSaving(true); | ||
| try { | ||
| await updateDhcpConfigurationAPI(data); | ||
| toast({ | ||
| title: "Success", | ||
| description: "DHCP configuration updated successfully", | ||
| }); | ||
|
|
||
| // Refresh service status | ||
| try { | ||
| const statusResponse = await getDhcpServiceStatusAPI(); | ||
| setServiceStatus(statusResponse.data.status as any); | ||
| } catch (error) { | ||
| console.warn('Failed to refresh service status:', error); | ||
| } | ||
| } catch (error: any) { | ||
| toast({ | ||
| title: "Error", | ||
| description: error?.data?.message || "Failed to update DHCP configuration", | ||
| variant: "destructive", | ||
| }); | ||
| } finally { | ||
| setIsSaving(false); | ||
| } | ||
| }; | ||
|
|
||
| // Add new subnet | ||
| const addSubnet = () => { | ||
| appendSubnet({ | ||
| id: uuidv4(), | ||
| network: '192.168.1.0', | ||
| netmask: '255.255.255.0', | ||
| rangeStart: '192.168.1.100', | ||
| rangeEnd: '192.168.1.200', | ||
| defaultGateway: '192.168.1.1', | ||
| domainNameServers: '8.8.8.8, 8.8.4.4', | ||
| broadcastAddress: '192.168.1.255', | ||
| subnetMask: '255.255.255.0' | ||
| }); | ||
| }; | ||
|
|
||
| // Add new host reservation | ||
| const addHostReservation = () => { | ||
| appendHost({ | ||
| id: uuidv4(), | ||
| hostname: '', | ||
| macAddress: '', | ||
| fixedAddress: '' | ||
| }); | ||
| }; | ||
|
|
||
| // Add new global option | ||
| const addGlobalOption = () => { | ||
| appendOption({ | ||
| id: uuidv4(), | ||
| name: '', | ||
| value: '' | ||
| }); | ||
| }; | ||
|
|
||
| // Control service | ||
| const handleServiceAction = async (action: string) => { | ||
| try { | ||
| await controlDhcpServiceAPI(action); | ||
| const statusResponse = await getDhcpServiceStatusAPI(); | ||
| setServiceStatus(statusResponse.data.status as any); | ||
| toast({ | ||
| title: "Success", | ||
| description: `DHCP service ${action} completed successfully`, | ||
| }); | ||
| } catch (error: any) { | ||
| toast({ | ||
| title: "Error", | ||
| description: error?.data?.message || `Failed to ${action} DHCP service`, | ||
| variant: "destructive", | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| if (isLoading) { | ||
| return ( | ||
| <div className="flex items-center justify-center p-8"> | ||
| <div className="text-center"> | ||
| <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto"></div> | ||
| <p className="mt-2 text-sm text-muted-foreground">Loading DHCP configuration...</p> | ||
| </div> | ||
| <Switch /> | ||
| </div> | ||
| <Separator /> | ||
| <div className="grid gap-4"> | ||
| <div className="grid grid-cols-2 gap-4"> | ||
| <div className="grid gap-2"> | ||
| <Label htmlFor="start-ip">Start IP</Label> | ||
| <Input id="start-ip" placeholder="192.168.1.100" /> | ||
| </div> | ||
| <div className="grid gap-2"> | ||
| <Label htmlFor="end-ip">End IP</Label> | ||
| <Input id="end-ip" placeholder="192.168.1.200" /> | ||
| </div> | ||
| </div> | ||
| <div className="grid grid-cols-2 gap-4"> | ||
| <div className="grid gap-2"> | ||
| <Label htmlFor="subnet-mask">Subnet Mask</Label> | ||
| <Input id="subnet-mask" placeholder="255.255.255.0" /> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <Form {...form}> | ||
| <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> | ||
| <div className="flex items-center justify-between"> | ||
| <div> | ||
| <h2 className="text-2xl font-bold">DHCP Configuration</h2> | ||
| <p className="text-muted-foreground">Configure your DHCP server settings</p> | ||
| </div> | ||
| <div className="grid gap-2"> | ||
| <Label htmlFor="lease-time">Lease Time (hours)</Label> | ||
| <Input id="lease-time" type="number" placeholder="24" /> | ||
| <div className="flex items-center gap-2"> | ||
| <div className={`px-2 py-1 rounded-full text-xs font-medium ${ | ||
| serviceStatus === 'running' ? 'bg-green-100 text-green-800' : | ||
| serviceStatus === 'stopped' ? 'bg-red-100 text-red-800' : | ||
| 'bg-gray-100 text-gray-800' | ||
| }`}> | ||
| Service: {serviceStatus} | ||
| </div> | ||
| {serviceStatus === 'stopped' && ( | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={() => handleServiceAction('start')} | ||
| > | ||
| Start Service | ||
| </Button> | ||
| )} | ||
| {serviceStatus === 'running' && ( | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={() => handleServiceAction('restart')} | ||
| > | ||
| Restart Service | ||
| </Button> | ||
| )} | ||
| </div> | ||
| </div> | ||
| <div className="grid gap-2"> | ||
| <Label htmlFor="gateway">Default Gateway</Label> | ||
| <Input id="gateway" placeholder="192.168.1.1" /> | ||
| </div> | ||
| <div className="grid gap-2"> | ||
| <Label>Static Leases</Label> | ||
| <ScrollArea className="h-[200px] w-full rounded-md border"> | ||
| <div className="p-4 space-y-4"> | ||
| {Array.from({ length: 2 }).map((_, i) => ( | ||
| <div key={i} className="grid grid-cols-3 gap-2"> | ||
| <Input placeholder="MAC Address" defaultValue={i === 0 ? "00:11:22:33:44:55" : "66:77:88:99:AA:BB"} /> | ||
| <Input placeholder="IP Address" defaultValue={i === 0 ? "192.168.1.10" : "192.168.1.11"} /> | ||
| <Button variant="outline" size="icon" className="w-full">+</Button> | ||
|
|
||
| <Tabs defaultValue="general" className="w-full"> | ||
| <TabsList className="grid w-full grid-cols-4"> | ||
| <TabsTrigger value="general">General</TabsTrigger> | ||
| <TabsTrigger value="subnets">Subnets</TabsTrigger> | ||
| <TabsTrigger value="reservations">Host Reservations</TabsTrigger> | ||
| <TabsTrigger value="options">Global Options</TabsTrigger> | ||
| </TabsList> | ||
|
|
||
| <TabsContent value="general" className="space-y-4"> | ||
| <Card> | ||
| <CardHeader> | ||
| <CardTitle className="flex items-center gap-2"> | ||
| <Server className="h-5 w-5" /> | ||
| DHCP Server Settings | ||
| </CardTitle> | ||
| </CardHeader> | ||
| <CardContent className="space-y-4"> | ||
| <FormField | ||
| control={form.control} | ||
| name="dhcpServerStatus" | ||
| render={({ field }) => ( | ||
| <FormItem className="flex items-center justify-between"> | ||
| <div> | ||
| <FormLabel>DHCP Server Status</FormLabel> | ||
| <FormDescription> | ||
| Enable or disable the DHCP server | ||
| </FormDescription> | ||
| </div> | ||
| <FormControl> | ||
| <Switch | ||
| checked={field.value} | ||
| onCheckedChange={field.onChange} | ||
| /> | ||
| </FormControl> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
|
|
||
| <div className="grid grid-cols-2 gap-4"> | ||
| <FormField | ||
| control={form.control} | ||
| name="domainName" | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>Domain Name</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="example.com" {...field} /> | ||
| </FormControl> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
|
|
||
| <FormField | ||
| control={form.control} | ||
| name="domainNameServers" | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>DNS Servers</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="8.8.8.8, 8.8.4.4" {...field} /> | ||
| </FormControl> | ||
| <FormDescription>Comma-separated list of DNS servers</FormDescription> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
| </div> | ||
| ))} | ||
|
|
||
| <div className="grid grid-cols-2 gap-4"> | ||
| <FormField | ||
| control={form.control} | ||
| name="defaultLeaseTime" | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>Default Lease Time (seconds)</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="86400" {...field} /> | ||
| </FormControl> | ||
| <FormDescription>Default: 86400 (24 hours)</FormDescription> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
|
|
||
| <FormField | ||
| control={form.control} | ||
| name="maxLeaseTime" | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>Max Lease Time (seconds)</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="604800" {...field} /> | ||
| </FormControl> | ||
| <FormDescription>Default: 604800 (7 days)</FormDescription> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="grid grid-cols-2 gap-4"> | ||
| <FormField | ||
| control={form.control} | ||
| name="authoritative" | ||
| render={({ field }) => ( | ||
| <FormItem className="flex items-center justify-between"> | ||
| <div> | ||
| <FormLabel>Authoritative Server</FormLabel> | ||
| <FormDescription> | ||
| Act as the authoritative DHCP server for this network | ||
| </FormDescription> | ||
| </div> | ||
| <FormControl> | ||
| <Switch | ||
| checked={field.value} | ||
| onCheckedChange={field.onChange} | ||
| /> | ||
| </FormControl> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
|
|
||
| <FormField | ||
| control={form.control} | ||
| name="ddnsUpdateStyle" | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>DDNS Update Style</FormLabel> | ||
| <Select onValueChange={field.onChange} defaultValue={field.value}> | ||
| <FormControl> | ||
| <SelectTrigger> | ||
| <SelectValue placeholder="Select DDNS style" /> | ||
| </SelectTrigger> | ||
| </FormControl> | ||
| <SelectContent> | ||
| <SelectItem value="none">None</SelectItem> | ||
| <SelectItem value="interim">Interim</SelectItem> | ||
| <SelectItem value="standard">Standard</SelectItem> | ||
| </SelectContent> | ||
| </Select> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
| </div> | ||
| </CardContent> | ||
| </Card> | ||
| </TabsContent> | ||
|
|
||
| <TabsContent value="subnets" className="space-y-4"> | ||
| <div className="flex items-center justify-between"> | ||
| <h3 className="text-lg font-medium">Network Subnets</h3> | ||
| <Button type="button" onClick={addSubnet} variant="outline"> | ||
| <PlusCircle className="h-4 w-4 mr-2" /> | ||
| Add Subnet | ||
| </Button> | ||
| </div> | ||
| </ScrollArea> | ||
|
|
||
| {subnetFields.map((subnet, index) => ( | ||
| <Card key={subnet.id}> | ||
| <CardHeader> | ||
| <div className="flex items-center justify-between"> | ||
| <CardTitle className="flex items-center gap-2"> | ||
| <Network className="h-4 w-4" /> | ||
| Subnet {index + 1} | ||
| </CardTitle> | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={() => removeSubnet(index)} | ||
| > | ||
| <Trash2 className="h-4 w-4" /> | ||
| </Button> | ||
| </div> | ||
| </CardHeader> | ||
| <CardContent className="space-y-4"> | ||
| <div className="grid grid-cols-2 gap-4"> | ||
| <FormField | ||
| control={form.control} | ||
| name={`subnets.${index}.network`} | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>Network Address</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="192.168.1.0" {...field} /> | ||
| </FormControl> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
|
|
||
| <FormField | ||
| control={form.control} | ||
| name={`subnets.${index}.netmask`} | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>Subnet Mask</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="255.255.255.0" {...field} /> | ||
| </FormControl> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="grid grid-cols-2 gap-4"> | ||
| <FormField | ||
| control={form.control} | ||
| name={`subnets.${index}.rangeStart`} | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>Range Start</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="192.168.1.100" {...field} /> | ||
| </FormControl> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
|
|
||
| <FormField | ||
| control={form.control} | ||
| name={`subnets.${index}.rangeEnd`} | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>Range End</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="192.168.1.200" {...field} /> | ||
| </FormControl> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="grid grid-cols-2 gap-4"> | ||
| <FormField | ||
| control={form.control} | ||
| name={`subnets.${index}.defaultGateway`} | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>Default Gateway</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="192.168.1.1" {...field} /> | ||
| </FormControl> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
|
|
||
| <FormField | ||
| control={form.control} | ||
| name={`subnets.${index}.domainNameServers`} | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>DNS Servers</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="8.8.8.8, 8.8.4.4" {...field} /> | ||
| </FormControl> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
| </div> | ||
| </CardContent> | ||
| </Card> | ||
| ))} | ||
|
|
||
| {subnetFields.length === 0 && ( | ||
| <Card className="border-dashed"> | ||
| <CardContent className="flex flex-col items-center justify-center py-8"> | ||
| <Network className="h-8 w-8 text-muted-foreground mb-2" /> | ||
| <p className="text-sm text-muted-foreground">No subnets configured</p> | ||
| <Button type="button" onClick={addSubnet} variant="ghost" className="mt-2"> | ||
| Add your first subnet | ||
| </Button> | ||
| </CardContent> | ||
| </Card> | ||
| )} | ||
| </TabsContent> | ||
|
|
||
| <TabsContent value="reservations" className="space-y-4"> | ||
| <div className="flex items-center justify-between"> | ||
| <h3 className="text-lg font-medium">Static Host Reservations</h3> | ||
| <Button type="button" onClick={addHostReservation} variant="outline"> | ||
| <PlusCircle className="h-4 w-4 mr-2" /> | ||
| Add Reservation | ||
| </Button> | ||
| </div> | ||
|
|
||
| {hostFields.map((host, index) => ( | ||
| <Card key={host.id}> | ||
| <CardHeader> | ||
| <div className="flex items-center justify-between"> | ||
| <CardTitle className="flex items-center gap-2"> | ||
| <Users className="h-4 w-4" /> | ||
| Host Reservation {index + 1} | ||
| </CardTitle> | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={() => removeHost(index)} | ||
| > | ||
| <Trash2 className="h-4 w-4" /> | ||
| </Button> | ||
| </div> | ||
| </CardHeader> | ||
| <CardContent className="space-y-4"> | ||
| <div className="grid grid-cols-3 gap-4"> | ||
| <FormField | ||
| control={form.control} | ||
| name={`hostReservations.${index}.hostname`} | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>Hostname</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="printer-01" {...field} /> | ||
| </FormControl> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
|
|
||
| <FormField | ||
| control={form.control} | ||
| name={`hostReservations.${index}.macAddress`} | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>MAC Address</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="00:11:22:33:44:55" {...field} /> | ||
| </FormControl> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
|
|
||
| <FormField | ||
| control={form.control} | ||
| name={`hostReservations.${index}.fixedAddress`} | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>Fixed IP Address</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="192.168.1.10" {...field} /> | ||
| </FormControl> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
| </div> | ||
| </CardContent> | ||
| </Card> | ||
| ))} | ||
|
|
||
| {hostFields.length === 0 && ( | ||
| <Card className="border-dashed"> | ||
| <CardContent className="flex flex-col items-center justify-center py-8"> | ||
| <Users className="h-8 w-8 text-muted-foreground mb-2" /> | ||
| <p className="text-sm text-muted-foreground">No host reservations configured</p> | ||
| <Button type="button" onClick={addHostReservation} variant="ghost" className="mt-2"> | ||
| Add your first reservation | ||
| </Button> | ||
| </CardContent> | ||
| </Card> | ||
| )} | ||
| </TabsContent> | ||
|
|
||
| <TabsContent value="options" className="space-y-4"> | ||
| <div className="flex items-center justify-between"> | ||
| <h3 className="text-lg font-medium">Global DHCP Options</h3> | ||
| <Button type="button" onClick={addGlobalOption} variant="outline"> | ||
| <PlusCircle className="h-4 w-4 mr-2" /> | ||
| Add Option | ||
| </Button> | ||
| </div> | ||
|
|
||
| {optionFields.map((option, index) => ( | ||
| <Card key={option.id}> | ||
| <CardHeader> | ||
| <div className="flex items-center justify-between"> | ||
| <CardTitle className="flex items-center gap-2"> | ||
| <Settings className="h-4 w-4" /> | ||
| Option {index + 1} | ||
| </CardTitle> | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={() => removeOption(index)} | ||
| > | ||
| <Trash2 className="h-4 w-4" /> | ||
| </Button> | ||
| </div> | ||
| </CardHeader> | ||
| <CardContent className="space-y-4"> | ||
| <div className="grid grid-cols-2 gap-4"> | ||
| <FormField | ||
| control={form.control} | ||
| name={`globalOptions.${index}.name`} | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>Option Name</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="ntp-servers" {...field} /> | ||
| </FormControl> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
|
|
||
| <FormField | ||
| control={form.control} | ||
| name={`globalOptions.${index}.value`} | ||
| render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>Option Value</FormLabel> | ||
| <FormControl> | ||
| <Input placeholder="pool.ntp.org" {...field} /> | ||
| </FormControl> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} | ||
| /> | ||
| </div> | ||
| </CardContent> | ||
| </Card> | ||
| ))} | ||
|
|
||
| {optionFields.length === 0 && ( | ||
| <Card className="border-dashed"> | ||
| <CardContent className="flex flex-col items-center justify-center py-8"> | ||
| <Settings className="h-8 w-8 text-muted-foreground mb-2" /> | ||
| <p className="text-sm text-muted-foreground">No global options configured</p> | ||
| <Button type="button" onClick={addGlobalOption} variant="ghost" className="mt-2"> | ||
| Add your first option | ||
| </Button> | ||
| </CardContent> | ||
| </Card> | ||
| )} | ||
| </TabsContent> | ||
| </Tabs> | ||
|
|
||
| <div className="flex gap-4"> | ||
| <Button type="submit" disabled={isSaving}> | ||
| {isSaving ? 'Saving...' : 'Save DHCP Configuration'} | ||
| </Button> | ||
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| onClick={() => form.reset()} | ||
| disabled={isSaving} | ||
| > | ||
| Reset | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| <Button>Save DHCP Configuration</Button> | ||
| </div> | ||
| </form> | ||
| </Form> | ||
| ); | ||
| } No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider component decomposition for maintainability.
This component is quite large (675 lines) and handles multiple responsibilities. Consider breaking it down into smaller, focused components for better maintainability.
Potential decomposition:
DHCPGeneralSettings- General configuration tabDHCPSubnetsTab- Subnet managementDHCPHostReservationsTab- Host reservations managementDHCPGlobalOptionsTab- Global options managementDHCPServiceStatus- Service status and controlsDHCPSubnetCard- Individual subnet configurationDHCPHostReservationCard- Individual host reservation
This would improve:
- Code reusability
- Testing granularity
- Maintainability
- Performance (smaller re-render scope)
🤖 Prompt for AI Agents
In apps/ui/src/features/configuration/dhcp/DHCPConfig.tsx around lines 18 to
675, the DHCPConfig component is very large and handles multiple
responsibilities, making it hard to maintain. Refactor by decomposing this
component into smaller focused components such as DHCPGeneralSettings for the
general tab, DHCPSubnetsTab for subnet management, DHCPHostReservationsTab for
host reservations, DHCPGlobalOptionsTab for global options, DHCPServiceStatus
for service status controls, and individual card components like DHCPSubnetCard
and DHCPHostReservationCard. Move the relevant JSX and logic into these
components and update DHCPConfig to compose them, improving maintainability,
reusability, and performance.
| try { | ||
| await controlDhcpServiceAPI(action); | ||
| const statusResponse = await getDhcpServiceStatusAPI(); | ||
| setServiceStatus(statusResponse.data.status as any); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix type coercion for service status.
Remove the type coercion and ensure proper typing.
- setServiceStatus(statusResponse.data.status as any);
+ setServiceStatus(statusResponse.data.status);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| setServiceStatus(statusResponse.data.status as any); | |
| setServiceStatus(statusResponse.data.status); |
🤖 Prompt for AI Agents
In apps/ui/src/features/configuration/dhcp/DHCPConfig.tsx at line 148, remove
the use of 'as any' type coercion on statusResponse.data.status. Instead, define
or use the correct TypeScript type for the service status and apply it directly
to ensure proper typing and type safety.
| // Refresh service status | ||
| try { | ||
| const statusResponse = await getDhcpServiceStatusAPI(); | ||
| setServiceStatus(statusResponse.data.status as any); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix type coercion for service status.
Another instance of unnecessary type coercion that should be resolved properly.
- setServiceStatus(statusResponse.data.status as any);
+ setServiceStatus(statusResponse.data.status);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| setServiceStatus(statusResponse.data.status as any); | |
| setServiceStatus(statusResponse.data.status); |
🤖 Prompt for AI Agents
In apps/ui/src/features/configuration/dhcp/DHCPConfig.tsx at line 94, the code
uses an unnecessary type coercion with 'as any' when setting the service status.
To fix this, replace the 'as any' cast by properly typing the statusResponse
data or by defining the expected type for service status and ensuring the
assignment matches that type without coercion.
| form.reset(formData as any); // Type coercion for form reset | ||
| setServiceStatus(statusResponse.data.status as any); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Eliminate type coercion with proper typing.
The as any type coercion is a code smell that bypasses TypeScript's type safety. This suggests a mismatch between the form data structure and the expected types.
- const formData = transformDhcpApiToForm(configResponse.data);
- form.reset(formData as any); // Type coercion for form reset
- setServiceStatus(statusResponse.data.status as any);
+ const formData = transformDhcpApiToForm(configResponse.data);
+ form.reset(formData);
+ setServiceStatus(statusResponse.data.status);Verify that the transformDhcpApiToForm function returns data compatible with DhcpConfigFormValues:
🏁 Script executed:
#!/bin/bash
# Description: Check the return type of transformDhcpApiToForm function
ast-grep --pattern 'export function transformDhcpApiToForm($_): $TYPE {
$$$
}'Length of output: 82
🏁 Script executed:
#!/bin/bash
# Locate the definition and signature of transformDhcpApiToForm to verify its return type
rg --context 3 "transformDhcpApiToForm" -n .Length of output: 8447
🏁 Script executed:
#!/bin/bash
# Display initializations around form and serviceStatus in DHCPConfig.tsx
sed -n '1,80p' apps/ui/src/features/configuration/dhcp/DHCPConfig.tsxLength of output: 3239
🏁 Script executed:
#!/bin/bash
# Locate and show the definition of getDhcpServiceStatusAPI to verify its return type
rg --context 5 "getDhcpServiceStatusAPI" -n apps/ui/src/lib/api/dhcpLength of output: 133
🏁 Script executed:
#!/bin/bash
# Search for getDhcpServiceStatusAPI definition across the repo
rg --context 5 "getDhcpServiceStatusAPI" -n .Length of output: 5392
🏁 Script executed:
#!/bin/bash
# Display imports and DhcpServiceResponse usage in DHCP API wrapper
sed -n '1,80p' apps/ui/src/lib/api/dhcp.tsLength of output: 2279
🏁 Script executed:
#!/bin/bash
# Verify the status field type in DhcpServiceResponse
rg --context 3 "export (interface|type) DhcpServiceResponse" -n packages/shared/srcLength of output: 494
Update typing and remove unnecessary any casts
The transformDhcpApiToForm helper already returns a DhcpConfigFormValues, so you can call form.reset(formData) without as any. However, the DHCP service status may also return "failed", which isn’t included in the current state type ('running' | 'stopped' | 'unknown'). You should either expand the state type to match DhcpServiceResponse['status'] or map "failed" to one of the existing values.
Files to update:
- apps/ui/src/features/configuration/dhcp/DHCPConfig.tsx
Suggested diff:
--- a/apps/ui/src/features/configuration/dhcp/DHCPConfig.tsx
+++ b/apps/ui/src/features/configuration/dhcp/DHCPConfig.tsx
@@ -6,7 +6,8 @@
import { v4 as uuidv4 } from 'uuid';
-import { dhcpConfigSchema, transformDhcpApiToForm, type DhcpConfigFormValues } from '@server-manager/shared/validators';
+import {
+ dhcpConfigSchema,
+ transformDhcpApiToForm,
+ type DhcpConfigFormValues,
+ type DhcpServiceResponse
+} from '@server-manager/shared/validators';
export function DHCPConfig() {
const [isLoading, setIsLoading] = React.useState(true);
@@ -8,7 +9,8 @@ export function DHCPConfig() {
const [isSaving, setIsSaving] = React.useState(false);
- const [serviceStatus, setServiceStatus] = React.useState<'running' | 'stopped' | 'unknown'>('unknown');
+ // Include all possible statuses from the API
+ const [serviceStatus, setServiceStatus] = React.useState<DhcpServiceResponse['status']>('unknown');
const form = useForm<DhcpConfigFormValues>({
resolver: zodResolver(dhcpConfigSchema),
@@ -60,7 +62,7 @@ React.useEffect(() => {
const formData = transformDhcpApiToForm(configResponse.data);
- form.reset(formData as any); // Type coercion for form reset
- setServiceStatus(statusResponse.data.status as any);
+ form.reset(formData);
+ setServiceStatus(statusResponse.data.status);
} catch (error) {
console.error('Failed to load DHCP configuration:', error);- Remove the
as anycasts onform.resetandsetServiceStatus - Import and use
DhcpServiceResponse['status']for the React state type - Adjust mapping if you prefer to treat
"failed"differently
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| form.reset(formData as any); // Type coercion for form reset | |
| setServiceStatus(statusResponse.data.status as any); | |
| import { v4 as uuidv4 } from 'uuid'; | |
| import { | |
| dhcpConfigSchema, | |
| transformDhcpApiToForm, | |
| type DhcpConfigFormValues, | |
| type DhcpServiceResponse | |
| } from '@server-manager/shared/validators'; | |
| export function DHCPConfig() { | |
| const [isLoading, setIsLoading] = React.useState(true); | |
| const [isSaving, setIsSaving] = React.useState(false); | |
| // Include all possible statuses from the API | |
| const [serviceStatus, setServiceStatus] = React.useState<DhcpServiceResponse['status']>('unknown'); | |
| const form = useForm<DhcpConfigFormValues>({ | |
| resolver: zodResolver(dhcpConfigSchema), | |
| }); | |
| React.useEffect(() => { | |
| // ...fetch configResponse & statusResponse... | |
| const formData = transformDhcpApiToForm(configResponse.data); | |
| form.reset(formData); | |
| setServiceStatus(statusResponse.data.status); | |
| }, []); | |
| // ...rest of component... | |
| } |
🤖 Prompt for AI Agents
In apps/ui/src/features/configuration/dhcp/DHCPConfig.tsx around lines 64-65,
remove the unnecessary 'as any' type coercions on form.reset and
setServiceStatus by ensuring formData is typed as DhcpConfigFormValues and the
service status state uses DhcpServiceResponse['status'] type. Update the React
state type for serviceStatus to include all possible status values from
DhcpServiceResponse, including "failed", or map "failed" to an existing status
if preferred, to maintain type safety without bypassing TypeScript checks.
| function getAuthHeaders() { | ||
| const token = localStorage.getItem('authToken'); | ||
| return { | ||
| 'Content-Type': 'application/json', | ||
| ...(token && { 'Authorization': `Bearer ${token}` }) | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add SSR compatibility check for localStorage access.
Accessing localStorage directly can cause issues in server-side rendering environments where localStorage is undefined.
function getAuthHeaders() {
- const token = localStorage.getItem('authToken');
+ const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;
return {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` })
};
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function getAuthHeaders() { | |
| const token = localStorage.getItem('authToken'); | |
| return { | |
| 'Content-Type': 'application/json', | |
| ...(token && { 'Authorization': `Bearer ${token}` }) | |
| }; | |
| } | |
| function getAuthHeaders() { | |
| const token = typeof window !== 'undefined' | |
| ? localStorage.getItem('authToken') | |
| : null; | |
| return { | |
| 'Content-Type': 'application/json', | |
| ...(token && { 'Authorization': `Bearer ${token}` }) | |
| }; | |
| } |
🤖 Prompt for AI Agents
In apps/ui/src/lib/api/dhcp.ts around lines 13 to 19, the function
getAuthHeaders accesses localStorage directly, which breaks in server-side
rendering environments where localStorage is undefined. To fix this, add a check
to ensure localStorage is available before accessing it, such as verifying if
typeof window !== 'undefined' and window.localStorage exists. Only retrieve the
token if localStorage is accessible; otherwise, return headers without the
Authorization field.
| netmask: z.string().refine(isValidNetmask, { | ||
| message: "Invalid subnet mask" | ||
| }), | ||
| rangeStart: z.string().refine(isValidIpAddress, { | ||
| message: "Invalid start IP address" | ||
| }), | ||
| rangeEnd: z.string().refine(isValidIpAddress, { | ||
| message: "Invalid end IP address" | ||
| }), | ||
| defaultGateway: z.string().refine(isValidIpAddress, { | ||
| message: "Invalid gateway IP address" | ||
| }), | ||
| domainNameServers: z.string().min(1, "At least one DNS server is required"), | ||
| broadcastAddress: z.string().refine((val) => { | ||
| return val === '' || isValidIpAddress(val); | ||
| }, { | ||
| message: "Invalid broadcast address" | ||
| }), | ||
| subnetMask: z.string().refine((val) => { | ||
| return val === '' || isValidNetmask(val); | ||
| }, { | ||
| message: "Invalid subnet mask" | ||
| }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove redundant subnet mask field to avoid confusion.
The schema defines both netmask (required) and subnetMask (optional) fields, which appear to serve the same purpose. This redundancy could lead to confusion and potential inconsistencies.
Consider removing the subnetMask field or clearly documenting the distinction if they serve different purposes.
🤖 Prompt for AI Agents
In packages/shared/src/validators/dhcpFormValidator.ts between lines 110 and
132, remove the redundant subnetMask field from the schema since netmask already
serves the purpose of defining the subnet mask. This will avoid confusion and
potential inconsistencies by having only one field for the subnet mask
validation.
| export const controlDhcpService = async (req: AuthRequest, res: Response) => { | ||
| try { | ||
| const { action } = req.params; | ||
| const validActions = ['start', 'stop', 'restart', 'reload', 'status']; | ||
|
|
||
| if (!validActions.includes(action)) { | ||
| return res.status(400).json({ | ||
| success: false, | ||
| message: `Invalid action: ${action}. Valid actions are: ${validActions.join(', ')}` | ||
| }); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add rate limiting for service control actions.
The service control endpoint lacks rate limiting, which could allow rapid start/stop/restart actions that might destabilize the service. Consider implementing rate limiting to prevent abuse.
Would you like me to generate a rate limiting implementation using a token bucket or sliding window approach?
🤖 Prompt for AI Agents
In apps/backend/src/controllers/dhcpController.ts around lines 496 to 507, the
controlDhcpService function lacks rate limiting, allowing rapid repeated actions
that could destabilize the service. Implement rate limiting middleware or logic
using a token bucket or sliding window algorithm to limit how frequently a
client can invoke start/stop/restart actions. Integrate this rate limiting
before processing the action parameter and return an appropriate error response
when the limit is exceeded.
| if (existsSync(filePath)) { | ||
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); | ||
| const backupFilePath = path.join(backupDir, `${filename}.${timestamp}.backup`); | ||
|
|
||
| try { | ||
| const existingContent = await readFile(filePath, 'utf8'); | ||
| await writeFile(backupFilePath, existingContent, 'utf8'); | ||
| logger.info(`Backup created: ${backupFilePath}`); | ||
| } catch (error) { | ||
| logger.warn(`Failed to create backup for ${filePath}:`, error); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Implement backup rotation to prevent disk space issues.
The current implementation creates unlimited timestamped backups which could fill disk space over time. Consider implementing a rotation strategy to keep only the N most recent backups.
Add backup rotation logic:
// Create backup if file exists
if (existsSync(filePath)) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupFilePath = path.join(backupDir, `${filename}.${timestamp}.backup`);
try {
const existingContent = await readFile(filePath, 'utf8');
await writeFile(backupFilePath, existingContent, 'utf8');
logger.info(`Backup created: ${backupFilePath}`);
+
+ // Rotate backups - keep only the 10 most recent
+ const backups = await fs.promises.readdir(backupDir);
+ const fileBackups = backups
+ .filter(f => f.startsWith(`${filename}.`) && f.endsWith('.backup'))
+ .sort()
+ .reverse();
+
+ // Remove old backups
+ for (let i = 10; i < fileBackups.length; i++) {
+ await fs.promises.unlink(path.join(backupDir, fileBackups[i]));
+ logger.info(`Removed old backup: ${fileBackups[i]}`);
+ }
} catch (error) {
logger.warn(`Failed to create backup for ${filePath}:`, error);
}
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/backend/src/controllers/dhcpController.ts around lines 78 to 89, the
backup creation code currently saves unlimited timestamped backups, risking disk
space exhaustion. Implement backup rotation by listing existing backups in the
backupDir matching the filename pattern, sorting them by creation or timestamp,
and deleting the oldest backups to keep only a fixed number (e.g., the N most
recent). Add this rotation logic after successfully creating a new backup to
maintain a controlled number of backup files.
…apshot and journal JSON files for SQLite schema versioning and tracking user table structure.
Introduce a complete DHCP controller and API client, including configuration management, service control, and validation features. Consolidate testing and enhance the overall architecture for better integration and usability.
Summary by CodeRabbit