feats: support catch-all for email
This commit is contained in:
@@ -8,26 +8,28 @@ import useSWR from "swr";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Icons } from "@/components/shared/icons";
|
||||
import { SkeletonSection } from "@/components/shared/section-skeleton";
|
||||
|
||||
export default function AppConfigs({}: {}) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [loginMethodCount, setLoginMethodCount] = useState(0);
|
||||
|
||||
const { data: configs, isLoading } = useSWR<Record<string, any>>(
|
||||
"/api/admin/configs",
|
||||
fetcher,
|
||||
);
|
||||
const {
|
||||
data: configs,
|
||||
isLoading,
|
||||
mutate,
|
||||
} = useSWR<Record<string, any>>("/api/admin/configs", fetcher);
|
||||
const [notification, setNotification] = useState("");
|
||||
const [catchAllEmails, setCatchAllEmails] = useState("");
|
||||
|
||||
const t = useTranslations("Setting");
|
||||
|
||||
@@ -35,6 +37,9 @@ export default function AppConfigs({}: {}) {
|
||||
if (!isLoading && configs?.system_notification) {
|
||||
setNotification(configs.system_notification);
|
||||
}
|
||||
if (!isLoading && configs?.catch_all_emails) {
|
||||
setCatchAllEmails(configs.catch_all_emails);
|
||||
}
|
||||
// 计算登录方式数量
|
||||
if (!isLoading) {
|
||||
let count = 0;
|
||||
@@ -55,6 +60,7 @@ export default function AppConfigs({}: {}) {
|
||||
});
|
||||
if (res.ok) {
|
||||
toast.success("Updated!");
|
||||
mutate();
|
||||
} else {
|
||||
toast.error("Failed!", {
|
||||
description: await res.text(),
|
||||
@@ -66,180 +72,329 @@ export default function AppConfigs({}: {}) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<SkeletonSection />
|
||||
<SkeletonSection />
|
||||
<Skeleton className="h-48 w-full rounded-lg" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-neutral-50 dark:bg-neutral-900">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-lg font-bold">{t("App Configs")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mt-3 space-y-6 border-t pt-6">
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">{t("User Registration")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("Allow users to sign up")}
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<Switch
|
||||
defaultChecked={configs.enable_user_registration}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_user_registration", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between">
|
||||
<div className="space-y-1 text-start leading-none">
|
||||
<p className="font-medium">{t("Login Methods")}</p>
|
||||
|
||||
<Card>
|
||||
<Collapsible className="group">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between bg-neutral-50 px-4 py-5 dark:bg-neutral-900">
|
||||
<div className="text-lg font-bold">{t("App Configs")}</div>
|
||||
<Icons.chevronDown className="ml-auto size-4" />
|
||||
<Icons.settings className="ml-3 size-4 transition-all group-hover:scale-110" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-3 border-t bg-neutral-100 p-4 dark:bg-neutral-800">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">{t("User Registration")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("Select the login methods that users can use to log in")}
|
||||
{t("Allow users to sign up")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Icons.chevronDown className="ml-auto mr-2 size-4" />
|
||||
<Badge>{loginMethodCount}</Badge>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-2 space-y-3 rounded-md bg-neutral-100 p-3">
|
||||
{configs && (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="flex items-center gap-2 text-sm">
|
||||
<Icons.github className="size-4" /> GitHub OAuth
|
||||
</p>
|
||||
<Switch
|
||||
defaultChecked={configs.enable_github_oauth}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_github_oauth", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="flex items-center gap-2 text-sm">
|
||||
<Icons.google className="size-4" />
|
||||
Google OAuth
|
||||
</p>
|
||||
<Switch
|
||||
defaultChecked={configs.enable_google_oauth}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_google_oauth", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="flex items-center gap-2 text-sm">
|
||||
<img
|
||||
src="/_static/images/linuxdo.webp"
|
||||
alt="linuxdo"
|
||||
className="size-4"
|
||||
/>
|
||||
LinuxDo OAuth
|
||||
</p>
|
||||
<Switch
|
||||
defaultChecked={configs.enable_liunxdo_oauth}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_liunxdo_oauth", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="flex items-center gap-2 text-sm">
|
||||
<Icons.resend className="size-4" />
|
||||
{t("Resend Email")}
|
||||
</p>
|
||||
<Switch
|
||||
defaultChecked={configs.enable_resend_email_login}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_resend_email_login", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="flex items-center gap-2 text-sm">
|
||||
<Icons.pwdKey className="size-4" />
|
||||
{t("Email Password")}
|
||||
</p>
|
||||
<Switch
|
||||
defaultChecked={configs.enable_email_password_login}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(
|
||||
v,
|
||||
"enable_email_password_login",
|
||||
"BOOLEAN",
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">{t("Subdomain Apply Mode")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Enable subdomain apply mode, each submission requires administrator review",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<Switch
|
||||
defaultChecked={configs.enable_subdomain_apply}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_subdomain_apply", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-start justify-start gap-3">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">{t("Notification")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Set system notification, this will be displayed in the header",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<Textarea
|
||||
className="h-16 max-h-32 min-h-9 resize-y"
|
||||
placeholder="Support HTML format, such as <div>info</div>"
|
||||
rows={5}
|
||||
defaultValue={configs.system_notification}
|
||||
value={notification}
|
||||
onChange={(e) => setNotification(e.target.value)}
|
||||
<Switch
|
||||
defaultChecked={configs.enable_user_registration}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_user_registration", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
className="h-9 text-nowrap"
|
||||
disabled={
|
||||
isPending || notification === configs.system_notification
|
||||
}
|
||||
onClick={() =>
|
||||
handleChange(notification, "system_notification", "STRING")
|
||||
}
|
||||
>
|
||||
{isPending && (
|
||||
<Icons.spinner className="mr-1 size-4 animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between">
|
||||
<div className="space-y-1 text-start leading-none">
|
||||
<p className="font-medium">{t("Login Methods")}</p>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("Select the login methods that users can use to log in")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Icons.chevronDown className="ml-auto mr-2 size-4" />
|
||||
<Badge>{loginMethodCount}</Badge>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-2 space-y-3 rounded-md bg-neutral-100 p-3 dark:bg-neutral-800">
|
||||
{configs && (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="flex items-center gap-2 text-sm">
|
||||
<Icons.github className="size-4" /> GitHub OAuth
|
||||
</p>
|
||||
<Switch
|
||||
defaultChecked={configs.enable_github_oauth}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_github_oauth", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="flex items-center gap-2 text-sm">
|
||||
<Icons.google className="size-4" />
|
||||
Google OAuth
|
||||
</p>
|
||||
<Switch
|
||||
defaultChecked={configs.enable_google_oauth}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_google_oauth", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="flex items-center gap-2 text-sm">
|
||||
<img
|
||||
src="/_static/images/linuxdo.webp"
|
||||
alt="linuxdo"
|
||||
className="size-4"
|
||||
/>
|
||||
LinuxDo OAuth
|
||||
</p>
|
||||
<Switch
|
||||
defaultChecked={configs.enable_liunxdo_oauth}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_liunxdo_oauth", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="flex items-center gap-2 text-sm">
|
||||
<Icons.resend className="size-4" />
|
||||
{t("Resend Email")}
|
||||
</p>
|
||||
<Switch
|
||||
defaultChecked={configs.enable_resend_email_login}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(
|
||||
v,
|
||||
"enable_resend_email_login",
|
||||
"BOOLEAN",
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="flex items-center gap-2 text-sm">
|
||||
<Icons.pwdKey className="size-4" />
|
||||
{t("Email Password")}
|
||||
</p>
|
||||
<Switch
|
||||
defaultChecked={configs.enable_email_password_login}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(
|
||||
v,
|
||||
"enable_email_password_login",
|
||||
"BOOLEAN",
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<div className="flex flex-col items-start justify-start gap-3">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">{t("Notification")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Set system notification, this will be displayed in the header",
|
||||
)}
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{configs && (
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<Textarea
|
||||
className="h-16 max-h-32 min-h-9 resize-y bg-white"
|
||||
placeholder="Support HTML format, such as <div>info</div>"
|
||||
rows={5}
|
||||
// defaultValue={configs.system_notification}
|
||||
value={notification}
|
||||
onChange={(e) => setNotification(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
className="h-9 text-nowrap"
|
||||
disabled={
|
||||
isPending || notification === configs.system_notification
|
||||
}
|
||||
onClick={() =>
|
||||
handleChange(
|
||||
notification,
|
||||
"system_notification",
|
||||
"STRING",
|
||||
)
|
||||
}
|
||||
>
|
||||
{isPending && (
|
||||
<Icons.spinner className="mr-1 size-4 animate-spin" />
|
||||
)}
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible className="group border-y">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between bg-neutral-50 px-4 py-5 dark:bg-neutral-900">
|
||||
<div className="text-lg font-bold">{t("Email Configs")}</div>
|
||||
<Icons.chevronDown className="ml-auto size-4" />
|
||||
<Icons.mail className="ml-3 size-4 transition-all group-hover:scale-110" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-3 border-t bg-neutral-100 p-4 dark:bg-neutral-800">
|
||||
<div className="space-y-6">
|
||||
{/* Catch-All */}
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="flex items-center gap-1 font-medium">
|
||||
Catch-All <Badge>Beta</Badge>
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Enable email catch-all, all user's email address which created on this platform will be redirected to the catch-all email address",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<Switch
|
||||
defaultChecked={configs.enable_email_catch_all}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_email_catch_all", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-start justify-start gap-3">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">{t("Catch-All Email Address")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Set catch-all email address, split by comma if more than one, such as: 1@a-com,2@b-com, Only works when email catch all is enabled",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<Textarea
|
||||
className="h-16 max-h-32 min-h-9 resize-y bg-white dark:bg-neutral-700"
|
||||
placeholder="1@a.com,2@b.com"
|
||||
rows={5}
|
||||
// defaultValue={configs.catch_all_emails}
|
||||
value={catchAllEmails}
|
||||
disabled={!configs.enable_email_catch_all}
|
||||
onChange={(e) => setCatchAllEmails(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
className="h-9 text-nowrap"
|
||||
disabled={
|
||||
isPending || catchAllEmails === configs.catch_all_emails
|
||||
}
|
||||
onClick={() =>
|
||||
handleChange(catchAllEmails, "catch_all_emails", "STRING")
|
||||
}
|
||||
>
|
||||
{isPending && (
|
||||
<Icons.spinner className="mr-1 size-4 animate-spin" />
|
||||
)}
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Message Pusher */}
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="flex items-center gap-1 font-medium">
|
||||
{t("Message Pusher")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Push message to third-party services, such as Telegram, 飞书 etc",
|
||||
)}
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<Switch
|
||||
defaultChecked={false}
|
||||
disabled
|
||||
// onCheckedChange={(v) =>
|
||||
// handleChange(v, "enable_email_catch_all", "BOOLEAN")
|
||||
// }
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* Webhook */}
|
||||
<div className="flex flex-col items-start justify-start gap-3">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">Webhook</p>
|
||||
<p className="text-xs text-muted-foreground"></p>
|
||||
</div>
|
||||
{configs && (
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<Textarea
|
||||
className="h-16 max-h-32 min-h-9 resize-y bg-white dark:bg-neutral-700"
|
||||
placeholder=""
|
||||
rows={5}
|
||||
// defaultValue={configs.catch_all_emails}
|
||||
// value={catchAllEmails}
|
||||
disabled
|
||||
// onChange={(e) => setCatchAllEmails(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
className="h-9 text-nowrap"
|
||||
disabled
|
||||
onClick={() =>
|
||||
handleChange(catchAllEmails, "catch_all_emails", "STRING")
|
||||
}
|
||||
>
|
||||
{isPending && (
|
||||
<Icons.spinner className="mr-1 size-4 animate-spin" />
|
||||
)}
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
<Collapsible className="group">
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between bg-neutral-50 px-4 py-5 dark:bg-neutral-900">
|
||||
<div className="text-lg font-bold">{t("Subdomain Configs")}</div>
|
||||
<Icons.chevronDown className="ml-auto size-4" />
|
||||
<Icons.globeLock className="ml-3 size-4 transition-all group-hover:scale-110" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-3 border-t bg-neutral-100 p-4 dark:bg-neutral-800">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">{t("Subdomain Apply Mode")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Enable subdomain apply mode, each submission requires administrator review",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<Switch
|
||||
defaultChecked={configs.enable_subdomain_apply}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_subdomain_apply", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { DashboardHeader } from "@/components/dashboard/header";
|
||||
import { SkeletonSection } from "@/components/shared/section-skeleton";
|
||||
|
||||
export default function DashboardRecordsLoading() {
|
||||
export default function SystemSettingsLoading() {
|
||||
return (
|
||||
<>
|
||||
<DashboardHeader heading="System Settings" text="" />
|
||||
<SkeletonSection />
|
||||
<SkeletonSection />
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<Skeleton className="h-32 w-full rounded-lg" />
|
||||
<Skeleton className="h-[400px] w-full rounded-lg" />
|
||||
</>
|
||||
|
||||
@@ -24,6 +24,8 @@ export async function GET(req: NextRequest) {
|
||||
"enable_liunxdo_oauth",
|
||||
"enable_resend_email_login",
|
||||
"enable_email_password_login",
|
||||
"enable_email_catch_all",
|
||||
"catch_all_emails",
|
||||
]);
|
||||
|
||||
return Response.json(configs, { status: 200 });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { OriginalEmail, saveForwardEmail } from "@/lib/dto/email";
|
||||
import { getMultipleConfigs } from "@/lib/dto/system-config";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
@@ -7,7 +8,34 @@ export async function POST(req: Request) {
|
||||
if (!data) {
|
||||
return Response.json("No email data received", { status: 400 });
|
||||
}
|
||||
await saveForwardEmail(data);
|
||||
|
||||
const configs = await getMultipleConfigs([
|
||||
"enable_email_catch_all",
|
||||
"catch_all_emails",
|
||||
]);
|
||||
|
||||
if (configs.enable_email_catch_all) {
|
||||
const validEmails = parseAndValidateEmails(configs.catch_all_emails);
|
||||
|
||||
if (validEmails.length === 0) {
|
||||
return Response.json(
|
||||
{ error: "No valid catch-all emails configured" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// 方案1: 转发给所有配置的邮箱
|
||||
const forwardPromises = validEmails.map((email) =>
|
||||
saveForwardEmail({ ...data, to: email }),
|
||||
);
|
||||
|
||||
await Promise.all(forwardPromises);
|
||||
|
||||
// 方案2: 只想转发给第一个邮箱
|
||||
// await saveForwardEmail({ ...data, to: validEmails[0] });
|
||||
} else {
|
||||
await saveForwardEmail(data);
|
||||
}
|
||||
|
||||
return Response.json({ status: 200 });
|
||||
} catch (error) {
|
||||
@@ -15,3 +43,30 @@ export async function POST(req: Request) {
|
||||
return Response.json({ status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
function isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email.trim());
|
||||
}
|
||||
|
||||
function parseAndValidateEmails(emailsString: string): string[] {
|
||||
if (!emailsString || typeof emailsString !== "string") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const emails = emailsString
|
||||
.split(",")
|
||||
.map((email) => email.trim())
|
||||
.filter((email) => email.length > 0);
|
||||
|
||||
const validEmails = emails.filter((email) => isValidEmail(email));
|
||||
|
||||
if (validEmails.length !== emails.length) {
|
||||
console.warn(
|
||||
"Some invalid email addresses found:",
|
||||
emails.filter((email) => !isValidEmail(email)),
|
||||
);
|
||||
}
|
||||
|
||||
return validEmails;
|
||||
}
|
||||
|
||||
+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.4",
|
||||
"versionName": "1.0.5",
|
||||
"versionCode": "1",
|
||||
"start_url": "/",
|
||||
"orientation": "portrait",
|
||||
|
||||
+8
-1
@@ -486,6 +486,13 @@
|
||||
"Email Password": "Email Password",
|
||||
"Your Password": "Your Password",
|
||||
"Update your password": "Update your password",
|
||||
"At least 6 characters, Max 32 characters": "At least 6 characters, Max 32 characters"
|
||||
"At least 6 characters, Max 32 characters": "At least 6 characters, Max 32 characters",
|
||||
"Subdomain Configs": "Subdomain Configs",
|
||||
"Email Configs": "Email Configs",
|
||||
"Enable email catch-all, all user's email address which created on this platform will be redirected to the catch-all email address": "Enable email catch all, user can use email address which created on this platform to register",
|
||||
"Catch-All Email Address": "Catch-All Email Address",
|
||||
"Set catch-all email address, split by comma if more than one, such as: 1@a-com,2@b-com, Only works when email catch all is enabled": "Set catch-all email address, split by comma if more than one, such as: 1@a.com,2@b.com, Only works when email catch all is enabled",
|
||||
"Message Pusher": "Message Pusher",
|
||||
"Push message to third-party services, such as Telegram, 飞书 etc.": "Push message to third-party services, such as Telegram, 飞书 etc"
|
||||
}
|
||||
}
|
||||
|
||||
+9
-2
@@ -477,7 +477,7 @@
|
||||
"User Registration": "用户注册",
|
||||
"Allow users to sign up": "是否允许用户注册",
|
||||
"Subdomain Apply Mode": "子域名申请模式",
|
||||
"Enable subdomain apply mode, each submission requires administrator review": "启用子域名申请模式,每次提交需要管理员审核",
|
||||
"Enable subdomain apply mode, each submission requires administrator review": "启用子域名申请模式,用户每次提交新子域名需要管理员审核(邮件通知管理员审核,审核通过后邮件通知用户)",
|
||||
"Notification": "系统通知",
|
||||
"Set system notification, this will be displayed in the header": "设置系统通知,将在网页顶部显示",
|
||||
"Login Methods": "登录方式",
|
||||
@@ -486,6 +486,13 @@
|
||||
"Email Password": "账号密码登录",
|
||||
"Your Password": "账号密码",
|
||||
"Update your password": "更新您的密码",
|
||||
"At least 6 characters, Max 32 characters": "密码长度至少6位,最多32位"
|
||||
"At least 6 characters, Max 32 characters": "密码长度至少6位,最多32位",
|
||||
"Subdomain Configs": "子域配置",
|
||||
"Email Configs": "电子邮件配置",
|
||||
"Enable email catch-all, all user's email address which created on this platform will be redirected to the catch-all email address": "启用 Catch-All,所有用户在此平台创建的邮箱所接收的邮件都会被转发到 Catch-All 设置的邮箱地址",
|
||||
"Catch-All Email Address": "Catch-All 邮箱地址",
|
||||
"Set catch-all email address, split by comma if more than one, such as: 1@a-com,2@b-com, Only works when email catch all is enabled": "设置 Catch-All 邮箱地址白名单 (仅支持在此平台创建的邮箱),多个邮箱地址请用逗号分隔,例如:1@a.com,2@b.com。仅在启用 Catch-All 时生效",
|
||||
"Message Pusher": "消息推送",
|
||||
"Push message to third-party services, such as Telegram, 飞书 etc": "推送消息到第三方服务,例如 Telegram,飞书等"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wr.do",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.5",
|
||||
"author": {
|
||||
"name": "oiov",
|
||||
"url": "https://github.com/oiov"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
INSERT INTO "system_configs"
|
||||
(
|
||||
"key",
|
||||
"value",
|
||||
"type",
|
||||
"description"
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'enable_email_catch_all',
|
||||
'false',
|
||||
'BOOLEAN',
|
||||
'是否启用 Email Catch-all 功能'
|
||||
);
|
||||
|
||||
INSERT INTO "system_configs"
|
||||
(
|
||||
"key",
|
||||
"value",
|
||||
"type",
|
||||
"description"
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'catch_all_emails',
|
||||
'',
|
||||
'STRING',
|
||||
'Email Catchall 邮箱列表,逗号分隔'
|
||||
);
|
||||
@@ -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.4",
|
||||
"versionName": "1.0.5",
|
||||
"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.4",
|
||||
"versionName": "1.0.5",
|
||||
"versionCode": "1",
|
||||
"start_url": "/",
|
||||
"orientation": "portrait",
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user