172 lines
4.4 KiB
TypeScript
172 lines
4.4 KiB
TypeScript
import { randomBytes } from "node:crypto";
|
|
import { spawn } from "node:child_process";
|
|
|
|
export type VerificationDeliveryMode = "fixed" | "email";
|
|
export type VerificationPurpose = "login" | "register" | "forgot-password";
|
|
|
|
export interface VerificationDeliveryResult {
|
|
delivered: boolean;
|
|
mode: VerificationDeliveryMode;
|
|
message: string;
|
|
status: number;
|
|
}
|
|
|
|
function purposeLabel(purpose: VerificationPurpose) {
|
|
switch (purpose) {
|
|
case "login":
|
|
return "登录";
|
|
case "register":
|
|
return "注册";
|
|
case "forgot-password":
|
|
return "重置密码";
|
|
default:
|
|
return "验证";
|
|
}
|
|
}
|
|
|
|
export function getVerificationDeliveryMode(): VerificationDeliveryMode {
|
|
return process.env.BOSS_AUTH_VERIFICATION_MODE === "email" ? "email" : "fixed";
|
|
}
|
|
|
|
export function getFixedVerificationCode() {
|
|
return process.env.BOSS_AUTH_FIXED_CODE?.trim() || "000000";
|
|
}
|
|
|
|
export function getVerificationDeliverySummary(code: string) {
|
|
if (getVerificationDeliveryMode() === "email") {
|
|
return "验证码邮件已发送,请检查邮箱。";
|
|
}
|
|
return `邮件验证码尚未切到真实投递,当前固定验证码为 ${code}。`;
|
|
}
|
|
|
|
function isLikelyEmailAccount(account: string) {
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(account);
|
|
}
|
|
|
|
function buildVerificationMessage({
|
|
account,
|
|
recipient,
|
|
purpose,
|
|
code,
|
|
}: {
|
|
account: string;
|
|
recipient: string;
|
|
purpose: VerificationPurpose;
|
|
code: string;
|
|
}) {
|
|
const domain = process.env.BOSS_MAIL_DOMAIN?.trim() || "boss.hyzq.net";
|
|
const fromAddress = process.env.BOSS_MAIL_FROM_ADDRESS?.trim() || `verify@${domain}`;
|
|
const fromName = process.env.BOSS_MAIL_FROM_NAME?.trim() || "Boss Verify";
|
|
const subject = `Boss ${purposeLabel(purpose)}验证码`;
|
|
const messageId = `<${Date.now()}.${randomBytes(4).toString("hex")}@${domain}>`;
|
|
const body = [
|
|
`你好,${account}`,
|
|
"",
|
|
recipient !== account ? `当前验证码投递到绑定邮箱:${recipient}` : undefined,
|
|
`你本次的 Boss ${purposeLabel(purpose)}验证码是:${code}`,
|
|
"验证码 5 分钟内有效。",
|
|
"",
|
|
"如果这不是你本人发起的操作,请忽略本邮件。",
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
|
|
const lines = [
|
|
`From: ${fromName} <${fromAddress}>`,
|
|
`To: ${recipient}`,
|
|
`Subject: ${subject}`,
|
|
`Date: ${new Date().toUTCString()}`,
|
|
`Message-ID: ${messageId}`,
|
|
"MIME-Version: 1.0",
|
|
"Content-Type: text/plain; charset=UTF-8",
|
|
"Content-Transfer-Encoding: 8bit",
|
|
"",
|
|
body,
|
|
"",
|
|
];
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
async function sendMail(message: string) {
|
|
const sendmailPath = process.env.BOSS_SENDMAIL_PATH?.trim() || "/usr/sbin/sendmail";
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
const child = spawn(sendmailPath, ["-t", "-i"], { stdio: ["pipe", "ignore", "pipe"] });
|
|
let stderr = "";
|
|
|
|
child.on("error", (error) => {
|
|
reject(error);
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk.toString("utf8");
|
|
});
|
|
child.on("close", (code) => {
|
|
if (code === 0) {
|
|
resolve();
|
|
return;
|
|
}
|
|
reject(new Error(stderr.trim() || `SENDMAIL_EXIT_${code ?? "UNKNOWN"}`));
|
|
});
|
|
|
|
child.stdin.end(message);
|
|
});
|
|
}
|
|
|
|
export async function deliverVerificationCode({
|
|
account,
|
|
recipient,
|
|
purpose,
|
|
code,
|
|
}: {
|
|
account: string;
|
|
recipient?: string | null;
|
|
purpose: VerificationPurpose;
|
|
code: string;
|
|
}): Promise<VerificationDeliveryResult> {
|
|
const mode = getVerificationDeliveryMode();
|
|
if (mode === "fixed") {
|
|
return {
|
|
delivered: true,
|
|
mode,
|
|
status: 200,
|
|
message: getVerificationDeliverySummary(code),
|
|
};
|
|
}
|
|
|
|
const finalRecipient = recipient?.trim() || account;
|
|
if (!isLikelyEmailAccount(finalRecipient)) {
|
|
return {
|
|
delivered: false,
|
|
mode,
|
|
status: 400,
|
|
message: "当前邮件验证码模式需要可接收验证码的邮箱账号或已绑定验证邮箱。",
|
|
};
|
|
}
|
|
|
|
try {
|
|
await sendMail(
|
|
buildVerificationMessage({
|
|
account,
|
|
recipient: finalRecipient,
|
|
purpose,
|
|
code,
|
|
}),
|
|
);
|
|
return {
|
|
delivered: true,
|
|
mode,
|
|
status: 200,
|
|
message: getVerificationDeliverySummary(code),
|
|
};
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "EMAIL_DELIVERY_FAILED";
|
|
return {
|
|
delivered: false,
|
|
mode,
|
|
status: 502,
|
|
message: `验证码邮件发送失败:${message}`,
|
|
};
|
|
}
|
|
}
|