-ต้นแบบ โครงสร้าง ไฟล์ API เส้น /api/ttc

This commit is contained in:
2025-11-17 09:03:36 +07:00
parent eefbb8e5dd
commit 9f9c9aa80d
25 changed files with 1019 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
// utils/errorList.js
export function manualError(key) {
switch (key) {
case "invalid_input":
return {
code: 400,
messageTh: "ข้อมูลที่ส่งมาไม่ถูกต้อง",
messageEn: "Invalid input data"
};
case "not_found":
return {
code: 404,
messageTh: "ไม่พบข้อมูลที่ร้องขอ",
messageEn: "Resource not found"
};
case "unauthorized":
return {
code: 401,
messageTh: "คุณไม่มีสิทธิ์เข้าถึงข้อมูลนี้",
messageEn: "Unauthorized access"
};
case "server_error":
return {
code: 500,
messageTh: "เกิดข้อผิดพลาดภายในระบบ",
messageEn: "Internal server error"
};
default:
return {
code: 500,
messageTh: "ข้อผิดพลาดที่ไม่ทราบสาเหตุ",
messageEn: "Unknown error occurred"
};
}
}

View File

@@ -0,0 +1,62 @@
import nodemailer from 'nodemailer'
import dotenv from 'dotenv'
dotenv.config()
export async function sendMockOtpMail(to, otp) {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
})
const html = `
<div style="
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #f6f9fc 0%, #ecf3f9 100%);
padding: 48px 24px;
text-align: center;
color: #1a1f36;
">
<div style="
max-width: 480px;
max-height: auto;
margin: 0 auto;
background-color: white;
border-radius: 16px;
padding: 40px;
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
">
<img src="https://cdn.discordapp.com/attachments/1416337856988971152/1431895137595822152/TTCLOGO-Photoroom.png?ex=68ff13c4&is=68fdc244&hm=5af0596e08b3b8a97dcdcca3d6a00d68a1081e6d642c033a4a1cbf8d03e660a6&" alt="Logo" style="height: 80px; margin-bottom: 24px;">
<h2 style="
color: #1a1f36;
font-size: 24px;
font-weight: 600;
margin-bottom: 16px;
">รหัส OTP สำหรับเปลี่ยนรหัสผ่าน</h2>
<p style="color: #4f566b; font-size: 16px; line-height: 1.5;">กรุณาใส่รหัสยืนยันด้านล่างนี้</p>
<div style="
font-size: 32px;
letter-spacing: 8px;
background: linear-gradient(135deg, #fa0000ff 0%, #ff5100ff 100%);
color: white;
padding: 16px 32px;
margin: 24px 0;
border-radius: 12px;
font-weight: 600;
">${otp}</div>
<p style="color: #4f566b; font-size: 14px; margin: 24px 0;">รหัสนี้จะหมดอายุใน 5 นาที</p>
<hr style="border: none; border-top: 1px solid #e6e8eb; margin: 24px 0;">
<p style="color: #697386; font-size: 13px;">หากคุณไม่ได้ร้องขอการเปลี่ยนรหัสผ่าน กรุณาละเว้นอีเมลนี้</p>
</div>
</div>
`
await transporter.sendMail({
from: `"Support" <${process.env.SMTP_USER}>`,
to,
subject: 'OTP สำหรับเปลี่ยนรหัสผ่าน',
html
})
}

View File

@@ -0,0 +1,41 @@
// utils/oftenError.js
import { manualError } from "./errorList.js";
export class OftenError extends Error {
/**
* ใช้ได้ 2 แบบ:
* 1. throw new OftenError("not_found")
* 2. throw new OftenError(400, "ไทย", "English")
*/
constructor(arg1, arg2, arg3) {
// แบบ lookup จาก key
if (typeof arg1 === "string" && !arg2 && !arg3) {
const found = manualError(arg1);
super(found.messageEn);
this.statusCode = found.code;
this.messageTh = found.messageTh;
this.messageEn = found.messageEn;
this.key = arg1;
}
// แบบ manual
else if (typeof arg1 === "number" && arg2 && arg3) {
super(arg3);
this.statusCode = arg1;
this.messageTh = arg2;
this.messageEn = arg3;
this.key = "manual";
}
// fallback
else {
super("Invalid error format");
this.statusCode = 500;
this.messageTh = "รูปแบบการสร้าง error ไม่ถูกต้อง";
this.messageEn = "Invalid error constructor format";
this.key = "invalid_format";
}
this.name = "OftenError";
}
}

View File

@@ -0,0 +1,3 @@
export function generateOTP(length = 6) {
return Math.floor(100000 + Math.random() * 900000).toString()
}

View File

@@ -0,0 +1,28 @@
import Redis from 'ioredis'
import dotenv from 'dotenv'
dotenv.config()
const redis = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
})
export async function saveOtp(email, otp) {
const key = `otp:${email}`
const ttl = parseInt(process.env.OTP_TTL_SECONDS || '300')
await redis.setex(key, ttl, otp)
}
export async function verifyOtp(email, otp) {
const key = `otp:${email}`
const stored = await redis.get(key)
if (!stored) return false
return stored === otp
}
export async function removeOtp(email) {
const key = `otp:${email}`
await redis.del(key)
}
export default redis

View File

@@ -0,0 +1,24 @@
// ===================================================
// ⚙️ Nuttakit Response Layer vFinal++++++
// ===================================================
export function sendError(thMsg = 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', enMsg = 'Unexpected error', code = 400) {
return {
code: String(code),
message: enMsg,
message_th: thMsg,
data: []
}
}
// ===================================================
// 🔹 Auto Success Response (ใช้โดย Global Handler เท่านั้น)
// ===================================================
export function formatSuccessResponse(data) {
return {
code: "200",
message: "successful",
message_th: "ดำเนินการสำเร็จ",
data: data || null
}
}

View File

@@ -0,0 +1,16 @@
import jwt from 'jsonwebtoken'
import dotenv from 'dotenv'
dotenv.config()
export function generateToken(payload) {
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '24h' })
}
export function verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET)
} catch (err) {
console.error("❌ JWT verify error:", err.message);
return null
}
}

View File

@@ -0,0 +1,11 @@
export function trim_all_array(data) {
if (!Array.isArray(data)) return data
for (let row of data) {
for (let key in row) {
if (typeof row[key] === 'string') {
row[key] = row[key].trim()
}
}
}
return data
}