diff --git a/.env.example b/.env.example index 379eff4..1116a71 100644 --- a/.env.example +++ b/.env.example @@ -25,14 +25,6 @@ DATABASE_URL='postgres://[user]:[password]@[neon_hostname]/[dbname]?sslmode=requ # ----------------------------------------------------------------------------- RESEND_API_KEY= -# ----------------------------------------------------------------------------- -# Cloudflare -# ----------------------------------------------------------------------------- -NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME=wr.do,uv.do -CLOUDFLARE_ZONE=[{"zone_id":"abc123", "zone_name": "wr.do"},{"zone_id":"abc456", "zone_name": "uv.do"}] -CLOUDFLARE_API_KEY= -CLOUDFLARE_EMAIL= - # Open Signup NEXT_PUBLIC_OPEN_SIGNUP=1 @@ -45,7 +37,5 @@ SCREENSHOTONE_BASE_URL=https://shot.wr.do # GitHub api token for getting gitHub stars count GITHUB_TOKEN= -# Short domains, split by "," -NEXT_PUBLIC_SHORT_DOMAINS=wr.do,uv.do diff --git a/README-zh.md b/README-zh.md index 9dfe684..998c3c1 100644 --- a/README-zh.md +++ b/README-zh.md @@ -56,11 +56,22 @@ pnpm install pnpm dev ``` +#### 初始化数据库 + +```bash +pnpm postinstall +pnpm db:push +``` + +#### 激活管理员面板 + +Follow https://localhost:3000/setup + ## 社区群组 -- Discord: https://discord.gg/AHPQYuZu3m -- 微信群: - +- Discord: https://discord.gg/AHPQYuZu3m +- 微信群: + ![](https://wr.do/s/group) ## 许可证 diff --git a/README.md b/README.md index a6e195c..8dc93d1 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,17 @@ pnpm install pnpm dev ``` +#### Init database + +```bash +pnpm postinstall +pnpm db:push +``` + +#### Setup Admin Panel + +Follow https://localhost:3000/setup + ## Legitimacy review - To avoid abuse, applications without website content will be rejected @@ -68,9 +79,9 @@ pnpm dev ## Community Group -- Discord: https://discord.gg/AHPQYuZu3m -- 微信群: - +- Discord: https://discord.gg/AHPQYuZu3m +- 微信群: + ![](https://wr.do/s/group) ## License diff --git a/app/(protected)/admin/domains/domain-list.tsx b/app/(protected)/admin/domains/domain-list.tsx new file mode 100644 index 0000000..300381c --- /dev/null +++ b/app/(protected)/admin/domains/domain-list.tsx @@ -0,0 +1,357 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { User } from "@prisma/client"; +import { PenLine, RefreshCwIcon } from "lucide-react"; +import { toast } from "sonner"; +import useSWR, { useSWRConfig } from "swr"; + +import { DomainFormData } from "@/lib/dto/domains"; +import { fetcher, timeAgo } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, +} from "@/components/ui/card"; +import { Modal } from "@/components/ui/modal"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Switch } from "@/components/ui/switch"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { DomainForm } from "@/components/forms/domain-form"; +import { FormType } from "@/components/forms/record-form"; +import { EmptyPlaceholder } from "@/components/shared/empty-placeholder"; +import { Icons } from "@/components/shared/icons"; +import { PaginationWrapper } from "@/components/shared/pagination"; + +export interface DomainListProps { + user: Pick; + action: string; +} + +function TableColumnSekleton() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default function DomainList({ user, action }: DomainListProps) { + const [isShowForm, setShowForm] = useState(false); + const [formType, setFormType] = useState("add"); + const [currentEditDomain, setCurrentEditDomain] = + useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + // const [isShowDomainInfo, setShowDomainInfo] = useState(false); + // const [selectedDomain, setSelectedDomain] = useState( + // null, + // ); + const [searchParams, setSearchParams] = useState({ + slug: "", + target: "", + userName: "", + }); + + const { mutate } = useSWRConfig(); + const { data, isLoading } = useSWR<{ + total: number; + list: DomainFormData[]; + }>( + `${action}?page=${currentPage}&size=${pageSize}&slug=${searchParams.slug}&userName=${searchParams.userName}&target=${searchParams.target}`, + fetcher, + ); + + const handleRefresh = () => { + mutate( + `${action}?page=${currentPage}&size=${pageSize}&slug=${searchParams.slug}&userName=${searchParams.userName}&target=${searchParams.target}`, + undefined, + ); + }; + + const handleChangeStatus = async ( + checked: boolean, + target: string, + domain: DomainFormData, + ) => { + const res = await fetch(action, { + method: "PUT", + body: JSON.stringify({ + id: domain.id, + enable_short_link: + target === "enable_short_link" ? checked : domain.enable_short_link, + enable_email: target === "enable_email" ? checked : domain.enable_email, + enable_dns: target === "enable_dns" ? checked : domain.enable_dns, + active: target === "active" ? checked : domain.active, + }), + }); + if (res.ok) { + const data = await res.json(); + if (data) { + toast.success("Successed!"); + } + } else { + toast.error("Activation failed!"); + } + }; + + return ( + <> + + + + Total Domains:{" "} + {data && data.total} + + +
+ + +
+
+ + {/*
+
+ { + setSearchParams({ + ...searchParams, + slug: e.target.value, + }); + }} + /> + {searchParams.slug && ( + + )} +
+
*/} + + + + + + Domain + + + Shorten Service + + + Email Service + + + DNS Service + + + Active + + + Updated + + + Actions + + + + + {isLoading ? ( + <> + + + + + + + ) : data && data.list && data.list.length ? ( + data.list.map((domain) => ( +
+ + + + {domain.domain_name} + + + + + handleChangeStatus( + value, + "enable_short_link", + domain, + ) + } + /> + + + + handleChangeStatus(value, "enable_email", domain) + } + /> + + + + handleChangeStatus(value, "enable_dns", domain) + } + /> + + + + handleChangeStatus(value, "active", domain) + } + /> + + + {timeAgo(domain.updatedAt as Date)} + + + + {domain.cf_zone_id && + domain.cf_api_key && + domain.cf_email && ( + + )} + + + {/* {isShowDomainInfo && selectedDomain?.id === domain.id && ( + + )} */} +
+ )) + ) : ( + + + No Domains + + You don't have any domains yet. Start creating one. + + + )} +
+ {data && Math.ceil(data.total / pageSize) > 1 && ( + + )} +
+
+
+ + {/* form */} + + + + + ); +} + +export function DomainInfo({ domain }: { domain: DomainFormData }) { + return <>{domain.domain_name}; +} diff --git a/app/(protected)/admin/domains/loading.tsx b/app/(protected)/admin/domains/loading.tsx new file mode 100644 index 0000000..3ee8119 --- /dev/null +++ b/app/(protected)/admin/domains/loading.tsx @@ -0,0 +1,12 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { DashboardHeader } from "@/components/dashboard/header"; + +export default function DashboardRecordsLoading() { + return ( + <> + + + + + ); +} diff --git a/app/(protected)/admin/domains/page.tsx b/app/(protected)/admin/domains/page.tsx new file mode 100644 index 0000000..626f20c --- /dev/null +++ b/app/(protected)/admin/domains/page.tsx @@ -0,0 +1,41 @@ +import { redirect } from "next/navigation"; + +import { getCurrentUser } from "@/lib/session"; +import { constructMetadata } from "@/lib/utils"; +import { DashboardHeader } from "@/components/dashboard/header"; + +import UserRecordsList from "../../dashboard/records/record-list"; +import DomainList from "./domain-list"; + +export const metadata = constructMetadata({ + title: "DNS Records - WR.DO", + description: "List and manage records.", +}); + +export default async function DashboardPage() { + const user = await getCurrentUser(); + + if (!user?.id) redirect("/login"); + + return ( + <> + + + + ); +} diff --git a/app/(protected)/admin/users/user-list.tsx b/app/(protected)/admin/users/user-list.tsx index a4e613f..ebdfa07 100644 --- a/app/(protected)/admin/users/user-list.tsx +++ b/app/(protected)/admin/users/user-list.tsx @@ -33,7 +33,6 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import StatusDot from "@/components/dashboard/status-dot"; import { UserForm } from "@/components/forms/user-form"; import { EmptyPlaceholder } from "@/components/shared/empty-placeholder"; import { Icons } from "@/components/shared/icons"; diff --git a/app/(protected)/dashboard/records/record-list.tsx b/app/(protected)/dashboard/records/record-list.tsx index 959b13f..9128e8f 100644 --- a/app/(protected)/dashboard/records/record-list.tsx +++ b/app/(protected)/dashboard/records/record-list.tsx @@ -189,7 +189,7 @@ export default function UserRecordsList({ user, action }: RecordListProps) { setShowForm(!isShowForm); }} > - Add record + Add Record diff --git a/app/(protected)/dashboard/scrape/page.tsx b/app/(protected)/dashboard/scrape/page.tsx index 4c9d8fc..5904135 100644 --- a/app/(protected)/dashboard/scrape/page.tsx +++ b/app/(protected)/dashboard/scrape/page.tsx @@ -2,7 +2,7 @@ import { redirect } from "next/navigation"; import { getCurrentUser } from "@/lib/session"; import { constructMetadata } from "@/lib/utils"; -import { ScrapeInfoCard } from "@/components/dashboard/dashboard-info-card"; +import { StaticInfoCard } from "@/components/dashboard/dashboard-info-card"; import { DashboardHeader } from "@/components/dashboard/header"; import DashboardScrapeCharts from "./charts"; @@ -26,22 +26,19 @@ export default async function DashboardPage() { linkText="Open API." />
- - -
- - - Add url + Add URL
@@ -345,7 +345,6 @@ export default function UserUrlsList({ user, action }: UrlListProps) { handleChangeStatu(value, short.id || "") diff --git a/app/(protected)/setup/guide.tsx b/app/(protected)/setup/guide.tsx new file mode 100644 index 0000000..ae46cb5 --- /dev/null +++ b/app/(protected)/setup/guide.tsx @@ -0,0 +1,358 @@ +"use client"; + +import { useEffect, useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { AnimatePresence, motion } from "framer-motion"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { toast } from "sonner"; + +import { siteConfig } from "@/config/site"; +import { cn, removeUrlSuffix } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Modal } from "@/components/ui/modal"; +import { Skeleton } from "@/components/ui/skeleton"; +import { FormSectionColumns } from "@/components/dashboard/form-section-columns"; +import { Icons } from "@/components/shared/icons"; + +export default function StepGuide({ + user, +}: { + user: { id: string; email: string }; +}) { + const router = useRouter(); + const [currentStep, setCurrentStep] = useState(1); + const [direction, setDirection] = useState(0); + const [completedSteps, setCompletedSteps] = useState([]); + const [isMobile, setIsMobile] = useState(false); + + const steps = [ + { + id: 1, + title: "Set up an administrator", + description: + "Begin by entering your website URL or selecting an example site to reimagine your website with modern themes.", + component: () => , + }, + { + id: 2, + title: "Add the first domain", + description: + "Check out your reimagined site and click to Migrate & Download.", + component: () => , + }, + { + id: 3, + title: "Congrats on completing setup 🎉", + description: + "Navigate to your GitHub dashboard where you'll manage your repository and project files.", + component: () => , + }, + ]; + + useEffect(() => { + const checkMobile = () => { + setIsMobile(window.innerWidth < 768); + }; + + checkMobile(); + window.addEventListener("resize", checkMobile); + + return () => { + window.removeEventListener("resize", checkMobile); + }; + }, []); + + const goToNextStep = () => { + if (currentStep < steps.length) { + setDirection(1); + setCurrentStep(currentStep + 1); + if (!completedSteps.includes(currentStep)) { + setCompletedSteps([...completedSteps, currentStep]); + } + } else if (currentStep === steps.length) { + router.push("/admin"); + } + }; + + const goToPreviousStep = () => { + if (currentStep > 1) { + setDirection(-1); + setCurrentStep(currentStep - 1); + } + }; + + const currentStepData = + steps.find((step) => step.id === currentStep) || steps[0]; + + const variants = { + enter: (direction: number) => ({ + x: direction > 0 ? 100 : -100, + opacity: 0, + }), + center: { + x: 0, + opacity: 1, + }, + exit: (direction: number) => ({ + x: direction < 0 ? 100 : -100, + opacity: 0, + }), + }; + + return ( + +
+
+

Admin Setup Guide

+
+ + {currentStep} + + of + {steps.length} +
+
+ + {/* Content area */} +
+ + +
+
+ + {currentStep} + + + {currentStepData.title} + +
+ + + {currentStepData.component()} + +
+
+
+
+
+ + + + + + +
+ ); +} + +function SetAdminRole({ id, email }: { id: string; email: string }) { + const [isPending, startTransition] = useTransition(); + const [isAdmin, setIsAdmin] = useState(false); + const handleSetAdmin = async () => { + startTransition(async () => { + const res = await fetch("/api/setup"); + if (res.ok) { + setIsAdmin(true); + } + }); + }; + + const ReadyBadge = ( + + + Ready + + ); + + return ( +
+
+ + Allow Sign Up: + + {siteConfig.openSignup ? ReadyBadge : } +
+ +
+ + Set {email} as ADMIN: + + {isAdmin ? ( + ReadyBadge + ) : ( + + )} +
+ +
+

+ 📢 Only by becoming an administrator can one access the admin panel + and add domain names. +

+

+ 📢 Administrators can set all user permissions, allocate quotas, view + and edit all resources (short links, subdomains, email), etc. +

+

+ 📢 Via{" "} + + quick start + {" "} + docs to get more information. +

+
+
+ ); +} + +function AddDomain({ onNextStep }: { onNextStep: () => void }) { + const [isPending, startTransition] = useTransition(); + const [domain, setDomain] = useState(""); + const handleCreateDomain = async () => { + if (!domain) { + toast.warning("Domain name cannot be empty"); + return; + } + startTransition(async () => { + const res = await fetch("/api/admin/domain", { + method: "POST", + body: JSON.stringify({ + data: { + domain_name: removeUrlSuffix(domain), + enable_short_link: true, + enable_email: true, + enable_dns: true, + cf_zone_id: "", + cf_api_key: "", + cf_email: "", + cf_api_key_encrypted: false, + max_short_links: 0, + max_email_forwards: 0, + max_dns_records: 0, + active: true, + }, + }), + }); + if (res.ok) { + onNextStep(); + } else { + toast.error("Created Failed!", { + description: await res.text(), + }); + } + }); + }; + return ( +
+ +
+ +
+ setDomain(e.target.value)} + /> +
+

+ Please enter a valid domain name (must be hosted on Cloudflare). +

+
+ +
+ + +
+
+
+ ); +} + +function Congrats() { + return <>; +} diff --git a/app/(protected)/setup/loading.tsx b/app/(protected)/setup/loading.tsx new file mode 100644 index 0000000..2c0fcc0 --- /dev/null +++ b/app/(protected)/setup/loading.tsx @@ -0,0 +1,15 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { DashboardHeader } from "@/components/dashboard/header"; + +export default function DashboardLoading() { + return ( + <> + + + + + ); +} diff --git a/app/(protected)/setup/page.tsx b/app/(protected)/setup/page.tsx new file mode 100644 index 0000000..087a833 --- /dev/null +++ b/app/(protected)/setup/page.tsx @@ -0,0 +1,28 @@ +import { redirect } from "next/navigation"; + +import { getAllUsersCount } from "@/lib/dto/user"; +import { getCurrentUser } from "@/lib/session"; +import { constructMetadata } from "@/lib/utils"; + +import SetupGuide from "./guide"; + +export const metadata = constructMetadata({ + title: "Setup Guide", + description: "Setup Guide", +}); + +export default async function SetupPage() { + const user = await getCurrentUser(); + + if (!user?.id) redirect("/login"); + + if (user.role === "ADMIN") redirect("/admin"); + + const count = await getAllUsersCount(); + + if (count === 1 && user.role === "USER") { + return ; + } + + return redirect("/admin"); +} diff --git a/app/api/admin/domain/route.ts b/app/api/admin/domain/route.ts new file mode 100644 index 0000000..4cb18b6 --- /dev/null +++ b/app/api/admin/domain/route.ts @@ -0,0 +1,147 @@ +import { NextRequest } from "next/server"; + +import { + createDomain, + deleteDomain, + getAllDomains, + invalidateDomainConfigCache, + updateDomain, +} from "@/lib/dto/domains"; +import { checkUserStatus } from "@/lib/dto/user"; +import { getCurrentUser } from "@/lib/session"; + +// Get domains list +export async function GET(req: NextRequest) { + try { + const user = checkUserStatus(await getCurrentUser()); + if (user instanceof Response) return user; + if (user.role !== "ADMIN") { + return Response.json("Unauthorized", { status: 401 }); + } + + // TODO: Add pagination + const domains = await getAllDomains(); + + return Response.json( + { list: domains, total: domains.length }, + { status: 200 }, + ); + } catch (error) { + console.error("[Error]", error); + return Response.json(error.message || "Server error", { status: 500 }); + } +} + +// Create domain +export async function POST(req: NextRequest) { + try { + const user = checkUserStatus(await getCurrentUser()); + if (user instanceof Response) return user; + if (user.role !== "ADMIN") { + return Response.json("Unauthorized", { status: 401 }); + } + + const { data } = await req.json(); + if (!data || !data.domain_name) { + return Response.json("domain_name is required", { status: 400 }); + } + + const newDomain = await createDomain({ + domain_name: data.domain_name, + enable_short_link: !!data.enable_short_link, + enable_email: !!data.enable_email, + enable_dns: !!data.enable_dns, + cf_zone_id: data.cf_zone_id, + cf_api_key: data.cf_api_key, + cf_email: data.cf_email, + cf_api_key_encrypted: false, + max_short_links: data.max_short_links, + max_email_forwards: data.max_email_forwards, + max_dns_records: data.max_dns_records, + active: true, + }); + + invalidateDomainConfigCache(); + + return Response.json(newDomain, { status: 200 }); + } catch (error) { + console.error("[Error]", error); + return Response.json(error.message || "Server error", { status: 500 }); + } +} + +// Update domain +export async function PUT(req: NextRequest) { + try { + const user = checkUserStatus(await getCurrentUser()); + if (user instanceof Response) return user; + if (user.role !== "ADMIN") { + return Response.json("Unauthorized", { status: 401 }); + } + + const { + domain_name, + enable_short_link, + enable_email, + enable_dns, + cf_zone_id, + cf_api_key, + cf_email, + max_short_links, + max_email_forwards, + max_dns_records, + active, + id, + } = await req.json(); + if (!id) { + return Response.json("domain id is required", { status: 400 }); + } + + const updatedDomain = await updateDomain(id, { + domain_name, + enable_short_link: !!enable_short_link, + enable_email: !!enable_email, + enable_dns: !!enable_dns, + active: !!active, + cf_zone_id, + cf_api_key, + cf_email, + cf_api_key_encrypted: false, + max_short_links, + max_email_forwards, + max_dns_records, + }); + + invalidateDomainConfigCache(); + + return Response.json(updatedDomain, { status: 200 }); + } catch (error) { + console.error("[Error]", error); + return Response.json(error.message || "Server error", { status: 500 }); + } +} + +// Delete domain +export async function DELETE(req: NextRequest) { + try { + const user = checkUserStatus(await getCurrentUser()); + if (user instanceof Response) return user; + if (user.role !== "ADMIN") { + return Response.json("Unauthorized", { status: 401 }); + } + + const { domain_name } = await req.json(); + if (!domain_name) { + return Response.json("domain_name is required", { status: 400 }); + } + + const deletedDomain = await deleteDomain(domain_name); + + invalidateDomainConfigCache(); + + return Response.json(deletedDomain, { status: 200 }); + } catch (error) { + console.error("[Error]", error); + return Response.json(error.message || "Server error", { status: 500 }); + } +} diff --git a/app/api/domain/route.ts b/app/api/domain/route.ts new file mode 100644 index 0000000..34b65c7 --- /dev/null +++ b/app/api/domain/route.ts @@ -0,0 +1,32 @@ +import { NextRequest } from "next/server"; + +import { FeatureMap, getDomainsByFeatureClient } from "@/lib/dto/domains"; +import { checkUserStatus } from "@/lib/dto/user"; +import { getCurrentUser } from "@/lib/session"; + +// Get domains by feature for frontend +export async function GET(req: NextRequest) { + try { + const user = checkUserStatus(await getCurrentUser()); + if (user instanceof Response) return user; + + const url = new URL(req.url); + const feature = url.searchParams.get("feature") || ""; + + if (!Object.keys(FeatureMap).includes(feature)) { + return Response.json( + "Invalid feature parameter. Use 'short', 'email', or 'record'.", + { + status: 400, + }, + ); + } + + const domainList = await getDomainsByFeatureClient(FeatureMap[feature]); + + return Response.json(domainList, { status: 200 }); + } catch (error) { + console.error("[Error]", error); + return Response.json(error.message || "Server error", { status: 500 }); + } +} diff --git a/app/api/record/add/route.ts b/app/api/record/add/route.ts index 89d6305..9346922 100644 --- a/app/api/record/add/route.ts +++ b/app/api/record/add/route.ts @@ -1,4 +1,3 @@ -import { env } from "@/env.mjs"; import { TeamPlanQuota } from "@/config/team"; import { createDNSRecord } from "@/lib/cloudflare"; import { @@ -6,31 +5,27 @@ import { getUserRecordByTypeNameContent, getUserRecordCount, } from "@/lib/dto/cloudflare-dns-record"; +import { getDomainsByFeature } from "@/lib/dto/domains"; import { checkUserStatus } from "@/lib/dto/user"; import { reservedDomains } from "@/lib/enums"; import { getCurrentUser } from "@/lib/session"; -import { generateSecret, parseZones } from "@/lib/utils"; +import { generateSecret } from "@/lib/utils"; export async function POST(req: Request) { try { const user = checkUserStatus(await getCurrentUser()); if (user instanceof Response) return user; - const { CLOUDFLARE_ZONE, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; - const zones = parseZones(CLOUDFLARE_ZONE || "[]"); - - if (!zones.length || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { - return Response.json("API key、zone iD and email are required", { + const zones = await getDomainsByFeature("enable_dns", true); + if (!zones.length) { + return Response.json("Please add at least one domain", { status: 400, - statusText: "API key、zone iD and email are required", + statusText: "Please add at least one domain", }); } const { total } = await getUserRecordCount(user.id); - if ( - user.role !== "ADMIN" && - total >= TeamPlanQuota[user.team].RC_NewRecords - ) { + if (total >= TeamPlanQuota[user.team].RC_NewRecords) { return Response.json("Your records have reached the free limit.", { status: 409, }); @@ -49,7 +44,7 @@ export async function POST(req: Request) { let matchedZone; for (const zone of zones) { - if (record.zone_name === zone.zone_name) { + if (record.zone_name === zone.domain_name) { matchedZone = zone; break; } @@ -85,22 +80,22 @@ export async function POST(req: Request) { } const data = await createDNSRecord( - matchedZone.zone_id, - CLOUDFLARE_API_KEY, - CLOUDFLARE_EMAIL, + matchedZone.cf_zone_id, + matchedZone.cf_api_key, + matchedZone.cf_email, record, ); if (!data.success || !data.result?.id) { - console.log("[data]", data); + // console.log("[data]", data); return Response.json(data.messages, { status: 501, }); } else { const res = await createUserRecord(user.id, { record_id: data.result.id, - zone_id: matchedZone.zone_id, - zone_name: matchedZone.zone_name, + zone_id: matchedZone.cf_zone_id, + zone_name: matchedZone.domain_name, name: data.result.name, type: data.result.type, content: data.result.content, diff --git a/app/api/record/admin/add/route.ts b/app/api/record/admin/add/route.ts index 49dbca6..6ca891c 100644 --- a/app/api/record/admin/add/route.ts +++ b/app/api/record/admin/add/route.ts @@ -1,4 +1,3 @@ -import { env } from "@/env.mjs"; import { TeamPlanQuota } from "@/config/team"; import { createDNSRecord } from "@/lib/cloudflare"; import { @@ -6,10 +5,11 @@ import { getUserRecordByTypeNameContent, getUserRecordCount, } from "@/lib/dto/cloudflare-dns-record"; +import { getDomainsByFeature } from "@/lib/dto/domains"; import { checkUserStatus, getUserByEmail } from "@/lib/dto/user"; import { reservedDomains } from "@/lib/enums"; import { getCurrentUser } from "@/lib/session"; -import { generateSecret, parseZones } from "@/lib/utils"; +import { generateSecret } from "@/lib/utils"; export async function POST(req: Request) { try { @@ -22,13 +22,11 @@ export async function POST(req: Request) { }); } - const { CLOUDFLARE_ZONE, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; - const zones = parseZones(CLOUDFLARE_ZONE || "[]"); - - if (!zones.length || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { - return Response.json("API key、zone iD and email are required", { + const zones = await getDomainsByFeature("enable_dns", true); + if (!zones.length) { + return Response.json("Please add at least one domain", { status: 400, - statusText: "API key、zone iD and email are required", + statusText: "Please add at least one domain", }); } @@ -60,7 +58,7 @@ export async function POST(req: Request) { let matchedZone; for (const zone of zones) { - if (record.zone_name === zone.zone_name) { + if (record.zone_name === zone.domain_name) { matchedZone = zone; break; } @@ -96,9 +94,9 @@ export async function POST(req: Request) { } const data = await createDNSRecord( - matchedZone.zone_id, - CLOUDFLARE_API_KEY, - CLOUDFLARE_EMAIL, + matchedZone.cf_zone_id, + matchedZone.cf_api_key, + matchedZone.cf_email, record, ); @@ -110,8 +108,8 @@ export async function POST(req: Request) { } else { const res = await createUserRecord(target_user.id, { record_id: data.result.id, - zone_id: matchedZone.zone_id, - zone_name: matchedZone.zone_name, + zone_id: matchedZone.cf_zone_id, + zone_name: matchedZone.domain_name, name: data.result.name, type: data.result.type, content: data.result.content, diff --git a/app/api/record/admin/delete/route.ts b/app/api/record/admin/delete/route.ts index 7e2ff9c..7445216 100644 --- a/app/api/record/admin/delete/route.ts +++ b/app/api/record/admin/delete/route.ts @@ -1,9 +1,8 @@ -import { env } from "@/env.mjs"; import { deleteDNSRecord } from "@/lib/cloudflare"; import { deleteUserRecord } from "@/lib/dto/cloudflare-dns-record"; +import { getDomainsByFeature } from "@/lib/dto/domains"; import { checkUserStatus } from "@/lib/dto/user"; import { getCurrentUser } from "@/lib/session"; -import { parseZones } from "@/lib/utils"; export async function POST(req: Request) { try { @@ -24,20 +23,15 @@ export async function POST(req: Request) { }); } - const { CLOUDFLARE_ZONE, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; - const zones = parseZones(CLOUDFLARE_ZONE || "[]"); - - if (!zones.length || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { - return Response.json( - "API key, zone configuration, and email are required", - { - status: 400, - statusText: "Missing required configuration", - }, - ); + const zones = await getDomainsByFeature("enable_dns", true); + if (!zones.length) { + return Response.json("Please add at least one domain", { + status: 400, + statusText: "Please add at least one domain", + }); } - const matchedZone = zones.find((zone) => zone.zone_id === zone_id); + const matchedZone = zones.find((zone) => zone.cf_zone_id === zone_id); if (!matchedZone) { return Response.json(`Invalid or unsupported zone_id: ${zone_id}`, { status: 400, @@ -48,9 +42,9 @@ export async function POST(req: Request) { // force delete await deleteUserRecord(userId, record_id, zone_id, active); await deleteDNSRecord( - matchedZone.zone_id, - CLOUDFLARE_API_KEY, - CLOUDFLARE_EMAIL, + matchedZone.cf_zone_id!, + matchedZone.cf_api_key!, + matchedZone.cf_email!, record_id, ); diff --git a/app/api/record/admin/route.ts b/app/api/record/admin/route.ts index cd6042e..082ddac 100644 --- a/app/api/record/admin/route.ts +++ b/app/api/record/admin/route.ts @@ -1,6 +1,5 @@ -import { env } from "@/env.mjs"; import { getUserRecords } from "@/lib/dto/cloudflare-dns-record"; -import { checkUserStatus, getUserByEmail } from "@/lib/dto/user"; +import { checkUserStatus } from "@/lib/dto/user"; import { getCurrentUser } from "@/lib/session"; export async function GET(req: Request) { diff --git a/app/api/record/admin/update/route.ts b/app/api/record/admin/update/route.ts index e8dc5c8..c07bfa0 100644 --- a/app/api/record/admin/update/route.ts +++ b/app/api/record/admin/update/route.ts @@ -1,9 +1,8 @@ -import { env } from "@/env.mjs"; import { updateDNSRecord } from "@/lib/cloudflare"; import { updateUserRecord } from "@/lib/dto/cloudflare-dns-record"; +import { getDomainsByFeature } from "@/lib/dto/domains"; import { checkUserStatus } from "@/lib/dto/user"; import { getCurrentUser } from "@/lib/session"; -import { parseZones } from "@/lib/utils"; export async function POST(req: Request) { try { @@ -16,16 +15,11 @@ export async function POST(req: Request) { }); } - const { CLOUDFLARE_ZONE, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; - const zones = parseZones(CLOUDFLARE_ZONE || "[]"); - - if (!zones.length || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { + const zones = await getDomainsByFeature("enable_dns", true); + if (!zones.length) { return Response.json( "API key, zone configuration, and email are required", - { - status: 400, - statusText: "Missing required configuration", - }, + { status: 401, statusText: "Missing required configuration" }, ); } @@ -44,7 +38,7 @@ export async function POST(req: Request) { let matchedZone; for (const zone of zones) { - if (record.zone_name === zone.zone_name) { + if (record.zone_name === zone.domain_name) { matchedZone = zone; break; } @@ -61,9 +55,9 @@ export async function POST(req: Request) { } const data = await updateDNSRecord( - matchedZone.zone_id, - CLOUDFLARE_API_KEY, - CLOUDFLARE_EMAIL, + matchedZone.cf_zone_id, + matchedZone.cf_api_key, + matchedZone.cf_email, recordId, { ...record, name: record_name }, ); @@ -80,8 +74,8 @@ export async function POST(req: Request) { const res = await updateUserRecord(userId, { record_id: data.result.id, - zone_id: matchedZone.zone_id, - zone_name: matchedZone.zone_name, + zone_id: matchedZone.cf_zone_id, + zone_name: matchedZone.domain_name, name: data.result.name, type: data.result.type, content: data.result.content, diff --git a/app/api/record/delete/route.ts b/app/api/record/delete/route.ts index 4d6d3de..e2bdebf 100644 --- a/app/api/record/delete/route.ts +++ b/app/api/record/delete/route.ts @@ -1,9 +1,9 @@ import { env } from "@/env.mjs"; import { deleteDNSRecord } from "@/lib/cloudflare"; import { deleteUserRecord } from "@/lib/dto/cloudflare-dns-record"; +import { getDomainsByFeature } from "@/lib/dto/domains"; import { checkUserStatus } from "@/lib/dto/user"; import { getCurrentUser } from "@/lib/session"; -import { parseZones } from "@/lib/utils"; export async function POST(req: Request) { try { @@ -12,20 +12,15 @@ export async function POST(req: Request) { const { record_id, zone_id, active } = await req.json(); - const { CLOUDFLARE_ZONE, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; - const zones = parseZones(CLOUDFLARE_ZONE || "[]"); - - if (!zones.length || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { - return Response.json( - "API key, zone configuration, and email are required", - { - status: 400, - statusText: "API key, zone configuration, and email are required", - }, - ); + const zones = await getDomainsByFeature("enable_dns", true); + if (!zones.length) { + return Response.json("Please add at least one domain", { + status: 400, + statusText: "Please add at least one domain", + }); } - const matchedZone = zones.find((zone) => zone.zone_id === zone_id); + const matchedZone = zones.find((zone) => zone.cf_zone_id === zone_id); if (!matchedZone) { return Response.json(`Invalid or unsupported zone_id: ${zone_id}`, { status: 400, @@ -34,9 +29,9 @@ export async function POST(req: Request) { } const res = await deleteDNSRecord( - matchedZone.zone_id, - CLOUDFLARE_API_KEY, - CLOUDFLARE_EMAIL, + matchedZone.cf_zone_id!, + matchedZone.cf_api_key!, + matchedZone.cf_email!, record_id, ); diff --git a/app/api/record/route.ts b/app/api/record/route.ts index b1de8bf..530a981 100644 --- a/app/api/record/route.ts +++ b/app/api/record/route.ts @@ -1,4 +1,3 @@ -import { env } from "@/env.mjs"; import { getUserRecords } from "@/lib/dto/cloudflare-dns-record"; import { checkUserStatus } from "@/lib/dto/user"; import { getCurrentUser } from "@/lib/session"; diff --git a/app/api/record/update/route.ts b/app/api/record/update/route.ts index 612a9dc..b04004d 100644 --- a/app/api/record/update/route.ts +++ b/app/api/record/update/route.ts @@ -1,13 +1,12 @@ -import { env } from "@/env.mjs"; import { updateDNSRecord } from "@/lib/cloudflare"; import { updateUserRecord, updateUserRecordState, } from "@/lib/dto/cloudflare-dns-record"; +import { getDomainsByFeature } from "@/lib/dto/domains"; import { checkUserStatus } from "@/lib/dto/user"; import { reservedDomains } from "@/lib/enums"; import { getCurrentUser } from "@/lib/session"; -import { parseZones } from "@/lib/utils"; // Update DNS record export async function POST(req: Request) { @@ -15,10 +14,8 @@ export async function POST(req: Request) { const user = checkUserStatus(await getCurrentUser()); if (user instanceof Response) return user; - const { CLOUDFLARE_ZONE, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; - const zones = parseZones(CLOUDFLARE_ZONE || "[]"); - - if (!zones.length || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { + const zones = await getDomainsByFeature("enable_dns", true); + if (!zones.length) { return Response.json( "API key, zone configuration, and email are required", { status: 401, statusText: "Missing required configuration" }, @@ -40,7 +37,7 @@ export async function POST(req: Request) { let matchedZone; for (const zone of zones) { - if (record.zone_name === zone.zone_name) { + if (record.zone_name === zone.domain_name) { matchedZone = zone; break; } @@ -64,14 +61,13 @@ export async function POST(req: Request) { } const data = await updateDNSRecord( - matchedZone.zone_id, - CLOUDFLARE_API_KEY, - CLOUDFLARE_EMAIL, + matchedZone.cf_zone_id, + matchedZone.cf_api_key, + matchedZone.cf_email, recordId, { ...record, name: record_name }, ); - console.log("[updateDNSRecord]", data); if (!data.success || !data.result?.id) { return Response.json( data.errors?.[0]?.message || "Failed to update DNS record", @@ -81,8 +77,8 @@ export async function POST(req: Request) { const res = await updateUserRecord(user.id, { record_id: data.result.id, - zone_id: matchedZone.zone_id, // Use matched zone_id - zone_name: matchedZone.zone_name, // Use matched zone_name + zone_id: matchedZone.cf_zone_id, + zone_name: matchedZone.domain_name, name: data.result.name, type: data.result.type, content: data.result.content, @@ -118,10 +114,8 @@ export async function PUT(req: Request) { const user = checkUserStatus(await getCurrentUser()); if (user instanceof Response) return user; - const { CLOUDFLARE_ZONE, CLOUDFLARE_API_KEY, CLOUDFLARE_EMAIL } = env; - const zones = parseZones(CLOUDFLARE_ZONE || "[]"); - - if (!zones.length || !CLOUDFLARE_API_KEY || !CLOUDFLARE_EMAIL) { + const zones = await getDomainsByFeature("enable_dns", true); + if (!zones.length) { return Response.json( "API key, zone configuration, and email are required", { status: 401, statusText: "Missing required configuration" }, @@ -136,7 +130,7 @@ export async function PUT(req: Request) { }); } - const matchedZone = zones.find((zone) => zone.zone_id === zone_id); + const matchedZone = zones.find((zone) => zone.cf_zone_id === zone_id); if (!matchedZone) { return Response.json(`Invalid or unsupported zone_id: ${zone_id}`, { status: 400, @@ -153,9 +147,9 @@ export async function PUT(req: Request) { isTargetAccessible = target_res.status === 200; } catch (fetchError) { isTargetAccessible = false; - console.log( - `[Fetch Error] Failed to access target ${target}: ${fetchError}`, - ); + // console.log( + // `[Fetch Error] Failed to access target ${target}: ${fetchError}`, + // ); } const res = await updateUserRecordState( diff --git a/app/api/setup/route.ts b/app/api/setup/route.ts new file mode 100644 index 0000000..0183d68 --- /dev/null +++ b/app/api/setup/route.ts @@ -0,0 +1,31 @@ +import { redirect } from "next/navigation"; + +import { + checkUserStatus, + getAllUsersCount, + setFirstUserAsAdmin, +} from "@/lib/dto/user"; +import { getCurrentUser } from "@/lib/session"; + +export async function GET(req: Request) { + try { + const user = checkUserStatus(await getCurrentUser()); + if (user instanceof Response) return user; + + const count = await getAllUsersCount(); + + if (count === 1 && user.role === "USER") { + const res = await setFirstUserAsAdmin(user.id); + if (res) { + return Response.json({ admin: res.role === "ADMIN" }, { status: 201 }); + } + return Response.json({ admin: false }, { status: 400 }); + } + + return redirect("/admin"); + } catch (error) { + return Response.json(error?.statusText || error, { + status: error.status || 500, + }); + } +} diff --git a/app/api/url/add/route.ts b/app/api/url/add/route.ts index 0a85f5e..643941f 100644 --- a/app/api/url/add/route.ts +++ b/app/api/url/add/route.ts @@ -1,4 +1,5 @@ import { TeamPlanQuota } from "@/config/team"; +import { getDomainsByFeature } from "@/lib/dto/domains"; import { createUserShortUrl } from "@/lib/dto/short-urls"; import { checkUserStatus } from "@/lib/dto/user"; import { getCurrentUser } from "@/lib/session"; @@ -23,6 +24,18 @@ export async function POST(req: Request) { const { target, url, prefix, visible, active, expiration, password } = createUrlSchema.parse(data); + + const zones = await getDomainsByFeature("enable_short_link"); + if ( + !zones.length || + !zones.map((zone) => zone.domain_name).includes(prefix) + ) { + return Response.json("Invalid domain", { + status: 400, + statusText: "Invalid domain", + }); + } + const res = await createUserShortUrl({ userId: user.id, userName: user.name || "Anonymous", diff --git a/app/api/url/route.ts b/app/api/url/route.ts index c5a57e6..a392071 100644 --- a/app/api/url/route.ts +++ b/app/api/url/route.ts @@ -1,4 +1,3 @@ -import { env } from "@/env.mjs"; import { getUserShortUrls } from "@/lib/dto/short-urls"; import { checkUserStatus } from "@/lib/dto/user"; import { getCurrentUser } from "@/lib/session"; diff --git a/app/api/v1/email/route.ts b/app/api/v1/email/route.ts index de7b7f5..e65fb1a 100644 --- a/app/api/v1/email/route.ts +++ b/app/api/v1/email/route.ts @@ -3,6 +3,7 @@ import { NextRequest, NextResponse } from "next/server"; import { siteConfig } from "@/config/site"; import { TeamPlanQuota } from "@/config/team"; import { checkApiKey } from "@/lib/dto/api-key"; +import { getDomainsByFeature } from "@/lib/dto/domains"; import { createUserEmail, deleteUserEmailByAddress } from "@/lib/dto/email"; import { reservedAddressSuffix } from "@/lib/enums"; import { restrictByTimeRange } from "@/lib/team"; @@ -53,7 +54,12 @@ export async function POST(req: NextRequest) { status: 400, }); } - if (!siteConfig.emailDomains.includes(suffix)) { + + const zones = await getDomainsByFeature("enable_email", true); + if ( + !zones.length || + !zones.map((zone) => zone.domain_name).includes(suffix) + ) { return NextResponse.json("Invalid email suffix address", { status: 400 }); } diff --git a/app/api/v1/short/route.ts b/app/api/v1/short/route.ts index 16aa43b..d42cb2d 100644 --- a/app/api/v1/short/route.ts +++ b/app/api/v1/short/route.ts @@ -1,5 +1,6 @@ import { TeamPlanQuota } from "@/config/team"; import { checkApiKey } from "@/lib/dto/api-key"; +import { getDomainsByFeature } from "@/lib/dto/domains"; import { createUserShortUrl } from "@/lib/dto/short-urls"; import { restrictByTimeRange } from "@/lib/team"; import { createUrlSchema } from "@/lib/validations/url"; @@ -41,6 +42,17 @@ export async function POST(req: Request) { }); } + const zones = await getDomainsByFeature("enable_short_link"); + if ( + !zones.length || + !zones.map((zone) => zone.domain_name).includes(prefix) + ) { + return Response.json("Invalid domain", { + status: 409, + statusText: "Invalid domain", + }); + } + const res = await createUserShortUrl({ userId: user.id, userName: user.name || "Anonymous", diff --git a/app/manifest.json b/app/manifest.json index 27ece42..ad20795 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -3,7 +3,7 @@ "short_name": "WR.DO", "description": "Shorten links with analytics, manage emails and control subdomains—all on one platform.", "appid": "com.wr.do", - "versionName": "0.5.1", + "versionName": "0.6.1", "versionCode": "1", "start_url": "/", "orientation": "portrait", diff --git a/components/dashboard/dashboard-info-card.tsx b/components/dashboard/dashboard-info-card.tsx index 174b067..4f48634 100644 --- a/components/dashboard/dashboard-info-card.tsx +++ b/components/dashboard/dashboard-info-card.tsx @@ -145,14 +145,12 @@ export function HeroCard({ ); } -export async function ScrapeInfoCard({ - userId, +export async function StaticInfoCard({ title, desc, link, icon = "users", }: { - userId: string; title: string; desc?: string; link: string; diff --git a/components/dashboard/form-section-columns.tsx b/components/dashboard/form-section-columns.tsx index e7d6cce..5a7132a 100644 --- a/components/dashboard/form-section-columns.tsx +++ b/components/dashboard/form-section-columns.tsx @@ -1,5 +1,7 @@ import React from "react"; +import { cn } from "@/lib/utils"; + interface SectionColumnsType { title: string; children: React.ReactNode; @@ -14,7 +16,12 @@ export function FormSectionColumns({ className, }: SectionColumnsType) { return ( -
+

{title}

{required && ( diff --git a/components/email/EmailSidebar.tsx b/components/email/EmailSidebar.tsx index 97f9855..851c9c2 100644 --- a/components/email/EmailSidebar.tsx +++ b/components/email/EmailSidebar.tsx @@ -77,9 +77,7 @@ export default function EmailSidebar({ const [isEdit, setIsEdit] = useState(false); const [showEmailModal, setShowEmailModal] = useState(false); const [isPending, startTransition] = useTransition(); - const [domainSuffix, setDomainSuffix] = useState( - siteConfig.shortDomains[0], - ); + const [domainSuffix, setDomainSuffix] = useState("wr.do"); const [showDeleteModal, setShowDeleteModal] = useState(false); const [emailToDelete, setEmailToDelete] = useState(null); const [deleteInput, setDeleteInput] = useState(""); @@ -109,6 +107,13 @@ export default function EmailSidebar({ }, ); + const { data: emailDomains, isLoading: isLoadingDomains } = useSWR< + { domain_name: string }[] + >("/api/domain?feature=email", fetcher, { + revalidateOnFocus: false, + dedupingInterval: 10000, + }); + if (!selectedEmailAddress && data && data.list.length > 0) { onSelectEmail(data.list[0].emailAddress); } @@ -595,25 +600,38 @@ export default function EmailSidebar({ isEdit ? selectedEmailAddress?.split("@")[0] || "" : "" } /> - + {isLoadingDomains ? ( + + ) : ( + + )} + )} + + +
+ +
+ ); +} diff --git a/components/forms/record-form.tsx b/components/forms/record-form.tsx index 0f26bd4..9265df2 100644 --- a/components/forms/record-form.tsx +++ b/components/forms/record-form.tsx @@ -5,11 +5,13 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { User } from "@prisma/client"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; +import useSWR from "swr"; import { siteConfig } from "@/config/site"; import { CreateDNSRecord, RecordType } from "@/lib/cloudflare"; import { UserRecordFormData } from "@/lib/dto/cloudflare-dns-record"; import { RECORD_TYPE_ENUMS, TTL_ENUMS } from "@/lib/enums"; +import { fetcher } from "@/lib/utils"; import { createRecordSchema } from "@/lib/validations/record"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -24,6 +26,7 @@ import { SelectTrigger, SelectValue, } from "../ui/select"; +import { Skeleton } from "../ui/skeleton"; import { Switch } from "../ui/switch"; export type FormData = CreateDNSRecord; @@ -77,6 +80,16 @@ export function RecordForm({ }, }); + // Fetch the record domains + const { data: recordDomains, isLoading } = useSWR<{ domain_name: string }[]>( + "/api/domain?feature=record", + fetcher, + { + revalidateOnFocus: false, + dedupingInterval: 10000, + }, + ); + const onSubmit = handleSubmit((data) => { if (type === "add") { handleCreateRecord(data); @@ -196,26 +209,36 @@ export function RecordForm({
- + {isLoading ? ( + + ) : ( + + )}

Required. Select a domain.

diff --git a/components/forms/url-form.tsx b/components/forms/url-form.tsx index fe937fc..c15f0b7 100644 --- a/components/forms/url-form.tsx +++ b/components/forms/url-form.tsx @@ -6,11 +6,12 @@ import { User } from "@prisma/client"; import { Sparkles } from "lucide-react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; +import useSWR from "swr"; import { siteConfig } from "@/config/site"; import { ShortUrlFormData } from "@/lib/dto/short-urls"; import { EXPIRATION_ENUMS } from "@/lib/enums"; -import { generateUrlSuffix } from "@/lib/utils"; +import { fetcher, generateUrlSuffix } from "@/lib/utils"; import { createUrlSchema } from "@/lib/validations/url"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -25,7 +26,7 @@ import { SelectTrigger, SelectValue, } from "../ui/select"; -import { Switch } from "../ui/switch"; +import { Skeleton } from "../ui/skeleton"; export type FormData = ShortUrlFormData; @@ -64,13 +65,22 @@ export function UrlForm({ target: initData?.target || "", url: initData?.url || "", active: initData?.active || 1, - prefix: initData?.prefix || siteConfig.shortDomains[0], + prefix: initData?.prefix || "wr.do", visible: initData?.visible || 0, expiration: initData?.expiration || "-1", password: initData?.password || "", }, }); + const { data: shortDomains, isLoading } = useSWR<{ domain_name: string }[]>( + "/api/domain?feature=short", + fetcher, + { + revalidateOnFocus: false, + dedupingInterval: 10000, + }, + ); + const onSubmit = handleSubmit((data) => { if (type === "add") { handleCreateUrl(data); @@ -191,25 +201,35 @@ export function UrlForm({
- + {isLoading ? ( + + ) : ( + + )} - - {/*
-

- Your Final URL: -

-

- {getValues("prefix")}/s/{getValues("url")} -

-
*/} - {/* -
- - setValue("visible", value ? 1 : 0)} - /> -
-

- Public or private short url. -

-
*/} - {/* -
- - setValue("active", value ? 1 : 0)} - /> -
-

- Enable or disable short url. -

-
*/}
{/* Action buttons */} diff --git a/components/shared/icons.tsx b/components/shared/icons.tsx index f905217..b53698e 100644 --- a/components/shared/icons.tsx +++ b/components/shared/icons.tsx @@ -301,4 +301,33 @@ export const Icons = { ), + cloudflare: ({ ...props }: LucideProps) => ( + + + + + + + + + + ), }; diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx index 0354dad..f14a5f6 100644 --- a/components/ui/badge.tsx +++ b/components/ui/badge.tsx @@ -15,6 +15,8 @@ const badgeVariants = cva( destructive: "bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground", outline: "text-foreground", + blue: "bg-blue-500 hover:bg-blue-600 border-transparent text-white", + green: "bg-green-500 hover:bg-green-600 border-transparent text-white", }, }, defaultVariants: { diff --git a/config/dashboard.ts b/config/dashboard.ts index 1f9c73e..e1a1294 100644 --- a/config/dashboard.ts +++ b/config/dashboard.ts @@ -11,7 +11,7 @@ export const sidebarLinks: SidebarNavItem[] = [ { href: "/dashboard", icon: "dashboard", title: "Dashboard" }, { href: "/dashboard/urls", icon: "link", title: "Short Urls" }, { href: "/emails", icon: "mail", title: "Emails" }, - { href: "/dashboard/records", icon: "globeLock", title: "DNS Records" }, + { href: "/dashboard/records", icon: "globe", title: "DNS Records" }, { href: "/chat", icon: "messages", title: "WRoom" }, ], }, @@ -54,6 +54,12 @@ export const sidebarLinks: SidebarNavItem[] = [ title: "Admin Panel", authorizeOnly: UserRole.ADMIN, }, + { + href: "/admin/domains", + icon: "globeLock", + title: "Domains", + authorizeOnly: UserRole.ADMIN, + }, { href: "/admin/users", icon: "users", diff --git a/config/site.ts b/config/site.ts index 7d7d5e3..4764c75 100644 --- a/config/site.ts +++ b/config/site.ts @@ -3,10 +3,7 @@ import { env } from "@/env.mjs"; const site_url = env.NEXT_PUBLIC_APP_URL; const open_signup = env.NEXT_PUBLIC_OPEN_SIGNUP; -const short_domains = env.NEXT_PUBLIC_SHORT_DOMAINS || ""; -const email_domains = env.NEXT_PUBLIC_EMAIL_DOMAINS || ""; const email_r2_domain = env.NEXT_PUBLIC_EMAIL_R2_DOMAIN || ""; -const record_domains = env.NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME || ""; export const siteConfig: SiteConfig = { name: "WR.DO", @@ -23,9 +20,6 @@ export const siteConfig: SiteConfig = { }, mailSupport: "support@wr.do", openSignup: open_signup === "1" ? true : false, - shortDomains: short_domains.split(","), - emailDomains: email_domains.split(","), - recordDomains: record_domains.split(","), emailR2Domain: email_r2_domain, }; diff --git a/content/docs/developer/cloudflare.mdx b/content/docs/developer/cloudflare.mdx index d947c6a..dbbc9f2 100644 --- a/content/docs/developer/cloudflare.mdx +++ b/content/docs/developer/cloudflare.mdx @@ -5,6 +5,49 @@ description: How to config the cloudflare api. Before you start, you must have a Cloudflare account and be hosted on Cloudflare. +## Overview + +Administrators can manage domain configurations at `/admin/domains`, including adding, deleting, and modifying domains. + +![](/_static/docs/domain-form.png) + +### Domain Configuration Form + +The `Short URL Service` and `Email Service` require no additional configuration and can be activated to enable short URL and email functionalities (email requires worker forwarding setup). + +To enable the `DNS Record Service`, you must complete the `Cloudflare Configs(Optional)` form with the following fields: + +- Zone ID +- API Token +- Email + +These fields are used to configure the Cloudflare API. If your domain is hosted through Cloudflare, you can find these details in the Cloudflare dashboard. + +### Zone ID + +The unique identifier for a domain hosted on Cloudflare, located at: + +https://dash.cloudflare.com/[account_id]/[zone_name] + +### API Token + +Visit https://dash.cloudflare.com/profile/api-tokens, and find the Global API Key under the API Tokens section. + +### Email + +Email for registering a Cloudflare account + + +You can manage domains hosted under different Cloudflare accounts, +provided the API Token and Email are sourced from the same account. + + +--- + +# This section is Deprecated since version v0.6.0 + +Before you start, you must have a Cloudflare account and be hosted on Cloudflare. + In this section, you can update these variables: ```js title=".env" diff --git a/content/docs/developer/installation.mdx b/content/docs/developer/installation.mdx index f874900..3412932 100644 --- a/content/docs/developer/installation.mdx +++ b/content/docs/developer/installation.mdx @@ -48,22 +48,12 @@ Copy/paste the `.env.example` in the `.env` file: | GITHUB_ID | `123465` | The ID of the GitHub OAuth client. | | GITHUB_SECRET | `123465` | The secret of the GitHub OAuth client. | | RESEND_API_KEY | `123465` | The API key for Resend. | -| CLOUDFLARE_ZONE | `[{"zone_id":"abc465","zone_name":"example.com"}]` | The zone info for Cloudflare. | -| NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME | `example.com,example2.com` | The zone name for Cloudflare. | -| CLOUDFLARE_API_KEY | `123465` | The API key for Cloudflare. | -| CLOUDFLARE_EMAIL | `123465` | The email for Cloudflare. | | NEXT_PUBLIC_OPEN_SIGNUP | `1` | Open signup. | | SCREENSHOTONE_BASE_URL | `https://api.example.com` | pending | | GITHUB_TOKEN | `ghp_sscsfarwetqet` | https://github.com/settings/tokens | -| NEXT_PUBLIC_SHORT_DOMAINS | `wr.do,uv.do` | The list of short domains. Separated by `,` | -| NEXT_PUBLIC_EMAIL_DOMAINS | `wr.do,uv.do` | The list of email domains. Separated by `,` | - -> `NEXT_PUBLIC_SHORT_DOMAINS`、`NEXT_PUBLIC_EMAIL_DOMAINS` is used to limit the short domain name and email domain name. - How to get `GOOGLE_CLIENT_ID`、`GITHUB_ID`, see [Authentification](/docs/developer/authentification). - How to get `RESEND_API_KEY`, see [Email](/docs/developer/email). -- How to get `CLOUDFLARE_ZONE`、`CLOUDFLARE_API_KEY`、`CLOUDFLARE_EMAIL`、`NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME`, see [Cloudflare Configs](/docs/developer/cloudflare). -- How to get `DATABASE_URL`, see [Database](/docs/developer/database). - How to active email worker, see [Email Worker](/docs/developer/cloudflare-email-worker). For step by step installation, see [Quick Start](/docs/developer/quick-start). diff --git a/content/docs/developer/quick-start.mdx b/content/docs/developer/quick-start.mdx index 715b395..4d63cb0 100644 --- a/content/docs/developer/quick-start.mdx +++ b/content/docs/developer/quick-start.mdx @@ -45,25 +45,23 @@ DATABASE_URL= ### Deploy Postgres -#### Manually install (Recommended) - -Via [migration.sql](https://github.com/oiov/wr.do/blob/main/prisma/migrations/20240705091917_init/migration.sql), -copy the sql code to the database to initialize the database schema. - -#### or - ```bash pnpm postinstall pnpm db:push ``` +#### Or manually init + +Via [migration.sql](https://github.com/oiov/wr.do/blob/main/prisma/migrations), +copy the sql code to the database to initialize the database schema. + ### Add the AUTH_SECRET Environment Variable The `AUTH_SECRET` environment variable is used to encrypt tokens and email verification hashes(NextAuth.js). You can generate one from https://generate-secret.vercel.app/32: ```js title=".env" -AUTH_SECRET=a3e686f39b2a878c6866e4604e6f1b1b +AUTH_SECRET=10000032bsfasfafk4lkkfsa ``` ## 2. Configure Authentication Service @@ -136,40 +134,7 @@ RESEND_API_KEY = re_your_resend_api_key; -## 3. Cloudflare Configs - -Before you start, you must have a Cloudflare account and be hosted on Cloudflare. - -### Add the CLOUDFLARE_ZONE Environment Variable - -A JSON array of objects, each containing a zone_id and zone_name for your Cloudflare zones. The zone_id is the unique identifier for a domain, and the zone_name is the domain name (e.g., example.com). - -> Follow [this way](https://dash.cloudflare.com/Your_Acount_Id/wr.do), and scroll down to `Zone ID`. - -### Add the CLOUDFLARE_API_KEY Environment Variable - - This is the API key that you use to authenticate requests to the Cloudflare API. You can generate or find your API key in the Cloudflare dashboard under the `profile` -> `api-tokens` section. - - > Follow [https://dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens), and scroll down to `API Token`, the `Global API Key` should be used. - -### Add the CLOUDFLARE_EMAIL Environment Variable - -This is the email address associated with your Cloudflare account. It is used for authentication alongside the API key. - -### Add the NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME Environment Variable - -A comma-separated list of domain names (e.g., `example.com,example2.com`) used for frontend display. These must correspond to the zone_name values in CLOUDFLARE_ZONE. - -In this section, you can update these variables: - -```js title=".env" -CLOUDFLARE_ZONE=[{"zone_id":"abc465","zone_name":"example.com"},{"zone_id":"abc465","zone_name":"example2.com"}] -NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME=example.com,example2.com -CLOUDFLARE_API_KEY=1234567890abcdef1234567890abcdef -CLOUDFLARE_EMAIL=user@example.com -``` - -## 4. Email Worker Configs +## 3. Email Worker Configs See detail in [Email Worker](/docs/developer/cloudflare-email-worker). @@ -187,20 +152,14 @@ Via: NEXT_PUBLIC_EMAIL_R2_DOMAIN=https://email-attachment.wr.do ``` -## 5. Add the Bussiness Configs +## 4. Add the Bussiness Configs ```js title=".env" # Allow anyone to sign up NEXT_PUBLIC_OPEN_SIGNUP=1 - -# Short domains. Separated by `,` -NEXT_PUBLIC_SHORT_DOMAINS=wr.do,uv.do - -# Email domains. Separated by `,` -NEXT_PUBLIC_EMAIL_DOMAINS=wr.do,uv.do ``` -## 6. Add the SCREENSHOTONE_BASE_URL Environment Variable +## 5. Add the SCREENSHOTONE_BASE_URL Environment Variable It's the base URL for the screenshotone API. @@ -211,20 +170,40 @@ Deploy docs via [here](https://jasonraimondi.github.io/url-to-png/) SCREENSHOTONE_BASE_URL=https://api.screenshotone.com ``` -## 7. Add the GITHUB_TOKEN Environment Variable +## 6. Add the GITHUB_TOKEN Environment Variable Via https://github.com/settings/tokens to get your token. ```js title=".env" GITHUB_TOKEN= ``` -## 8. Start the Dev Server + +## 7. Start the Dev Server ```bash pnpm dev ``` Via [http://localhost:3000](http://localhost:3000) +## 8. Setup System + +#### Create the first account and Change the account's role to ADMIN + +Follow the steps below: + +- 1. Via [http://localhost:3000/login](http://localhost:3000/login), login with your account. +- 2. Via [http://localhost:3000/setup](http://localhost:3000/setup), change the account's role to ADMIN. +- 3. Then follow the **panel guide** to config the system and add the first domain. + +![](/_static/docs/setup-1.png) + +![](/_static/docs/setup-2.png) + + + After change the account's role to ADMIN, then you can refresh the website and access http://localhost:3000/admin. + + You must add at least one domain to start using short links, email or subdomain management features. + ## Q & A @@ -238,17 +217,6 @@ https://dash.cloudflare.com/[account_id]/[zone_name]/ssl-tls/configuration Change the `SSL/TLS Encryption` Mode to `Full` in the Cloudflare dashboard. -### How can I access the admin panel after first deployment? - -You need to first register an account and log in, -and modify the `role` field of this account to `ADMIN` in the `users` table of the database. -Then, refresh the website and access http://localhost:3000/admin. - - -Although it may not be convenient to do so, this is currently the fastest way to become an administrator. -In future versions, we will implement the function of automatically setting up administrators as soon as possible. - - ### How can I change the team plan quota? Via team.ts: diff --git a/env.mjs b/env.mjs index 605b671..6b5023a 100644 --- a/env.mjs +++ b/env.mjs @@ -15,19 +15,13 @@ export const env = createEnv({ LinuxDo_CLIENT_SECRET: z.string().optional(), DATABASE_URL: z.string().min(1), RESEND_API_KEY: z.string().optional(), - CLOUDFLARE_ZONE: z.string().min(1), - CLOUDFLARE_API_KEY: z.string().min(1), - CLOUDFLARE_EMAIL: z.string().min(1), SCREENSHOTONE_BASE_URL: z.string().optional(), GITHUB_TOKEN: z.string().optional(), }, client: { NEXT_PUBLIC_APP_URL: z.string().min(1), NEXT_PUBLIC_OPEN_SIGNUP: z.string().min(1).default("1"), - NEXT_PUBLIC_SHORT_DOMAINS: z.string().min(1).default(""), - NEXT_PUBLIC_EMAIL_DOMAINS: z.string().min(1).default(""), NEXT_PUBLIC_EMAIL_R2_DOMAIN: z.string().min(1), - NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME: z.string().min(1), }, runtimeEnv: { NEXTAUTH_URL: process.env.NEXTAUTH_URL, @@ -40,14 +34,7 @@ export const env = createEnv({ RESEND_API_KEY: process.env.RESEND_API_KEY, NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_OPEN_SIGNUP: process.env.NEXT_PUBLIC_OPEN_SIGNUP, - NEXT_PUBLIC_SHORT_DOMAINS: process.env.NEXT_PUBLIC_SHORT_DOMAINS, - NEXT_PUBLIC_EMAIL_DOMAINS: process.env.NEXT_PUBLIC_EMAIL_DOMAINS, NEXT_PUBLIC_EMAIL_R2_DOMAIN: process.env.NEXT_PUBLIC_EMAIL_R2_DOMAIN, - NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME: - process.env.NEXT_PUBLIC_CLOUDFLARE_ZONE_NAME, - CLOUDFLARE_ZONE: process.env.CLOUDFLARE_ZONE, - CLOUDFLARE_API_KEY: process.env.CLOUDFLARE_API_KEY, - CLOUDFLARE_EMAIL: process.env.CLOUDFLARE_EMAIL, SCREENSHOTONE_BASE_URL: process.env.SCREENSHOTONE_BASE_URL, GITHUB_TOKEN: process.env.GITHUB_TOKEN, LinuxDo_CLIENT_ID: process.env.LinuxDo_CLIENT_ID, diff --git a/lib/domainConfig.ts b/lib/domainConfig.ts new file mode 100644 index 0000000..b1fe152 --- /dev/null +++ b/lib/domainConfig.ts @@ -0,0 +1,36 @@ +import { getAllDomains, invalidateDomainConfigCache } from "@/lib/dto/domains"; + +export async function getDomainConfig() { + return await getAllDomains(); +} + +export async function getCloudflareCredentials(domain_name: string) { + try { + const domains = await getAllDomains(); + const domain = domains.find((d) => d.domain_name === domain_name); + if (!domain || !domain.cf_api_key || !domain.cf_email) { + throw new Error( + `No Cloudflare credentials found for domain: ${domain_name}`, + ); + } + + let apiKey = domain.cf_api_key; + if (domain.cf_api_key_encrypted) { + // TODO + apiKey = decrypt(apiKey); + } + + return { + api_key: apiKey, + email: domain.cf_email, + }; + } catch (error) { + throw new Error(`Failed to fetch credentials: ${error.message}`); + } +} + +function decrypt(encryptedKey: string) { + return encryptedKey; // Replace with actual decryption logic +} + +export { invalidateDomainConfigCache }; diff --git a/lib/dto/domains.ts b/lib/dto/domains.ts new file mode 100644 index 0000000..41dff10 --- /dev/null +++ b/lib/dto/domains.ts @@ -0,0 +1,136 @@ +import { Domain } from "@prisma/client"; + +import { prisma } from "../db"; + +// In-memory cache +let domainConfigCache: Domain[] | null = null; +let lastCacheUpdate = 0; +const CACHE_DURATION = 60 * 1000; // Cache for 1 minute in memory + +export const FeatureMap = { + short: "enable_short_link", + email: "enable_email", + record: "enable_dns", +}; + +export interface DomainConfig { + domain_name: string; + enable_short_link: boolean; + enable_email: boolean; + enable_dns: boolean; + cf_zone_id: string | null; + cf_api_key: string | null; + cf_email: string | null; + cf_api_key_encrypted: boolean; + max_short_links: number | null; + max_email_forwards: number | null; + max_dns_records: number | null; + active: boolean; +} + +export interface DomainFormData extends DomainConfig { + id?: string; + createdAt: Date; + updatedAt: Date; +} + +export async function getAllDomains() { + try { + const now = Date.now(); + if (domainConfigCache && now - lastCacheUpdate < CACHE_DURATION) { + return domainConfigCache; + } + + const domains = await prisma.domain.findMany({ + // where: { active: true }, + }); + + domainConfigCache = domains; + lastCacheUpdate = now; + return domains; + } catch (error) { + throw new Error(`Failed to fetch domain config: ${error.message}`); + } +} + +export async function getDomainsByFeature( + feature: string, + admin: boolean = false, +) { + try { + const now = Date.now(); + if (domainConfigCache && now - lastCacheUpdate < CACHE_DURATION) { + return domainConfigCache; + } + + const domains = await prisma.domain.findMany({ + where: { [feature]: true }, + select: { + domain_name: true, + enable_short_link: admin, + enable_email: admin, + enable_dns: admin, + cf_zone_id: admin, + cf_api_key: admin, + cf_email: admin, + }, + }); + return domains; + } catch (error) { + throw new Error(`Failed to fetch domain config: ${error.message}`); + } +} + +export async function getDomainsByFeatureClient(feature: string) { + try { + const domains = await prisma.domain.findMany({ + where: { [feature]: true }, + select: { + domain_name: true, + }, + }); + return domains; + } catch (error) { + throw new Error(`Failed to fetch domain config: ${error.message}`); + } +} + +export async function createDomain(data: DomainConfig) { + try { + const createdDomain = await prisma.domain.create({ data }); + return createdDomain; + } catch (error) { + throw new Error(`Failed to create domain: ${error.message}`); + } +} + +export async function updateDomain(id: string, data) { + try { + const updatedDomain = await prisma.domain.update({ + where: { id }, + data: { + ...data, + updatedAt: new Date(), + }, + }); + return updatedDomain; + } catch (error) { + throw new Error(`Failed to update domain: ${error.message}`); + } +} + +export async function deleteDomain(domain_name: string) { + try { + const deletedDomain = await prisma.domain.delete({ + where: { domain_name }, + }); + return deletedDomain; + } catch (error) { + throw new Error(`Failed to delete domain`); + } +} + +export function invalidateDomainConfigCache() { + domainConfigCache = null; + lastCacheUpdate = 0; +} diff --git a/lib/dto/user.ts b/lib/dto/user.ts index 8004a1f..9d91f71 100644 --- a/lib/dto/user.ts +++ b/lib/dto/user.ts @@ -85,6 +85,17 @@ export async function getAllUsersCount() { } } +export async function setFirstUserAsAdmin(userId: string) { + try { + return await prisma.user.update({ + where: { id: userId }, + data: { role: UserRole.ADMIN }, + }); + } catch (error) { + return null; + } +} + export async function getAllUsersActiveApiKeyCount() { try { return await prisma.user.count({ where: { apiKey: { not: null } } }); diff --git a/lib/utils.ts b/lib/utils.ts index 752eb01..9b2c1ab 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -388,25 +388,3 @@ export function extractHost(url: string): string { const match = url.match(regex); return match ? match[1] : ""; } - -// 解析 CLOUDFLARE_ZONE 环境变量并返回结构化的域名配置 -export function parseZones(raw: string) { - let zones; - - try { - zones = JSON.parse(raw); - } catch (error) { - return []; - } - - if (!Array.isArray(zones)) { - return []; - } - - const parsedZones = zones.map((zone) => { - const { zone_id, zone_name } = zone; - return { zone_id, zone_name }; - }); - - return parsedZones; -} diff --git a/lib/validations/domain.ts b/lib/validations/domain.ts new file mode 100644 index 0000000..5af3b04 --- /dev/null +++ b/lib/validations/domain.ts @@ -0,0 +1,17 @@ +import * as z from "zod"; + +export const createDomainSchema = z.object({ + id: z.string().optional(), + domain_name: z.string().min(2), + enable_short_link: z.boolean(), + enable_email: z.boolean(), + enable_dns: z.boolean(), + cf_zone_id: z.string().optional(), + cf_api_key: z.string().optional(), + cf_email: z.string().optional(), + cf_api_key_encrypted: z.boolean().default(false), + max_short_links: z.number().optional(), + max_email_forwards: z.number().optional(), + max_dns_records: z.number().optional(), + active: z.boolean().default(true), +}); diff --git a/package.json b/package.json index 65bfb75..17d9a3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wr.do", - "version": "0.5.1", + "version": "0.6.1", "author": { "name": "oiov", "url": "https://github.com/oiov" diff --git a/prisma/migrations/20240705091917_init/migration.sql b/prisma/migrations/20240705091917_init/migration.sql index 9cb9c4b..4096392 100644 --- a/prisma/migrations/20240705091917_init/migration.sql +++ b/prisma/migrations/20240705091917_init/migration.sql @@ -274,7 +274,7 @@ CREATE TABLE "user_send_emails" "html" TEXT DEFAULT '', "replyTo" TEXT DEFAULT '', "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX "user_send_emails_userId_idx" ON "user_send_emails" ("userId"); diff --git a/prisma/migrations/20250520104322/migration.sql b/prisma/migrations/20250520104322/migration.sql new file mode 100644 index 0000000..b5fc73c --- /dev/null +++ b/prisma/migrations/20250520104322/migration.sql @@ -0,0 +1,18 @@ +CREATE TABLE domains +( + "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), + "domain_name" TEXT NOT NULL UNIQUE, + "enable_short_link" BOOLEAN DEFAULT FALSE, + "enable_email" BOOLEAN DEFAULT FALSE, + "enable_dns" BOOLEAN DEFAULT FALSE, + "cf_zone_id" TEXT NOT NULL, + "cf_api_key" TEXT NOT NULL, + "cf_email" TEXT NOT NULL, + "cf_api_key_encrypted" BOOLEAN DEFAULT FALSE, + "max_short_links" INTEGER, + "max_email_forwards" INTEGER, + "max_dns_records" INTEGER, + "active" BOOLEAN DEFAULT TRUE, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4c4a4c3..eb5c069 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -247,3 +247,24 @@ model UserSendEmail { @@index([createdAt]) @@map("user_send_emails") } + +model Domain { + id String @id @default(uuid()) + domain_name String @unique + enable_short_link Boolean @default(false) + enable_email Boolean @default(false) + enable_dns Boolean @default(false) + cf_zone_id String? + cf_api_key String? + cf_email String? + cf_api_key_encrypted Boolean + max_short_links Int? + max_email_forwards Int? + max_dns_records Int? + active Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) + + @@map("domains") +} diff --git a/public/_static/docs/domain-form.png b/public/_static/docs/domain-form.png new file mode 100644 index 0000000..9b8b21d Binary files /dev/null and b/public/_static/docs/domain-form.png differ diff --git a/public/_static/docs/setup-1.png b/public/_static/docs/setup-1.png new file mode 100644 index 0000000..b266c68 Binary files /dev/null and b/public/_static/docs/setup-1.png differ diff --git a/public/_static/docs/setup-2.png b/public/_static/docs/setup-2.png new file mode 100644 index 0000000..4d61f0a Binary files /dev/null and b/public/_static/docs/setup-2.png differ diff --git a/public/manifest.json b/public/manifest.json index 27ece42..ad20795 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -3,7 +3,7 @@ "short_name": "WR.DO", "description": "Shorten links with analytics, manage emails and control subdomains—all on one platform.", "appid": "com.wr.do", - "versionName": "0.5.1", + "versionName": "0.6.1", "versionCode": "1", "start_url": "/", "orientation": "portrait", diff --git a/public/site.webmanifest b/public/site.webmanifest index 27ece42..ad20795 100644 --- a/public/site.webmanifest +++ b/public/site.webmanifest @@ -3,7 +3,7 @@ "short_name": "WR.DO", "description": "Shorten links with analytics, manage emails and control subdomains—all on one platform.", "appid": "com.wr.do", - "versionName": "0.5.1", + "versionName": "0.6.1", "versionCode": "1", "start_url": "/", "orientation": "portrait", diff --git a/public/sitemap-0.xml b/public/sitemap-0.xml index 43ec284..0c7b719 100644 --- a/public/sitemap-0.xml +++ b/public/sitemap-0.xml @@ -1,39 +1,39 @@ -https://wr.do/robots.txt2025-05-10T03:08:41.904Zdaily0.7 -https://wr.do/pricing2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/privacy2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/terms2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/chat2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/password-prompt2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/developer/authentification2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/developer/cloudflare2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/developer/cloudflare-email-worker2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/developer/components2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/developer/config-files2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/developer/database2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/developer/email2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/developer/installation2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/developer/markdown-files2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/developer/quick-start2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/dns-records2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/emails2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/examples/cloudflare2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/examples/other2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/examples/vercel2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/examples/zeabur2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/open-api2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/open-api/icon2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/open-api/markdown2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/open-api/meta-info2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/open-api/qrcode2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/open-api/screenshot2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/open-api/text2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/plan2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/quick-start2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/short-urls2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/docs/wroom2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/manifest.json2025-05-10T03:08:41.905Zdaily0.7 -https://wr.do/opengraph-image.jpg2025-05-10T03:08:41.905Zdaily0.7 +https://wr.do/robots.txt2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/chat2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/developer/authentification2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/developer/cloudflare2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/developer/cloudflare-email-worker2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/developer/components2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/developer/config-files2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/developer/database2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/developer/email2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/developer/installation2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/developer/markdown-files2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/developer/quick-start2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/dns-records2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/emails2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/examples/cloudflare2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/examples/other2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/examples/vercel2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/examples/zeabur2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/open-api2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/open-api/icon2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/open-api/markdown2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/open-api/meta-info2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/open-api/qrcode2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/open-api/screenshot2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/open-api/text2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/plan2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/quick-start2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/short-urls2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/docs/wroom2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/password-prompt2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/pricing2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/privacy2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/terms2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/manifest.json2025-05-20T14:43:25.018Zdaily0.7 +https://wr.do/opengraph-image.jpg2025-05-20T14:43:25.018Zdaily0.7 \ No newline at end of file diff --git a/public/sw.js.map b/public/sw.js.map index 8f16fe4..0691226 100644 --- a/public/sw.js.map +++ b/public/sw.js.map @@ -1 +1 @@ -{"version":3,"file":"sw.js","sources":["../../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/afe9c4c3006025fb7f0a64cab65de247/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"sw.js","sources":["../../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/6217469e87d14bd5106dd3d5117a6cdd/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":"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"} \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts index a9b500d..01212c4 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -17,9 +17,6 @@ export type SiteConfig = { oichat: string; }; openSignup: boolean; - shortDomains: string[]; - emailDomains: string[]; - recordDomains: string[]; emailR2Domain: string; };