427 lines
14 KiB
TypeScript
427 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from 'react';
|
|
import { z } from 'zod';
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from '@/components/ui/form';
|
|
import { Input } from '@/components/ui/input';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { useToast } from '@/hooks/use-toast';
|
|
import { ArrowLeft, Loader2 } from 'lucide-react';
|
|
|
|
interface DNSRecord {
|
|
id: string;
|
|
type: string;
|
|
name: string;
|
|
data: string;
|
|
priority?: number;
|
|
ttl: number;
|
|
}
|
|
|
|
interface RecordFormProps {
|
|
apiKey: string;
|
|
domain: string;
|
|
record?: DNSRecord | null;
|
|
onCancel: () => void;
|
|
onSuccess: () => void;
|
|
}
|
|
|
|
const recordSchema = z.object({
|
|
type: z.string(),
|
|
name: z.string(),
|
|
data: z.string(),
|
|
priority: z.number().optional(),
|
|
ttl: z.number().min(60, 'TTL must be at least 60 seconds'),
|
|
// SSHFP specific fields
|
|
algorithm: z.number().min(0).max(4).optional(),
|
|
fptype: z.number().min(0).max(2).optional(),
|
|
fingerprint: z.string().optional(),
|
|
});
|
|
|
|
export function RecordForm({ apiKey, domain, record, onCancel, onSuccess }: RecordFormProps) {
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const { toast } = useToast();
|
|
|
|
const form = useForm<z.infer<typeof recordSchema>>({
|
|
resolver: zodResolver(recordSchema),
|
|
defaultValues: {
|
|
type: record?.type || 'A',
|
|
name: record?.name || '',
|
|
data: record?.data || '',
|
|
priority: record?.priority,
|
|
ttl: record?.ttl || 3600,
|
|
algorithm: undefined,
|
|
fptype: undefined,
|
|
fingerprint: '',
|
|
},
|
|
});
|
|
|
|
const watchType = form.watch('type');
|
|
const watchAlgorithm = form.watch('algorithm');
|
|
const watchFptype = form.watch('fptype');
|
|
const watchFingerprint = form.watch('fingerprint');
|
|
|
|
// Update data field when SSHFP components change
|
|
const updateSSHFPData = () => {
|
|
if (watchType === 'SSHFP' &&
|
|
watchAlgorithm !== undefined &&
|
|
watchFptype !== undefined &&
|
|
watchFingerprint) {
|
|
const sshfpData = `${watchAlgorithm} ${watchFptype} ${watchFingerprint}`;
|
|
form.setValue('data', sshfpData);
|
|
}
|
|
};
|
|
|
|
// Parse SSHFP data when editing an existing record
|
|
const parseSSHFPData = (data: string) => {
|
|
if (record?.type === 'SSHFP' && data) {
|
|
const parts = data.split(' ');
|
|
if (parts.length >= 3) {
|
|
form.setValue('algorithm', parseInt(parts[0]));
|
|
form.setValue('fptype', parseInt(parts[1]));
|
|
form.setValue('fingerprint', parts.slice(2).join(' '));
|
|
}
|
|
}
|
|
};
|
|
|
|
// Parse SSHFP data on initial load
|
|
useState(() => {
|
|
if (record?.type === 'SSHFP' && record.data) {
|
|
parseSSHFPData(record.data);
|
|
}
|
|
});
|
|
|
|
async function onSubmit(values: z.infer<typeof recordSchema>) {
|
|
// Ensure SSHFP data is properly formatted
|
|
if (values.type === 'SSHFP') {
|
|
updateSSHFPData();
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
const url = record
|
|
? `/api/vultr/domains/${domain}/records/${record.id}`
|
|
: `/api/vultr/domains/${domain}/records`;
|
|
|
|
const method = record ? 'PUT' : 'POST';
|
|
|
|
// Remove SSHFP specific fields before sending to API
|
|
const { algorithm, fptype, fingerprint, ...apiValues } = values;
|
|
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': apiKey,
|
|
},
|
|
body: JSON.stringify(apiValues),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to save record');
|
|
}
|
|
|
|
toast({
|
|
title: 'Success',
|
|
description: `DNS record ${record ? 'updated' : 'created'} successfully.`,
|
|
});
|
|
|
|
onSuccess();
|
|
} catch (error) {
|
|
console.error('Error saving record:', error);
|
|
toast({
|
|
title: 'Error',
|
|
description: `Failed to ${record ? 'update' : 'create'} DNS record.`,
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center">
|
|
<Button variant="ghost" onClick={onCancel} className="mr-4">
|
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
Back
|
|
</Button>
|
|
<h3 className="text-lg font-medium">
|
|
{record ? 'Edit DNS Record' : 'Add DNS Record'}
|
|
</h3>
|
|
</div>
|
|
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<FormField
|
|
control={form.control}
|
|
name="type"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Record Type</FormLabel>
|
|
<Select
|
|
onValueChange={(value) => {
|
|
field.onChange(value);
|
|
// Reset SSHFP fields if changing from SSHFP
|
|
if (value !== 'SSHFP') {
|
|
form.setValue('algorithm', undefined);
|
|
form.setValue('fptype', undefined);
|
|
form.setValue('fingerprint', '');
|
|
}
|
|
}}
|
|
defaultValue={field.value}
|
|
disabled={!!record}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select record type" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="A">A</SelectItem>
|
|
<SelectItem value="AAAA">AAAA</SelectItem>
|
|
<SelectItem value="CNAME">CNAME</SelectItem>
|
|
<SelectItem value="MX">MX</SelectItem>
|
|
<SelectItem value="TXT">TXT</SelectItem>
|
|
<SelectItem value="NS">NS</SelectItem>
|
|
<SelectItem value="SRV">SRV</SelectItem>
|
|
<SelectItem value="CAA">CAA</SelectItem>
|
|
<SelectItem value="SSHFP">SSHFP</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormDescription>
|
|
The type of DNS record to create.
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="name"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Name</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="e.g., www" {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
Subdomain name (@ for root domain)
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
{watchType !== 'SSHFP' && (
|
|
<FormField
|
|
control={form.control}
|
|
name="data"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Value</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder={
|
|
watchType === 'A'
|
|
? '192.168.1.1'
|
|
: watchType === 'CNAME'
|
|
? 'example.com'
|
|
: 'Record value'
|
|
}
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
{watchType === 'A' || watchType === 'AAAA'
|
|
? 'IP address'
|
|
: watchType === 'CNAME'
|
|
? 'Target domain'
|
|
: watchType === 'TXT'
|
|
? 'Text content'
|
|
: 'Record value'}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
{watchType === 'SSHFP' && (
|
|
<>
|
|
<FormField
|
|
control={form.control}
|
|
name="algorithm"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Algorithm</FormLabel>
|
|
<Select
|
|
onValueChange={(value) => {
|
|
field.onChange(parseInt(value));
|
|
updateSSHFPData();
|
|
}}
|
|
value={field.value?.toString()}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select algorithm" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="1">1 - RSA</SelectItem>
|
|
<SelectItem value="2">2 - DSA</SelectItem>
|
|
<SelectItem value="3">3 - ECDSA</SelectItem>
|
|
<SelectItem value="4">4 - ED25519</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormDescription>
|
|
SSH key algorithm
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="fptype"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Fingerprint Type</FormLabel>
|
|
<Select
|
|
onValueChange={(value) => {
|
|
field.onChange(parseInt(value));
|
|
updateSSHFPData();
|
|
}}
|
|
value={field.value?.toString()}
|
|
>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select fingerprint type" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="1">1 - SHA-1</SelectItem>
|
|
<SelectItem value="2">2 - SHA-256</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormDescription>
|
|
Fingerprint hash algorithm
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="fingerprint"
|
|
render={({ field }) => (
|
|
<FormItem className="col-span-2">
|
|
<FormLabel>Fingerprint</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="e.g., 123456789abcdef67890123456789abcdef67890"
|
|
{...field}
|
|
onChange={(e) => {
|
|
field.onChange(e.target.value);
|
|
setTimeout(updateSSHFPData, 0);
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
Hexadecimal fingerprint value
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{watchType === 'MX' && (
|
|
<FormField
|
|
control={form.control}
|
|
name="priority"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Priority</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="number"
|
|
placeholder="10"
|
|
{...field}
|
|
onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
Priority for MX records (lower values have higher priority)
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="ttl"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>TTL (seconds)</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="number"
|
|
placeholder="3600"
|
|
{...field}
|
|
onChange={(e) => field.onChange(parseInt(e.target.value) || 3600)}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
Time to live in seconds (min: 60)
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-3">
|
|
<Button type="button" variant="outline" onClick={onCancel}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={isSubmitting}>
|
|
{isSubmitting ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
{record ? 'Updating...' : 'Creating...'}
|
|
</>
|
|
) : (
|
|
record ? 'Update Record' : 'Create Record'
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</div>
|
|
);
|
|
}
|