From ae97fe895eceda1205d94cf34fc7a1cd938e85fa Mon Sep 17 00:00:00 2001 From: oiov Date: Thu, 19 Jun 2025 21:15:35 +0800 Subject: [PATCH] feats(domain): configurable limit configs --- app/api/admin/domain/route.ts | 9 +++ app/api/email/route.ts | 6 -- app/api/v1/email/route.ts | 17 ++++-- app/api/v1/short/route.ts | 8 +++ app/manifest.json | 2 +- components/email/EmailSidebar.tsx | 9 ++- components/forms/domain-form.tsx | 61 +++++++++++++++++++ components/forms/record-form.tsx | 12 +++- components/forms/url-form.tsx | 27 +++++--- lib/dto/domains.ts | 11 +++- lib/validations/domain.ts | 3 + lib/validations/record.ts | 6 +- lib/validations/url.ts | 5 +- locales/en.json | 6 +- locales/zh.json | 6 +- package.json | 2 +- .../migrations/20250619121324/migration.sql | 9 +++ prisma/schema.prisma | 3 + public/manifest.json | 2 +- public/site.webmanifest | 2 +- public/sitemap-0.xml | 6 +- public/sw.js.map | 2 +- 22 files changed, 171 insertions(+), 43 deletions(-) create mode 100644 prisma/migrations/20250619121324/migration.sql diff --git a/app/api/admin/domain/route.ts b/app/api/admin/domain/route.ts index 8806479..f6cb71e 100644 --- a/app/api/admin/domain/route.ts +++ b/app/api/admin/domain/route.ts @@ -64,6 +64,9 @@ export async function POST(req: NextRequest) { max_short_links: data.max_short_links, max_email_forwards: data.max_email_forwards, max_dns_records: data.max_dns_records, + min_url_length: data.min_url_length, + min_email_length: data.min_email_length, + min_record_length: data.min_record_length, active: true, }); @@ -93,6 +96,9 @@ export async function PUT(req: NextRequest) { cf_email, cf_record_types, resend_api_key, + min_url_length, + min_email_length, + min_record_length, max_short_links, max_email_forwards, max_dns_records, @@ -115,6 +121,9 @@ export async function PUT(req: NextRequest) { cf_record_types, cf_api_key_encrypted: false, resend_api_key, + min_url_length, + min_email_length, + min_record_length, max_short_links, max_email_forwards, max_dns_records, diff --git a/app/api/email/route.ts b/app/api/email/route.ts index b573bfa..d4901c5 100644 --- a/app/api/email/route.ts +++ b/app/api/email/route.ts @@ -62,12 +62,6 @@ export async function POST(req: NextRequest) { } const prefix = emailAddress.split("@")[0]; - if (!prefix || prefix.length < 5) { - return NextResponse.json("Email address length must be at least 5", { - status: 400, - }); - } - if (reservedAddressSuffix.includes(prefix)) { return NextResponse.json("Invalid email address", { status: 400 }); } diff --git a/app/api/v1/email/route.ts b/app/api/v1/email/route.ts index 7335a29..ecbefe6 100644 --- a/app/api/v1/email/route.ts +++ b/app/api/v1/email/route.ts @@ -50,12 +50,6 @@ export async function POST(req: NextRequest) { } const [prefix, suffix] = emailAddress.split("@"); - if (!prefix || prefix.length < 5) { - return NextResponse.json("Email address length must be at least 5", { - status: 400, - }); - } - const zones = await getDomainsByFeature("enable_email", true); if ( !zones.length || @@ -64,6 +58,17 @@ export async function POST(req: NextRequest) { return NextResponse.json("Invalid email suffix address", { status: 400 }); } + const limit_len = + zones.find((zone) => zone.domain_name === suffix)?.min_email_length ?? 3; + if (!prefix || prefix.length < limit_len) { + return NextResponse.json( + `Email address length must be at least ${limit_len}`, + { + status: 400, + }, + ); + } + if (reservedAddressSuffix.includes(prefix)) { return NextResponse.json("Invalid email address", { status: 400 }); } diff --git a/app/api/v1/short/route.ts b/app/api/v1/short/route.ts index c868e11..8af90e4 100644 --- a/app/api/v1/short/route.ts +++ b/app/api/v1/short/route.ts @@ -55,6 +55,14 @@ export async function POST(req: Request) { }); } + const limit_len = + zones.find((zone) => zone.domain_name === prefix)?.min_url_length ?? 3; + if (!url || url.length < limit_len) { + return Response.json(`Slug length must be at least ${limit_len}`, { + status: 400, + }); + } + const res = await createUserShortUrl({ userId: user.id, userName: user.name || "Anonymous", diff --git a/app/manifest.json b/app/manifest.json index 5778d65..81138ea 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": "1.0.3", + "versionName": "1.0.4", "versionCode": "1", "start_url": "/", "orientation": "portrait", diff --git a/components/email/EmailSidebar.tsx b/components/email/EmailSidebar.tsx index b792ac5..bc0e7cf 100644 --- a/components/email/EmailSidebar.tsx +++ b/components/email/EmailSidebar.tsx @@ -107,7 +107,7 @@ export default function EmailSidebar({ ); const { data: emailDomains, isLoading: isLoadingDomains } = useSWR< - { domain_name: string }[] + { domain_name: string; min_email_length: number }[] >("/api/domain?feature=email", fetcher, { revalidateOnFocus: false, dedupingInterval: 10000, @@ -127,8 +127,11 @@ export default function EmailSidebar({ const totalPages = data ? Math.ceil(data.total / pageSize) : 0; const handleSubmitEmail = async (emailSuffix: string) => { - if (!emailSuffix || emailSuffix.length < 5) { - toast.error("Email address characters must be at least 5"); + const limit_len = + emailDomains?.find((d) => d.domain_name === domainSuffix) + ?.min_email_length ?? 1; + if (!emailSuffix || emailSuffix.length < limit_len) { + toast.error(`Email address characters must be at least ${limit_len}`); return; } if (/[^a-zA-Z0-9_\-\.]/.test(emailSuffix)) { diff --git a/components/forms/domain-form.tsx b/components/forms/domain-form.tsx index ded5b86..d8ab89e 100644 --- a/components/forms/domain-form.tsx +++ b/components/forms/domain-form.tsx @@ -80,6 +80,9 @@ export function DomainForm({ cf_record_types: initData?.cf_record_types || "CNAME,A,TXT", cf_api_key_encrypted: initData?.cf_api_key_encrypted || false, resend_api_key: initData?.resend_api_key || "", + min_url_length: initData?.min_url_length, + min_email_length: initData?.min_email_length, + min_record_length: initData?.min_record_length, max_short_links: initData?.max_short_links || 0, max_email_forwards: initData?.max_email_forwards || 0, max_dns_records: initData?.max_dns_records || 0, @@ -549,6 +552,64 @@ export function DomainForm({ + + +

+ {t("Limit Configs")} ({t("Optional")}) +

+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ {/* Action buttons */}
{type === "edit" && ( diff --git a/components/forms/record-form.tsx b/components/forms/record-form.tsx index 442e1a0..187199c 100644 --- a/components/forms/record-form.tsx +++ b/components/forms/record-form.tsx @@ -66,6 +66,7 @@ export function RecordForm({ initData?.type || "CNAME", ); const [currentZoneName, setCurrentZoneName] = useState(initData?.zone_name); + const [limitLen, setLimitLen] = useState(3); const [email, setEmail] = useState(initData?.user.email || user.email); const [allowedRecordTypes, setAllowedRecordTypes] = useState([]); const isAdmin = action.indexOf("admin") > -1; @@ -93,7 +94,11 @@ export function RecordForm({ // Fetch the record domains const { data: recordDomains, isLoading } = useSWR< - { domain_name: string; cf_record_types: string }[] + { + domain_name: string; + cf_record_types: string; + min_record_length: number; + }[] >("/api/domain?feature=record", fetcher, { revalidateOnFocus: false, dedupingInterval: 10000, @@ -131,6 +136,10 @@ export function RecordForm({ .find((d) => d.domain_name === validDefaultDomain)! .cf_record_types.split(","), ); + setLimitLen( + recordDomains.find((d) => d.domain_name === currentZoneName) + ?.min_record_length || 3, + ); } }, [currentZoneName, recordDomains, validDefaultDomain]); @@ -413,6 +422,7 @@ export function RecordForm({ id="name" className="flex-1 shadow-inner" size={32} + minLength={limitLen} {...register("name")} /> {["CNAME", "A", "AAAA"].includes(currentRecordType) && ( diff --git a/components/forms/url-form.tsx b/components/forms/url-form.tsx index a3e0231..57fb71e 100644 --- a/components/forms/url-form.tsx +++ b/components/forms/url-form.tsx @@ -5,6 +5,7 @@ import { SetStateAction, useEffect, useMemo, + useState, useTransition, } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -58,6 +59,8 @@ export function UrlForm({ }: RecordFormProps) { const [isPending, startTransition] = useTransition(); const [isDeleting, startDeleteTransition] = useTransition(); + const [currentPrefix, setCurrentPrefix] = useState(initData?.prefix || ""); + const [limitLen, setLimitLen] = useState(3); const t = useTranslations("List"); const { @@ -79,14 +82,12 @@ export function UrlForm({ }, }); - const { data: shortDomains, isLoading } = useSWR<{ domain_name: string }[]>( - "/api/domain?feature=short", - fetcher, - { - revalidateOnFocus: false, - dedupingInterval: 10000, - }, - ); + const { data: shortDomains, isLoading } = useSWR< + { domain_name: string; min_url_length: number }[] + >("/api/domain?feature=short", fetcher, { + revalidateOnFocus: false, + dedupingInterval: 10000, + }); const validDefaultDomain = useMemo(() => { if (!shortDomains?.length) return undefined; @@ -104,9 +105,17 @@ export function UrlForm({ useEffect(() => { if (validDefaultDomain) { setValue("prefix", validDefaultDomain); + setCurrentPrefix(validDefaultDomain); } }, [validDefaultDomain]); + useEffect(() => { + setLimitLen( + shortDomains?.find((d) => d.domain_name === currentPrefix) + ?.min_url_length || 3, + ); + }, [currentPrefix]); + const onSubmit = handleSubmit((data) => { if (type === "add") { handleCreateUrl(data); @@ -233,6 +242,7 @@ export function UrlForm({