initial commit
This commit is contained in:
commit
8a67761c9a
83 changed files with 15510 additions and 0 deletions
426
components/record-form.tsx
Normal file
426
components/record-form.tsx
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue