feat(auth): 添加忘记密码功能

- 添加忘记密码页面,支持通过邮箱重置密码
- 添加重置密码页面
- 登录页面添加忘记密码链接
- 添加邮件发送功能
- 完善所有8种语言的翻译 (en-US, zh-CN, ja-JP, ko-KR, de-DE, fr-FR, it-IT, ug-CN)
This commit is contained in:
2026-03-09 20:45:18 +08:00
parent d2a3d32376
commit 5406543cbe
15 changed files with 558 additions and 19 deletions

104
src/lib/email.ts Normal file
View File

@@ -0,0 +1,104 @@
import nodemailer from "nodemailer";
import { createLogger } from "@/lib/logger";
const log = createLogger("email");
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT) || 587,
secure: process.env.SMTP_SECURE === "true",
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
interface SendEmailOptions {
to: string;
subject: string;
html: string;
text?: string;
}
export async function sendEmail({ to, subject, html, text }: SendEmailOptions) {
try {
const info = await transporter.sendMail({
from: process.env.SMTP_FROM || process.env.SMTP_USER,
to,
subject,
html,
text,
});
log.info("Email sent", { to, subject, messageId: info.messageId });
return { success: true, messageId: info.messageId };
} catch (error) {
log.error("Failed to send email", { to, subject, error });
return { success: false, error };
}
}
export function generateVerificationEmailHtml(url: string, userName: string) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.button { display: inline-block; padding: 12px 24px; background-color: #3b82f6; color: white; text-decoration: none; border-radius: 6px; }
.footer { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px; }
</style>
</head>
<body>
<div class="container">
<h1>验证您的邮箱地址</h1>
<p>您好,${userName}</p>
<p>感谢您注册。请点击下方按钮验证您的邮箱地址:</p>
<p>
<a href="${url}" class="button">验证邮箱</a>
</p>
<p>或者复制以下链接到浏览器:</p>
<p style="word-break: break-all; color: #666;">${url}</p>
<p>此链接将在 24 小时后过期。</p>
<div class="footer">
<p>如果您没有注册此账户,请忽略此邮件。</p>
</div>
</div>
</body>
</html>
`;
}
export function generateResetPasswordEmailHtml(url: string, userName: string) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.button { display: inline-block; padding: 12px 24px; background-color: #3b82f6; color: white; text-decoration: none; border-radius: 6px; }
.footer { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px; }
</style>
</head>
<body>
<div class="container">
<h1>重置您的密码</h1>
<p>您好,${userName}</p>
<p>我们收到了重置您账户密码的请求。请点击下方按钮设置新密码:</p>
<p>
<a href="${url}" class="button">重置密码</a>
</p>
<p>或者复制以下链接到浏览器:</p>
<p style="word-break: break-all; color: #666;">${url}</p>
<p>此链接将在 1 小时后过期。</p>
<div class="footer">
<p>如果您没有请求重置密码,请忽略此邮件,您的密码不会被更改。</p>
</div>
</div>
</body>
</html>
`;
}