feats(domain): configurable limit configs
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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" && (
|
||||
|
||||
@@ -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) && (
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,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,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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user