- Implement DNSProviderCard component for displaying individual DNS provider details. - Create DNSProviderForm component for adding and editing DNS providers. - Add DNSProviderSelector component for selecting DNS providers in forms. - Introduce useDNSProviders hook for fetching and managing DNS provider data. - Add DNSProviders page for listing and managing DNS providers. - Update layout to include DNS Providers navigation. - Enhance UI components with new badge styles and improved layouts. - Add default provider schemas for various DNS providers. - Integrate translation strings for DNS provider management. - Update Vite configuration for improved chunking and performance.
16 KiB
DNS Challenge Frontend Research - Issue #21
Overview
This document outlines the frontend architecture analysis and implementation plan for DNS challenge support in the Charon proxy manager. DNS challenges are required for wildcard certificate issuance via Let's Encrypt/ACME.
1. Existing Frontend Architecture Analysis
Technology Stack
| Layer | Technology |
|---|---|
| Framework | React 18+ with TypeScript |
| State Management | TanStack Query (React Query) |
| Routing | React Router |
| UI Components | Custom component library (Radix UI primitives) |
| Styling | Tailwind CSS with custom design tokens |
| Internationalization | react-i18next |
| HTTP Client | Axios |
| Icons | Lucide React |
Directory Structure
frontend/src/
├── api/ # API client functions (typed, async)
├── components/ # Reusable UI components
│ ├── ui/ # Design system primitives
│ ├── layout/ # Layout components (PageShell, etc.)
│ └── dialogs/ # Modal dialogs
├── hooks/ # Custom React hooks (data fetching)
├── pages/ # Page-level components
└── utils/ # Utility functions
Key Architectural Patterns
-
API Layer (
frontend/src/api/)- Each domain has its own API file (e.g.,
certificates.ts,smtp.ts) - Uses typed interfaces for request/response
- All functions are async, returning Promise types
- Axios client with base URL
/api/v1
- Each domain has its own API file (e.g.,
-
Custom Hooks (
frontend/src/hooks/)- Wrap TanStack Query for data fetching
- Naming convention:
use{Resource}(e.g.,useCertificates,useDomains) - Return
{ data, isLoading, error, refetch }pattern
-
Page Components (
frontend/src/pages/)- Use
PageShellwrapper for consistent layout - Header with title, description, and action buttons
- Card-based content organization
- Use
-
Form Patterns
- Use controlled components with
useState useMutationfor form submissions- Toast notifications for success/error feedback
- Inline validation with error state
- Use controlled components with
2. UI Component Patterns Identified
Design System Components (frontend/src/components/ui/)
| Component | Purpose | Usage |
|---|---|---|
Button |
Primary actions, variants: primary, secondary, danger, ghost | All forms |
Input |
Text/password inputs with label, error, helper text support | Forms |
Select |
Radix-based dropdown with proper styling | Provider selection |
Card |
Content containers with header/content/footer | Page sections |
Dialog |
Modal dialogs for forms/confirmations | Add/edit modals |
Alert |
Info/warning/error banners | Notifications |
Badge |
Status indicators | Provider status |
Label |
Form field labels | All form fields |
Textarea |
Multi-line text input | Advanced config |
Switch |
Toggle switches | Enable/disable |
Form Patterns (from SMTPSettings.tsx, ProxyHostForm.tsx)
// Standard form structure
const [formData, setFormData] = useState({ /* initial state */ })
const mutation = useMutation({
mutationFn: async () => { /* API call */ },
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['resource'] })
toast.success(t('resource.success'))
},
onError: (error) => {
toast.error(error.message)
},
})
// Form submission
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
mutation.mutate()
}
Password/Credential Input Pattern
From Input.tsx:
- Built-in password visibility toggle
- Uses
type="password"with eye icon - Supports
helperTextfor guidance autoCompleteattributes for security
Test Connection Pattern (from RemoteServerForm.tsx, SMTPSettings.tsx)
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle')
const handleTestConnection = async () => {
setTestStatus('testing')
try {
await testConnection(/* params */)
setTestStatus('success')
setTimeout(() => setTestStatus('idle'), 3000)
} catch {
setTestStatus('error')
setTimeout(() => setTestStatus('idle'), 3000)
}
}
3. Proposed New Components
3.1 DNS Providers Page (DNSProviders.tsx)
Location: frontend/src/pages/DNSProviders.tsx
Purpose: Manage DNS provider configurations for DNS-01 challenges
Layout:
┌─────────────────────────────────────────────────────────┐
│ DNS Providers [+ Add Provider] │
│ Configure DNS providers for wildcard certificates │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Alert: DNS providers enable wildcard certificates │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Cloudflare │ │ Route53 │ │
│ │ ✓ Configured │ │ Not configured │ │
│ │ [Edit] [Test] [Del] │ │ [Configure] │ │
│ └─────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Features:
- List configured DNS providers
- Add/Edit/Delete provider configurations
- Test DNS propagation
- Status badges (configured, error, pending)
3.2 DNS Provider Form (DNSProviderForm.tsx)
Location: frontend/src/components/DNSProviderForm.tsx
Purpose: Add/edit DNS provider configuration
Key Features:
- Dynamic form fields based on provider type
- Credential inputs with password masking
- Test connection button
- Validation feedback
Provider-Specific Fields:
| Provider | Required Fields |
|---|---|
| Cloudflare | API Token OR (API Key + Email) |
| Route53 | Access Key ID, Secret Access Key, Region |
| DigitalOcean | API Token |
| Google Cloud DNS | Service Account JSON |
| Namecheap | API User, API Key |
| GoDaddy | API Key, API Secret |
| Azure DNS | Client ID, Client Secret, Subscription ID, Resource Group |
3.3 Provider Selector Dropdown (DNSProviderSelector.tsx)
Location: frontend/src/components/DNSProviderSelector.tsx
Purpose: Dropdown to select DNS provider when requesting wildcard certificates
Usage: Integrated into ProxyHostForm.tsx or certificate request flow
<DNSProviderSelector
value={selectedProviderId}
onChange={setSelectedProviderId}
required={isWildcard}
/>
3.4 DNS Propagation Test Component (DNSPropagationTest.tsx)
Location: frontend/src/components/DNSPropagationTest.tsx
Purpose: Visual feedback for DNS TXT record propagation
Features:
- Shows test domain and expected TXT record
- Real-time propagation status
- Retry button
- Success/failure indicators
4. API Hooks Needed
4.1 useDNSProviders Hook
Location: frontend/src/hooks/useDNSProviders.ts
export function useDNSProviders() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['dns-providers'],
queryFn: getDNSProviders,
})
return {
providers: data || [],
isLoading,
error,
refetch,
}
}
4.2 useDNSProviderMutations Hook
Location: frontend/src/hooks/useDNSProviders.ts
export function useDNSProviderMutations() {
const queryClient = useQueryClient()
const createMutation = useMutation({
mutationFn: createDNSProvider,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['dns-providers'] }),
})
const updateMutation = useMutation({
mutationFn: updateDNSProvider,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['dns-providers'] }),
})
const deleteMutation = useMutation({
mutationFn: deleteDNSProvider,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['dns-providers'] }),
})
const testMutation = useMutation({
mutationFn: testDNSProvider,
})
return { createMutation, updateMutation, deleteMutation, testMutation }
}
5. API Client Functions Needed
5.1 frontend/src/api/dnsProviders.ts
import client from './client'
/** Supported DNS provider types */
export type DNSProviderType =
| 'cloudflare'
| 'route53'
| 'digitalocean'
| 'gcloud'
| 'namecheap'
| 'godaddy'
| 'azure'
/** DNS Provider configuration */
export interface DNSProvider {
id: number
name: string
provider_type: DNSProviderType
is_default: boolean
created_at: string
updated_at: string
last_used_at?: string
status: 'active' | 'error' | 'unconfigured'
}
/** Provider-specific credentials (varies by type) */
export interface DNSProviderCredentials {
// Cloudflare
api_token?: string
api_key?: string
email?: string
// Route53
access_key_id?: string
secret_access_key?: string
region?: string
// DigitalOcean
token?: string
// GCloud
service_account_json?: string
project_id?: string
// Generic
[key: string]: string | undefined
}
/** Request payload for creating/updating provider */
export interface DNSProviderRequest {
name: string
provider_type: DNSProviderType
credentials: DNSProviderCredentials
is_default?: boolean
}
/** Test result response */
export interface DNSTestResult {
success: boolean
message?: string
error?: string
propagation_time_ms?: number
}
// API functions
export const getDNSProviders = async (): Promise<DNSProvider[]> => {
const response = await client.get<DNSProvider[]>('/dns-providers')
return response.data
}
export const getDNSProvider = async (id: number): Promise<DNSProvider> => {
const response = await client.get<DNSProvider>(`/dns-providers/${id}`)
return response.data
}
export const createDNSProvider = async (data: DNSProviderRequest): Promise<DNSProvider> => {
const response = await client.post<DNSProvider>('/dns-providers', data)
return response.data
}
export const updateDNSProvider = async (
id: number,
data: Partial<DNSProviderRequest>
): Promise<DNSProvider> => {
const response = await client.put<DNSProvider>(`/dns-providers/${id}`, data)
return response.data
}
export const deleteDNSProvider = async (id: number): Promise<void> => {
await client.delete(`/dns-providers/${id}`)
}
export const testDNSProvider = async (id: number): Promise<DNSTestResult> => {
const response = await client.post<DNSTestResult>(`/dns-providers/${id}/test`)
return response.data
}
export const testDNSProviderCredentials = async (
data: DNSProviderRequest
): Promise<DNSTestResult> => {
const response = await client.post<DNSTestResult>('/dns-providers/test', data)
return response.data
}
6. Files to Create/Modify
New Files to Create
| File | Purpose |
|---|---|
frontend/src/pages/DNSProviders.tsx |
DNS providers management page |
frontend/src/components/DNSProviderForm.tsx |
Add/edit provider form |
frontend/src/components/DNSProviderSelector.tsx |
Provider dropdown selector |
frontend/src/components/DNSPropagationTest.tsx |
Propagation test UI |
frontend/src/api/dnsProviders.ts |
API client functions |
frontend/src/hooks/useDNSProviders.ts |
Data fetching hooks |
frontend/src/data/dnsProviderSchemas.ts |
Provider field definitions |
Existing Files to Modify
| File | Modification |
|---|---|
frontend/src/App.tsx |
Add route for /dns-providers |
frontend/src/components/layout/Layout.tsx |
Add navigation link |
frontend/src/pages/Certificates.tsx |
Add DNS provider selector for wildcard requests |
frontend/src/components/ProxyHostForm.tsx |
Add DNS provider option when wildcard domain detected |
frontend/src/api/certificates.ts |
Add dns_provider_id to certificate request types |
7. Translation Keys Needed
Add to frontend/src/locales/en/translation.json:
{
"dnsProviders": {
"title": "DNS Providers",
"description": "Configure DNS providers for wildcard certificate issuance",
"addProvider": "Add Provider",
"editProvider": "Edit Provider",
"deleteProvider": "Delete Provider",
"deleteConfirm": "Are you sure you want to delete this DNS provider?",
"testConnection": "Test Connection",
"testSuccess": "DNS provider connection successful",
"testFailed": "DNS provider test failed",
"providerType": "Provider Type",
"providerName": "Provider Name",
"credentials": "Credentials",
"setAsDefault": "Set as Default",
"default": "Default",
"status": {
"active": "Active",
"error": "Error",
"unconfigured": "Not Configured"
},
"providers": {
"cloudflare": "Cloudflare",
"route53": "Amazon Route 53",
"digitalocean": "DigitalOcean",
"gcloud": "Google Cloud DNS",
"namecheap": "Namecheap",
"godaddy": "GoDaddy",
"azure": "Azure DNS"
},
"fields": {
"apiToken": "API Token",
"apiKey": "API Key",
"email": "Email",
"accessKeyId": "Access Key ID",
"secretAccessKey": "Secret Access Key",
"region": "Region",
"serviceAccountJson": "Service Account JSON",
"projectId": "Project ID"
},
"hints": {
"cloudflare": "Use an API token with Zone:DNS:Edit permissions",
"route53": "IAM user with route53:ChangeResourceRecordSets permission",
"digitalocean": "Personal access token with write scope"
},
"propagation": {
"title": "DNS Propagation",
"checking": "Checking DNS propagation...",
"success": "DNS record propagated successfully",
"failed": "DNS propagation check failed",
"retry": "Retry Check"
}
}
}
8. UI/UX Considerations
Provider Selection Flow
- User creates proxy host with wildcard domain (e.g.,
*.example.com) - System detects wildcard and shows DNS provider requirement
- User selects existing provider OR creates new one inline
- On save, backend uses DNS challenge for certificate
Security Considerations
- Credentials masked by default (password inputs)
- API tokens never returned in full from backend (masked)
- Clear warning about credential storage
- Option to test without saving
Error Handling
- Clear error messages for invalid credentials
- Specific guidance for each provider's setup
- Link to provider documentation
- Retry mechanisms for transient failures
9. Implementation Priority
-
Phase 1: API layer and hooks
dnsProviders.tsAPI clientuseDNSProviders.tshooks
-
Phase 2: Core UI components
DNSProviders.tsxpageDNSProviderForm.tsxform
-
Phase 3: Integration
- Navigation and routing
- Certificate/ProxyHost form integration
- Provider selector component
-
Phase 4: Polish
- Translations
- Error handling refinement
- Propagation test UI
10. Related Backend Requirements
The frontend implementation depends on these backend API endpoints:
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/v1/dns-providers |
List all providers |
| POST | /api/v1/dns-providers |
Create provider |
| GET | /api/v1/dns-providers/:id |
Get provider details |
| PUT | /api/v1/dns-providers/:id |
Update provider |
| DELETE | /api/v1/dns-providers/:id |
Delete provider |
| POST | /api/v1/dns-providers/:id/test |
Test saved provider |
| POST | /api/v1/dns-providers/test |
Test credentials before saving |
Research completed: 2026-01-01