-template

This commit is contained in:
2025-11-17 08:59:59 +07:00
parent 1ac5084043
commit eefbb8e5dd
19 changed files with 694 additions and 128 deletions

View File

@@ -1,19 +1,28 @@
#project
PJ_NAME=exthernal-mobile-api
PJ_NAME=exthernal-template-api
# database
PG_HOST=localhost
PG_USER=postgres
PG_PASS=1234
PG_DB=postgres
PG_PASS=123456
PG_DB=ttc
PG_PORT=5432
# EMAIL
SMTP_USER=lalisakuty@gmail.com
SMTP_PASS=lurl pckw qugk tzob
# REDIS
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
OTP_TTL_SECONDS=300
# JWT-TOKENS
JWT_SECRET=MY_SUPER_SECRET
JWT_SECRET=5b8273b2f79602e6b3987d3a9b018c66fd15e14848ff73ab1d332942c11eac80
# DEV_HINT
DEVHINT=true
DEVHINT_LEVEL=3
#PORT
PORT=4000
PORT=1000

View File

@@ -2,30 +2,30 @@ import express from 'express'
import cors from 'cors'
import dotenv from 'dotenv'
import router from './routes/route.js'
import { validateJsonFormat } from './middlewares/validate.js'
import { globalResponseHandler } from './middlewares/responseHandler.js'
dotenv.config()
const app = express()
app.use(cors())
// ✅ ตรวจจับ JSON format error ก่อน parser
app.use(express.json({ limit: '10mb' }))
app.use(validateJsonFormat)
app.use('/api', router)
// middleware จัดการ error กลาง
app.use(globalResponseHandler);
app.use((err, req, res, next) => {
if (err instanceof OftenError) {
res.status(err.statusCode).json({
type: err.type,
messageTh: err.messageTh,
messageEn: err.messageEn
});
} else {
res.status(500).json({ message: "Unexpected error" });
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
console.error('🟥 Invalid JSON Received:', err.message)
return res.status(400).json({
code: "400",
message: "Invalid JSON format",
message_th: "โครงสร้าง JSON ไม่ถูกต้อง",
data: []
})
}
});
next()
})
app.use('/api/template', router)
app.listen(process.env.PORT, () => {
console.log(`${process.env.PJ_NAME} running on port ${process.env.PORT}`)

View File

@@ -0,0 +1,45 @@
import { AccountingSearchService } from '../services/accountingSearchService.js'
import { sendError } from '../utils/response.js'
// import { OftenError } from '../utils/oftenError.js'
import { GeneralService } from '../share/generalservice.js';
import { trim_all_array } from '../utils/trim.js'
import { verifyToken, generateToken } from '../utils/token.js'
export class accountingSearch {
constructor() {
this.generalService = new GeneralService();
this.accountingSearchService = new AccountingSearchService();
}
async onNavigate(req, res) {
this.generalService.devhint(1, 'accountingSearch.js', 'onNavigate() start');
let organization = req.body.organization;
const prommis = await this.onAccountingSearch(req, res, organization);
return prommis;
}
async onAccountingSearch(req, res, database) {
let idx = -1
let aryResult = []
try {
// let username = req.body.request.username;
// let password = req.body.request.password;
let token = req.body.request.token;
const decoded = verifyToken(token);
let id = decoded.id
let username = decoded.name
database = decoded.organization
aryResult = await this.accountingSearchService.getAccountingSearch(database, id, username); // เช็คกับ db กลาง ส่ง jwttoken ออกมา
// this.generalService.devhint(1, 'accountingSearch.js', 'Login success');
} catch (error) {
idx = 1;
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
if (!aryResult) return sendError('ไม่พบการมีอยู่ของข้อมูล', 'Cannot Find Any Data');
return aryResult
}
}
}

View File

@@ -0,0 +1,54 @@
import { AccountingSetupService } from '../services/accountingSetupService.js'
import { sendError } from '../utils/response.js'
// import { OftenError } from '../utils/oftenError.js'
import { GeneralService } from '../share/generalservice.js';
import { trim_all_array } from '../utils/trim.js'
import { verifyToken, generateToken } from '../utils/token.js'
export class accountingSetup {
constructor() {
this.generalService = new GeneralService();
this.AccountingSetupService = new AccountingSetupService();
}
async onNavigate(req, res) {
this.generalService.devhint(1, 'accountingSetup.js', 'onNavigate() start');
let organization = req.body.organization;
const prommis = await this.onAccountingSetup(req, res, organization);
return prommis;
}
async onAccountingSetup(req, res, database) {
let idx = -1
let result = []
try {
// let username = req.body.request.username;
// let password = req.body.request.password;
let token = req.body.request.token;
const decoded = verifyToken(token);
database = decoded.organization
result = await this.AccountingSetupService.getAccountingSetup(database); // เช็คกับ db กลาง ส่ง jwttoken ออกมา
// this.generalService.devhint(1, 'accountingSetup.js', 'Login success');
} catch (error) {
idx = 1;
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
if (!result) return sendError('ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'Invalid credentials');
// แยกกลุ่ม income / expense
let income = result.filter(item => item.dtltblcod === 'ACTCAT_INC').map(({ dtltblcod, ...rest }) => rest);
let expense = result.filter(item => item.dtltblcod === 'ACTCAT_EXP').map(({ dtltblcod, ...rest }) => rest);
let arydiy = {
income ,
expense
};
return arydiy
}
}
}

View File

@@ -0,0 +1,142 @@
import { AccountingSumService } from '../services/accountingSumService.js'
import { sendError } from '../utils/response.js'
import { GeneralService } from '../share/generalservice.js';
import { trim_all_array } from '../utils/trim.js'
import { verifyToken, generateToken } from '../utils/token.js'
export class accountingSum {
constructor() {
this.generalService = new GeneralService();
this.accountingSumService = new AccountingSumService();
}
async onNavigate(req, res) {
this.generalService.devhint(1, 'AccountingSum.js', 'onNavigate() start');
let organization = req.body.organization;
const prommis = await this.onAccountingSum(req, res, organization);
return prommis;
}
async onAccountingSum(req, res, database) {
let idx = -1
let result = []
var aryResult
try {
let token = req.body.request.token;
const decoded = verifyToken(token);
let id = decoded.id
let username = decoded.name
database = decoded.organization
result = await this.accountingSumService.getAccountingSum(database, id); // เช็คกับ db กลาง ส่ง jwttoken ออกมา
// if(result){
// if(result.acttyp == 'e'){
// // (ยังไม่มีการใช้งาน)
// }
// }
} catch (error) {
idx = 1;
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
if (!result) return sendError('ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'Invalid credentials');
try {
// ✅ 1) เตรียม data สำหรับใช้คำนวณ
// ถ้า service คืนมาเป็น { code, message, data: [...] }
const data = Array.isArray(result)
? result
: Array.isArray(result.data)
? result.data
: [];
// ถ้าไม่มีข้อมูลก็ไม่ต้องคำนวณ
if (!data.length) {
return result;
}
// ✅ 2) แยก income / expense
const incomeList = data.filter(i => i.acttyp === 'i');
const expenseList = data.filter(e => e.acttyp === 'e');
const totalIncome = incomeList.reduce((sum, i) => sum + parseFloat(i.actqty || 0), 0);
const totalExpense = expenseList.reduce((sum, e) => sum + parseFloat(e.actqty || 0), 0);
const netProfit = totalIncome - totalExpense;
const profitRate = totalIncome > 0 ? (netProfit / totalIncome) * 100 : 0;
const adjustedProfitRate = profitRate + 1.9;
// ✅ 3) แนบ summary (เหมือนที่เราทำไปก่อนหน้า)
var summary = {
totalIncome: totalIncome.toFixed(2),
totalExpense: totalExpense.toFixed(2),
netProfit: netProfit.toFixed(2),
profitRate: profitRate.toFixed(2) + ' %',
adjustedProfitRate: adjustedProfitRate.toFixed(2) + ' %',
period: '30 วัน'
};
// ✅ 4) ดึงสีจาก dtlmst (แนะนำให้เรียกจาก service เพิ่ม)
// ตัวอย่างสมมติ: คุณไป query มาจาก service ก่อนหน้าแล้วได้เป็น object แบบนี้
// key = ชื่อหมวด (actcatnam หรือ code), value = color
const categoryColorMap = await this.accountingSumService.getCategoryColorMap(database);
// ตัวอย่างที่คาดหวังจาก service:
// { 'ค่าอาหาร': '#FF6384', 'ค่าเดินทาง': '#36A2EB', 'ขายสินค้า': '#4BC0C0', ... }
// ✅ 5) สรุปยอดตามหมวด แล้วคำนวณ % สำหรับ expense
const expenseAgg = {};
expenseList.forEach(row => {
const key = row.actcatnam; // หรือใช้รหัส category ถ้ามี เช่น row.actcatcod
const amount = parseFloat(row.actqty || 0);
expenseAgg[key] = (expenseAgg[key] || 0) + amount;
});
const incomeAgg = {};
incomeList.forEach(row => {
const key = row.actcatnam;
const amount = parseFloat(row.actqty || 0);
incomeAgg[key] = (incomeAgg[key] || 0) + amount;
});
const expensePie = Object.entries(expenseAgg).map(([cat, value]) => {
const percent = totalExpense > 0 ? (value / totalExpense) * 100 : 0;
const color = categoryColorMap[cat] || '#CCCCCC'; // fallback สี default
return {
label: cat,
value: value.toFixed(2),
percent: percent.toFixed(2),
color
};
});
const incomePie = Object.entries(incomeAgg).map(([cat, value]) => {
const percent = totalIncome > 0 ? (value / totalIncome) * 100 : 0;
const color = categoryColorMap[cat] || '#CCCCCC';
return {
label: cat,
value: value.toFixed(2),
percent: percent.toFixed(2),
color
};
});
// ✅ 6) แนบข้อมูล pie chart เข้า result
var pie = {
expense: expensePie,
income: incomePie
};
} catch (err) {
console.error('calculate summary/pie error:', err);
}
let arydiy = {
summary,
pie
}
return arydiy;
}
}
}

View File

@@ -0,0 +1,14 @@
import { verifyToken } from '../utils/token.js'
import { sendError } from '../utils/response.js'
export function authMiddleware(req, res, next) {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (!token) return sendError('ไม่พบ Token', 'Missing token', 401)
const decoded = verifyToken(token)
if (!decoded) return sendError('Token ไม่ถูกต้อง', 'Invalid token', 403)
req.user = decoded
next()
}

View File

@@ -0,0 +1,20 @@
import { formatSuccessResponse } from '../utils/response.js'
export function globalResponseHandler(req, res, next) {
const oldJson = res.json.bind(res)
res.json = (data) => {
if (!data) return oldJson(formatSuccessResponse(null))
// ถ้า code ไม่ใช่ 200 → ตั้ง HTTP status ให้ตรงกับ code
if (data?.code && String(data.code) !== '200') {
res.status(Number(data.code) || 400)
return oldJson(data)
}
res.status(200)
return oldJson(formatSuccessResponse(data))
}
next()
}

View File

@@ -1,4 +1,4 @@
import { sendResponse } from '../utils/response.js'
import { sendError } from '../utils/response.js'
/**
* ✅ Middleware สำหรับตรวจสอบความถูกต้องของ JSON body
@@ -7,7 +7,7 @@ import { sendResponse } from '../utils/response.js'
export function validateJsonFormat(err, req, res, next) {
if (err instanceof SyntaxError && 'body' in err) {
console.error('[Invalid JSON Format]', err.message)
return sendResponse(res, 400, 'รูปแบบ บอร์ดี้ ไม่ถูกต้อง', 'Invalid Body format')
return sendError('รูปแบบ บอร์ดี้ ไม่ถูกต้อง', 'Invalid Body format')
}
next()
}

View File

@@ -0,0 +1,37 @@
import Redis from 'ioredis';
import { GeneralService } from '../share/generalservice.js';
// import { sendError } from './response.js';
export async function verifyEmailHandler(req, res) {
const redis = new Redis();
const generalService = new GeneralService();
try {
const { email, token } = req.query;
const schema = req.body?.organization || 'nuttakit'; // 🧩 ใช้ schema ตาม org
const storedData = await redis.get(`verify:${email}`);
if (!storedData) {
return res.status(400).send('ลิงก์หมดอายุหรือไม่ถูกต้อง');
}
const { fname, lname, hashedPwd, token: storedToken } = JSON.parse(storedData);
if (token !== storedToken) {
return res.status(400).send('Token ไม่ถูกต้อง');
}
let sql = `
INSERT INTO ${schema}.usrmst (usrnam, usrthinam, usrthilstnam, usrpwd, usrrol)
VALUES ($1, $2, $3, $4, 'U')
`;
let param = [email, fname, lname, hashedPwd];
await generalService.executeQueryParam(sql, param);
await redis.del(`verify:${email}`);
res.send(`<h2>✅ ยืนยันอีเมลสำเร็จ บัญชีของคุณถูกสร้างแล้ว (${schema})</h2>`);
} catch (error) {
console.error('❌ [Verify Email Error]', error);
res.status(500).send('เกิดข้อผิดพลาดในระบบ');
}
}

View File

@@ -1,14 +1,59 @@
import express from 'express'
import { accountingSetup } from '../controllers/accountingSetup.js'
import { accountingSearch } from '../controllers/accountingSearch.js'
import { accountingSum } from '../controllers/accountingSum.js'
// import { authMiddleware } from '../middlewares/auth.js'
// import { sendResponse } from '../utils/response.js'
const router = express.Router()
const controller_accountingSetup_post = new accountingSetup()
const controller_accountingSearch_post = new accountingSearch()
const controller_accountingSum_post = new accountingSum()
router.post('/accountingSetup', async (req, res) => {
const result = await controller_accountingSetup_post.onNavigate(req, res)
if (result) return res.json(result)
})
// import { userController } from '../controllers/userController.js'
///////////////////////////////////////////////////////////////////////////
// const controller_user_post = userController()
router.post('/accountingSearch', async (req, res) => {
const result = await controller_accountingSearch_post.onNavigate(req, res)
if (result) return res.json(result)
})
///////////////////////////////////////////////////////////////////////////
// router.post('/user', async (req, res) => { const data = await controller_user_post.onNavigate(req, res); if (data) return sendResponse(res, 200, null, null, data)})
router.post('/accountingSum', async (req, res) => {
const result = await controller_accountingSum_post.onNavigate(req, res)
if (result) return res.json(result)
})
// // ===================================================
// // 🔹 BIOMETRIC LOGIN
// // ===================================================
// router.post('/biometric/login', async (req, res) => {
// const data = await controller_login_post.onBiometricLogin(req, res)
// if (data)
// return sendResponse(res, 200, 'เข้าสู่ระบบผ่าน Biometric สำเร็จ', 'Biometric login succeed', data)
// })
// // ===================================================
// // 🔹 BIOMETRIC REGISTER (ต้อง login ก่อน)
// // ===================================================
// router.post('/biometric/register', authMiddleware, async (req, res) => {
// const data = await controller_login_post.onBiometricRegister(req, res)
// if (data)
// return sendResponse(res, 200, 'ผูก Biometric สำเร็จ', 'Biometric registered', data)
// })
// // ===================================================
// // 🔹 TOKEN RENEW (ต่ออายุ Token)
// // ===================================================
// router.post('/token/renew', authMiddleware, async (req, res) => {
// const data = await controller_login_post.onRenewToken(req, res)
// if (data)
// return sendResponse(res, 200, 'ออก Token ใหม่สำเร็จ', 'Token renewed', data)
// })
export default router

View File

@@ -0,0 +1,27 @@
import { GeneralService } from '../share/generalservice.js'
export class AccountingSearchService {
constructor() {
this.generalService = new GeneralService()
}
async getAccountingSearch(database, id, username) {
const sql = `
SELECT
actseq,
actnum,
acttyp,
${database}.translatedtl('ACTTYP', acttyp) as acttypnam,
${database}.translatedtl_multi(ARRAY['ACTCAT_INC', 'ACTCAT_EXP'], actcat) as actcatnam,
actqty,
actcmt,
actacpdtm
FROM ${database}.actmst
WHERE actnum = $1
`
const params = [id]
const result = await this.generalService.executeQueryParam(database, sql, params);
return result
}
}

View File

@@ -0,0 +1,27 @@
import { GeneralService } from '../share/generalservice.js'
export class AccountingSetupService {
constructor() {
this.generalService = new GeneralService()
}
async getAccountingSetup(database) {
const sql = `
SELECT
dtlnam,
dtlcod,
dtltblcod
FROM ${database}.dtlmst
WHERE dtltblcod IN ('ACTTYP', 'ACTCAT_INC', 'ACTCAT_EXP');
`
const params = []
const result = await this.generalService.executeQueryParam(database, sql, params);
return result
}
}
// SELECT
// acttyp,
// dtlname
// FROM ${database}.actmst
// LEFT JOIN ${database}.dtlmst ON dtltblcod = 'acttyp' and acttyp = dtlcod

View File

@@ -0,0 +1,44 @@
import { GeneralService } from '../share/generalservice.js'
export class AccountingSumService {
constructor() {
this.generalService = new GeneralService()
}
async getAccountingSum(database, id) {
const sql = `
SELECT
actseq,
actnum,
acttyp,
${database}.translatedtl('ACTTYP', acttyp) as acttypnam,
${database}.translatedtl_multi(ARRAY['ACTCAT_INC', 'ACTCAT_EXP'], actcat) as actcatnam,
actqty,
actcmt,
actacpdtm
FROM ${database}.actmst
WHERE actnum = $1
`
const params = [id]
const result = await this.generalService.executeQueryParam(database, sql, params);
return result
}
async getCategoryColorMap(database) {
const sql = `
SELECT dtlcod, dtlnam, dtlmsc as dtlclr
FROM ${database}.dtlmst
WHERE dtltblcod IN ('ACTCAT_INC', 'ACTCAT_EXP')
`;
const params = []
const rows = await this.generalService.executeQueryParam(database, sql, params);
const map = {};
rows.forEach(r => {
map[r.dtlnam] = r.dtlclr; // ใช้ชื่อหมวดเป็น key
});
return map;
}
}

View File

@@ -1,95 +1,95 @@
import { connection } from '../config/db.js'
import dotenv from 'dotenv'
import { sendError } from '../utils/response.js'
dotenv.config()
// ===================================================
// 🧩 Internal DevHint System
// ===================================================
function devhint(level, location, message, extra = null) {
const isEnabled = process.env.DEVHINT === 'true'
const currentLevel = parseInt(process.env.DEVHINT_LEVEL || '1', 10)
export class GeneralService {
devhint(level, location, message, extra = null) {
const isEnabled = process.env.DEVHINT === 'true'
const currentLevel = parseInt(process.env.DEVHINT_LEVEL || '1', 10)
if (!isEnabled || level > currentLevel) return
if (!isEnabled || level > currentLevel) return
const timestamp = new Date().toISOString()
const prefix = `🧩 [DEVHINT:${location}]`
const formatted = `${prefix}${message} (${timestamp})`
const timestamp = new Date().toISOString()
const prefix = `🧩 [DEVHINT:${location}]`
const formatted = `${prefix}${message} (${timestamp})`
if (extra) console.log(formatted, '\n', extra)
else console.log(formatted)
}
// ===================================================
// ✅ executeQueryConditions()
// ===================================================
export async function executeQueryConditions(database, baseQuery, conditions = {}) {
devhint(2, 'generalservice.js', 'executeQueryConditions() start')
let whereClauses = []
let params = []
let idx = 1
for (const [key, value] of Object.entries(conditions)) {
if (value === undefined || value === null || value === '') continue
const match = String(value).match(/^(ILIKE|LIKE)\s+(.+)$/i)
if (match) {
const operator = match[1].toUpperCase()
const pattern = match[2].trim()
whereClauses.push(`${key} ${operator} $${idx}`)
params.push(pattern)
// 🔹 highlight jumpout
if (message.includes('Jumpout')) {
console.log('\x1b[31m%s\x1b[0m', formatted) // แดง = jumpout
} else if (message.includes('Error')) {
console.log('\x1b[33m%s\x1b[0m', formatted) // เหลือง = error
} else {
whereClauses.push(`${key} = $${idx}`)
params.push(value)
console.log(formatted)
}
idx++
if (extra) console.log(extra)
}
let finalQuery = baseQuery
if (whereClauses.length > 0) {
finalQuery += ' AND ' + whereClauses.join(' AND ')
// ===================================================
// ✅ executeQueryParam() — เจ๊งจริง แล้ว controller catch ได้จริง
// ===================================================
async executeQueryParam(database, sql, params = []) {
const formattedSQL = sql.replace(/\${database}/g, database)
try {
this.devhint(2, 'executeQueryParam', `📤 Executing Query`, `sql = ${formattedSQL}`)
const result = await connection.query(formattedSQL, params)
this.devhint(2, 'executeQueryParam', `✅ Query Success (${result.rowCount} rows)`)
return result.rows
} catch (err) {
this.devhint(1, 'executeQueryParam', `❌ SQL Error`, err.message)
console.error('🧨 SQL Error:', err.message)
throw new Error(`SQL_EXECUTION_FAILED::${err.message}`) // ✅ “เจ๊ง” แล้วโยนขึ้น controller จริง
}
}
const formattedSQL = finalQuery.replace(/\${database}/g, database)
// ===================================================
// ✅ executeQueryConditions() — เหมือนกัน
// ===================================================
async executeQueryConditions(database, baseQuery, conditions = {}) {
this.devhint(2, 'GeneralService', 'executeQueryConditions() start')
// 🧩 แสดงเฉพาะเมื่อ DEVHINT_LEVEL >= 2
devhint(2, 'executeQueryConditions', `📤 Executing Query`, {
database,
sql: formattedSQL,
params
})
let whereClauses = []
let params = []
let idx = 1
const result = await connection.query(formattedSQL, params)
for (const [key, value] of Object.entries(conditions)) {
if (value === undefined || value === null || value === '') continue
const match = String(value).match(/^(ILIKE|LIKE)\s+(.+)$/i)
if (match) {
const operator = match[1].toUpperCase()
const pattern = match[2].trim()
whereClauses.push(`${key} ${operator} $${idx}`)
params.push(pattern)
} else {
whereClauses.push(`${key} = $${idx}`)
params.push(value)
}
idx++
}
devhint(2, 'executeQueryConditions', `✅ Query Success (${result.rowCount} rows)`)
return result.rows
let finalQuery = baseQuery
if (whereClauses.length > 0) finalQuery += ' AND ' + whereClauses.join(' AND ')
const formattedSQL = finalQuery.replace(/\${database}/g, database)
try {
this.devhint(2, 'executeQueryConditions', `📤 Executing Query`, {
database,
sql: formattedSQL,
params
})
const result = await connection.query(formattedSQL, params)
this.devhint(2, 'executeQueryConditions', `✅ Query Success (${result.rowCount} rows)`)
return result.rows
} catch (err) {
this.devhint(1, 'executeQueryConditions', `❌ SQL Error`, err.message)
console.error('🧨 SQL Error:', err.message)
throw new Error(`SQL_EXECUTION_FAILED::${err.message}`) // ✅ “เจ๊งจริง” ส่งถึง controller แน่นอน
}
}
}
// ===================================================
// ✅ executeQueryParam()
// ===================================================
export async function executeQueryParam(database, sql, params = []) {
const formattedSQL = sql.replace(/\${database}/g, database)
devhint(2, 'executeQueryParam', `📤 Executing Query`, {
database,
sql: formattedSQL,
params
})
const result = await connection.query(formattedSQL, params)
devhint(2, 'executeQueryParam', `✅ Query Success (${result.rowCount} rows)`)
return result.rows
}
// ===================================================
// Export สำหรับ controller หรืออื่นๆ เรียกใช้ได้ด้วย
// ===================================================
export { devhint }
@@ -175,5 +175,3 @@ export { devhint }
// else console.log(formatted)
// }
// }

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,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

@@ -1,29 +1,24 @@
/**
* sendResponse
* ----------------------------------------------
* ส่ง response แบบมาตรฐาน รองรับข้อความ 2 ภาษา
* ----------------------------------------------
* @param {object} res - Express response object
* @param {number} status - HTTP Status code (200, 400, 500, etc.)
* @param {string} msg_th - ข้อความภาษาไทย
* @param {string} msg_en - ข้อความภาษาอังกฤษ
* @param {any} [data=null] - optional data
*/
export function sendResponse(res, status = 200, msg_th = null, msg_en = null, data = null) {
const isError = status >= 400
// ===================================================
// ⚙️ Nuttakit Response Layer vFinal++++++
// ===================================================
// ✅ ถ้าไม่ใช่ error และไม่มีข้อความ → ใช้ข้อความ default
const message_th = msg_th || (isError ? 'เกิดข้อผิดพลาด' : 'สำเร็จ')
const message_en = msg_en || (isError ? 'Error occurred' : 'Succeed')
const response = {
status: isError ? 'error' : 'succeed',
message: {
th: message_th,
en: message_en
},
data
export function sendError(thMsg = 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', enMsg = 'Unexpected error', code = 400) {
return {
code: String(code),
message: enMsg,
message_th: thMsg,
data: []
}
}
res.status(status).json(response)
}
// ===================================================
// 🔹 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
}
}