feats: support muti email providers
This commit is contained in:
@@ -26,7 +26,9 @@ LinuxDo_CLIENT_SECRET=
|
||||
# Email api (https://resend.com) for Auth login (NextAuth) and send email
|
||||
# -----------------------------------------------------------------------------
|
||||
RESEND_API_KEY=
|
||||
RESEND_FROM_EMAIL="wrdo <support@wr.do>"
|
||||
BREVO_API_KEY=
|
||||
EMAIL_FROM=service@wr.do
|
||||
EMAIL_FROM_NAME=WRDO
|
||||
|
||||
# Google Analytics
|
||||
NEXT_PUBLIC_GOOGLE_ID=
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function AppConfigs({}: {}) {
|
||||
if (configs?.enable_google_oauth) count++;
|
||||
if (configs?.enable_github_oauth) count++;
|
||||
if (configs?.enable_liunxdo_oauth) count++;
|
||||
if (configs?.enable_resend_email_login) count++;
|
||||
// if (configs?.enable_resend_email_login) count++;
|
||||
if (configs?.enable_email_password_login) count++;
|
||||
setLoginMethodCount(count);
|
||||
}
|
||||
@@ -133,62 +133,7 @@ export default function AppConfigs({}: {}) {
|
||||
<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">
|
||||
<div className="flex items-center justify-between gap-3 transition-all duration-100 hover:cursor-pointer hover:shadow-sm">
|
||||
<p className="flex items-center gap-2 text-sm">
|
||||
<Icons.pwdKey className="size-4" />
|
||||
{t("Email Password")}
|
||||
@@ -204,6 +149,61 @@ export default function AppConfigs({}: {}) {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3 transition-all duration-100 hover:cursor-pointer hover:shadow-sm">
|
||||
<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 transition-all duration-100 hover:cursor-pointer hover:shadow-sm">
|
||||
<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 transition-all duration-100 hover:cursor-pointer hover:shadow-sm">
|
||||
<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> */}
|
||||
</>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
@@ -499,7 +499,7 @@ export default function AppConfigs({}: {}) {
|
||||
</p>
|
||||
<p className="text-start text-xs text-muted-foreground">
|
||||
{t(
|
||||
"If enabled, forward all received emails to other platform email addresses (Send with Resend)",
|
||||
"If enabled, forward all received emails to other platform email addresses (Send with Resend or Brevo)",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -840,6 +840,7 @@ export default function AppConfigs({}: {}) {
|
||||
{t(
|
||||
"Send email notifications for subdomain application status updates; Notifies administrators when users submit applications and notifies users of approval results; Only available when subdomain application mode is enabled",
|
||||
)}
|
||||
. (Brevo)
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
|
||||
@@ -288,9 +288,14 @@ export default function DomainList({ user, action }: DomainListProps) {
|
||||
handleChangeStatus(value, "enable_email", domain)
|
||||
}
|
||||
/>
|
||||
{domain.resend_api_key && (
|
||||
<Icons.resend className="mx-0.5 size-4" />
|
||||
)}
|
||||
{domain.email_provider === "Resend" &&
|
||||
domain.resend_api_key && (
|
||||
<Icons.resend className="mx-0.5 size-4" />
|
||||
)}
|
||||
{domain.email_provider === "Brevo" &&
|
||||
domain.brevo_api_key && (
|
||||
<Icons.brevo className="mx-0.5 size-4" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="col-span-1 hidden items-center gap-1 sm:flex">
|
||||
<Switch
|
||||
|
||||
@@ -32,7 +32,9 @@ export async function POST(req: NextRequest) {
|
||||
cf_email: target_domain.cf_email,
|
||||
cf_record_types: target_domain.cf_record_types,
|
||||
cf_api_key_encrypted: false,
|
||||
email_provider: target_domain.email_provider,
|
||||
resend_api_key: target_domain.resend_api_key,
|
||||
brevo_api_key: target_domain.brevo_api_key,
|
||||
max_short_links: target_domain.max_short_links,
|
||||
max_email_forwards: target_domain.max_email_forwards,
|
||||
max_dns_records: target_domain.max_dns_records,
|
||||
|
||||
@@ -60,7 +60,9 @@ export async function POST(req: NextRequest) {
|
||||
cf_email: data.cf_email,
|
||||
cf_record_types: data.cf_record_types,
|
||||
cf_api_key_encrypted: false,
|
||||
email_provider: data.email_provider,
|
||||
resend_api_key: data.resend_api_key,
|
||||
brevo_api_key: data.brevo_api_key,
|
||||
max_short_links: data.max_short_links,
|
||||
max_email_forwards: data.max_email_forwards,
|
||||
max_dns_records: data.max_dns_records,
|
||||
@@ -95,7 +97,9 @@ export async function PUT(req: NextRequest) {
|
||||
cf_api_key,
|
||||
cf_email,
|
||||
cf_record_types,
|
||||
email_provider,
|
||||
resend_api_key,
|
||||
brevo_api_key,
|
||||
min_url_length,
|
||||
min_email_length,
|
||||
min_record_length,
|
||||
@@ -120,6 +124,8 @@ export async function PUT(req: NextRequest) {
|
||||
cf_email,
|
||||
cf_record_types,
|
||||
cf_api_key_encrypted: false,
|
||||
email_provider,
|
||||
brevo_api_key,
|
||||
resend_api_key,
|
||||
min_url_length,
|
||||
min_email_length,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { Resend } from "resend";
|
||||
|
||||
import { checkDomainIsConfiguratedResend } from "@/lib/dto/domains";
|
||||
import { checkDomainIsConfiguratedEmailProvider } from "@/lib/dto/domains";
|
||||
import { getUserSendEmailCount, saveUserSendEmail } from "@/lib/dto/email";
|
||||
import { getPlanQuota } from "@/lib/dto/plan";
|
||||
import { checkUserStatus } from "@/lib/dto/user";
|
||||
import { brevoSendEmail } from "@/lib/email/brevo";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { restrictByTimeRange } from "@/lib/team";
|
||||
import { isValidEmail } from "@/lib/utils";
|
||||
@@ -36,30 +37,26 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json("Invalid email address", { status: 403 });
|
||||
}
|
||||
|
||||
const resend_key = await checkDomainIsConfiguratedResend(
|
||||
from.split("@")[1],
|
||||
);
|
||||
const { email_key, provider } =
|
||||
await checkDomainIsConfiguratedEmailProvider(from.split("@")[1]);
|
||||
|
||||
if (!resend_key) {
|
||||
if (!email_key) {
|
||||
return NextResponse.json(
|
||||
"This domain is not configured for sending emails",
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const resend = new Resend(resend_key);
|
||||
const { error } = await resend.emails.send({
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.log("Resend error:", error); // ???如果删掉这句log,下面一行读取error的message会返回undefined
|
||||
return NextResponse.json(`${error.message}`, {
|
||||
status: 400,
|
||||
});
|
||||
switch (provider) {
|
||||
case "Resend":
|
||||
const resend = new Resend(email_key);
|
||||
await resend.emails.send({ from, to, subject, html });
|
||||
break;
|
||||
case "Brevo":
|
||||
await brevoSendEmail({ from, to, subject, html });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
await saveUserSendEmail(user.id, from, to, subject, html);
|
||||
|
||||
@@ -10,7 +10,9 @@ import { getDomainsByFeature } from "@/lib/dto/domains";
|
||||
import { getPlanQuota } from "@/lib/dto/plan";
|
||||
import { getMultipleConfigs } from "@/lib/dto/system-config";
|
||||
import { checkUserStatus, getFirstAdminUser } from "@/lib/dto/user";
|
||||
import { applyRecordEmailHtml, resend } from "@/lib/email";
|
||||
import { brevoSendEmail } from "@/lib/email/brevo";
|
||||
import { resend } from "@/lib/email/resend";
|
||||
import { applyRecordEmailHtml } from "@/lib/email/templates";
|
||||
import { reservedDomains } from "@/lib/enums";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { generateSecret } from "@/lib/utils";
|
||||
@@ -117,8 +119,10 @@ export async function POST(req: Request) {
|
||||
}
|
||||
const admin_user = await getFirstAdminUser();
|
||||
if (configs.enable_subdomain_status_email_pusher && admin_user) {
|
||||
await resend.emails.send({
|
||||
from: env.RESEND_FROM_EMAIL,
|
||||
await brevoSendEmail({
|
||||
key: "", // env
|
||||
from: env.EMAIL_FROM,
|
||||
fromName: env.EMAIL_FROM_NAME,
|
||||
to: admin_user.email || "",
|
||||
subject: "New record pending approval",
|
||||
html: applyRecordEmailHtml({
|
||||
@@ -128,6 +132,7 @@ export async function POST(req: Request) {
|
||||
type: record.type,
|
||||
name: record.name,
|
||||
content: record.content,
|
||||
comment: record.comment,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import { updateUserRecordReview } from "@/lib/dto/cloudflare-dns-record";
|
||||
import { getDomainsByFeature } from "@/lib/dto/domains";
|
||||
import { getMultipleConfigs } from "@/lib/dto/system-config";
|
||||
import { checkUserStatus, getUserById } from "@/lib/dto/user";
|
||||
import { applyRecordToUserEmailHtml, resend } from "@/lib/email";
|
||||
import { brevoSendEmail } from "@/lib/email/brevo";
|
||||
import { resend } from "@/lib/email/resend";
|
||||
import { applyRecordToUserEmailHtml } from "@/lib/email/templates";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
@@ -72,8 +74,10 @@ export async function POST(req: Request) {
|
||||
]);
|
||||
const userInfo = await getUserById(userId);
|
||||
if (configs.enable_subdomain_status_email_pusher && userInfo) {
|
||||
await resend.emails.send({
|
||||
from: env.RESEND_FROM_EMAIL,
|
||||
await brevoSendEmail({
|
||||
key: "",
|
||||
from: env.EMAIL_FROM,
|
||||
fromName: env.EMAIL_FROM_NAME,
|
||||
to: userInfo.email || "",
|
||||
subject: "Your subdomain has been applied",
|
||||
html: applyRecordToUserEmailHtml({
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { getConfiguredResendDomains } from "@/lib/dto/domains";
|
||||
import { getConfiguredEmailDomains } from "@/lib/dto/domains";
|
||||
import { OriginalEmail, saveForwardEmail } from "@/lib/dto/email";
|
||||
import { getMultipleConfigs } from "@/lib/dto/system-config";
|
||||
import { resend } from "@/lib/email";
|
||||
import { brevoSendEmail } from "@/lib/email/brevo";
|
||||
import { resend } from "@/lib/email/resend";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
@@ -141,20 +142,19 @@ async function handleExternalForward(data: OriginalEmail, configs: any) {
|
||||
throw new Error("No valid forward emails configured");
|
||||
}
|
||||
|
||||
const sender = await getConfiguredResendDomains();
|
||||
if (sender.length === 0) {
|
||||
const senders = await getConfiguredEmailDomains();
|
||||
if (senders.length === 0) {
|
||||
throw new Error("No configured resend domains");
|
||||
}
|
||||
|
||||
const { error } = await resend.emails.send({
|
||||
from: `Forwarding@${sender[0].domain_name}`,
|
||||
const options = {
|
||||
from: `Forwarding@${senders[0].domain_name}`,
|
||||
to: validEmails,
|
||||
subject: data.subject ?? "No subject",
|
||||
html: `${data.html ?? data.text} <br><hr><p style="font-size: '12px'; color: '#888'; font-family: 'monospace';text-align: 'center'">This email was forwarded from ${data.to}. Powered by <a href="https://wr.do">WR.DO</a>.</p>`,
|
||||
});
|
||||
if (error) {
|
||||
console.log("[Resend Error]", error);
|
||||
}
|
||||
};
|
||||
|
||||
await brevoSendEmail(options);
|
||||
}
|
||||
|
||||
async function handleNormalEmail(data: OriginalEmail) {
|
||||
|
||||
@@ -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.1.5",
|
||||
"versionName": "1.1.6",
|
||||
"versionCode": "1",
|
||||
"start_url": "/",
|
||||
"orientation": "portrait",
|
||||
|
||||
@@ -2,13 +2,11 @@ import type { NextAuthConfig } from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import Github from "next-auth/providers/github";
|
||||
import Google from "next-auth/providers/google";
|
||||
import Resend from "next-auth/providers/resend";
|
||||
|
||||
// import Resend from "next-auth/providers/resend";
|
||||
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
import { siteConfig } from "./config/site";
|
||||
import { getVerificationEmailHtml, resend } from "./lib/email";
|
||||
|
||||
const linuxDoProvider: any = {
|
||||
id: "linuxdo",
|
||||
name: "Linux Do",
|
||||
@@ -46,27 +44,27 @@ export default {
|
||||
clientId: env.GITHUB_ID,
|
||||
clientSecret: env.GITHUB_SECRET,
|
||||
}),
|
||||
Resend({
|
||||
apiKey: env.RESEND_API_KEY,
|
||||
from: env.RESEND_FROM_EMAIL || "wrdo <support@wr.do>",
|
||||
async sendVerificationRequest({ identifier: email, url, provider }) {
|
||||
try {
|
||||
const { error } = await resend.emails.send({
|
||||
from: provider.from || "no-reply@wr.do",
|
||||
to: [email],
|
||||
subject: "Verify your email address",
|
||||
html: getVerificationEmailHtml({ url, appName: siteConfig.name }),
|
||||
});
|
||||
// Resend({
|
||||
// apiKey: env.RESEND_API_KEY,
|
||||
// from: env.EMAIL_FROM || "wrdo <support@wr.do>",
|
||||
// async sendVerificationRequest({ identifier: email, url, provider }) {
|
||||
// try {
|
||||
// const { error } = await resend.emails.send({
|
||||
// from: provider.from || "no-reply@wr.do",
|
||||
// to: [email],
|
||||
// subject: "Verify your email address",
|
||||
// html: getVerificationEmailHtml({ url, appName: siteConfig.name }),
|
||||
// });
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Resend error: ${JSON.stringify(error)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending verification email", error);
|
||||
throw new Error("Error sending verification email");
|
||||
}
|
||||
},
|
||||
}),
|
||||
// if (error) {
|
||||
// throw new Error(`Resend error: ${JSON.stringify(error)}`);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error("Error sending verification email", error);
|
||||
// throw new Error("Error sending verification email");
|
||||
// }
|
||||
// },
|
||||
// }),
|
||||
linuxDoProvider,
|
||||
Credentials({
|
||||
name: "Credentials",
|
||||
|
||||
@@ -23,6 +23,13 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "../ui/collapsible";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { Switch } from "../ui/switch";
|
||||
|
||||
export type FormData = DomainFormData;
|
||||
@@ -79,7 +86,9 @@ export function DomainForm({
|
||||
cf_email: initData?.cf_email || "",
|
||||
cf_record_types: initData?.cf_record_types || "CNAME,A,TXT",
|
||||
cf_api_key_encrypted: initData?.cf_api_key_encrypted || false,
|
||||
email_provider: initData?.email_provider || "",
|
||||
resend_api_key: initData?.resend_api_key || "",
|
||||
brevo_api_key: initData?.brevo_api_key || "",
|
||||
min_url_length: initData?.min_url_length,
|
||||
min_email_length: initData?.min_email_length,
|
||||
min_record_length: initData?.min_record_length,
|
||||
@@ -497,17 +506,16 @@ export function DomainForm({
|
||||
|
||||
<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-5 flex gap-2 text-xs font-semibold text-neutral-400">
|
||||
{t("Resend Configs")} ({t("Optional")})
|
||||
<Icons.resend className="mx-0.5 size-4" />
|
||||
<h2 className="absolute left-2 top-4 flex gap-2 text-xs font-semibold text-neutral-400">
|
||||
{t("Email Service Configs")} ({t("Optional")})
|
||||
</h2>
|
||||
{ReadyBadge(
|
||||
{/* {ReadyBadge(
|
||||
currentEmailStatus,
|
||||
isCheckedResendConfig,
|
||||
isCheckingResend,
|
||||
"resend",
|
||||
)}
|
||||
<Icons.chevronDown className="ml-2 size-4" />
|
||||
)} */}
|
||||
<Icons.chevronDown className="ml-auto size-4" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
{!currentEmailStatus && (
|
||||
@@ -516,10 +524,45 @@ export function DomainForm({
|
||||
{t("Associate with 'Email Service' status")}
|
||||
</div>
|
||||
)}
|
||||
{/* 邮件服务商选择框 */}
|
||||
<FormSectionColumns title="">
|
||||
<div className="flex w-full items-start justify-between gap-2">
|
||||
<Label className="mt-2.5 text-nowrap" htmlFor="zone_id">
|
||||
{t("API Key")} ({t("send email service")}):
|
||||
{t("Email Provider")}:
|
||||
</Label>
|
||||
<div className="w-full sm:w-3/5">
|
||||
<Select
|
||||
defaultValue={initData?.email_provider || "Resend"}
|
||||
onValueChange={(value) => {
|
||||
setValue("email_provider", value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="flex-1 bg-neutral-50 shadow-inner">
|
||||
<SelectValue placeholder="Select a email provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Resend">
|
||||
<span className="flex items-center gap-1">
|
||||
<Icons.resend className="size-4" />
|
||||
<span>Resend</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="Brevo">
|
||||
<span className="flex items-center gap-1">
|
||||
<Icons.brevo className="size-4" />
|
||||
<span>Brevo</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</FormSectionColumns>
|
||||
|
||||
<FormSectionColumns title="">
|
||||
<div className="flex w-full items-start justify-between gap-2">
|
||||
<Label className="mt-2.5 text-nowrap" htmlFor="zone_id">
|
||||
{t("Resend API Key")}:
|
||||
</Label>
|
||||
<div className="w-full sm:w-3/5">
|
||||
<Input
|
||||
@@ -550,6 +593,40 @@ export function DomainForm({
|
||||
</div>
|
||||
</div>
|
||||
</FormSectionColumns>
|
||||
<FormSectionColumns title="">
|
||||
<div className="flex w-full items-start justify-between gap-2">
|
||||
<Label className="mt-2.5 text-nowrap" htmlFor="zone_id">
|
||||
{t("Brevo API Key")}:
|
||||
</Label>
|
||||
<div className="w-full sm:w-3/5">
|
||||
<Input
|
||||
id="target"
|
||||
className="flex-1 bg-neutral-50 shadow-inner"
|
||||
size={32}
|
||||
{...register("brevo_api_key")}
|
||||
disabled={!currentEmailStatus}
|
||||
/>
|
||||
<div className="flex flex-col justify-between p-1">
|
||||
{errors?.brevo_api_key ? (
|
||||
<p className="pb-0.5 text-[13px] text-red-600">
|
||||
{errors.brevo_api_key.message}
|
||||
</p>
|
||||
) : (
|
||||
<p className="pb-0.5 text-[13px] text-muted-foreground">
|
||||
{t("Optional")}.{" "}
|
||||
<Link
|
||||
className="text-blue-500"
|
||||
href="/docs/developer/email"
|
||||
target="_blank"
|
||||
>
|
||||
{t("How to get brevo api key?")}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormSectionColumns>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
placeholder="name@example.com"
|
||||
placeholder="email@example.com"
|
||||
type="email"
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
@@ -262,9 +262,9 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
|
||||
{t("Sign In / Sign Up")}
|
||||
</Button>
|
||||
|
||||
<p className="rounded-md border border-dashed bg-muted px-3 py-2 text-xs text-muted-foreground">
|
||||
{/* <p className="rounded-md border border-dashed bg-muted px-3 py-2 text-xs text-muted-foreground">
|
||||
📢 {t("Unregistered users will automatically create an account")}.
|
||||
</p>
|
||||
</p> */}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
@@ -360,7 +360,7 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
|
||||
(loginMethod["resend"] || loginMethod["credentials"]) &&
|
||||
rendeSeparator()}
|
||||
|
||||
{loginMethod["resend"] && loginMethod["credentials"] ? (
|
||||
{/* {loginMethod["resend"] && loginMethod["credentials"] ? (
|
||||
<Tabs defaultValue="resend">
|
||||
<TabsList className="mb-2 w-full justify-center">
|
||||
<TabsTrigger value="resend">{t("Email Code")}</TabsTrigger>
|
||||
@@ -374,7 +374,9 @@ export function UserAuthForm({ className, type, ...props }: UserAuthFormProps) {
|
||||
{rendeResend()}
|
||||
{rendeCredentials()}
|
||||
</>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{loginMethod["credentials"] && <>{rendeCredentials()}</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function Notification() {
|
||||
className="relative flex max-h-24 w-full items-center justify-center bg-muted text-sm text-primary"
|
||||
>
|
||||
<div
|
||||
className="max-w-3xl flex-1 px-8 py-2.5 text-center"
|
||||
className="flex-1 px-8 py-2.5 text-center"
|
||||
dangerouslySetInnerHTML={{ __html: data.system_notification }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -620,6 +620,20 @@ export const Icons = {
|
||||
</defs>
|
||||
</svg>
|
||||
),
|
||||
brevo: ({ ...props }: LucideProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#059669"
|
||||
d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12a12 12 0 0 0 12-12A12 12 0 0 0 12 0M7.2 4.8h5.747c2.34 0 3.895 1.406 3.895 3.516c0 1.022-.348 1.862-1.09 2.588C17.189 11.812 18 13.22 18 14.785c0 2.86-2.64 5.016-6.164 5.016H7.199v-15zm2.085 1.952v5.537h.07c.233-.432.858-.796 2.249-1.226c2.039-.659 3.037-1.52 3.037-2.655c0-.998-.766-1.656-1.924-1.656zm4.87 5.266c-.766.385-1.67.748-2.76 1.11c-1.229.387-2.11 1.386-2.11 2.407v2.315h2.365c2.387 0 4.149-1.34 4.149-3.155c0-1.067-.625-2.087-1.645-2.677z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
disabledLink: ({ ...props }: LucideProps) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -5,45 +5,63 @@ description: 如何配置项目中的邮件服务
|
||||
|
||||
<DocsLang en="/docs/developer/email" zh="/docs/developer/email-zh" />
|
||||
|
||||
在 WR.DO 项目中,有两个功能依赖于 Resend:
|
||||
|
||||
- 邮箱验证登录(魔法链接)
|
||||
- 邮件发送功能(如果你需要接收邮件功能,请参考 [cloudflare-email-worker](/docs/developer/cloudflare-email-worker))。
|
||||
|
||||
`.env` 文件中配置的 `RESEND_API_KEY` 和 `RESEND_FROM_EMAIL` 用于登录功能,
|
||||
而邮件发送功能所需的 Resend 密钥需要你在登录后台管理面板(`/admin/domains`)后,在域名配置中自行添加。
|
||||
|
||||
<Callout type="note">
|
||||
这两个功能可以使用同一个密钥,因为它们本质上都是通过 Resend 发送邮件。
|
||||
<Callout type="warning" twClass="mt-4">
|
||||
此模块配置在 v1.1.5 版本之后经历了较大改变,注意修改对应配置。
|
||||
- 移除了 `RESEND_API_KEY`,替换为 `BREVO_API_KEY` (在较早版本仅默认使用 Resend 服务,由于官方账号被Resend封禁,不得不切换服务商所以做出此次变动)
|
||||
- 默认使用 Resend 发送系统通知邮件替换为了 Brevo
|
||||
- `RESEND_FROM_EMAIL` 替换为 `EMAIL_FROM`, 新增 `EMAIL_FROM_NAME` 环境变量
|
||||
</Callout>
|
||||
|
||||
以下将演示如何配置登录所需的 Resend 密钥。
|
||||
本项目的邮件服务模块具备接收和发送邮件的能力,本篇将介绍如何配置项目中的邮件**发送**服务。
|
||||
|
||||
## 步骤
|
||||
> 如果你需要配置接收邮件功能,请参考文档 [cloudflare-email-worker](/docs/developer/cloudflare-email-worker)
|
||||
|
||||
<Callout type="note">
|
||||
邮件部分配置类似于 [resend](https://resend.com/) 的文档。
|
||||
如果你想查阅官方文档,可以参考
|
||||
[这里](https://authjs.dev/getting-started/installation#setup-environment)。
|
||||
</Callout>
|
||||
目前支持的发件服务商:
|
||||
|
||||
<Steps>
|
||||
- [Resend](https://resend.com) (免费额度:每天最多发送 100 封,支持绑定 1 个域名)
|
||||
- [Brevo](https://www.brevo.com) (免费额度:每天最多发送 300 封,支持绑定多个域名)
|
||||
|
||||
### 创建账号
|
||||
后续会根据需求接入其他发件方式。
|
||||
|
||||
如果你还没有 Resend 账号,请按照 [这里](https://resend.com/signup) 的注册流程操作。
|
||||
## 环境变量
|
||||
|
||||
> Resend 免费账号提供每天发送 100 个邮件额度,绑定 1 个域名,足够一般用户使用。
|
||||
```js
|
||||
BREVO_API_KEY=your-brevo-api-key
|
||||
EMAIL_FROM=support@your-domain.com
|
||||
EMAIL_FROM_NAME=WR.DO
|
||||
```
|
||||
|
||||
### 创建 API 密钥
|
||||
注册并登录 Brevo 控制台 [app.brevo.com/settings/keys/api](https://app.brevo.com/settings/keys/api) 页面创建一个密钥,将其复制并粘贴到环境变量中。
|
||||
|
||||
注意,在此处配置的 `BREVO_API_KEY` 默认用于`子域名申请通知`功能,不会用于发送邮件。也可将此key填入下方的域名配置中作为邮件服务模块的发件者。
|
||||
|
||||
## Brevo
|
||||
|
||||
在本项目中,有以下几处会使用到 Brevo 发送邮件:
|
||||
|
||||
- 邮件服务模块(`/email`)
|
||||
- 子域名申请通知(在系统设置中,默认关闭此功能)
|
||||
- 邮箱验证功能(开发中 `/dashboard/settings`)
|
||||
|
||||
同样在Brevo 控制台创建 API 密钥,然后回到系统的 localhost:3000/admin/system 页面, 在**域名管理**项中点击**添加域名**,并在子项**邮件服务商**中填写对应的 API 密钥保存即可:
|
||||
|
||||

|
||||
|
||||
之后,你可以在 Brevo 控制台的 [Domain](https://app.brevo.com/senders/domain/list) 页面绑定域名,根据提示添加解析记录完成配置即可:
|
||||
|
||||

|
||||
|
||||
最后在本系统依次添加域名,配置完成如下所示:
|
||||
|
||||

|
||||
|
||||
## Resend
|
||||
|
||||
在本项目中,仅在邮件服务模块会使用 Resend 发送邮件。
|
||||
|
||||
#### 创建 API 密钥
|
||||
|
||||
登录 Resend 后,它会提示你创建第一个 API 密钥。
|
||||
|
||||
将其复制并粘贴到你的 `.env` 文件中:
|
||||
将其复制并粘贴,后续步骤与 Brevo 类似,需要先绑定域名并配置解析记录。
|
||||
|
||||
```js
|
||||
RESEND_API_KEY = re_your_resend_api_key;
|
||||
RESEND_FROM_EMAIL="you <support@your-domain.com>"
|
||||
````
|
||||
|
||||
</Steps>
|
||||
@@ -1,50 +1,68 @@
|
||||
---
|
||||
title: Email
|
||||
description: How to manage emails in this project.
|
||||
title: Email Configuration
|
||||
description: How to configure email services in your project
|
||||
---
|
||||
|
||||
<DocsLang en="/docs/developer/email" zh="/docs/developer/email-zh" />
|
||||
|
||||
In the WR.DO project, there are two features that rely on Resend,
|
||||
one is email login (magic link), and the other is email sending feature (if you need to receive email feature,
|
||||
please refer to /docs/developer/cloudflare-email-worker).
|
||||
<Callout type="warning" twClass="mt-4">
|
||||
The configuration of this module underwent significant changes after version v1.1.5. Please update your configuration accordingly:
|
||||
|
||||
The `RESEND_API_KEY` and `RESEND_SROM_SMAIL` configured in the `.env` file are used for login feature,
|
||||
while the Resend key required for email sending feature needs to be added by
|
||||
yourself in the domain configuration after logging into the admin panel (`/admin/domains`).
|
||||
|
||||
<Callout type="note">
|
||||
Two features can use the same key, as both essentially use Resend to send emails.
|
||||
- Removed `RESEND_API_KEY`, replaced with `BREVO_API_KEY` (earlier versions used Resend service by default. Due to the official account being banned by Resend, we had to switch service providers)
|
||||
- Changed default email service for system notifications from Resend to Brevo
|
||||
- `RESEND_FROM_EMAIL` replaced with `EMAIL_FROM`
|
||||
- New environment variable added: `EMAIL_FROM_NAME`
|
||||
</Callout>
|
||||
|
||||
The following will demonstrate how to configure the Resend key required for login.
|
||||
The WR.DO project's email service module has the capability to receive and send emails. This guide will introduce how to configure the email **sending** service in your project.
|
||||
|
||||
## Steps
|
||||
> If you need to configure email receiving functionality, please refer to the [cloudflare-email-worker](/docs/developer/cloudflare-email-worker) documentation.
|
||||
|
||||
<Callout type="note">
|
||||
The email part is similar at the [resend](https://resend.com/) documentation.
|
||||
You can find the official documentation
|
||||
[here](https://authjs.dev/getting-started/installation#setup-environment) if
|
||||
you want.
|
||||
</Callout>
|
||||
Currently supported email service providers:
|
||||
|
||||
<Steps>
|
||||
- [Resend](https://resend.com) (Free tier: up to 100 emails per day, supports binding 1 domain)
|
||||
- [Brevo](https://www.brevo.com) (Free tier: up to 300 emails per day, supports binding multiple domains)
|
||||
|
||||
### Create an account
|
||||
Additional email service providers will be integrated based on requirements.
|
||||
|
||||
If don't have an account on Resend, just follow their steps after signup [here](https://resend.com/signup).
|
||||
|
||||
> Resend's free account offers a daily email limit of 100 emails, bound to 1 domain name, which is sufficient for ordinary users.
|
||||
|
||||
### Create an API key
|
||||
|
||||
After signin on Resend, he propurse you to create your first API key.
|
||||
|
||||
Copy/paste in your `.env` file.
|
||||
## Environment Variables
|
||||
|
||||
```js
|
||||
RESEND_API_KEY = re_your_resend_api_key;
|
||||
RESEND_FROM_EMAIL="you <support@your-domain.com>"
|
||||
BREVO_API_KEY=your-brevo-api-key
|
||||
EMAIL_FROM=support@your-domain.com
|
||||
EMAIL_FROM_NAME=WR.DO
|
||||
```
|
||||
|
||||
</Steps>
|
||||
Register and log in to the Brevo console at [app.brevo.com/settings/keys/api](https://app.brevo.com/settings/keys/api) to create an API key. Copy and paste it into your environment variables.
|
||||
|
||||
Note: The `BREVO_API_KEY` configured here is used by default for the `subdomain application notification` feature and is not used for sending emails directly. You can also fill this key in the domain configuration below as the sender for the email service module.
|
||||
|
||||
## Brevo
|
||||
|
||||
In this project, Brevo is used to send emails in the following scenarios:
|
||||
|
||||
- Email service module (`/email`)
|
||||
- Subdomain application notification (in system settings, this feature is disabled by default)
|
||||
- Email verification functionality (under development `/dashboard/settings`)
|
||||
|
||||
Similarly, create an API key in the Brevo console, then go to the localhost:3000/admin/system page in your system. In the **Domain Management** section, click **Add Domain**, and fill in the corresponding API key in the **Email Service Provider** field and save:
|
||||
|
||||

|
||||
|
||||
After that, you can bind your domain on the [Domain](https://app.brevo.com/senders/domain/list) page in the Brevo console. Follow the prompts to add DNS records to complete the configuration:
|
||||
|
||||

|
||||
|
||||
Finally, add the domain in this system sequentially. The completed configuration will look like this:
|
||||
|
||||

|
||||
|
||||
## Resend
|
||||
|
||||
In this project, Resend is only used for sending emails through the email service module.
|
||||
|
||||
#### Creating an API Key
|
||||
|
||||
After logging in to Resend, it will prompt you to create your first API key.
|
||||
|
||||
Copy and paste it. The subsequent steps are similar to Brevo - you need to bind your domain first and configure DNS records.
|
||||
@@ -59,8 +59,9 @@ pnpm install
|
||||
| GOOGLE\_CLIENT\_SECRET | `123465` | Google OAuth 客户端的密钥。 |
|
||||
| GITHUB\_ID | `123465` | GitHub OAuth 客户端的 ID。 |
|
||||
| GITHUB\_SECRET | `123465` | GitHub OAuth 客户端的密钥。 |
|
||||
| RESEND\_API\_KEY | `123465` | Resend 的 API 密钥。 |
|
||||
| RESEND\_FROM\_EMAIL | `"you <support@your-domain.com>"` | 用于发送邮件的邮箱地址。 |
|
||||
| BREVO_API_KEY | `123465` | Brevo API key |
|
||||
| EMAIL_FROM | `support@your-domain.com` | 发件人邮箱地址. |
|
||||
| EMAIL_FROM_NAME | `WR.DO` | 发件人. |
|
||||
| SCREENSHOTONE\_BASE\_URL | `https://api.example.com` | 待补充 |
|
||||
| GITHUB\_TOKEN | `ghp_sscsfarwetqet` | [https://github.com/settings/tokens](https://github.com/settings/tokens) |
|
||||
| NEXT_PUBLIC_GOOGLE_ID | `G-EWREW323` | Google Analytics ID |
|
||||
|
||||
@@ -60,8 +60,9 @@ Copy/paste the `.env.example` in the `.env` file:
|
||||
| GOOGLE_CLIENT_SECRET | `123465` | The secret of the Google OAuth client. |
|
||||
| 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. |
|
||||
| RESEND_FROM_EMAIL | `"you <support@your-domain.com>"` | The email address to send emails from. |
|
||||
| BREVO_API_KEY | `123465` | The API key for Brevo. |
|
||||
| EMAIL_FROM | `"support@your-domain.com"` | The email address to send emails from. |
|
||||
| EMAIL_FROM_NAME | `"WRDO"` | The name to send emails from. |
|
||||
| SCREENSHOTONE_BASE_URL | `https://api.example.com` | The base URL of the screenshotone API. |
|
||||
| GITHUB_TOKEN | `ghp_sscsfarwetqet` | https://github.com/settings/tokens |
|
||||
| NEXT_PUBLIC_GOOGLE_ID | `G-EWREW323` | The ID of the Google Analytics. |
|
||||
|
||||
@@ -82,7 +82,8 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
* Google
|
||||
* Github
|
||||
* LinuxDo
|
||||
* Resend 邮件验证
|
||||
* Resend 邮件验证(1.1.5版本后已移除)
|
||||
* 邮箱密码登陆
|
||||
|
||||
### Google 配置
|
||||
|
||||
@@ -121,6 +122,10 @@ LinuxDo_CLIENT_SECRET=
|
||||
|
||||
### Resend 邮件验证配置
|
||||
|
||||
<Callout type="warning" twClass="mt-4">
|
||||
注意,此登陆方式已在 1.1.5 版本之后被移除,可忽略此配置,最新邮件相关配置请参考文档 [邮件配置](/docs/developer/email-zh)。
|
||||
</Callout>
|
||||
|
||||
<Callout type="note">
|
||||
邮件部分与 [resend](https://resend.com/) 的文档类似。
|
||||
如果你想了解详细配置,可以查阅官方文档:
|
||||
|
||||
@@ -77,7 +77,8 @@ We provide the following authentication services:
|
||||
- Google
|
||||
- Github
|
||||
- LinuxDo
|
||||
- Resend Email Verification
|
||||
- Resend Email Verification (Removed after version 1.1.5)
|
||||
- Email Password Login
|
||||
|
||||
### Google config
|
||||
|
||||
@@ -112,6 +113,11 @@ See config tutorial in [Connect LinuxDo](https://connect.linux.do).
|
||||
|
||||
### Resend Email Verification config
|
||||
|
||||
<Callout type="warning" twClass="mt-4">
|
||||
Please note that this login method has been removed after version 1.1.5 and can be ignored.
|
||||
For the latest email configuration, please refer to the document [Email Configuration](/docs/developer/email).
|
||||
</Callout>
|
||||
|
||||
<Callout type="note">
|
||||
The email part is similar at the [resend](https://resend.com/) documentation.
|
||||
You can find the official documentation
|
||||
|
||||
@@ -16,7 +16,9 @@ services:
|
||||
LinuxDo_CLIENT_ID: ${LinuxDo_CLIENT_ID}
|
||||
LinuxDo_CLIENT_SECRET: ${LinuxDo_CLIENT_SECRET}
|
||||
RESEND_API_KEY: ${RESEND_API_KEY}
|
||||
RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL}
|
||||
BREVO_API_KEY: ${BREVO_API_KEY}
|
||||
EMAIL_FROM: ${EMAIL_FROM}
|
||||
EMAIL_FROM_NAME: ${EMAIL_FROM_NAME}
|
||||
NEXT_PUBLIC_EMAIL_R2_DOMAIN: ${NEXT_PUBLIC_EMAIL_R2_DOMAIN}
|
||||
NEXT_PUBLIC_GOOGLE_ID: ${NEXT_PUBLIC_GOOGLE_ID}
|
||||
SCREENSHOTONE_BASE_URL: ${SCREENSHOTONE_BASE_URL}
|
||||
|
||||
@@ -17,7 +17,9 @@ services:
|
||||
LinuxDo_CLIENT_ID: ${LinuxDo_CLIENT_ID}
|
||||
LinuxDo_CLIENT_SECRET: ${LinuxDo_CLIENT_SECRET}
|
||||
RESEND_API_KEY: ${RESEND_API_KEY}
|
||||
RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL}
|
||||
BREVO_API_KEY: ${BREVO_API_KEY}
|
||||
EMAIL_FROM: ${EMAIL_FROM}
|
||||
EMAIL_FROM_NAME: ${EMAIL_FROM_NAME}
|
||||
NEXT_PUBLIC_EMAIL_R2_DOMAIN: ${NEXT_PUBLIC_EMAIL_R2_DOMAIN}
|
||||
NEXT_PUBLIC_GOOGLE_ID: ${NEXT_PUBLIC_GOOGLE_ID}
|
||||
SCREENSHOTONE_BASE_URL: ${SCREENSHOTONE_BASE_URL}
|
||||
|
||||
8
env.mjs
8
env.mjs
@@ -14,7 +14,9 @@ export const env = createEnv({
|
||||
LinuxDo_CLIENT_SECRET: z.string().optional(),
|
||||
DATABASE_URL: z.string().optional(),
|
||||
RESEND_API_KEY: z.string().optional(),
|
||||
RESEND_FROM_EMAIL: z.string().optional(),
|
||||
BREVO_API_KEY: z.string().optional(),
|
||||
EMAIL_FROM: z.string().optional(),
|
||||
EMAIL_FROM_NAME: z.string().optional(),
|
||||
SCREENSHOTONE_BASE_URL: z.string().optional(),
|
||||
GITHUB_TOKEN: z.string().optional(),
|
||||
},
|
||||
@@ -34,7 +36,9 @@ export const env = createEnv({
|
||||
GITHUB_SECRET: process.env.GITHUB_SECRET,
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
RESEND_API_KEY: process.env.RESEND_API_KEY,
|
||||
RESEND_FROM_EMAIL: process.env.RESEND_FROM_EMAIL,
|
||||
BREVO_API_KEY: process.env.BREVO_API_KEY,
|
||||
EMAIL_FROM: process.env.EMAIL_FROM,
|
||||
EMAIL_FROM_NAME: process.env.EMAIL_FROM_NAME,
|
||||
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
|
||||
NEXT_PUBLIC_EMAIL_R2_DOMAIN: process.env.NEXT_PUBLIC_EMAIL_R2_DOMAIN,
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL: process.env.NEXT_PUBLIC_SUPPORT_EMAIL,
|
||||
|
||||
@@ -16,7 +16,9 @@ export interface DomainConfig {
|
||||
cf_email: string | null;
|
||||
cf_record_types: string;
|
||||
cf_api_key_encrypted: boolean;
|
||||
email_provider: string;
|
||||
resend_api_key: string | null;
|
||||
brevo_api_key: string | null;
|
||||
min_url_length: number;
|
||||
min_email_length: number;
|
||||
min_record_length: number;
|
||||
@@ -118,26 +120,35 @@ export async function getDomainByName(domain_name: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function checkDomainIsConfiguratedResend(domain_name: string) {
|
||||
export async function checkDomainIsConfiguratedEmailProvider(
|
||||
domain_name: string,
|
||||
) {
|
||||
try {
|
||||
const domain = await prisma.domain.findUnique({
|
||||
where: { domain_name },
|
||||
select: {
|
||||
email_provider: true,
|
||||
resend_api_key: true,
|
||||
brevo_api_key: true,
|
||||
},
|
||||
});
|
||||
return domain?.resend_api_key;
|
||||
if (domain?.email_provider === "Resend")
|
||||
return { email_key: domain?.resend_api_key, provider: "Resend" };
|
||||
if (domain?.email_provider === "Brevo")
|
||||
return { email_key: domain?.brevo_api_key, provider: "Brevo" };
|
||||
return { email_key: null, provider: null };
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch domain config: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getConfiguredResendDomains() {
|
||||
export async function getConfiguredEmailDomains() {
|
||||
try {
|
||||
const domains = await prisma.domain.findMany({
|
||||
where: { resend_api_key: { not: null } },
|
||||
where: { email_provider: "Brevo" },
|
||||
select: {
|
||||
domain_name: true,
|
||||
brevo_api_key: true,
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
|
||||
126
lib/email/brevo.ts
Normal file
126
lib/email/brevo.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import * as brevo from "@getbrevo/brevo";
|
||||
|
||||
export interface EmailRecipient {
|
||||
email: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface EmailSender {
|
||||
email: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface SendEmailOptions {
|
||||
from?: string;
|
||||
fromName?: string;
|
||||
to: string | string[] | EmailRecipient | EmailRecipient[];
|
||||
subject: string;
|
||||
html: string;
|
||||
sender?: EmailSender;
|
||||
textContent?: string;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
export interface EmailResponse {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
error?: any;
|
||||
}
|
||||
|
||||
// 初始化 API 实例
|
||||
let apiInstance: brevo.TransactionalEmailsApi | null = null;
|
||||
|
||||
function getApiInstance(key?: string): brevo.TransactionalEmailsApi {
|
||||
if (!apiInstance) {
|
||||
const apiKey = key || process.env.BREVO_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("BREVO_API_KEY is not defined in environment variables");
|
||||
}
|
||||
|
||||
apiInstance = new brevo.TransactionalEmailsApi();
|
||||
apiInstance.setApiKey(brevo.TransactionalEmailsApiApiKeys.apiKey, apiKey);
|
||||
}
|
||||
return apiInstance;
|
||||
}
|
||||
|
||||
// 处理收件人格式
|
||||
function formatRecipients(
|
||||
to: string | string[] | EmailRecipient | EmailRecipient[],
|
||||
): EmailRecipient[] {
|
||||
if (typeof to === "string") {
|
||||
return [{ email: to, name: to.split("@")[0] }];
|
||||
}
|
||||
if (Array.isArray(to)) {
|
||||
return to.map((item) => {
|
||||
// 如果是字符串,转换为对象格式
|
||||
if (typeof item === "string") {
|
||||
return {
|
||||
email: item,
|
||||
name: item.includes("@") ? item.split("@")[0] : item,
|
||||
};
|
||||
}
|
||||
// 如果已经是对象格式,直接返回
|
||||
return item;
|
||||
});
|
||||
}
|
||||
return [to];
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送单封邮件
|
||||
* @param options - 邮件配置选项
|
||||
* @returns 返回发送结果的Promise
|
||||
*/
|
||||
export async function brevoSendEmail(
|
||||
options: SendEmailOptions,
|
||||
): Promise<EmailResponse> {
|
||||
const {
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
from,
|
||||
fromName,
|
||||
sender = {
|
||||
email: from || process.env.EMAIL_FROM || "service@wr.do",
|
||||
name: fromName || process.env.EMAIL_FROM_NAME || "WRDO",
|
||||
},
|
||||
textContent,
|
||||
key,
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const api = getApiInstance(key);
|
||||
const recipients = formatRecipients(to);
|
||||
|
||||
// 构建邮件对象
|
||||
const sendSmtpEmail = new brevo.SendSmtpEmail();
|
||||
sendSmtpEmail.to = recipients;
|
||||
sendSmtpEmail.sender = sender;
|
||||
sendSmtpEmail.subject = subject;
|
||||
sendSmtpEmail.htmlContent = html;
|
||||
|
||||
if (textContent) {
|
||||
sendSmtpEmail.textContent = textContent;
|
||||
}
|
||||
|
||||
const data = await api.sendTransacEmail(sendSmtpEmail);
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
console.error("Send email error:", error);
|
||||
return { success: false, error };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量发送邮件
|
||||
* @param emailList - 邮件列表
|
||||
* @returns 返回所有邮件发送结果
|
||||
*/
|
||||
export async function sendBatchEmails(
|
||||
emailList: SendEmailOptions[],
|
||||
): Promise<PromiseSettledResult<EmailResponse>[]> {
|
||||
const results = await Promise.allSettled(
|
||||
emailList.map((emailOptions) => brevoSendEmail(emailOptions)),
|
||||
);
|
||||
return results;
|
||||
}
|
||||
5
lib/email/resend.ts
Normal file
5
lib/email/resend.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Resend } from "resend";
|
||||
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
export const resend = new Resend(env.RESEND_API_KEY || "re_key");
|
||||
@@ -1,9 +1,3 @@
|
||||
import { Resend } from "resend";
|
||||
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
export const resend = new Resend(env.RESEND_API_KEY || "re_key");
|
||||
|
||||
export function getVerificationEmailHtml({
|
||||
url,
|
||||
appName,
|
||||
@@ -151,6 +145,7 @@ export function applyRecordEmailHtml({
|
||||
type,
|
||||
name,
|
||||
content,
|
||||
comment,
|
||||
}: {
|
||||
appUrl: string;
|
||||
appName: string;
|
||||
@@ -158,6 +153,7 @@ export function applyRecordEmailHtml({
|
||||
type: string;
|
||||
name: string;
|
||||
content: string;
|
||||
comment: string;
|
||||
}) {
|
||||
return `
|
||||
<html>
|
||||
@@ -233,6 +229,10 @@ export function applyRecordEmailHtml({
|
||||
<th>Content</th>
|
||||
<td>${content}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Comment</th>
|
||||
<td>${comment}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="button-container">
|
||||
@@ -11,7 +11,9 @@ export const createDomainSchema = z.object({
|
||||
cf_email: z.string().optional(),
|
||||
cf_record_types: z.string().optional(),
|
||||
cf_api_key_encrypted: z.boolean().default(false),
|
||||
email_provider: z.string().optional(),
|
||||
resend_api_key: z.string().optional(),
|
||||
brevo_api_key: z.string().optional(),
|
||||
max_short_links: z.number().optional(),
|
||||
max_email_forwards: z.number().optional(),
|
||||
max_dns_records: z.number().optional(),
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
"API Key": "API Key",
|
||||
"send email service": "send email service",
|
||||
"How to get resend api key?": "How to get resend api key?",
|
||||
"How to get brevo api key?": "How to get brevo api key?",
|
||||
"Analytics": "Analytics",
|
||||
"Edit URL": "Edit URL",
|
||||
"Plan Name": "Plan",
|
||||
@@ -228,7 +229,11 @@
|
||||
"Export": "Export",
|
||||
"Export All": "Export All",
|
||||
"Export Active": "Export Active",
|
||||
"Export Basic Info": "Export Basic Info"
|
||||
"Export Basic Info": "Export Basic Info",
|
||||
"Email Service Configs": "Email Service Configs",
|
||||
"Email Provider": "Email Provider",
|
||||
"Resend API Key": "Resend API Key",
|
||||
"Brevo API Key": "Brevo API Key"
|
||||
},
|
||||
"Components": {
|
||||
"Dashboard": "Dashboard",
|
||||
@@ -658,7 +663,7 @@
|
||||
"Max File Count": "Max File Count",
|
||||
"Password": "Password",
|
||||
"Email Forwarding": "Email Forwarding",
|
||||
"If enabled, forward all received emails to other platform email addresses (Send with Resend)": "If enabled, forward all received emails to other platform email addresses (Send with Resend)",
|
||||
"If enabled, forward all received emails to other platform email addresses (Send with Resend or Brevo)": "If enabled, forward all received emails to other platform email addresses (Send with Resend or Brevo)",
|
||||
"Forward Email Targets": "Forward Email Targets",
|
||||
"Set forward email address targets, split by comma if more than one, such as: 1@a-com,2@b-com, Only works when email forwarding is enabled": "Set forward email address targets, split by comma if more than one, such as: 1@a.com,2@b.com, Only works when email forwarding is enabled",
|
||||
"Email Forward White List": "Email Forward White List",
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
"API Key": "API 密钥",
|
||||
"send email service": "用于发送邮件服务",
|
||||
"How to get resend api key?": "如何获取 Resend API 密钥?",
|
||||
"How to get brevo api key?": "如何获取 Brevo API 密钥?",
|
||||
"Analytics": "访客分析",
|
||||
"Edit URL": "编辑短链",
|
||||
"Plan Name": "计划名称",
|
||||
@@ -228,7 +229,11 @@
|
||||
"Export": "导出",
|
||||
"Export All": "导出所有链接",
|
||||
"Export Active": "导出有效链接",
|
||||
"Export Basic Info": "导出基本信息"
|
||||
"Export Basic Info": "导出基本信息",
|
||||
"Email Service Configs": "邮件服务商",
|
||||
"Email Provider": "邮件提供商",
|
||||
"Resend API Key": "Resend 密钥",
|
||||
"Brevo API Key": "Brevo 密钥"
|
||||
},
|
||||
"Components": {
|
||||
"Dashboard": "用户面板",
|
||||
@@ -658,7 +663,7 @@
|
||||
"Save Modifications": "保存修改",
|
||||
"Password": "账户密码",
|
||||
"Email Forwarding": "邮件转发",
|
||||
"If enabled, forward all received emails to other platform email addresses (Send with Resend)": "如果启用,则将所有收到的电子邮件转发到其他平台邮箱 (使用 Resend 发送)",
|
||||
"If enabled, forward all received emails to other platform email addresses (Send with Resend or Brevo)": "如果启用,则将所有收到的电子邮件转发到其他平台邮箱 (使用 Resend 或 Brevo 发送)",
|
||||
"Forward Email Targets": "目标收件箱",
|
||||
"Set forward email address targets, split by comma if more than one, such as: 1@a-com,2@b-com, Only works when email forwarding is enabled": "设置转发目标邮箱,多个邮件地址请用逗号分隔,例如:1@a.com,2@b.com,仅在启用邮件转发时生效",
|
||||
"Email Forward White List": "转发邮箱白名单",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wr.do",
|
||||
"version": "1.1.5",
|
||||
"version": "1.1.6",
|
||||
"author": {
|
||||
"name": "oiov",
|
||||
"url": "https://github.com/oiov"
|
||||
@@ -27,6 +27,7 @@
|
||||
"@auth/prisma-adapter": "^2.4.1",
|
||||
"@aws-sdk/client-s3": "^3.840.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.840.0",
|
||||
"@getbrevo/brevo": "^3.0.1",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@mantine/hooks": "^8.0.1",
|
||||
"@prisma/client": "^5.17.0",
|
||||
@@ -68,6 +69,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/ms": "^2.1.0",
|
||||
"@types/next-pwa": "^5.6.9",
|
||||
"@types/nodemailer": "^7.0.2",
|
||||
"@types/three": "^0.176.0",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"@uiw/react-json-view": "2.0.0-alpha.26",
|
||||
@@ -103,7 +105,6 @@
|
||||
"next-pwa": "^5.6.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"next-view-transitions": "^0.3.0",
|
||||
"nodemailer": "^6.9.14",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"peerjs": "^1.5.4",
|
||||
"prop-types": "^15.8.1",
|
||||
|
||||
1018
pnpm-lock.yaml
generated
1018
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
4
prisma/migrations/20251015133210/migration.sql
Normal file
4
prisma/migrations/20251015133210/migration.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "domains" ADD COLUMN "brevo_api_key" TEXT;
|
||||
|
||||
ALTER TABLE "domains" ADD COLUMN "email_provider" TEXT NOT NULL DEFAULT 'Resend';
|
||||
@@ -265,7 +265,9 @@ model Domain {
|
||||
cf_email String?
|
||||
cf_record_types String @default("CNAME,A,TXT")
|
||||
cf_api_key_encrypted Boolean
|
||||
email_provider String @default("resend")
|
||||
resend_api_key String?
|
||||
brevo_api_key String?
|
||||
max_short_links Int?
|
||||
max_email_forwards Int?
|
||||
max_dns_records Int?
|
||||
|
||||
BIN
public/_static/docs/brevo-domain.png
Normal file
BIN
public/_static/docs/brevo-domain.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 303 KiB |
BIN
public/_static/docs/domain-form-email.png
Normal file
BIN
public/_static/docs/domain-form-email.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
BIN
public/_static/docs/domain-list.png
Normal file
BIN
public/_static/docs/domain-list.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
@@ -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.1.5",
|
||||
"versionName": "1.1.6",
|
||||
"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.1.5",
|
||||
"versionName": "1.1.6",
|
||||
"versionCode": "1",
|
||||
"start_url": "/",
|
||||
"orientation": "portrait",
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user