diff --git a/actions/cloudflare-dns-record.ts b/actions/cloudflare-dns-record.ts index d376c32..7ef3d35 100644 --- a/actions/cloudflare-dns-record.ts +++ b/actions/cloudflare-dns-record.ts @@ -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, + }, + }); +} diff --git a/app/(protected)/dashboard/page.tsx b/app/(protected)/dashboard/page.tsx index 1e1fcc3..2d5d6d5 100644 --- a/app/(protected)/dashboard/page.tsx +++ b/app/(protected)/dashboard/page.tsx @@ -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 ( <> - - 、 - - - No record created - - You don't have any record yet. Start creating record. - - - + + ); } diff --git a/app/(protected)/dashboard/record-list.tsx b/app/(protected)/dashboard/record-list.tsx new file mode 100644 index 0000000..97d9bbd --- /dev/null +++ b/app/(protected)/dashboard/record-list.tsx @@ -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; +} + +function TableColumnSekleton({ className }: { className?: string }) { + return ( + + + + + + + + + + + + + + + + + + + + + ); +} + +export default function UserRecordsList({ user }: RecordListProps) { + const [isShowForm, setShowForm] = useState(false); + const [formType, setFormType] = useState("add"); + const [currentEditRecord, setCurrentEditRecord] = + useState(null); + + const { data, error, isLoading } = useSWR( + "/api/record", + fetcher, + { + revalidateOnFocus: false, + }, + ); + + return ( + <> + + +
+ Records + + All Dns Records + +
+ +
+ + {isShowForm && ( + + )} + + + + + Type + + + Name + + + Content + + + TTL + + + Status + + + Actions + + + + + {isLoading ? ( + <> + + + + ) : data && data.length > 0 ? ( + data.map((record) => ( + + + + {record.type} + + + {record.name} + + {record.content} + + + { + TTL_ENUMS.find((ttl) => ttl.value === `${record.ttl}`) + ?.label + } + + + + + + + + + )) + ) : ( + + + No records + + You don't have any record yet. Start creating record. + + + + )} + +
+
+
+ + ); +} diff --git a/app/api/record/add/route.ts b/app/api/record/add/route.ts index 523f5a3..90dbdae 100644 --- a/app/api/record/add/route.ts +++ b/app/api/record/add/route.ts @@ -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", diff --git a/app/api/record/delete/route.ts b/app/api/record/delete/route.ts index aa62706..65e700a 100644 --- a/app/api/record/delete/route.ts +++ b/app/api/record/delete/route.ts @@ -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", + }); + } } diff --git a/app/api/record/route.ts b/app/api/record/route.ts index e69de29..7f94fc0 100644 --- a/app/api/record/route.ts +++ b/app/api/record/route.ts @@ -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", + }); + } +} diff --git a/app/api/record/update/route.ts b/app/api/record/update/route.ts index e69de29..fb573e3 100644 --- a/app/api/record/update/route.ts +++ b/app/api/record/update/route.ts @@ -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", + }); + } +} diff --git a/components/dashboard/form-section-columns.tsx b/components/dashboard/form-section-columns.tsx index fa2df5b..f50958f 100644 --- a/components/dashboard/form-section-columns.tsx +++ b/components/dashboard/form-section-columns.tsx @@ -8,7 +8,9 @@ interface SectionColumnsType { export function FormSectionColumns({ title, children }: SectionColumnsType) { return (
-

{title}

+

+ {title} +

{children}
); diff --git a/components/dashboard/status-dot.tsx b/components/dashboard/status-dot.tsx new file mode 100644 index 0000000..0f65828 --- /dev/null +++ b/components/dashboard/status-dot.tsx @@ -0,0 +1,12 @@ +import { cn } from "@/lib/utils"; + +export default function StatusDot({ status }: { status: number }) { + return ( +
+ ); +} diff --git a/components/forms/add-record-form.tsx b/components/forms/record-form.tsx similarity index 66% rename from components/forms/add-record-form.tsx rename to components/forms/record-form.tsx index cfb0a8d..ab3710d 100644 --- a/components/forms/add-record-form.tsx +++ b/components/forms/record-form.tsx @@ -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; + isShowForm: boolean; + setShowForm: Dispatch>; + 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({ 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 (
{}} name={"type"} - defaultValue="CNAME" disabled + defaultValue="CNAME" > @@ -122,10 +183,10 @@ export function AddRecordForm({ user }: AddRecordFormProps) { )}
- +
-