mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-08-28 11:17:07 +00:00
Enhance DNS record management by adding auto IP detection, notes field, and improved UI.
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/e33594f7-ad24-43f5-a4e1-0d9d2f261947.jpg
This commit is contained in:
@@ -28,6 +28,11 @@ export function Sidebar() {
|
|||||||
href: "/",
|
href: "/",
|
||||||
icon: <LayoutDashboard className="w-5 h-5 mr-3" />,
|
icon: <LayoutDashboard className="w-5 h-5 mr-3" />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Organizations",
|
||||||
|
href: "/organizations",
|
||||||
|
icon: <Users className="w-5 h-5 mr-3" />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Domains",
|
title: "Domains",
|
||||||
href: "/domains",
|
href: "/domains",
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ChevronLeft } from "lucide-react";
|
||||||
|
import { useLocation } from "wouter";
|
||||||
|
|
||||||
|
interface BackButtonProps {
|
||||||
|
to?: string;
|
||||||
|
fallbackPath?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BackButton({ to, fallbackPath = "/", className = "" }: BackButtonProps) {
|
||||||
|
const [_, setLocation] = useLocation();
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (to) {
|
||||||
|
setLocation(to);
|
||||||
|
} else if (window.history.length > 2) {
|
||||||
|
window.history.back();
|
||||||
|
} else {
|
||||||
|
setLocation(fallbackPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleBack}
|
||||||
|
className={`flex items-center gap-1 px-2 ${className}`}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
<span>Back</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -54,10 +54,16 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
|
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
|
||||||
import { RecentActivity } from "@/components/activity/recent-activity";
|
import { RecentActivity } from "@/components/activity/recent-activity";
|
||||||
import { Pagination } from "@/components/shared/pagination";
|
import { Pagination } from "@/components/shared/pagination";
|
||||||
import { Loader2, Home, Plus, Pencil, Trash2, ArrowLeft } from "lucide-react";
|
import { Loader2, Home, Plus, Pencil, Trash2, ArrowLeft, FileText } from "lucide-react";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
|
|
||||||
@@ -69,6 +75,8 @@ const dnsRecordSchema = z.object({
|
|||||||
ttl: z.number().int().min(1).default(3600),
|
ttl: z.number().int().min(1).default(3600),
|
||||||
proxied: z.boolean().default(false),
|
proxied: z.boolean().default(false),
|
||||||
isActive: z.boolean().default(true),
|
isActive: z.boolean().default(true),
|
||||||
|
isAutoIP: z.boolean().default(false),
|
||||||
|
notes: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function DnsRecordsPage() {
|
export default function DnsRecordsPage() {
|
||||||
@@ -113,6 +121,8 @@ export default function DnsRecordsPage() {
|
|||||||
ttl: 3600,
|
ttl: 3600,
|
||||||
proxied: false,
|
proxied: false,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
isAutoIP: false,
|
||||||
|
notes: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,9 +133,11 @@ export default function DnsRecordsPage() {
|
|||||||
name: selectedRecord.name,
|
name: selectedRecord.name,
|
||||||
type: selectedRecord.type as any,
|
type: selectedRecord.type as any,
|
||||||
content: selectedRecord.content,
|
content: selectedRecord.content,
|
||||||
ttl: selectedRecord.ttl,
|
ttl: selectedRecord.ttl ?? 3600,
|
||||||
proxied: selectedRecord.proxied,
|
proxied: selectedRecord.proxied ?? false,
|
||||||
isActive: selectedRecord.isActive,
|
isActive: selectedRecord.isActive,
|
||||||
|
isAutoIP: selectedRecord.isAutoIP ?? false,
|
||||||
|
notes: selectedRecord.notes ?? "",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
form.reset({
|
form.reset({
|
||||||
@@ -135,6 +147,8 @@ export default function DnsRecordsPage() {
|
|||||||
ttl: 3600,
|
ttl: 3600,
|
||||||
proxied: false,
|
proxied: false,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
isAutoIP: false,
|
||||||
|
notes: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [selectedRecord, form]);
|
}, [selectedRecord, form]);
|
||||||
@@ -289,7 +303,7 @@ export default function DnsRecordsPage() {
|
|||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
<BreadcrumbSeparator />
|
<BreadcrumbSeparator />
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
<BreadcrumbLink>{domain?.name || 'Loading...'}</BreadcrumbLink>
|
<BreadcrumbLink href="#">{domain?.name || 'Loading...'}</BreadcrumbLink>
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
@@ -345,6 +359,11 @@ export default function DnsRecordsPage() {
|
|||||||
<TableCell className="font-medium">{record.name}</TableCell>
|
<TableCell className="font-medium">{record.name}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="outline">{record.type}</Badge>
|
<Badge variant="outline">{record.type}</Badge>
|
||||||
|
{record.isAutoIP && (record.type === 'A' || record.type === 'AAAA') && (
|
||||||
|
<Badge variant="outline" className="ml-1 bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300">
|
||||||
|
Auto IP
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="font-mono text-sm">
|
<TableCell className="font-mono text-sm">
|
||||||
{record.content.length > 30
|
{record.content.length > 30
|
||||||
@@ -369,21 +388,37 @@ export default function DnsRecordsPage() {
|
|||||||
: "Never"}
|
: "Never"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Button
|
<div className="flex items-center justify-end">
|
||||||
variant="ghost"
|
{record.notes && (
|
||||||
size="icon"
|
<TooltipProvider>
|
||||||
onClick={() => handleEditRecord(record)}
|
<Tooltip>
|
||||||
>
|
<TooltipTrigger asChild>
|
||||||
<Pencil className="h-4 w-4" />
|
<Button variant="ghost" size="icon">
|
||||||
</Button>
|
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||||
<Button
|
</Button>
|
||||||
variant="ghost"
|
</TooltipTrigger>
|
||||||
size="icon"
|
<TooltipContent side="left">
|
||||||
onClick={() => handleDeleteRecord(record)}
|
<p className="font-normal">{record.notes}</p>
|
||||||
className="text-destructive"
|
</TooltipContent>
|
||||||
>
|
</Tooltip>
|
||||||
<Trash2 className="h-4 w-4" />
|
</TooltipProvider>
|
||||||
</Button>
|
)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleEditRecord(record)}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleDeleteRecord(record)}
|
||||||
|
className="text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
@@ -559,6 +594,46 @@ export default function DnsRecordsPage() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{(form.watch("type") === "A" || form.watch("type") === "AAAA") && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isAutoIP"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel className="text-base">Auto IP Address</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically determine IP address using STUN
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="notes"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Notes</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Optional notes about this record" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Add any additional information about this record
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -724,6 +799,46 @@ export default function DnsRecordsPage() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{(form.watch("type") === "A" || form.watch("type") === "AAAA") && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isAutoIP"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel className="text-base">Auto IP Address</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically determine IP address using STUN
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="notes"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Notes</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Optional notes about this record" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Add any additional information about this record
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
+36
-6
@@ -271,7 +271,20 @@ export class MemStorage implements IStorage {
|
|||||||
const id = this.recordIdCounter++;
|
const id = this.recordIdCounter++;
|
||||||
const createdAt = new Date();
|
const createdAt = new Date();
|
||||||
const lastUpdated = new Date();
|
const lastUpdated = new Date();
|
||||||
const newRecord: DnsRecord = { id, ...record, lastUpdated, createdAt };
|
const newRecord: DnsRecord = {
|
||||||
|
id,
|
||||||
|
domainId: record.domainId,
|
||||||
|
name: record.name,
|
||||||
|
type: record.type,
|
||||||
|
content: record.content,
|
||||||
|
ttl: record.ttl ?? 3600,
|
||||||
|
proxied: record.proxied ?? false,
|
||||||
|
isActive: record.isActive ?? true,
|
||||||
|
isAutoIP: record.isAutoIP ?? false,
|
||||||
|
notes: record.notes ?? null,
|
||||||
|
lastUpdated,
|
||||||
|
createdAt
|
||||||
|
};
|
||||||
this.recordsMap.set(id, newRecord);
|
this.recordsMap.set(id, newRecord);
|
||||||
return newRecord;
|
return newRecord;
|
||||||
}
|
}
|
||||||
@@ -305,7 +318,14 @@ export class MemStorage implements IStorage {
|
|||||||
async createProvider(provider: InsertProvider): Promise<Provider> {
|
async createProvider(provider: InsertProvider): Promise<Provider> {
|
||||||
const id = this.providerIdCounter++;
|
const id = this.providerIdCounter++;
|
||||||
const createdAt = new Date();
|
const createdAt = new Date();
|
||||||
const newProvider: Provider = { id, ...provider, createdAt };
|
const newProvider: Provider = {
|
||||||
|
id,
|
||||||
|
name: provider.name,
|
||||||
|
type: provider.type,
|
||||||
|
credentials: provider.credentials ?? null,
|
||||||
|
isActive: provider.isActive ?? true,
|
||||||
|
createdAt
|
||||||
|
};
|
||||||
this.providersMap.set(id, newProvider);
|
this.providersMap.set(id, newProvider);
|
||||||
return newProvider;
|
return newProvider;
|
||||||
}
|
}
|
||||||
@@ -341,7 +361,17 @@ export class MemStorage implements IStorage {
|
|||||||
async createApiToken(token: InsertApiToken): Promise<ApiToken> {
|
async createApiToken(token: InsertApiToken): Promise<ApiToken> {
|
||||||
const id = this.apiTokenIdCounter++;
|
const id = this.apiTokenIdCounter++;
|
||||||
const createdAt = new Date();
|
const createdAt = new Date();
|
||||||
const newToken: ApiToken = { id, ...token, createdAt };
|
const newToken: ApiToken = {
|
||||||
|
id,
|
||||||
|
name: token.name,
|
||||||
|
token: token.token,
|
||||||
|
organizationId: token.organizationId,
|
||||||
|
permissions: token.permissions ?? null,
|
||||||
|
createdBy: token.createdBy,
|
||||||
|
isActive: token.isActive ?? true,
|
||||||
|
expiresAt: token.expiresAt ?? null,
|
||||||
|
createdAt
|
||||||
|
};
|
||||||
this.apiTokensMap.set(id, newToken);
|
this.apiTokensMap.set(id, newToken);
|
||||||
return newToken;
|
return newToken;
|
||||||
}
|
}
|
||||||
@@ -374,9 +404,9 @@ export class MemStorage implements IStorage {
|
|||||||
id,
|
id,
|
||||||
recordId,
|
recordId,
|
||||||
action,
|
action,
|
||||||
previousValue,
|
previousValue: previousValue ?? null,
|
||||||
newValue,
|
newValue: newValue ?? null,
|
||||||
userId,
|
userId: userId ?? null,
|
||||||
timestamp
|
timestamp
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ export const dnsRecords = pgTable("dns_records", {
|
|||||||
ttl: integer("ttl").default(3600),
|
ttl: integer("ttl").default(3600),
|
||||||
proxied: boolean("proxied").default(false),
|
proxied: boolean("proxied").default(false),
|
||||||
isActive: boolean("is_active").default(true).notNull(),
|
isActive: boolean("is_active").default(true).notNull(),
|
||||||
|
isAutoIP: boolean("is_auto_ip").default(false).notNull(),
|
||||||
|
notes: text("notes"),
|
||||||
lastUpdated: timestamp("last_updated"),
|
lastUpdated: timestamp("last_updated"),
|
||||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
@@ -117,6 +119,8 @@ export const insertDnsRecordSchema = createInsertSchema(dnsRecords).pick({
|
|||||||
ttl: true,
|
ttl: true,
|
||||||
proxied: true,
|
proxied: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
isAutoIP: true,
|
||||||
|
notes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const insertProviderSchema = createInsertSchema(providers).pick({
|
export const insertProviderSchema = createInsertSchema(providers).pick({
|
||||||
|
|||||||
Reference in New Issue
Block a user