chore: add curd
This commit is contained in:
@@ -98,6 +98,24 @@ export async function getUserRecordCount(userId: string, active: number = 1) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserRecordByTypeNameContent(
|
||||
userId: string,
|
||||
type: string,
|
||||
name: string,
|
||||
content: string,
|
||||
active: number = 1,
|
||||
) {
|
||||
return await prisma.userRecord.findMany({
|
||||
where: {
|
||||
userId,
|
||||
type,
|
||||
content,
|
||||
// name,
|
||||
active,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteUserRecord(
|
||||
userId: string,
|
||||
record_id: string,
|
||||
@@ -113,3 +131,43 @@ export async function deleteUserRecord(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateUserRecord(
|
||||
userId: string,
|
||||
data: UserRecordFormData,
|
||||
) {
|
||||
return await prisma.userRecord.update({
|
||||
where: {
|
||||
userId,
|
||||
record_id: data.record_id,
|
||||
zone_id: data.zone_id,
|
||||
active: data.active,
|
||||
},
|
||||
data: {
|
||||
type: data.type,
|
||||
name: data.name,
|
||||
content: data.content,
|
||||
ttl: data.ttl,
|
||||
comment: data.comment,
|
||||
proxied: data.proxied,
|
||||
modified_on: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
export async function updateUserRecordState(
|
||||
userId: string,
|
||||
record_id: string,
|
||||
zone_id: string,
|
||||
active: number,
|
||||
) {
|
||||
return await prisma.userRecord.update({
|
||||
where: {
|
||||
userId,
|
||||
record_id,
|
||||
zone_id,
|
||||
},
|
||||
data: {
|
||||
active,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ import { redirect } from "next/navigation";
|
||||
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { constructMetadata } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DashboardHeader } from "@/components/dashboard/header";
|
||||
import { AddRecordForm } from "@/components/forms/add-record-form";
|
||||
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
|
||||
|
||||
import UserRecordsList from "./record-list";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "Dashboard – Next Template",
|
||||
description: "Create and manage content.",
|
||||
title: "Dashboard - WRDO",
|
||||
description: "List and manage records.",
|
||||
});
|
||||
|
||||
export default async function DashboardPage() {
|
||||
@@ -19,19 +18,8 @@ export default async function DashboardPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader
|
||||
heading="Dashboard"
|
||||
// text={`Current Role : ${user?.role}`}
|
||||
/>
|
||||
<AddRecordForm user={{ id: user.id, name: user.name || "" }} />、
|
||||
<EmptyPlaceholder>
|
||||
<EmptyPlaceholder.Icon name="post" />
|
||||
<EmptyPlaceholder.Title>No record created</EmptyPlaceholder.Title>
|
||||
<EmptyPlaceholder.Description>
|
||||
You don't have any record yet. Start creating record.
|
||||
</EmptyPlaceholder.Description>
|
||||
<Button>Add Record</Button>
|
||||
</EmptyPlaceholder>
|
||||
<DashboardHeader heading="Dashboard" />
|
||||
<UserRecordsList user={{ id: user.id, name: user.name || "" }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { UserRecordFormData } from "@/actions/cloudflare-dns-record";
|
||||
import { User } from "@prisma/client";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import useSWR from "swr";
|
||||
|
||||
import { TTL_ENUMS } from "@/lib/cloudflare";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import StatusDot from "@/components/dashboard/status-dot";
|
||||
import { FormType, RecordForm } from "@/components/forms/record-form";
|
||||
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
|
||||
|
||||
export interface RecordListProps {
|
||||
user: Pick<User, "id" | "name">;
|
||||
}
|
||||
|
||||
function TableColumnSekleton({ className }: { className?: string }) {
|
||||
return (
|
||||
<TableRow className="grid grid-cols-6 items-center">
|
||||
<TableCell className="col-span-1">
|
||||
<Skeleton className="h-5 w-24" />
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1">
|
||||
<Skeleton className="h-5 w-24" />
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1">
|
||||
<Skeleton className="h-5 w-24" />
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1">
|
||||
<Skeleton className="h-5 w-16" />
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 flex justify-center">
|
||||
<Skeleton className="h-5 w-16" />
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 flex justify-center">
|
||||
<Skeleton className="h-5 w-16" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default function UserRecordsList({ user }: RecordListProps) {
|
||||
const [isShowForm, setShowForm] = useState(false);
|
||||
const [formType, setFormType] = useState<FormType>("add");
|
||||
const [currentEditRecord, setCurrentEditRecord] =
|
||||
useState<UserRecordFormData | null>(null);
|
||||
|
||||
const { data, error, isLoading } = useSWR<UserRecordFormData[]>(
|
||||
"/api/record",
|
||||
fetcher,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="xl:col-span-2">
|
||||
<CardHeader className="flex flex-row items-center">
|
||||
<div className="grid gap-2">
|
||||
<CardTitle>Records</CardTitle>
|
||||
<CardDescription className="text-balance">
|
||||
All Dns Records
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
className="ml-auto w-[120px] shrink-0 gap-1"
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
setCurrentEditRecord(null);
|
||||
setShowForm(false);
|
||||
setFormType("add");
|
||||
setShowForm(!isShowForm);
|
||||
}}
|
||||
>
|
||||
Add record
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isShowForm && (
|
||||
<RecordForm
|
||||
user={{ id: user.id, name: user.name || "" }}
|
||||
isShowForm={isShowForm}
|
||||
setShowForm={setShowForm}
|
||||
type={formType}
|
||||
initData={currentEditRecord}
|
||||
/>
|
||||
)}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="grid grid-cols-6 items-center">
|
||||
<TableHead className="col-span-1 flex items-center font-bold">
|
||||
Type
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex items-center font-bold">
|
||||
Name
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex items-center font-bold">
|
||||
Content
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex items-center font-bold">
|
||||
TTL
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex items-center justify-center font-bold">
|
||||
Status
|
||||
</TableHead>
|
||||
<TableHead className="col-span-1 flex items-center justify-center font-bold">
|
||||
Actions
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<TableColumnSekleton className="col-span-1" />
|
||||
<TableColumnSekleton className="col-span-1" />
|
||||
</>
|
||||
) : data && data.length > 0 ? (
|
||||
data.map((record) => (
|
||||
<TableRow className="grid animate-fade-in grid-cols-6 items-center animate-in">
|
||||
<TableCell className="col-span-1">
|
||||
<Badge className="text-xs" variant="outline">
|
||||
{record.type}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1">{record.name}</TableCell>
|
||||
<TableCell className="col-span-1">
|
||||
{record.content}
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1">
|
||||
{
|
||||
TTL_ENUMS.find((ttl) => ttl.value === `${record.ttl}`)
|
||||
?.label
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 flex justify-center">
|
||||
<StatusDot status={record.active} />
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 flex justify-center">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => {
|
||||
setCurrentEditRecord(record);
|
||||
setShowForm(false);
|
||||
setFormType("edit");
|
||||
setShowForm(!isShowForm);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<EmptyPlaceholder>
|
||||
<EmptyPlaceholder.Icon name="post" />
|
||||
<EmptyPlaceholder.Title>No records</EmptyPlaceholder.Title>
|
||||
<EmptyPlaceholder.Description>
|
||||
You don't have any record yet. Start creating record.
|
||||
</EmptyPlaceholder.Description>
|
||||
<Button>Add Record</Button>
|
||||
</EmptyPlaceholder>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
createUserRecord,
|
||||
getUserRecordByTypeNameContent,
|
||||
getUserRecordCount,
|
||||
} from "@/actions/cloudflare-dns-record";
|
||||
|
||||
@@ -40,22 +41,40 @@ export async function POST(req: Request) {
|
||||
proxied: false,
|
||||
};
|
||||
|
||||
// return Response.json(record);
|
||||
// check quota
|
||||
const user_records_count = await getUserRecordCount(user.id);
|
||||
if (user_records_count >= NEXT_PUBLIC_FREE_RECORD_QUOTA) {
|
||||
if (
|
||||
Number(NEXT_PUBLIC_FREE_RECORD_QUOTA) > 0 &&
|
||||
user_records_count >= Number(NEXT_PUBLIC_FREE_RECORD_QUOTA)
|
||||
) {
|
||||
return Response.json("Your records have reached the free limit.", {
|
||||
status: 409,
|
||||
statusText: "Your records have reached the free limit.",
|
||||
});
|
||||
}
|
||||
|
||||
const user_record = await getUserRecordByTypeNameContent(
|
||||
user.id,
|
||||
record.type,
|
||||
record.name,
|
||||
record.content,
|
||||
1,
|
||||
);
|
||||
|
||||
if (user_record && user_record.length > 0) {
|
||||
return Response.json("Record already exists", {
|
||||
status: 403,
|
||||
statusText: "Record already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const data = await createDNSRecord(
|
||||
CLOUDFLARE_ZONE_ID,
|
||||
CLOUDFLARE_API_KEY,
|
||||
CLOUDFLARE_EMAIL,
|
||||
record,
|
||||
);
|
||||
if (!data.success || !data.result) {
|
||||
if (!data.success || !data.result?.id) {
|
||||
return Response.json(data.errors, {
|
||||
status: 501,
|
||||
statusText: `Error occurred. ${data.errors}`,
|
||||
@@ -86,6 +105,7 @@ export async function POST(req: Request) {
|
||||
return Response.json(res.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Response.json(error, {
|
||||
status: 500,
|
||||
statusText: "Server error",
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
import { deleteUserRecord } from "@/actions/cloudflare-dns-record";
|
||||
|
||||
import { env } from "@/env.mjs";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
|
||||
export async function DELETE(req: Request) {
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
} catch (error) {}
|
||||
const user = await getCurrentUser();
|
||||
if (!user?.id) {
|
||||
return Response.json("Unauthorized", {
|
||||
status: 401,
|
||||
statusText: "Unauthorized",
|
||||
});
|
||||
}
|
||||
|
||||
const { record_id, zone_id, active } = await req.json();
|
||||
const { CLOUDFLARE_ZONE_ID, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env;
|
||||
|
||||
await deleteUserRecord(user.id, record_id, zone_id, active);
|
||||
// await
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Response.json(error, {
|
||||
status: 500,
|
||||
statusText: "Server error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { getUserRecords } from "@/actions/cloudflare-dns-record";
|
||||
|
||||
import { env } from "@/env.mjs";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user?.id) {
|
||||
return Response.json("Unauthorized", {
|
||||
status: 401,
|
||||
statusText: "Unauthorized",
|
||||
});
|
||||
}
|
||||
|
||||
// const { CLOUDFLARE_ZONE_ID, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env;
|
||||
|
||||
const user_records = await getUserRecords(user.id, 1);
|
||||
|
||||
return Response.json(user_records);
|
||||
} catch (error) {
|
||||
return Response.json(error, {
|
||||
status: 500,
|
||||
statusText: "Server error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { env } from "@/env.mjs";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return Response.json(error, {
|
||||
status: 500,
|
||||
statusText: "Server error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ interface SectionColumnsType {
|
||||
export function FormSectionColumns({ title, children }: SectionColumnsType) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 items-center gap-x-12 gap-y-2 py-2">
|
||||
<h2 className="col-span-4 text-lg font-semibold leading-none">{title}</h2>
|
||||
<h2 className="col-span-4 text-base font-semibold leading-none">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="col-span-6">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function StatusDot({ status }: { status: number }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-[9px] w-[9px] rounded-full",
|
||||
status === 1 ? "bg-green-500" : "bg-yellow-500",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { Dispatch, SetStateAction, useState, useTransition } from "react";
|
||||
import { UserRecordFormData } from "@/actions/cloudflare-dns-record";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { User } from "@prisma/client";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -29,13 +30,24 @@ import {
|
||||
|
||||
export type FormData = CreateDNSRecord;
|
||||
|
||||
interface AddRecordFormProps {
|
||||
export type FormType = "add" | "edit";
|
||||
|
||||
export interface RecordFormProps {
|
||||
user: Pick<User, "id" | "name">;
|
||||
isShowForm: boolean;
|
||||
setShowForm: Dispatch<SetStateAction<boolean>>;
|
||||
type: FormType;
|
||||
initData?: UserRecordFormData | null;
|
||||
}
|
||||
|
||||
export function AddRecordForm({ user }: AddRecordFormProps) {
|
||||
export function RecordForm({
|
||||
user,
|
||||
isShowForm,
|
||||
setShowForm,
|
||||
type,
|
||||
initData,
|
||||
}: RecordFormProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isShow, setShow] = useState(false);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -44,33 +56,82 @@ export function AddRecordForm({ user }: AddRecordFormProps) {
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(createRecordSchema),
|
||||
defaultValues: {
|
||||
type: "CNAME",
|
||||
ttl: 1,
|
||||
proxied: false,
|
||||
type: initData?.type || "CNAME",
|
||||
ttl: initData?.ttl || 1,
|
||||
proxied: initData?.proxied || false,
|
||||
comment: initData?.comment || "",
|
||||
name: initData?.name || "",
|
||||
content: initData?.content || "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
startTransition(async () => {
|
||||
const response = await fetch("/api/record/add", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
records: [data],
|
||||
}),
|
||||
});
|
||||
if (!response.ok || response.status !== 200) {
|
||||
toast.error("Add Record Failed", {
|
||||
description: response.statusText,
|
||||
});
|
||||
} else {
|
||||
const res = await response.json();
|
||||
toast.success(`Created record [${res?.name}] successfully`);
|
||||
setShow(false);
|
||||
if (type === "add") {
|
||||
handleCreateRecord(data);
|
||||
} else if (type === "edit") {
|
||||
handleUpdateRecord(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return isShow ? (
|
||||
const handleCreateRecord = async (data: CreateDNSRecord) => {
|
||||
const response = await fetch("/api/record/add", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
records: [data],
|
||||
}),
|
||||
});
|
||||
if (!response.ok || response.status !== 200) {
|
||||
toast.error("Created Failed!", {
|
||||
description: response.statusText,
|
||||
});
|
||||
} else {
|
||||
const res = await response.json();
|
||||
toast.success(`Created successfully!`);
|
||||
setShowForm(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateRecord = async (data: CreateDNSRecord) => {
|
||||
const response = await fetch("/api/record/update", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
records: [data],
|
||||
}),
|
||||
});
|
||||
if (!response.ok || response.status !== 200) {
|
||||
toast.error("Update Failed", {
|
||||
description: response.statusText,
|
||||
});
|
||||
} else {
|
||||
const res = await response.json();
|
||||
toast.success(`Update successfully!`);
|
||||
setShowForm(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRecord = async () => {
|
||||
const response = await fetch("/api/record/delete", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
record_id: initData?.record_id,
|
||||
zone_id: initData?.zone_id,
|
||||
active: initData?.active,
|
||||
}),
|
||||
});
|
||||
if (!response.ok || response.status !== 200) {
|
||||
toast.error("Delete Failed", {
|
||||
description: response.statusText,
|
||||
});
|
||||
} else {
|
||||
await response.json();
|
||||
toast.success(`Delete successfully!`);
|
||||
setShowForm(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
className="rounded-lg border border-dashed p-4 shadow-sm animate-in fade-in-50"
|
||||
onSubmit={onSubmit}
|
||||
@@ -80,8 +141,8 @@ export function AddRecordForm({ user }: AddRecordFormProps) {
|
||||
<Select
|
||||
onValueChange={(value: RecordType) => {}}
|
||||
name={"type"}
|
||||
defaultValue="CNAME"
|
||||
disabled
|
||||
defaultValue="CNAME"
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a type" />
|
||||
@@ -122,10 +183,10 @@ export function AddRecordForm({ user }: AddRecordFormProps) {
|
||||
)}
|
||||
</div>
|
||||
</FormSectionColumns>
|
||||
<FormSectionColumns title="Target">
|
||||
<FormSectionColumns title="Content">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<Label className="sr-only" htmlFor="target">
|
||||
Target
|
||||
<Label className="sr-only" htmlFor="content">
|
||||
Content
|
||||
</Label>
|
||||
<Input
|
||||
id="content"
|
||||
@@ -152,7 +213,7 @@ export function AddRecordForm({ user }: AddRecordFormProps) {
|
||||
<FormSectionColumns title="TTL">
|
||||
<Select
|
||||
onValueChange={(value: RecordType) => {}}
|
||||
name={"ttl"}
|
||||
name="ttl"
|
||||
defaultValue="1"
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
@@ -198,11 +259,20 @@ export function AddRecordForm({ user }: AddRecordFormProps) {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
{type === "edit" && (
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
className="mr-auto w-[80px] px-0"
|
||||
onClick={() => handleDeleteRecord()}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="reset"
|
||||
variant={"destructive"}
|
||||
variant={"outline"}
|
||||
className="w-[80px] px-0"
|
||||
onClick={() => setShow(false)}
|
||||
onClick={() => setShowForm(false)}
|
||||
>
|
||||
Cancle
|
||||
</Button>
|
||||
@@ -215,18 +285,10 @@ export function AddRecordForm({ user }: AddRecordFormProps) {
|
||||
{isPending ? (
|
||||
<Icons.spinner className="size-4 animate-spin" />
|
||||
) : (
|
||||
<p>Save</p>
|
||||
<p>{type === "edit" ? "Update" : "Save"}</p>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<Button
|
||||
className="w-[120px]"
|
||||
variant="default"
|
||||
onClick={() => setShow(true)}
|
||||
>
|
||||
Add record
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
+119
-28
@@ -11,6 +11,46 @@ export interface CreateDNSRecord {
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export interface DNSRecordResponse {
|
||||
success: boolean;
|
||||
errors: {
|
||||
code: number;
|
||||
message: string;
|
||||
}[];
|
||||
messages: {
|
||||
code: number;
|
||||
message: string;
|
||||
}[];
|
||||
result?: DNSRecordResult;
|
||||
result_info?: {
|
||||
count: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
total_count: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DNSRecordResult {
|
||||
id: string;
|
||||
zone_id: string;
|
||||
zone_name: string;
|
||||
name: string;
|
||||
type: string;
|
||||
content: string;
|
||||
proxiable: boolean;
|
||||
proxied: boolean;
|
||||
ttl: number;
|
||||
meta: {
|
||||
auto_added: boolean;
|
||||
managed_by_apps: boolean;
|
||||
managed_by_argo_tunnel: boolean;
|
||||
};
|
||||
comment: string;
|
||||
tags: string[];
|
||||
created_on: string;
|
||||
modified_on: string;
|
||||
}
|
||||
|
||||
export type RecordType = "A" | "CNAME";
|
||||
|
||||
export const RECORD_TYPE_ENUMS = [
|
||||
@@ -45,39 +85,23 @@ export const TTL_ENUMS = [
|
||||
label: "1d",
|
||||
},
|
||||
];
|
||||
|
||||
export interface CreateDNSRecordResponse {
|
||||
success: boolean;
|
||||
errors: any[];
|
||||
messages: any[];
|
||||
result?: {
|
||||
id: string;
|
||||
zone_id: string;
|
||||
zone_name: string;
|
||||
name: string;
|
||||
type: string;
|
||||
content: string;
|
||||
proxiable: boolean;
|
||||
proxied: boolean;
|
||||
ttl: number;
|
||||
meta: {
|
||||
auto_added: boolean;
|
||||
managed_by_apps: boolean;
|
||||
managed_by_argo_tunnel: boolean;
|
||||
};
|
||||
comment: string;
|
||||
tags: string[];
|
||||
created_on: string;
|
||||
modified_on: string;
|
||||
};
|
||||
}
|
||||
export const STATUS_ENUMS = [
|
||||
{
|
||||
value: 1,
|
||||
label: "Active",
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
label: "Inactive",
|
||||
},
|
||||
];
|
||||
|
||||
export const createDNSRecord = async (
|
||||
zoneId: string,
|
||||
apiKey: string,
|
||||
email: string,
|
||||
record: CreateDNSRecord,
|
||||
): Promise<CreateDNSRecordResponse> => {
|
||||
): Promise<DNSRecordResponse> => {
|
||||
try {
|
||||
const url = `${CLOUDFLARE_API_URL}/zones/${zoneId}/dns_records`;
|
||||
|
||||
@@ -111,7 +135,7 @@ export const deleteDNSRecord = async (
|
||||
apiKey: string,
|
||||
email: string,
|
||||
recordId: string,
|
||||
): Promise<Pick<CreateDNSRecordResponse, "result">> => {
|
||||
): Promise<Pick<DNSRecordResponse, "result">> => {
|
||||
try {
|
||||
const url = `${CLOUDFLARE_API_URL}/zones/${zoneId}/dns_records/${recordId}`;
|
||||
|
||||
@@ -138,3 +162,70 @@ export const deleteDNSRecord = async (
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDNSRecordsList = async (
|
||||
zoneId: string,
|
||||
apiKey: string,
|
||||
email: string,
|
||||
type?: RecordType,
|
||||
name?: string,
|
||||
page = 1,
|
||||
perPage = 100,
|
||||
): Promise<DNSRecordResult[]> => {
|
||||
try {
|
||||
const url = `${CLOUDFLARE_API_URL}/zones/${zoneId}/dns_records?page=${page}&per_page=${perPage}&type=${type}&name=${name}`;
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"X-Auth-Email": email,
|
||||
"X-Auth-Key": apiKey,
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDNSRecordDetail = async (
|
||||
zoneId: string,
|
||||
apiKey: string,
|
||||
email: string,
|
||||
recordId: string,
|
||||
): Promise<DNSRecordResponse> => {
|
||||
try {
|
||||
const url = `${CLOUDFLARE_API_URL}/zones/${zoneId}/dns_records/${recordId}`;
|
||||
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"X-Auth-Email": email,
|
||||
"X-Auth-Key": apiKey,
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -165,7 +165,6 @@ export const placeholderBlurhash =
|
||||
export function generateSecret(length: number = 16): string {
|
||||
// 使用 crypto.randomBytes 生成随机字节
|
||||
const buffer = crypto.randomBytes(length);
|
||||
console.log(buffer);
|
||||
// 将字节转换为十六进制字符串
|
||||
return buffer.toString("hex");
|
||||
}
|
||||
|
||||
@@ -25,6 +25,51 @@ const nextConfig = {
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: ["@prisma/client"],
|
||||
},
|
||||
redirects() {
|
||||
return [
|
||||
{
|
||||
source: "/0",
|
||||
destination: "https://www.oiov.dev",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/9",
|
||||
destination: "https://f8dd841.webp.li/IMG20240703084254.jpg",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/ai",
|
||||
destination: "https://oi.sorapi.dev/?ref=wrdo",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/cps",
|
||||
destination:
|
||||
"https://u3b6zhbgfp.feishu.cn/docx/OfHyd7LG5o0UJFx0xIdc0FYjnSf?from=wrdo",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/x",
|
||||
destination: "https://x.com/yesmoree",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/solo",
|
||||
destination: "https://solo.oiov.dev",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/rmbg",
|
||||
destination: "https://remover.wr.do",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/llk",
|
||||
destination: "https://www.oiov.dev/blog/llk",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = withContentlayer(nextConfig);
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"sharp": "^0.33.4",
|
||||
"shiki": "^1.11.0",
|
||||
"sonner": "^1.5.0",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.9.1",
|
||||
|
||||
Generated
+23
@@ -203,6 +203,9 @@ importers:
|
||||
sonner:
|
||||
specifier: ^1.5.0
|
||||
version: 1.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
swr:
|
||||
specifier: ^2.2.5
|
||||
version: 2.2.5(react@18.3.1)
|
||||
tailwind-merge:
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0
|
||||
@@ -5392,6 +5395,11 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
swr@2.2.5:
|
||||
resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==}
|
||||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0
|
||||
|
||||
tailwind-merge@2.2.0:
|
||||
resolution: {integrity: sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ==}
|
||||
|
||||
@@ -5646,6 +5654,11 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
use-sync-external-store@1.2.2:
|
||||
resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
@@ -11934,6 +11947,12 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
swr@2.2.5(react@18.3.1):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 18.3.1
|
||||
use-sync-external-store: 1.2.2(react@18.3.1)
|
||||
|
||||
tailwind-merge@2.2.0:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.4
|
||||
@@ -12239,6 +12258,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.3
|
||||
|
||||
use-sync-external-store@1.2.2(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
Reference in New Issue
Block a user