บทที่ 7backend|12 KB
บทที่ 7: Email Service และ OTP
สารบัญเนื้อหา
บทที่ 7: Email Service และ OTP
🎯 เป้าหมายการเรียนรู้
หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:
- ส่ง email ด้วย Nodemailer
- สร้างระบบ OTP (One-Time Password)
- ออกแบบ HTML email templates
- จัดการ SMTP configuration
- Handle email errors
📚 ทฤษฎีและแนวคิด
Email Service คืออะไร?
Email Service ใช้สำหรับ:
- 📧 ส่ง OTP สำหรับ reset password
- ✉️ Welcome email
- 🔔 Notifications
- 📊 Reports
OTP (One-Time Password)
OTP คือรหัสใช้ครั้งเดียวสำหรับยืนยันตัวตน
คุณสมบัติ:
- ⏱️ หมดอายุ (5-10 นาที)
- 🔢 6 หลัก (ตัวเลข)
- 🔒 ใช้ได้ครั้งเดียว
- 🎲 Random generation
💻 โค้ดจริงจาก QUOM App
1. Email Service Setup
ไฟล์: backend/src/services/email.js
import nodemailer from "nodemailer";
let transporter = null;
function getTransporter() {
if (transporter) return transporter;
// Production: Use real SMTP
if (process.env.SMTP_HOST && process.env.SMTP_USER) {
transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || "587"),
secure: process.env.SMTP_SECURE === "true",
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
} else {
// Development: Log to console
console.warn("⚠️ No SMTP configured. Emails logged to console.");
transporter = nodemailer.createTransport({
streamTransport: true,
newline: "unix",
buffer: true,
});
}
return transporter;
}
อธิบายทีละบรรทัด (Imports):
import nodemailer from "nodemailer"- Library มาตรฐานสำหรับส่ง Email ใน Node.js (รองรับ Gmail, Outlook, AWS SES)
อธิบาย:
SMTP Configuration
{
host: "smtp.gmail.com", // SMTP server
port: 587, // Port (587 = TLS, 465 = SSL)
secure: false, // true for 465, false for 587
auth: {
user: "your-email@gmail.com",
pass: "your-app-password"
}
}
Environment Variables
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM="QUOM App <noreply@quom.app>"
2. Send OTP Email
export async function sendOTPEmail(email, code) {
try {
const transport = getTransporter();
const mailOptions = {
from: process.env.SMTP_FROM || '"QUOM App" <noreply@quom.app>',
to: email,
subject: "รหัส OTP สำหรับรีเซ็ตรหัสผ่าน - QUOM",
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: 'Prompt', sans-serif;
background: #f8fafc;
margin: 0;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.header {
background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%);
padding: 40px 20px;
text-align: center;
}
.header h1 {
color: white;
margin: 0;
font-size: 28px;
}
.content { padding: 40px 30px; }
.otp-box {
background: #EEF2FF;
border: 2px dashed #3B82F6;
border-radius: 12px;
padding: 30px;
text-align: center;
margin: 30px 0;
}
.otp-code {
font-size: 42px;
font-weight: 900;
color: #3B82F6;
letter-spacing: 8px;
font-family: 'Courier New', monospace;
}
.footer {
background: #f8fafc;
padding: 20px;
text-align: center;
color: #64748B;
font-size: 14px;
}
.warning {
background: #FEF2F2;
border-left: 4px solid #EF4444;
padding: 15px;
margin: 20px 0;
color: #991B1B;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔐 QUOM Password Reset</h1>
</div>
<div class="content">
<h2>รหัส OTP ของคุณ</h2>
<p>
คุณได้ขอรีเซ็ตรหัสผ่านสำหรับบัญชี QUOM
กรุณาใช้รหัส OTP ด้านล่าง:
</p>
<div class="otp-box">
<div class="otp-code">${code}</div>
<p>รหัสนี้จะหมดอายุใน <strong>5 นาที</strong></p>
</div>
<div class="warning">
<strong>⚠️ คำเตือน:</strong>
หากคุณไม่ได้ขอรีเซ็ตรหัสผ่าน กรุณาเพิกเฉยต่ออีเมลนี้
</div>
<p>เพื่อความปลอดภัย อย่าแชร์รหัส OTP นี้กับผู้อื่น</p>
</div>
<div class="footer">
<p>© 2026 QUOM App. All rights reserved.</p>
<p>This is an automated message, please do not reply.</p>
</div>
</div>
</body>
</html>
`,
text: `
รหัส OTP สำหรับรีเซ็ตรหัสผ่าน QUOM
รหัส OTP ของคุณคือ: ${code}
รหัสนี้จะหมดอายุใน 5 นาที
หากคุณไม่ได้ขอรีเซ็ตรหัสผ่าน กรุณาเพิกเฉยต่ออีเมลนี้
© 2026 QUOM App
`,
};
const info = await transport.sendMail(mailOptions);
// Development mode logging
if (!process.env.SMTP_HOST) {
console.log("📧 OTP Email (DEV MODE):");
console.log("To:", email);
console.log("OTP Code:", code);
} else {
console.log("✅ OTP email sent to:", email);
}
return true;
} catch (error) {
console.error("❌ Failed to send OTP email:", error);
throw new Error("Failed to send OTP email");
}
}
อธิบาย:
HTML Email Template
- Inline CSS: Email clients ไม่รองรับ
<style>tag - Table Layout: ใช้
<table>สำหรับ layout (เก่าแต่ทำงานได้ทุกที่) - Fallback Text: มี plain text version
OTP Display
<div class="otp-code">${code}</div>
- ฟอนต์ใหญ่ (42px)
- Letter spacing (8px)
- Monospace font
3. OTP Generation
function generateOTP() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
// ตัวอย่าง: "123456", "789012"
อธิบาย:
Math.random(): 0.0 - 0.999999* 900000: 0 - 899999+ 100000: 100000 - 999999Math.floor(): ตัดทศนิยม.toString(): แปลงเป็น string
4. OTP Storage (In-Memory)
// Simple in-memory store
const otpStore = new Map(); // email -> { code, expiresAt }
function saveOTP(email, code) {
const expiresAt = Date.now() + 5 * 60 * 1000; // 5 minutes
otpStore.set(email, { code, expiresAt });
}
function verifyOTP(email, code) {
const stored = otpStore.get(email);
if (!stored) {
return { valid: false, reason: "OTP not found" };
}
if (Date.now() > stored.expiresAt) {
otpStore.delete(email);
return { valid: false, reason: "OTP expired" };
}
if (stored.code !== code) {
return { valid: false, reason: "Invalid OTP" };
}
// Valid - delete after use
otpStore.delete(email);
return { valid: true };
}
5. Forgot Password Flow
// Step 1: Request OTP
authRouter.post("/forgot-password", async (req, res) => {
const { email } = req.body;
const user = await User.findOne({ email });
if (!user) {
// Don't reveal if email exists
return res.json({ ok: true, message: "OTP sent if email exists" });
}
const code = generateOTP();
saveOTP(email, code);
await sendOTPEmail(email, code);
res.json({ ok: true, message: "OTP sent" });
});
// Step 2: Verify OTP & Reset Password
authRouter.post("/reset-password", async (req, res) => {
const { email, otp, newPassword } = req.body;
const result = verifyOTP(email, otp);
if (!result.valid) {
return res.status(400).json({ ok: false, message: result.reason });
}
const user = await User.findOne({ email });
if (!user) {
return res.status(404).json({ ok: false, message: "User not found" });
}
user.passwordHash = await bcrypt.hash(newPassword, 10);
await user.save();
res.json({ ok: true, message: "Password reset successful" });
});
🔒 Security Best Practices
1. Rate Limiting
// Limit OTP requests
const otpRequests = new Map(); // email -> count
function canRequestOTP(email) {
const count = otpRequests.get(email) || 0;
if (count >= 3) {
return false; // Max 3 requests per hour
}
otpRequests.set(email, count + 1);
setTimeout(() => otpRequests.delete(email), 60 * 60 * 1000);
return true;
}
2. Don't Reveal Email Existence
// ✅ ดี
return res.json({ message: "OTP sent if email exists" });
// ❌ อันตราย
if (!user) {
return res.json({ message: "Email not found" });
}
3. Short Expiration
// ✅ ดี - 5 minutes
const expiresAt = Date.now() + 5 * 60 * 1000;
// ❌ อันตราย - 1 hour
const expiresAt = Date.now() + 60 * 60 * 1000;
📝 แบบฝึกหัด
โจทย์ที่ 1: Welcome Email
// TODO: ส่ง welcome email เมื่อ register
// - มี username
// - มีลิงก์ไปยังแอป
โจทย์ที่ 2: Database OTP Storage
// TODO: เก็บ OTP ใน MongoDB แทน in-memory
// - สร้าง OTP model
// - Auto-delete หมดอายุ (TTL index)
โจทย์ที่ 3: Email Queue
// TODO: ใช้ Bull Queue สำหรับส่ง email
// - ส่งแบบ async
// - Retry ถ้า fail
📌 สรุป
✅ Nodemailer - ส่ง email
✅ SMTP - Configuration
✅ OTP - Generation และ verification
✅ HTML Templates - สวยงาม responsive
✅ Security - Rate limiting, expiration
ไฟล์ที่เกี่ยวข้อง
🔜 บทถัดไป
บทที่ 8: Middleware และ Error Handling