-ต้นแบบ โครงสร้าง ไฟล์ API เส้น /api/ttc
This commit is contained in:
28
exthernal-ttc-api/.env
Normal file
28
exthernal-ttc-api/.env
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#project
|
||||||
|
PJ_NAME=exthernal-ttc-api
|
||||||
|
|
||||||
|
# database
|
||||||
|
PG_HOST=localhost
|
||||||
|
PG_USER=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=5b8273b2f79602e6b3987d3a9b018c66fd15e14848ff73ab1d332942c11eac80
|
||||||
|
|
||||||
|
# DEV_HINT
|
||||||
|
DEVHINT=true
|
||||||
|
DEVHINT_LEVEL=3
|
||||||
|
|
||||||
|
#PORT
|
||||||
|
PORT=1011
|
||||||
29
exthernal-ttc-api/.vscode/launch.json
vendored
Normal file
29
exthernal-ttc-api/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Run API (Nodemon Debug)",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "nodemon",
|
||||||
|
"program": "${workspaceFolder}/src/app.js",
|
||||||
|
"restart": true,
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen",
|
||||||
|
"envFile": "${workspaceFolder}/.env",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"runtimeArgs": ["--inspect=9229"],
|
||||||
|
"skipFiles": ["<node_internals>/**"],
|
||||||
|
// "env": {
|
||||||
|
// "PJ_NAME": "exthernal-mobile-api",
|
||||||
|
// "PG_HOST": "localhost",
|
||||||
|
// "PG_USER": "postgres",
|
||||||
|
// "PG_PASS": "1234",
|
||||||
|
// "PG_DB": "postgres",
|
||||||
|
// "PG_PORT": "5432",
|
||||||
|
// "JWT_SECRET": "MY_SUPER_SECRET",
|
||||||
|
// "PORT": "4000"
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
22
exthernal-ttc-api/package.json
Normal file
22
exthernal-ttc-api/package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "exthernal-mobile-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "External Mobile API following Nuttakit Controller Pattern vFinal",
|
||||||
|
"type": "module",
|
||||||
|
"main": "src/app.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/app.js",
|
||||||
|
"dev": "nodemon src/app.js"
|
||||||
|
},
|
||||||
|
"author": "Nuttakit Pothong",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"pg": "^8.12.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
exthernal-ttc-api/src/app.js
Normal file
32
exthernal-ttc-api/src/app.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import cors from 'cors'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import router from './routes/route.js'
|
||||||
|
import { globalResponseHandler } from './middlewares/responseHandler.js'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
app.use(cors())
|
||||||
|
app.use(express.json({ limit: '10mb' }))
|
||||||
|
|
||||||
|
app.use(globalResponseHandler);
|
||||||
|
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
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/ttc', router)
|
||||||
|
|
||||||
|
app.listen(process.env.PORT, () => {
|
||||||
|
console.log(`✅ ${process.env.PJ_NAME} running on port ${process.env.PORT}`)
|
||||||
|
})
|
||||||
13
exthernal-ttc-api/src/config/db.js
Normal file
13
exthernal-ttc-api/src/config/db.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import pkg from 'pg'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
const { Pool } = pkg
|
||||||
|
|
||||||
|
export const connection = new Pool({
|
||||||
|
host: process.env.PG_HOST,
|
||||||
|
user: process.env.PG_USER,
|
||||||
|
password: process.env.PG_PASS,
|
||||||
|
database: process.env.PG_DB,
|
||||||
|
port: process.env.PG_PORT,
|
||||||
|
})
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
exthernal-ttc-api/src/controllers/accountingSumController.js
Normal file
142
exthernal-ttc-api/src/controllers/accountingSumController.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
exthernal-ttc-api/src/middlewares/auth.js
Normal file
14
exthernal-ttc-api/src/middlewares/auth.js
Normal 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()
|
||||||
|
}
|
||||||
20
exthernal-ttc-api/src/middlewares/responseHandler.js
Normal file
20
exthernal-ttc-api/src/middlewares/responseHandler.js
Normal 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()
|
||||||
|
}
|
||||||
24
exthernal-ttc-api/src/middlewares/validate.js
Normal file
24
exthernal-ttc-api/src/middlewares/validate.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { sendError } from '../utils/response.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ Middleware สำหรับตรวจสอบความถูกต้องของ JSON body
|
||||||
|
* ป้องกัน body-parser crash (SyntaxError)
|
||||||
|
*/
|
||||||
|
export function validateJsonFormat(err, req, res, next) {
|
||||||
|
if (err instanceof SyntaxError && 'body' in err) {
|
||||||
|
console.error('[Invalid JSON Format]', err.message)
|
||||||
|
return sendError('รูปแบบ บอร์ดี้ ไม่ถูกต้อง', 'Invalid Body format')
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * ✅ ตรวจสอบ body/query/params ว่ามีค่า organization หรือไม่
|
||||||
|
// */
|
||||||
|
// export function validateRequest(req, res, next) {
|
||||||
|
// const { organization } = req.body || {}
|
||||||
|
// if (!organization) {
|
||||||
|
// return sendResponse(res, 400, 'ไม่พบค่า organization', 'Missing organization')
|
||||||
|
// }
|
||||||
|
// next()
|
||||||
|
// }
|
||||||
37
exthernal-ttc-api/src/middlewares/verifyEmailHandler.js
Normal file
37
exthernal-ttc-api/src/middlewares/verifyEmailHandler.js
Normal 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('เกิดข้อผิดพลาดในระบบ');
|
||||||
|
}
|
||||||
|
}
|
||||||
59
exthernal-ttc-api/src/routes/route.js
Normal file
59
exthernal-ttc-api/src/routes/route.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { accountingSetup } from '../controllers/accountingSetupController.js'
|
||||||
|
import { accountingSearch } from '../controllers/accountingSearchController.js'
|
||||||
|
import { accountingSum } from '../controllers/accountingSumController.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)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
router.post('/accountingSearch', async (req, res) => {
|
||||||
|
const result = await controller_accountingSearch_post.onNavigate(req, res)
|
||||||
|
if (result) return res.json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
27
exthernal-ttc-api/src/services/accountingSearchService.js
Normal file
27
exthernal-ttc-api/src/services/accountingSearchService.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
27
exthernal-ttc-api/src/services/accountingSetupService.js
Normal file
27
exthernal-ttc-api/src/services/accountingSetupService.js
Normal 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
|
||||||
44
exthernal-ttc-api/src/services/accountingSumService.js
Normal file
44
exthernal-ttc-api/src/services/accountingSumService.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
177
exthernal-ttc-api/src/share/generalservice.js
Normal file
177
exthernal-ttc-api/src/share/generalservice.js
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import { connection } from '../config/db.js'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { sendError } from '../utils/response.js'
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
const timestamp = new Date().toISOString()
|
||||||
|
const prefix = `🧩 [DEVHINT:${location}]`
|
||||||
|
const formatted = `${prefix} → ${message} (${timestamp})`
|
||||||
|
|
||||||
|
// 🔹 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 {
|
||||||
|
console.log(formatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extra) console.log(extra)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// ✅ 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 จริง
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// ✅ executeQueryConditions() — เหมือนกัน
|
||||||
|
// ===================================================
|
||||||
|
async executeQueryConditions(database, baseQuery, conditions = {}) {
|
||||||
|
this.devhint(2, 'GeneralService', '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)
|
||||||
|
} else {
|
||||||
|
whereClauses.push(`${key} = $${idx}`)
|
||||||
|
params.push(value)
|
||||||
|
}
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
|
||||||
|
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 แน่นอน
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ===================================================
|
||||||
|
// Export สำหรับ controller หรืออื่นๆ เรียกใช้ได้ด้วย
|
||||||
|
// ===================================================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * ✅ executeQueryParam (ของเดิม)
|
||||||
|
// * ใช้กับ SQL + database schema + params
|
||||||
|
// */
|
||||||
|
// export async function executeQueryParam(sql, database, params = []) {
|
||||||
|
// try {
|
||||||
|
// if (!database) throw new Error('Database is not defined')
|
||||||
|
|
||||||
|
// const formattedSQL = sql.replace(/\${database}/g, database)
|
||||||
|
// console.log(`[DB:${database}] → ${formattedSQL}`)
|
||||||
|
// const result = await connection.query(formattedSQL, params)
|
||||||
|
// return result.rows
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('[executeQueryParam Error]', err.message)
|
||||||
|
// throw err
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ executeQueryConditions (ใหม่)
|
||||||
|
* ใช้สร้าง WHERE อัตโนมัติจาก object เงื่อนไข
|
||||||
|
* ตัวที่ไม่มีค่า (null, undefined, '') จะไม่ถูกนำมาสร้างใน WHERE
|
||||||
|
*/
|
||||||
|
// export async function executeQueryConditions(database, baseQuery, conditions = {}) {
|
||||||
|
// try {
|
||||||
|
// if (!database) throw new Error('Database is not defined')
|
||||||
|
|
||||||
|
// let whereClauses = []
|
||||||
|
// let params = []
|
||||||
|
// let idx = 1
|
||||||
|
|
||||||
|
// for (const [key, value] of Object.entries(conditions)) {
|
||||||
|
// if (value === undefined || value === null || value === '') continue
|
||||||
|
|
||||||
|
// // ✅ ตรวจว่า value มีคำว่า LIKE หรือ ILIKE ไหม
|
||||||
|
// 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++
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let finalQuery = baseQuery
|
||||||
|
// if (whereClauses.length > 0) {
|
||||||
|
// finalQuery += ' AND ' + whereClauses.join(' AND ')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const formattedSQL = finalQuery.replace(/\${database}/g, database)
|
||||||
|
// console.log(`[DB:${database}] → ${formattedSQL}`)
|
||||||
|
|
||||||
|
// const result = await connection.query(formattedSQL, params)
|
||||||
|
// return result.rows
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error('[executeQueryConditions Error]', err.message)
|
||||||
|
// throw err
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * 🧩 devhint — Debug tracer ที่เปิดปิดได้จาก .env
|
||||||
|
// * @param {string} fileName - ชื่อไฟล์หรือโมดูล (เช่น 'usercontroller.js')
|
||||||
|
// * @param {string} message - ข้อความหรือจุดใน flow (เช่น 'onNavigate')
|
||||||
|
// * @param {object|string} [extra] - ข้อมูลเพิ่มเติม (optional)
|
||||||
|
// */
|
||||||
|
// export function devhint(fileName, message, extra = null) {
|
||||||
|
// if (process.env.DEVHINT === 'true') {
|
||||||
|
// const timestamp = new Date().toISOString()
|
||||||
|
// const prefix = `🧩 [DEVHINT:${fileName}]`
|
||||||
|
// const formatted = `${prefix} → ${message} (${timestamp})`
|
||||||
|
// if (extra) console.log(formatted, '\n', extra)
|
||||||
|
// else console.log(formatted)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
40
exthernal-ttc-api/src/utils/errorList.js
Normal file
40
exthernal-ttc-api/src/utils/errorList.js
Normal 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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
62
exthernal-ttc-api/src/utils/mailer.js
Normal file
62
exthernal-ttc-api/src/utils/mailer.js
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
41
exthernal-ttc-api/src/utils/oftenError.js
Normal file
41
exthernal-ttc-api/src/utils/oftenError.js
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
3
exthernal-ttc-api/src/utils/otp.js
Normal file
3
exthernal-ttc-api/src/utils/otp.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function generateOTP(length = 6) {
|
||||||
|
return Math.floor(100000 + Math.random() * 900000).toString()
|
||||||
|
}
|
||||||
28
exthernal-ttc-api/src/utils/redis.js
Normal file
28
exthernal-ttc-api/src/utils/redis.js
Normal 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
|
||||||
24
exthernal-ttc-api/src/utils/response.js
Normal file
24
exthernal-ttc-api/src/utils/response.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
16
exthernal-ttc-api/src/utils/token.js
Normal file
16
exthernal-ttc-api/src/utils/token.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
11
exthernal-ttc-api/src/utils/trim.js
Normal file
11
exthernal-ttc-api/src/utils/trim.js
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user