diff --git a/@template/.env b/@template/.env
index 88c90b1..2ad4ae9 100644
--- a/@template/.env
+++ b/@template/.env
@@ -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
diff --git a/@template/src/app.js b/@template/src/app.js
index 28d6e29..c665dcc 100644
--- a/@template/src/app.js
+++ b/@template/src/app.js
@@ -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}`)
diff --git a/@template/src/controllers/accountingSearchController.js b/@template/src/controllers/accountingSearchController.js
new file mode 100644
index 0000000..6890fc8
--- /dev/null
+++ b/@template/src/controllers/accountingSearchController.js
@@ -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
+ }
+ }
+}
diff --git a/@template/src/controllers/accountingSetupController.js b/@template/src/controllers/accountingSetupController.js
new file mode 100644
index 0000000..2651508
--- /dev/null
+++ b/@template/src/controllers/accountingSetupController.js
@@ -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
+ }
+ }
+}
diff --git a/@template/src/controllers/accountingSumController.js b/@template/src/controllers/accountingSumController.js
new file mode 100644
index 0000000..532d0fb
--- /dev/null
+++ b/@template/src/controllers/accountingSumController.js
@@ -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;
+ }
+ }
+}
diff --git a/@template/src/middlewares/auth.js b/@template/src/middlewares/auth.js
new file mode 100644
index 0000000..d99d548
--- /dev/null
+++ b/@template/src/middlewares/auth.js
@@ -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()
+}
diff --git a/@template/src/middlewares/responseHandler.js b/@template/src/middlewares/responseHandler.js
new file mode 100644
index 0000000..ce05cab
--- /dev/null
+++ b/@template/src/middlewares/responseHandler.js
@@ -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()
+}
diff --git a/@template/src/middlewares/validate.js b/@template/src/middlewares/validate.js
index a0d69bb..e4993e3 100644
--- a/@template/src/middlewares/validate.js
+++ b/@template/src/middlewares/validate.js
@@ -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()
}
diff --git a/@template/src/middlewares/verifyEmailHandler.js b/@template/src/middlewares/verifyEmailHandler.js
new file mode 100644
index 0000000..caa253f
--- /dev/null
+++ b/@template/src/middlewares/verifyEmailHandler.js
@@ -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(`
✅ ยืนยันอีเมลสำเร็จ บัญชีของคุณถูกสร้างแล้ว (${schema})
`);
+ } catch (error) {
+ console.error('❌ [Verify Email Error]', error);
+ res.status(500).send('เกิดข้อผิดพลาดในระบบ');
+ }
+}
diff --git a/@template/src/routes/route.js b/@template/src/routes/route.js
index 3a15be7..be10822 100644
--- a/@template/src/routes/route.js
+++ b/@template/src/routes/route.js
@@ -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
diff --git a/@template/src/services/accountingSearchService.js b/@template/src/services/accountingSearchService.js
new file mode 100644
index 0000000..bb0150a
--- /dev/null
+++ b/@template/src/services/accountingSearchService.js
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/@template/src/services/accountingSetupService.js b/@template/src/services/accountingSetupService.js
new file mode 100644
index 0000000..a3aba89
--- /dev/null
+++ b/@template/src/services/accountingSetupService.js
@@ -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
\ No newline at end of file
diff --git a/@template/src/services/accountingSumService.js b/@template/src/services/accountingSumService.js
new file mode 100644
index 0000000..caedd62
--- /dev/null
+++ b/@template/src/services/accountingSumService.js
@@ -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;
+ }
+
+}
\ No newline at end of file
diff --git a/@template/src/share/generalservice.js b/@template/src/share/generalservice.js
index da57ea2..c2cada3 100644
--- a/@template/src/share/generalservice.js
+++ b/@template/src/share/generalservice.js
@@ -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)
// }
// }
-
-
diff --git a/@template/src/utils/mailer.js b/@template/src/utils/mailer.js
new file mode 100644
index 0000000..0237e29
--- /dev/null
+++ b/@template/src/utils/mailer.js
@@ -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 = `
+
+
+

+
รหัส OTP สำหรับเปลี่ยนรหัสผ่าน
+
กรุณาใส่รหัสยืนยันด้านล่างนี้
+
${otp}
+
รหัสนี้จะหมดอายุใน 5 นาที
+
+
หากคุณไม่ได้ร้องขอการเปลี่ยนรหัสผ่าน กรุณาละเว้นอีเมลนี้
+
+
+`
+
+ await transporter.sendMail({
+ from: `"Support" <${process.env.SMTP_USER}>`,
+ to,
+ subject: 'OTP สำหรับเปลี่ยนรหัสผ่าน',
+ html
+ })
+}
diff --git a/@template/src/utils/otp.js b/@template/src/utils/otp.js
new file mode 100644
index 0000000..78185b7
--- /dev/null
+++ b/@template/src/utils/otp.js
@@ -0,0 +1,3 @@
+export function generateOTP(length = 6) {
+ return Math.floor(100000 + Math.random() * 900000).toString()
+}
diff --git a/@template/src/utils/redis.js b/@template/src/utils/redis.js
new file mode 100644
index 0000000..fd9023c
--- /dev/null
+++ b/@template/src/utils/redis.js
@@ -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
diff --git a/@template/src/utils/response.js b/@template/src/utils/response.js
index 018e113..ed277ac 100644
--- a/@template/src/utils/response.js
+++ b/@template/src/utils/response.js
@@ -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)
-}
\ No newline at end of file
+// ===================================================
+// 🔹 Auto Success Response (ใช้โดย Global Handler เท่านั้น)
+// ===================================================
+export function formatSuccessResponse(data) {
+ return {
+ code: "200",
+ message: "successful",
+ message_th: "ดำเนินการสำเร็จ",
+ data: data || null
+ }
+}
diff --git a/@template/src/utils/token.js b/@template/src/utils/token.js
new file mode 100644
index 0000000..6fb2920
--- /dev/null
+++ b/@template/src/utils/token.js
@@ -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
+ }
+}