feats(domain): configurable limit configs

This commit is contained in:
oiov
2025-06-19 21:15:35 +08:00
parent fd3567d48e
commit ae97fe895e
22 changed files with 171 additions and 43 deletions
+9
View File
@@ -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,
-6
View File
@@ -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 });
}
+11 -6
View File
@@ -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 });
}
+8
View File
@@ -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",
+1 -1
View File
@@ -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",
+6 -3
View File
@@ -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)) {
+61
View File
@@ -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({
</CollapsibleContent>
</Collapsible>
<Collapsible className="relative mt-2 rounded-md bg-neutral-100 p-4 dark:bg-neutral-800">
<CollapsibleTrigger className="flex w-full items-center justify-between">
<h2 className="absolute left-2 top-4 flex gap-2 text-xs font-semibold text-neutral-400">
{t("Limit Configs")} ({t("Optional")})
</h2>
<Icons.chevronDown className="ml-auto size-4" />
</CollapsibleTrigger>
<CollapsibleContent className="mt-3 space-y-2">
<div className="flex w-full items-center justify-between gap-2">
<Label className="cursor-pointer" htmlFor="min_url_length">
{t("Min URL Length")}:
</Label>
<Input
id="target"
className="max-w-20 flex-1 bg-neutral-50 shadow-inner"
size={32}
type="number"
defaultValue={initData?.min_url_length ?? 3}
{...register("min_url_length", {
valueAsNumber: true,
})}
/>
</div>
<div className="flex w-full items-center justify-between gap-2">
<Label className="cursor-pointer" htmlFor="min_email_length">
{t("Min Email Length")}:
</Label>
<Input
id="target"
className="max-w-20 flex-1 bg-neutral-50 shadow-inner"
size={32}
type="number"
defaultValue={initData?.min_email_length ?? 3}
{...register("min_email_length", {
valueAsNumber: true,
})}
/>
</div>
<div className="flex w-full items-center justify-between gap-2">
<Label className="cursor-pointer" htmlFor="min_subdomain_length">
{t("Min Subdomain Length")}:
</Label>
<Input
id="target"
className="max-w-20 flex-1 bg-neutral-50 shadow-inner"
size={32}
type="number"
defaultValue={initData?.min_record_length ?? 3}
{...register("min_record_length", {
valueAsNumber: true,
})}
/>
</div>
</CollapsibleContent>
</Collapsible>
{/* Action buttons */}
<div className="mt-3 flex justify-end gap-3">
{type === "edit" && (
+11 -1
View File
@@ -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<string[]>([]);
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) && (
+19 -8
View File
@@ -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({
<Select
onValueChange={(value: string) => {
setValue("prefix", value);
setCurrentPrefix(value);
}}
name="prefix"
defaultValue={validDefaultDomain}
@@ -260,6 +270,7 @@ export function UrlForm({
id="url"
className="w-full rounded-none pl-[8px] shadow-inner"
size={20}
minLength={limitLen}
{...register("url")}
disabled={type === "edit"}
/>
+10 -1
View File
@@ -17,6 +17,9 @@ export interface DomainConfig {
cf_record_types: string;
cf_api_key_encrypted: boolean;
resend_api_key: string | null;
min_url_length: number;
min_email_length: number;
min_record_length: number;
max_short_links: number | null;
max_email_forwards: number | null;
max_dns_records: number | null;
@@ -70,13 +73,16 @@ export async function getDomainsByFeature(
where: { [feature]: true },
select: {
domain_name: true,
cf_record_types: true,
min_url_length: true,
min_email_length: true,
min_record_length: true,
enable_short_link: admin,
enable_email: admin,
enable_dns: admin,
cf_zone_id: admin,
cf_api_key: admin,
cf_email: admin,
cf_record_types: true,
},
});
return domains;
@@ -92,6 +98,9 @@ export async function getDomainsByFeatureClient(feature: string) {
select: {
domain_name: true,
cf_record_types: true,
min_url_length: true,
min_email_length: true,
min_record_length: true,
},
orderBy: {
updatedAt: "desc",
+3
View File
@@ -15,5 +15,8 @@ export const createDomainSchema = z.object({
max_short_links: z.number().optional(),
max_email_forwards: z.number().optional(),
max_dns_records: z.number().optional(),
min_url_length: z.number().min(1).default(1),
min_email_length: z.number().min(1).default(1),
min_record_length: z.number().min(1).default(1),
active: z.boolean().default(true),
});
+1 -5
View File
@@ -11,13 +11,9 @@ export const createRecordSchema = z.object({
name: z
.string()
.regex(/^[a-zA-Z0-9-_]+$/, "Invalid characters")
.min(2)
.max(32),
content: z
.string()
// .regex(/^[a-zA-Z0-9-.]+$/, "Invalid characters")
.min(1)
.max(32),
content: z.string().min(1).max(32),
ttl: z.number().min(1).max(36000).default(1),
proxied: z.boolean().default(false),
comment: z.string().optional(),
+2 -3
View File
@@ -16,12 +16,11 @@ import { isValidUrl } from "../utils";
*/
const urlPattern = /^(?!-)[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*(?<!-)$/;
const targetPattern =
/^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(\/[^\s<>"{}|\\^`\[\]]*)?(\?[^\s<>"{}|\\^`\[\]]*)?(\#[^\s<>"{}|\\^`\[\]]*)?$/;
// const targetPattern = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(:\d+)?(\/[^\s<>"{}|\\^`\[\]]*)?(\?[^\s<>"{}|\\^`\[\]]*)?(\#[^\s<>"{}|\\^`\[\]]*)?$/;
export const createUrlSchema = z.object({
id: z.string().optional(),
target: z.string().min(1).regex(targetPattern, "Invalid target URL format"),
target: z.string().min(1),
url: z.string().min(1).regex(urlPattern, "Invalid URL format"),
expiration: z.string().default("-1"),
visible: z.number().default(1),
+5 -1
View File
@@ -167,7 +167,11 @@
"Record Types": "Record Types",
"Allowed record types": "Allowed record types",
"use `,` to separate": "use `,` to separate",
"Agree": "Agree"
"Agree": "Agree",
"Min URL Length": "Min URL Length",
"Min Email Length": "Min Email Length",
"Min Subdomain Length": "Min Subdomain Length",
"Limit Configs": "Limit Configs"
},
"Components": {
"Dashboard": "Dashboard",
+5 -1
View File
@@ -167,7 +167,11 @@
"Record Types": "DNS 记录类型",
"Allowed record types": "请填写标准的 DNS 记录类型",
"use `,` to separate": "使用 `,` 分隔",
"Agree": "同意"
"Agree": "同意",
"Min URL Length": "短链后缀最短长度",
"Min Email Length": "邮箱前缀最短长度",
"Min Subdomain Length": "子域名前缀最短长度",
"Limit Configs": "域名限制"
},
"Components": {
"Dashboard": "用户面板",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wr.do",
"version": "1.0.3",
"version": "1.0.4",
"author": {
"name": "oiov",
"url": "https://github.com/oiov"
@@ -0,0 +1,9 @@
-- AlterTable
ALTER TABLE "domains" ADD COLUMN "min_url_length" INTEGER NOT NULL DEFAULT 1;
ALTER TABLE "domains" ADD COLUMN "min_email_length" INTEGER NOT NULL DEFAULT 1;
ALTER TABLE "domains" ADD COLUMN "min_record_length" INTEGER NOT NULL DEFAULT 1;
+3
View File
@@ -268,6 +268,9 @@ model Domain {
max_short_links Int?
max_email_forwards Int?
max_dns_records Int?
min_url_length Int @default(1)
min_email_length Int @default(1)
min_record_length Int @default(1)
active Boolean @default(true)
createdAt DateTime @default(now())
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+3 -3
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://wr.do/robots.txt</loc><lastmod>2025-06-17T11:50:43.688Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/manifest.json</loc><lastmod>2025-06-17T11:50:43.688Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/opengraph-image.jpg</loc><lastmod>2025-06-17T11:50:43.688Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/robots.txt</loc><lastmod>2025-06-19T12:05:04.728Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/manifest.json</loc><lastmod>2025-06-19T12:05:04.729Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/opengraph-image.jpg</loc><lastmod>2025-06-19T12:05:04.729Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
</urlset>
+1 -1
View File
File diff suppressed because one or more lines are too long