forked from ttc/micro-service-api
-first commit
This commit is contained in:
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
11
@knowleadge/controller/structer.txt
Normal file
11
@knowleadge/controller/structer.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
src/
|
||||||
|
├── app.js
|
||||||
|
├── route.js
|
||||||
|
├── controllers/
|
||||||
|
│ └── user.controller.js
|
||||||
|
├── services/
|
||||||
|
│ └── user.service.js
|
||||||
|
├── config/
|
||||||
|
│ └── db.js
|
||||||
|
└── utils/
|
||||||
|
└── response.js
|
||||||
20
@knowleadge/dbchange/11112025.txt
Normal file
20
@knowleadge/dbchange/11112025.txt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE actmst ( --accounting master
|
||||||
|
actseq INTEGER NOT NULL,
|
||||||
|
actnum INTEGER NOT NULL,
|
||||||
|
acttyp VARCHAR(1) NOT NULL, -- 'e' = expense (รายจ่าย), 'i' = income (รายรับ)
|
||||||
|
actcat VARCHAR(50), -- หมวดหมู่ a,b,c,d,e → รายละเอียดจาก dtlmst table
|
||||||
|
actqty NUMERIC(12, 2) NOT NULL, -- จำนวนเงิน รองรับสูงถึงหลักล้าน
|
||||||
|
actcmt TEXT, -- คำอธิบายเพิ่มเติม
|
||||||
|
actacpdtm CHAR(12) NOT NULL,-- รูปแบบ: ddMMyyyyHHmm เช่น '111120251200' = 11 พ.ย. 2025 เวลา 12:00
|
||||||
|
PRIMARY KEY (actseq, actnum)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dtlmst (
|
||||||
|
dtltblcod VARCHAR(20) NOT NULL, -- รหัสกลุ่ม เช่น ACTTYP, ACTCAT
|
||||||
|
dtlcod VARCHAR(10) NOT NULL, -- รหัสค่าภายในกลุ่ม เช่น 'e', 'i'
|
||||||
|
dtlnam VARCHAR(100), -- ชื่อภาษาไทย เช่น 'รายจ่าย'
|
||||||
|
dtleng VARCHAR(100), -- ชื่อภาษาอังกฤษ เช่น 'expense'
|
||||||
|
dtlmsc VARCHAR(100), -- อื่น ๆ เช่น รหัสสี หรือหมายเหตุ
|
||||||
|
CONSTRAINT dtlmst_pkey PRIMARY KEY (dtltblcod, dtlcod)
|
||||||
|
);
|
||||||
23
@knowleadge/knowleadge.txt
Normal file
23
@knowleadge/knowleadge.txt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
🧩 ROUTE CONCEPT (Pattern ของเรา)
|
||||||
|
---------------------------------
|
||||||
|
✅ route.js เดียวต่อ microservice
|
||||||
|
✅ 1 route → 1 controller
|
||||||
|
✅ ไม่มี async / logic / try-catch ใน route
|
||||||
|
✅ controller รับ req/res แล้วจัดการ flow ทั้งหมด
|
||||||
|
✅ controller มี onNavigate(), onUserController()
|
||||||
|
✅ service แยกเฉพาะ database logic
|
||||||
|
|
||||||
|
⚙️ ROUTE TEMPLATE
|
||||||
|
const controller_post = userController()
|
||||||
|
router.post('/', (req, res) => controller_post.onNavigate(req, res))
|
||||||
|
router.get('/', (req, res) => controller_post.onNavigate(req, res))
|
||||||
|
|
||||||
|
🧠 CONTROLLER FLOW
|
||||||
|
- onNavigate() → check input, call onUserController()
|
||||||
|
- onUserController() → switch(req.method)
|
||||||
|
- catch → set idx=1 → send error
|
||||||
|
- finally → ถ้า idx=-1 → send success
|
||||||
|
|
||||||
|
💾 SERVICE FLOW
|
||||||
|
- connect pgsql
|
||||||
|
- return rows or affected row
|
||||||
53
@knowleadge/setup_project.txt
Normal file
53
@knowleadge/setup_project.txt
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
🚀 PART 1 : สร้างโปรเจ็กต์หลัก
|
||||||
|
📁 1. เปิดโฟลเดอร์หลักของระบบ
|
||||||
|
E:\Skyz\micro-service-api
|
||||||
|
📦 2. สร้างไฟล์หลัก
|
||||||
|
mkdir shared
|
||||||
|
mkdir gateway-api
|
||||||
|
mkdir exthernal-mobile-api
|
||||||
|
|
||||||
|
🧱 PART 2 : ติดตั้ง Node.js โปรเจกต์
|
||||||
|
🌐 1. สร้าง package.json ในแต่ละ service
|
||||||
|
|
||||||
|
|
||||||
|
cd gateway-api
|
||||||
|
npm init -y
|
||||||
|
cd ../exthernal-mobile-api
|
||||||
|
npm init -y
|
||||||
|
|
||||||
|
|
||||||
|
📦 2. ติดตั้ง dependencies หลัก
|
||||||
|
npm install express pg cors dotenv
|
||||||
|
|
||||||
|
npm install express-session connect-redis ioredis
|
||||||
|
|
||||||
|
npm install --save-dev nodemon
|
||||||
|
|
||||||
|
npm install -g nodemon
|
||||||
|
|
||||||
|
npm install jsonwebtoken
|
||||||
|
|
||||||
|
ติดตั้ง Redis
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
✏️ 3. เพิ่ม script ใน package.json
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/app.js",
|
||||||
|
"dev": "nodemon src/app.js"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
⚙️ วิธีรันแต่ละ service
|
||||||
|
|
||||||
|
จาก root (micro-service-api):
|
||||||
|
|
||||||
|
# เริ่มโปรแกรมหลัก
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# หรือเริ่มเฉพาะ API ย่อย
|
||||||
|
npm run start:mobile
|
||||||
|
npm run start:wep
|
||||||
19
@template/.env
Normal file
19
@template/.env
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#project
|
||||||
|
PJ_NAME=exthernal-mobile-api
|
||||||
|
|
||||||
|
# database
|
||||||
|
PG_HOST=localhost
|
||||||
|
PG_USER=postgres
|
||||||
|
PG_PASS=1234
|
||||||
|
PG_DB=postgres
|
||||||
|
PG_PORT=5432
|
||||||
|
|
||||||
|
# JWT-TOKENS
|
||||||
|
JWT_SECRET=MY_SUPER_SECRET
|
||||||
|
|
||||||
|
# DEV_HINT
|
||||||
|
DEVHINT=true
|
||||||
|
DEVHINT_LEVEL=3
|
||||||
|
|
||||||
|
#PORT
|
||||||
|
PORT=4000
|
||||||
29
@template/.vscode/launch.json
vendored
Normal file
29
@template/.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
@template/package.json
Normal file
22
@template/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
@template/src/app.js
Normal file
32
@template/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 { validateJsonFormat } from './middlewares/validate.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((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" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(process.env.PORT, () => {
|
||||||
|
console.log(`✅ ${process.env.PJ_NAME} running on port ${process.env.PORT}`)
|
||||||
|
})
|
||||||
13
@template/src/config/db.js
Normal file
13
@template/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,
|
||||||
|
})
|
||||||
43
@template/src/controllers/userController.js
Normal file
43
@template/src/controllers/userController.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { userService } from '../services/userservice.js'
|
||||||
|
import { trim_all_array } from '../utils/trim.js'
|
||||||
|
import { sendResponse } from '../utils/response.js'
|
||||||
|
|
||||||
|
export function userController() {
|
||||||
|
|
||||||
|
async function onNavigate(req, res) {
|
||||||
|
const database = req.body.organization
|
||||||
|
if (!database) throw new Error('Missing organization')
|
||||||
|
|
||||||
|
const prommis = await onUserController(req, res, database)
|
||||||
|
return prommis
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUserController(req, res, database) {
|
||||||
|
let idx = -1
|
||||||
|
let result
|
||||||
|
const { usrnam, usreml } = req.body
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await userService.createUser(database, usrnam, usreml)
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
idx = 1
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (idx === 1) {
|
||||||
|
return sendResponse(res, 400, 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error')
|
||||||
|
}
|
||||||
|
|
||||||
|
trim_all_array(result)
|
||||||
|
const array_diy = {
|
||||||
|
result,
|
||||||
|
count: result.length,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_diy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { onNavigate }
|
||||||
|
}
|
||||||
24
@template/src/middlewares/validate.js
Normal file
24
@template/src/middlewares/validate.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { sendResponse } 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 sendResponse(res, 400, 'รูปแบบ บอร์ดี้ ไม่ถูกต้อง', '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()
|
||||||
|
// }
|
||||||
14
@template/src/routes/route.js
Normal file
14
@template/src/routes/route.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import express from 'express'
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// import { userController } from '../controllers/userController.js'
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// const controller_user_post = userController()
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
// router.post('/user', async (req, res) => { const data = await controller_user_post.onNavigate(req, res); if (data) return sendResponse(res, 200, null, null, data)})
|
||||||
|
|
||||||
|
export default router
|
||||||
13
@template/src/services/userservice.js
Normal file
13
@template/src/services/userservice.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { executeQueryParam } from '../share/generalservice.js'
|
||||||
|
|
||||||
|
export const userService = {
|
||||||
|
async createUser(database, usrnam, usreml) {
|
||||||
|
const sql = `
|
||||||
|
SELECT * FROM ${database}.usrmst
|
||||||
|
WHERE usrnam = $1 OR usreml = $2
|
||||||
|
`
|
||||||
|
const params = [usrnam, usreml]
|
||||||
|
const result = await executeQueryParam(sql, database, params)
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
}
|
||||||
179
@template/src/share/generalservice.js
Normal file
179
@template/src/share/generalservice.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { connection } from '../config/db.js'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
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)
|
||||||
|
|
||||||
|
if (!isEnabled || level > currentLevel) return
|
||||||
|
|
||||||
|
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)
|
||||||
|
} 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)
|
||||||
|
|
||||||
|
// 🧩 แสดงเฉพาะเมื่อ DEVHINT_LEVEL >= 2
|
||||||
|
devhint(2, 'executeQueryConditions', `📤 Executing Query`, {
|
||||||
|
database,
|
||||||
|
sql: formattedSQL,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await connection.query(formattedSQL, params)
|
||||||
|
|
||||||
|
devhint(2, 'executeQueryConditions', `✅ Query Success (${result.rowCount} rows)`)
|
||||||
|
return result.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// ✅ 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 }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * ✅ 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
@template/src/utils/errorList.js
Normal file
40
@template/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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
41
@template/src/utils/oftenError.js
Normal file
41
@template/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";
|
||||||
|
}
|
||||||
|
}
|
||||||
29
@template/src/utils/response.js
Normal file
29
@template/src/utils/response.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
// ✅ ถ้าไม่ใช่ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(status).json(response)
|
||||||
|
}
|
||||||
11
@template/src/utils/trim.js
Normal file
11
@template/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
|
||||||
|
}
|
||||||
15
docker/@knowledge/addroute-service.txt
Normal file
15
docker/@knowledge/addroute-service.txt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
port:8002
|
||||||
|
|
||||||
|
-gateway service
|
||||||
|
-สร้าง service
|
||||||
|
-เลือก Protocol, Host, Port and Path
|
||||||
|
-เลือก protocol:http
|
||||||
|
-host:host.docker.internal
|
||||||
|
-path: เลือกที่จะให้ไปภายใน
|
||||||
|
-Port: service พอร์ทอะไร
|
||||||
|
|
||||||
|
-Routes
|
||||||
|
-service: เลือก ที่ๆเราจะให้ไป service นั้นๆ
|
||||||
|
-Protocols: http, https
|
||||||
|
path: เส้นทางภายนอก ที่จะให้วิ่งเข้าservice
|
||||||
|
Methods: Methods ที่อณุญาติ
|
||||||
17
docker/@knowledge/setup.txt
Normal file
17
docker/@knowledge/setup.txt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
สร้าง network ใหม่ด้วยตัวเอง:
|
||||||
|
docker network create kong-api-gateway
|
||||||
|
|
||||||
|
เริ่มระบบฐานข้อมูลก่อน
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
สร้างตาราง (migrations)
|
||||||
|
|
||||||
|
docker compose run --rm kong-migrations kong migrations bootstrap
|
||||||
|
|
||||||
|
รัน Kong gateway
|
||||||
|
|
||||||
|
docker compose up -d kong
|
||||||
|
|
||||||
|
ตรวจสอบชื่อ container
|
||||||
|
|
||||||
|
docker ps
|
||||||
70
docker/kong-gateway-api/docker-compose.yml
Normal file
70
docker/kong-gateway-api/docker-compose.yml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# 🔹 สร้าง network เฉพาะของ Kong (อย่าสลับชื่อ)
|
||||||
|
networks:
|
||||||
|
kong-api-gateway:
|
||||||
|
name: kong-api-gateway
|
||||||
|
driver: bridge # ใช้ network แบบ bridge (ค่า default)
|
||||||
|
external: true
|
||||||
|
services:
|
||||||
|
# 🧱 DATABASE ของ Kong
|
||||||
|
kong-database:
|
||||||
|
image: postgres:15
|
||||||
|
container_name: kong-database
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: kong # user ที่ใช้เชื่อมต่อ
|
||||||
|
POSTGRES_DB: kong # ชื่อ database
|
||||||
|
POSTGRES_PASSWORD: kongpass # รหัสผ่าน
|
||||||
|
ports:
|
||||||
|
- "55432:5432" # ✅ เปลี่ยนพอร์ต host เป็น 55432 กันชนกับ Postgres หลัก
|
||||||
|
volumes:
|
||||||
|
- kong-data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- kong-api-gateway # ✅ ผูก network เฉพาะ Kong
|
||||||
|
|
||||||
|
# 🧩 Migrations (ใช้สร้างตารางใน DB ครั้งแรกเท่านั้น)
|
||||||
|
kong-migrations: # ✅ ต้องอยู่ระดับเดียวกับ kong-database
|
||||||
|
image: kong:3.5
|
||||||
|
container_name: kong-migrations
|
||||||
|
depends_on:
|
||||||
|
- kong-database
|
||||||
|
environment:
|
||||||
|
KONG_DATABASE: postgres
|
||||||
|
KONG_PG_HOST: kong-database
|
||||||
|
KONG_PG_USER: kong
|
||||||
|
KONG_PG_PASSWORD: kongpass
|
||||||
|
KONG_PG_DATABASE: kong
|
||||||
|
networks:
|
||||||
|
- kong-api-gateway
|
||||||
|
|
||||||
|
# 🚪 ตัว Kong API Gateway หลัก
|
||||||
|
kong:
|
||||||
|
image: kong:3.5
|
||||||
|
container_name: kong-api-gateway # ✅ ตั้งชื่อให้ชัดเจน
|
||||||
|
depends_on:
|
||||||
|
- kong-database
|
||||||
|
environment:
|
||||||
|
KONG_DATABASE: postgres
|
||||||
|
KONG_PG_HOST: kong-database
|
||||||
|
KONG_PG_USER: kong # ✅ เพิ่ม USER
|
||||||
|
KONG_PG_PASSWORD: kongpass
|
||||||
|
KONG_PG_DATABASE: kong # ✅ เพิ่ม DATABASE
|
||||||
|
KONG_ADMIN_LISTEN: 0.0.0.0:8001
|
||||||
|
KONG_PROXY_LISTEN: 0.0.0.0:8000, 0.0.0.0:8443 ssl
|
||||||
|
|
||||||
|
KONG_ADMIN_GUI_URL: http://localhost:8002
|
||||||
|
KONG_ADMIN_GUI_LISTEN: 0.0.0.0:8002
|
||||||
|
|
||||||
|
KONG_ADMIN_ACCESS_LOG: /dev/stdout
|
||||||
|
KONG_ADMIN_ERROR_LOG: /dev/stderr
|
||||||
|
ports:
|
||||||
|
- "8000:8000" # proxy (public)
|
||||||
|
- "8443:8443" # proxy https
|
||||||
|
- "8001:8001" # admin api
|
||||||
|
- "8002:8002" # ✅ เพิ่มพอร์ตสำหรับ Kong Manager UI
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway" # ให้ container มองเห็น host (Windows/Mac)
|
||||||
|
networks:
|
||||||
|
- kong-api-gateway
|
||||||
|
|
||||||
|
# 💾 Volume สำหรับเก็บข้อมูล Postgres
|
||||||
|
volumes:
|
||||||
|
kong-data:
|
||||||
19
exthernal-accountingwep-api/.env
Normal file
19
exthernal-accountingwep-api/.env
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#project
|
||||||
|
PJ_NAME=exthernal-mobile-api
|
||||||
|
|
||||||
|
# database
|
||||||
|
PG_HOST=localhost
|
||||||
|
PG_USER=postgres
|
||||||
|
PG_PASS=123456
|
||||||
|
PG_DB=ttc
|
||||||
|
PG_PORT=5432
|
||||||
|
|
||||||
|
# JWT-TOKENS
|
||||||
|
JWT_SECRET=MY_SUPER_SECRET
|
||||||
|
|
||||||
|
# DEV_HINT
|
||||||
|
DEVHINT=true
|
||||||
|
DEVHINT_LEVEL=3
|
||||||
|
|
||||||
|
#PORT
|
||||||
|
PORT=1012
|
||||||
29
exthernal-accountingwep-api/.vscode/launch.json
vendored
Normal file
29
exthernal-accountingwep-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-accountingwep-api/package.json
Normal file
22
exthernal-accountingwep-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-accountingwep-api/src/app.js
Normal file
32
exthernal-accountingwep-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 { validateJsonFormat } from './middlewares/validate.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((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" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(process.env.PORT, () => {
|
||||||
|
console.log(`✅ ${process.env.PJ_NAME} running on port ${process.env.PORT}`)
|
||||||
|
})
|
||||||
13
exthernal-accountingwep-api/src/config/db.js
Normal file
13
exthernal-accountingwep-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,85 @@
|
|||||||
|
import { LoginService } from '../services/loginservice.js'
|
||||||
|
import { sendResponse } 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 loginController {
|
||||||
|
|
||||||
|
constructor() { // contructor zone
|
||||||
|
this.generalService = new GeneralService()
|
||||||
|
this.loginService = new LoginService()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// 🔹 LOGIN ปกติ
|
||||||
|
// ===================================================
|
||||||
|
async onNavigate(req, res) {
|
||||||
|
// Note: ตามที่ตกลงกันไว้ — ไม่ตรวจ organization ใน controller
|
||||||
|
// (middleware จะตรวจค่า organization / request format ให้แล้ว)
|
||||||
|
this.generalService.devhint(1, 'logincontroller.js', 'onNavigate() start')
|
||||||
|
|
||||||
|
// อ้างถึง organization จาก body เพื่อใช้ใน onLoginController (ครั้งแรกอาจว่าง)
|
||||||
|
// แต่ไม่ต้อง return error ถ้าไม่มี — เพราะ middleware ทำหน้าที่ตรวจเบื้องต้นแล้ว
|
||||||
|
let organization = req.body.organization
|
||||||
|
|
||||||
|
// เรียก logic controller แบบเดียว (ต้อง return value เท่านั้น)
|
||||||
|
const prommis = await this.onLoginController(req, res, organization)
|
||||||
|
return prommis // ห้ามเปลี่ยนตรงนี้ตาม pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
async onLoginController(req, res, database) {
|
||||||
|
let idx = -1
|
||||||
|
let result = []
|
||||||
|
try {
|
||||||
|
// const { username, password } = request // ห้ามทำแบบนี้อีกเด็ดขาด เราจะทำแบบด้านล่างแทน จดจำเลย
|
||||||
|
|
||||||
|
let username = req.body.request.username;
|
||||||
|
let password = req.body.request.password;
|
||||||
|
|
||||||
|
|
||||||
|
// if (!username || !password)
|
||||||
|
// return sendResponse(res, 400, 'ข้อมูลไม่ครบ', 'Missing username or password')// เราจะไม่ทำแบบนี้กันอีกแล้ว
|
||||||
|
result = await this.loginService.verifyLogin(database, username, password) // เช็คกับ db กลาง ส่ง jwttoken ออกมา
|
||||||
|
// if (!result)
|
||||||
|
// return sendResponse(res, 401, 'ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'Invalid credentials')
|
||||||
|
this.generalService.devhint(1, 'logincontroller.js', 'Login success')
|
||||||
|
} catch (error) {
|
||||||
|
idx = 1
|
||||||
|
} finally { // สำคัญมากต้อง จดจำไม่มีดัดแปลง อัปเดทเลย เรื่อง idx
|
||||||
|
if(result == 0){ return sendResponse(res, 400, 'ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'username or password is incorrect') }
|
||||||
|
if(idx == 1){ return sendResponse(res, 400, 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error') }
|
||||||
|
if(result) { return result }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onBiometricLogin(req, res) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const result = await this.loginService.loginWithBiometric(organization, biometric_id)
|
||||||
|
} catch (error) {
|
||||||
|
idx = 1
|
||||||
|
} finally { // สำคัญมากต้อง จดจำไม่มีดัดแปลง อัปเดทเลย เรื่อง idx
|
||||||
|
if(idx == 1){ return sendResponse(res, 400, 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error') }
|
||||||
|
}
|
||||||
|
return { result, timestamp: new Date().toISOString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
async onBiometricRegister(req, res) {
|
||||||
|
const { organization, request } = req.body || {}
|
||||||
|
const { biometric_id } = request || {}
|
||||||
|
const userId = req.user.id
|
||||||
|
|
||||||
|
const result = await this.loginService.registerBiometric(organization, userId, biometric_id)
|
||||||
|
return { result, timestamp: new Date().toISOString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
async onRenewToken(req, res) {
|
||||||
|
const user = req.user
|
||||||
|
const newToken = generateToken({ id: user.id, name: user.name })
|
||||||
|
return { token: newToken, renewed_at: new Date().toISOString() }
|
||||||
|
}
|
||||||
|
}
|
||||||
14
exthernal-accountingwep-api/src/middlewares/auth.js
Normal file
14
exthernal-accountingwep-api/src/middlewares/auth.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { verifyToken } from '../utils/token.js'
|
||||||
|
import { sendResponse } from '../utils/response.js'
|
||||||
|
|
||||||
|
export function authMiddleware(req, res, next) {
|
||||||
|
const authHeader = req.headers['authorization']
|
||||||
|
const token = authHeader && authHeader.split(' ')[1]
|
||||||
|
if (!token) return sendResponse(res, 401, 'ไม่พบ Token', 'Missing token')
|
||||||
|
|
||||||
|
const decoded = verifyToken(token)
|
||||||
|
if (!decoded) return sendResponse(res, 403, 'Token ไม่ถูกต้อง', 'Invalid token')
|
||||||
|
|
||||||
|
req.user = decoded
|
||||||
|
next()
|
||||||
|
}
|
||||||
24
exthernal-accountingwep-api/src/middlewares/validate.js
Normal file
24
exthernal-accountingwep-api/src/middlewares/validate.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { sendResponse } 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 sendResponse(res, 400, 'รูปแบบ บอร์ดี้ ไม่ถูกต้อง', '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()
|
||||||
|
// }
|
||||||
48
exthernal-accountingwep-api/src/routes/route.js
Normal file
48
exthernal-accountingwep-api/src/routes/route.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// ===================================================
|
||||||
|
// ⚙️ route.js (Nuttakit Pattern vFinal++++)
|
||||||
|
// ===================================================
|
||||||
|
import express from 'express'
|
||||||
|
import { loginController } from '../controllers/logincontroller.js'
|
||||||
|
import { authMiddleware } from '../middlewares/auth.js'
|
||||||
|
import { sendResponse } from '../utils/response.js'
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
const controller_login_post = new loginController()
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// 🔹 LOGIN ปกติ
|
||||||
|
// ===================================================
|
||||||
|
router.post('/login', async (req, res) => {
|
||||||
|
const data = await controller_login_post.onNavigate(req, res)
|
||||||
|
if (data)
|
||||||
|
return sendResponse(res, 200, 'เข้าสู่ระบบสำเร็จ', 'Login success', data)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// 🔹 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
|
||||||
122
exthernal-accountingwep-api/src/services/loginservice.js
Normal file
122
exthernal-accountingwep-api/src/services/loginservice.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import bcrypt from 'bcrypt'
|
||||||
|
import { GeneralService } from '../share/generalservice.js'
|
||||||
|
import { generateToken } from '../utils/token.js'
|
||||||
|
// ===================================================
|
||||||
|
// 📦 LoginService Class
|
||||||
|
// ===================================================
|
||||||
|
export class LoginService {
|
||||||
|
// ===================================================
|
||||||
|
// Zone 1️⃣ : Declaration & Constructor
|
||||||
|
// ===================================================
|
||||||
|
constructor() {
|
||||||
|
this.generalService = new GeneralService()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// 🔹 Verify Login — Username/Password
|
||||||
|
// ===================================================
|
||||||
|
async verifyLogin(database, username, password) {
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', `verifyLogin() start for username=${username}`)
|
||||||
|
|
||||||
|
// Zone 1️⃣ : Declaration
|
||||||
|
let user = null
|
||||||
|
let token = null
|
||||||
|
|
||||||
|
// Zone 2️⃣ : Query user
|
||||||
|
let sql = `
|
||||||
|
SELECT usrseq, usrnam, usrrol, usrpwd, usrthinam, usrthilstnam
|
||||||
|
FROM ${database}.usrmst
|
||||||
|
WHERE usrnam = $1
|
||||||
|
`
|
||||||
|
let params = [username] // ✅ ห้ามลืมเด็ดขาด
|
||||||
|
const rows = await this.generalService.executeQueryParam(database, sql, params)
|
||||||
|
this.generalService.devhint(3, 'loginservice.js', `query done, found=${rows.length}`)
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', 'no user found')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zone 3️⃣ : Validate password
|
||||||
|
user = rows[0]
|
||||||
|
const match = await bcrypt.compare(password, user.usrpwd)
|
||||||
|
if (!match) {
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', 'password mismatch')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zone 4️⃣ : Generate JWT Token
|
||||||
|
token = generateToken({
|
||||||
|
id: user.usrseq,
|
||||||
|
name: user.usrnam,
|
||||||
|
role: user.usrrol,
|
||||||
|
organization: database
|
||||||
|
})
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', 'token generated successfully')
|
||||||
|
|
||||||
|
// Zone 5️⃣ : Return Raw Result
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
...user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// 🔹 Login ผ่าน Biometric
|
||||||
|
// ===================================================
|
||||||
|
async loginWithBiometric(database, biometric_id) {
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', `loginWithBiometric() start for biometric_id=${biometric_id}`)
|
||||||
|
|
||||||
|
// Zone 1️⃣ : Declaration
|
||||||
|
let sql = ''
|
||||||
|
let params = []
|
||||||
|
|
||||||
|
// Zone 2️⃣ : Query
|
||||||
|
sql = `
|
||||||
|
SELECT usrid, usrnam, usrrol
|
||||||
|
FROM ${database}.usrmst
|
||||||
|
WHERE biometric_id = $1
|
||||||
|
`
|
||||||
|
params = [biometric_id]
|
||||||
|
const rows = await this.generalService.executeQueryParam(database, sql, params)
|
||||||
|
if (rows.length === 0) {
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', 'no biometric found')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zone 3️⃣ : Generate Token
|
||||||
|
const user = rows[0]
|
||||||
|
const token = generateToken({
|
||||||
|
id: user.usrid,
|
||||||
|
name: user.usrnam,
|
||||||
|
role: user.usrrol,
|
||||||
|
organization: database
|
||||||
|
})
|
||||||
|
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', 'biometric token generated')
|
||||||
|
return { token, user }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// 🔹 Register Biometric (หลัง login)
|
||||||
|
// ===================================================
|
||||||
|
async registerBiometric(database, usrid, biometric_id) {
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', `registerBiometric() start user=${usrid}`)
|
||||||
|
|
||||||
|
// Zone 1️⃣ : Declaration
|
||||||
|
let sql = ''
|
||||||
|
let params = []
|
||||||
|
|
||||||
|
// Zone 2️⃣ : Query
|
||||||
|
sql = `
|
||||||
|
UPDATE ${database}.usrmst
|
||||||
|
SET biometric_id = $1
|
||||||
|
WHERE usrid = $2
|
||||||
|
`
|
||||||
|
params = [biometric_id, usrid]
|
||||||
|
await this.generalService.executeQueryParam(database, sql, params)
|
||||||
|
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', 'biometric registered')
|
||||||
|
return { message: 'Biometric registered successfully' }
|
||||||
|
}
|
||||||
|
}
|
||||||
171
exthernal-accountingwep-api/src/share/generalservice.js
Normal file
171
exthernal-accountingwep-api/src/share/generalservice.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { connection } from '../config/db.js'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
export class GeneralService {
|
||||||
|
// constructor() {
|
||||||
|
// this.devhint = this.devhint.bind(this)
|
||||||
|
// }
|
||||||
|
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})`
|
||||||
|
if (extra) console.log(formatted, '\n', extra)
|
||||||
|
else console.log(formatted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// ✅ 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)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// ✅ executeQueryParam()
|
||||||
|
// ===================================================
|
||||||
|
// ===================================================
|
||||||
|
async executeQueryParam(database, sql, params = []) {
|
||||||
|
const formattedSQL = sql.replace(/\${database}/g, database)
|
||||||
|
|
||||||
|
this.devhint(2, 'executeQueryParam', `📤 Executing Query`, `sql = ${formattedSQL}`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await connection.query(formattedSQL, params)
|
||||||
|
this.devhint(2, 'executeQueryParam', `✅ Query Success (${result.rowCount} rows)`)
|
||||||
|
return result.rows
|
||||||
|
} catch (err) {
|
||||||
|
this.devhint(2, 'executeQueryParam', `❌ Query Failed`, err.message)
|
||||||
|
console.error('SQL Error:', err)
|
||||||
|
throw err // <– ส่งต่อ error เพื่อ 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-accountingwep-api/src/utils/errorList.js
Normal file
40
exthernal-accountingwep-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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
41
exthernal-accountingwep-api/src/utils/oftenError.js
Normal file
41
exthernal-accountingwep-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";
|
||||||
|
}
|
||||||
|
}
|
||||||
43
exthernal-accountingwep-api/src/utils/response.js
Normal file
43
exthernal-accountingwep-api/src/utils/response.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// 🧩 Unified Response Handler (vFinal+)
|
||||||
|
// ===================================================
|
||||||
|
// ===================================================
|
||||||
|
// 📁 src/utils/response.js
|
||||||
|
// ===================================================
|
||||||
|
export function sendResponse(res, status, msg_th = null, msg_en = null, data = null) {
|
||||||
|
const safeData = safeJson(data)
|
||||||
|
const success = status < 400
|
||||||
|
const response = {
|
||||||
|
status: success ? 'succeed' : 'error',
|
||||||
|
message: {
|
||||||
|
th: msg_th ?? (success ? 'สำเร็จ' : 'เกิดข้อผิดพลาด'),
|
||||||
|
en: msg_en ?? (success ? 'Succeed' : 'Error')
|
||||||
|
},
|
||||||
|
data: safeData
|
||||||
|
}
|
||||||
|
res.status(status).json(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ป้องกัน circular reference
|
||||||
|
function safeJson(obj) {
|
||||||
|
try {
|
||||||
|
if (obj && typeof obj === 'object') {
|
||||||
|
return JSON.parse(JSON.stringify(obj))
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
} catch (err) {
|
||||||
|
return '[Unserializable Object]'
|
||||||
|
}
|
||||||
|
}
|
||||||
15
exthernal-accountingwep-api/src/utils/token.js
Normal file
15
exthernal-accountingwep-api/src/utils/token.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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 {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
11
exthernal-accountingwep-api/src/utils/trim.js
Normal file
11
exthernal-accountingwep-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
|
||||||
|
}
|
||||||
28
exthernal-login-api/.env
Normal file
28
exthernal-login-api/.env
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#project
|
||||||
|
PJ_NAME=exthernal-mobile-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=1012
|
||||||
29
exthernal-login-api/.vscode/launch.json
vendored
Normal file
29
exthernal-login-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-login-api/package.json
Normal file
22
exthernal-login-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-login-api/src/app.js
Normal file
32
exthernal-login-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', router)
|
||||||
|
|
||||||
|
app.listen(process.env.PORT, () => {
|
||||||
|
console.log(`✅ ${process.env.PJ_NAME} running on port ${process.env.PORT}`)
|
||||||
|
})
|
||||||
13
exthernal-login-api/src/config/db.js
Normal file
13
exthernal-login-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,85 @@
|
|||||||
|
// import { LoginService } from '../services/loginservice.js'
|
||||||
|
// // import { sendResponse } 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 loginController {
|
||||||
|
|
||||||
|
// constructor() { // contructor zone
|
||||||
|
// this.generalService = new GeneralService()
|
||||||
|
// this.loginService = new LoginService()
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// 🔹 LOGIN ปกติ
|
||||||
|
// ===================================================
|
||||||
|
async onNavigate(req, res) {
|
||||||
|
// Note: ตามที่ตกลงกันไว้ — ไม่ตรวจ organization ใน controller
|
||||||
|
// (middleware จะตรวจค่า organization / request format ให้แล้ว)
|
||||||
|
this.generalService.devhint(1, 'logincontroller.js', 'onNavigate() start')
|
||||||
|
|
||||||
|
// อ้างถึง organization จาก body เพื่อใช้ใน onLoginController (ครั้งแรกอาจว่าง)
|
||||||
|
// แต่ไม่ต้อง return error ถ้าไม่มี — เพราะ middleware ทำหน้าที่ตรวจเบื้องต้นแล้ว
|
||||||
|
let organization = req.body.organization
|
||||||
|
|
||||||
|
// เรียก logic controller แบบเดียว (ต้อง return value เท่านั้น)
|
||||||
|
const prommis = await this.onLoginController(req, res, organization)
|
||||||
|
return prommis // ห้ามเปลี่ยนตรงนี้ตาม pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
async onLoginController(req, res, database) {
|
||||||
|
let idx = -1
|
||||||
|
let result = []
|
||||||
|
try {
|
||||||
|
// const { username, password } = request // ห้ามทำแบบนี้อีกเด็ดขาด เราจะทำแบบด้านล่างแทน จดจำเลย
|
||||||
|
|
||||||
|
let username = req.body.request.username;
|
||||||
|
let password = req.body.request.password;
|
||||||
|
|
||||||
|
|
||||||
|
// if (!username || !password)
|
||||||
|
// return sendResponse(res, 400, 'ข้อมูลไม่ครบ', 'Missing username or password')// เราจะไม่ทำแบบนี้กันอีกแล้ว
|
||||||
|
result = await this.loginService.verifyLogin(database, username, password) // เช็คกับ db กลาง ส่ง jwttoken ออกมา
|
||||||
|
// if (!result)
|
||||||
|
// return sendResponse(res, 401, 'ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'Invalid credentials')
|
||||||
|
this.generalService.devhint(1, 'logincontroller.js', 'Login success')
|
||||||
|
} catch (error) {
|
||||||
|
idx = 1
|
||||||
|
} finally { // สำคัญมากต้อง จดจำไม่มีดัดแปลง อัปเดทเลย เรื่อง idx
|
||||||
|
if(result == 0){ return sendResponse(res, 400, 'ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'username or password is incorrect') }
|
||||||
|
if(idx == 1){ return sendResponse(res, 400, 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error') }
|
||||||
|
if(result) { return result }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onBiometricLogin(req, res) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const result = await this.loginService.loginWithBiometric(organization, biometric_id)
|
||||||
|
} catch (error) {
|
||||||
|
idx = 1
|
||||||
|
} finally { // สำคัญมากต้อง จดจำไม่มีดัดแปลง อัปเดทเลย เรื่อง idx
|
||||||
|
if(idx == 1){ return sendResponse(res, 400, 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error') }
|
||||||
|
}
|
||||||
|
return { result, timestamp: new Date().toISOString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
async onBiometricRegister(req, res) {
|
||||||
|
const { organization, request } = req.body || {}
|
||||||
|
const { biometric_id } = request || {}
|
||||||
|
const userId = req.user.id
|
||||||
|
|
||||||
|
const result = await this.loginService.registerBiometric(organization, userId, biometric_id)
|
||||||
|
return { result, timestamp: new Date().toISOString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
async onRenewToken(req, res) {
|
||||||
|
const user = req.user
|
||||||
|
const newToken = generateToken({ id: user.id, name: user.name })
|
||||||
|
return { token: newToken, renewed_at: new Date().toISOString() }
|
||||||
|
}
|
||||||
|
}
|
||||||
67
exthernal-login-api/src/controllers/loginController.js
Normal file
67
exthernal-login-api/src/controllers/loginController.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { LoginService } from '../services/loginservice.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 loginController {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.generalService = new GeneralService();
|
||||||
|
this.loginService = new LoginService();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onNavigate(req, res) {
|
||||||
|
this.generalService.devhint(1, 'logincontroller.js', 'onNavigate() start');
|
||||||
|
let organization = req.body.organization;
|
||||||
|
const prommis = await this.onLoginController(req, res, organization);
|
||||||
|
return prommis;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onLoginController(req, res, database) {
|
||||||
|
let idx = -1
|
||||||
|
let result = []
|
||||||
|
try {
|
||||||
|
let username = req.body.request.username;
|
||||||
|
let password = req.body.request.password;
|
||||||
|
|
||||||
|
result = await this.loginService.verifyLogin(database, username, password); // เช็คกับ db กลาง ส่ง jwttoken ออกมา
|
||||||
|
this.generalService.devhint(1, 'logincontroller.js', 'Login success');
|
||||||
|
} catch (error) {
|
||||||
|
idx = 1;
|
||||||
|
} finally {
|
||||||
|
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
|
||||||
|
if (!result) return sendError('ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'Invalid credentials');
|
||||||
|
if(result) { return result }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// async onBiometricLogin(req, res) {
|
||||||
|
// try {
|
||||||
|
|
||||||
|
// const result = await this.loginService.loginWithBiometric(organization, biometric_id)
|
||||||
|
// } catch (error) {
|
||||||
|
// idx = 1
|
||||||
|
// } finally { // สำคัญมากต้อง จดจำไม่มีดัดแปลง อัปเดทเลย เรื่อง idx
|
||||||
|
// if(idx == 1){ return sendResponse(res, 400, 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error') }
|
||||||
|
// }
|
||||||
|
// return { result, timestamp: new Date().toISOString() }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async onBiometricRegister(req, res) {
|
||||||
|
// const { organization, request } = req.body || {}
|
||||||
|
// const { biometric_id } = request || {}
|
||||||
|
// const userId = req.user.id
|
||||||
|
|
||||||
|
// const result = await this.loginService.registerBiometric(organization, userId, biometric_id)
|
||||||
|
// return { result, timestamp: new Date().toISOString() }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async onRenewToken(req, res) {
|
||||||
|
// const user = req.user
|
||||||
|
// const newToken = generateToken({ id: user.id, name: user.name })
|
||||||
|
// return { token: newToken, renewed_at: new Date().toISOString() }
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { sendError } from '../../utils/response.js';
|
||||||
|
import { OTPVerifyService } from '../../services/otpverifyservice.js';
|
||||||
|
import { GeneralService } from '../../share/generalservice.js';
|
||||||
|
|
||||||
|
export class OtpVerifyController {
|
||||||
|
constructor() {
|
||||||
|
this.otpVerifyService = new OTPVerifyService();
|
||||||
|
this.generalService = new GeneralService();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onNavigate(req, res) {
|
||||||
|
let idx = -1, result = [];
|
||||||
|
try {
|
||||||
|
const { email, otp } = req.body.request;
|
||||||
|
result = await this.otpVerifyService.verifyOtp(email, otp);
|
||||||
|
} catch (error) {
|
||||||
|
idx = 1;
|
||||||
|
this.generalService.devhint(1, 'otpverifycontroller.js', 'Jumpout', error.message);
|
||||||
|
} finally {
|
||||||
|
if (idx === 1) return sendError('เกิดข้อผิดพลาดในการตรวจสอบ OTP', 'OTP verification error');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { OtpService } from '../../services/otpservice.js'
|
||||||
|
import { sendError } from '../../utils/response.js'
|
||||||
|
import { GeneralService } from '../../share/generalservice.js'
|
||||||
|
|
||||||
|
export class OtpController {
|
||||||
|
constructor() {
|
||||||
|
this.otpService = new OtpService()
|
||||||
|
this.generalService = new GeneralService()
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSendOtp(req, res) {
|
||||||
|
let idx = -1, result = []
|
||||||
|
try {
|
||||||
|
const email = req.body.request.email
|
||||||
|
result = await this.otpService.sendOtp(email)
|
||||||
|
this.generalService.devhint(1, 'otpcontroller.js', `OTP sent to ${email} OTP: ${result.otp}`)
|
||||||
|
} catch (error) {
|
||||||
|
idx = 1
|
||||||
|
this.generalService.devhint(1, 'otpcontroller.js', 'Jumpout Detected', error.message)
|
||||||
|
} finally {
|
||||||
|
delete result.otp
|
||||||
|
if (idx === 1) return sendError('ไม่สามารถส่ง OTP ได้', 'Send failed')
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ResetPasswordService } from '../../services/resetpasswordservice.js';
|
||||||
|
import { sendError } from '../../utils/response.js';
|
||||||
|
import { GeneralService } from '../../share/generalservice.js';
|
||||||
|
|
||||||
|
export class ResetPasswordController {
|
||||||
|
constructor() {
|
||||||
|
this.resetPasswordService = new ResetPasswordService();
|
||||||
|
this.generalService = new GeneralService();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onNavigate(req, res) {
|
||||||
|
let idx = -1, result = [];
|
||||||
|
try {
|
||||||
|
const { email, token, newPassword } = req.body.request;
|
||||||
|
result = await this.resetPasswordService.resetPassword(email, token, newPassword);
|
||||||
|
} catch (error) {
|
||||||
|
idx = 1;
|
||||||
|
this.generalService.devhint(1, 'resetpasswordcontroller.js', 'Jumpout', error.message);
|
||||||
|
} finally {
|
||||||
|
if (idx === 1) return sendError('ไม่สามารถรีเซ็ตรหัสผ่านได้', 'Password reset error');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
exthernal-login-api/src/controllers/registercontroller.js
Normal file
26
exthernal-login-api/src/controllers/registercontroller.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { RegisterService } from '../services/registerservice.js';
|
||||||
|
import { sendError } from '../utils/response.js';
|
||||||
|
import { GeneralService } from '../share/generalservice.js';
|
||||||
|
|
||||||
|
export class RegisterController {
|
||||||
|
constructor() {
|
||||||
|
this.registerService = new RegisterService();
|
||||||
|
this.generalService = new GeneralService();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onNavigate(req, res) {
|
||||||
|
let idx = -1, result = [];
|
||||||
|
try {
|
||||||
|
const { organization, request } = req.body;
|
||||||
|
const { email, fname, lname, password } = request;
|
||||||
|
result = await this.registerService.requestRegistration(organization, email, fname, lname, password);
|
||||||
|
} catch (error) {
|
||||||
|
idx = 1;
|
||||||
|
this.generalService.devhint(1, 'registercontroller.js', 'Jumpout', error.message);
|
||||||
|
result = error; // จะถูก Global Handler จัด format
|
||||||
|
} finally {
|
||||||
|
if (idx === 1) return result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
exthernal-login-api/src/controllers/verifyemailcontroller.js
Normal file
46
exthernal-login-api/src/controllers/verifyemailcontroller.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { VerifyEmailService } from '../services/verifyemailservice.js';
|
||||||
|
import { sendError } from '../utils/response.js';
|
||||||
|
import { GeneralService } from '../share/generalservice.js';
|
||||||
|
|
||||||
|
export class VerifyEmailController {
|
||||||
|
constructor() {
|
||||||
|
this.verifyService = new VerifyEmailService();
|
||||||
|
this.generalService = new GeneralService();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onVerifyEmail(req, res) {
|
||||||
|
let idx = -1;
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// token/email can come from query (link) or body
|
||||||
|
const email = (req.query && req.query.email) || (req.body && req.body.request && req.body.request.email);
|
||||||
|
const token = (req.query && req.query.token) || (req.body && req.body.request && req.body.request.token);
|
||||||
|
const schema = (req.query && req.query.organization) || (req.body && req.body.organization) || 'nuttakit';
|
||||||
|
|
||||||
|
if (!email || !token) {
|
||||||
|
return sendError('Missing token or email', 'Missing token or email', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// call service to do Redis + DB work
|
||||||
|
result = await this.verifyService.verifyAndCreate({ email, token, schema });
|
||||||
|
|
||||||
|
// devhint success
|
||||||
|
this.generalService.devhint(1, 'verifyemailcontroller.js', `Verify succeeded for ${email} schema=${schema}`);
|
||||||
|
} catch (error) {
|
||||||
|
idx = 1;
|
||||||
|
// If service threw sendError object, it may be an object not Error
|
||||||
|
const errMsg = (error && error.message) ? error.message : JSON.stringify(error);
|
||||||
|
this.generalService.devhint(1, 'verifyemailcontroller.js', 'Jumpout Detected', errMsg);
|
||||||
|
} finally {
|
||||||
|
if (idx === 1) return sendError('ไม่สามารถยืนยันอีเมลได้', 'Verification failed', 400);
|
||||||
|
// standard success response (globalResponseHandler will wrap it)
|
||||||
|
return {
|
||||||
|
code: '200',
|
||||||
|
message: 'successful',
|
||||||
|
message_th: 'ยืนยันอีเมลสำเร็จ',
|
||||||
|
data: result
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
exthernal-login-api/src/middlewares/auth.js
Normal file
14
exthernal-login-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-login-api/src/middlewares/responseHandler.js
Normal file
20
exthernal-login-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-login-api/src/middlewares/validate.js
Normal file
24
exthernal-login-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-login-api/src/middlewares/verifyEmailHandler.js
Normal file
37
exthernal-login-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('เกิดข้อผิดพลาดในระบบ');
|
||||||
|
}
|
||||||
|
}
|
||||||
52
exthernal-login-api/src/routes/route.js
Normal file
52
exthernal-login-api/src/routes/route.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { loginController } from '../controllers/loginController.js'
|
||||||
|
import { authMiddleware } from '../middlewares/auth.js'
|
||||||
|
import { OtpController } from '../../src/controllers/otpController/otpcontroller.js'
|
||||||
|
import { OtpVerifyController } from '../../src/controllers/otpController/otpVerifycontroller.js'
|
||||||
|
import { RegisterController } from '../controllers/registercontroller.js';
|
||||||
|
import { VerifyEmailController } from '../controllers/verifyemailcontroller.js';
|
||||||
|
import { ResetPasswordController } from '../controllers/otpController/resetpasswordcontroller.js';
|
||||||
|
|
||||||
|
// 🧱 Controller Instances
|
||||||
|
const registerController = new RegisterController();
|
||||||
|
const verifyEmailController = new VerifyEmailController();
|
||||||
|
const resetPasswordController = new ResetPasswordController();;
|
||||||
|
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
const controller_login_post = new loginController()
|
||||||
|
const otpController = new OtpController()
|
||||||
|
const otpVerifyController = new OtpVerifyController()
|
||||||
|
|
||||||
|
router.post('/login/login', async (req, res) => {const result = await controller_login_post.onNavigate(req, res); return res.json(result)})
|
||||||
|
|
||||||
|
router.post('/login/otp/send', async (req, res) => {
|
||||||
|
const result = await otpController.onSendOtp(req, res)
|
||||||
|
if (result) return res.json(result)
|
||||||
|
})
|
||||||
|
router.post('/login/register', async (req, res) => {
|
||||||
|
const result = await registerController.onNavigate(req, res);
|
||||||
|
if (result) return res.json(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/login/verify-email', async (req, res) => {
|
||||||
|
const result = await verifyEmailController.onVerifyEmail(req, res);
|
||||||
|
if (result?.code && result.code !== '200') return res.status(400).send(result.message_th);
|
||||||
|
// ถ้า controller ส่ง HTML string กลับมา → render ตรง ๆ
|
||||||
|
if (typeof result === 'string') return res.send(result);
|
||||||
|
return res.json(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
router.post('/login/reset-password', async (req, res) => {
|
||||||
|
const result = await resetPasswordController.onNavigate(req, res);
|
||||||
|
if (result) return res.json(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
router.post('/login/otp/verify', async (req, res) => {
|
||||||
|
const result = await otpVerifyController.onNavigate(req, res)
|
||||||
|
if (result) return res.json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
106
exthernal-login-api/src/services/loginservice.js
Normal file
106
exthernal-login-api/src/services/loginservice.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import bcrypt from 'bcrypt'
|
||||||
|
import { GeneralService } from '../share/generalservice.js'
|
||||||
|
import { generateToken } from '../utils/token.js'
|
||||||
|
|
||||||
|
export class LoginService {
|
||||||
|
constructor() {
|
||||||
|
this.generalService = new GeneralService()
|
||||||
|
}
|
||||||
|
async verifyLogin(database, username, password) {
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', `verifyLogin() start for username=${username}`)
|
||||||
|
|
||||||
|
let user = null
|
||||||
|
let token = null
|
||||||
|
|
||||||
|
let sql = `
|
||||||
|
SELECT usrseq, usrnam, usrorg, usrrol, usrpwd, usrthinam, usrthilstnam
|
||||||
|
FROM nuttakit.usrmst
|
||||||
|
WHERE usrnam = $1
|
||||||
|
`
|
||||||
|
let params = [username]
|
||||||
|
const rows = await this.generalService.executeQueryParam(database, sql, params)
|
||||||
|
this.generalService.devhint(3, 'loginservice.js', `query done, found=${rows.length}`)
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', 'no user found')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
user = rows[0]
|
||||||
|
const match = await bcrypt.compare(password, user.usrpwd)
|
||||||
|
if (match === false) {
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', 'password mismatch')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
token = generateToken({
|
||||||
|
id: user.usrseq,
|
||||||
|
name: user.usrnam,
|
||||||
|
realname: user.usrthinam,
|
||||||
|
lastname: user.usrthilstnam,
|
||||||
|
role: user.usrrol,
|
||||||
|
organization: user.usrorg
|
||||||
|
})
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', 'token generated successfully')
|
||||||
|
|
||||||
|
|
||||||
|
delete user.usrseq
|
||||||
|
delete user.usrnam
|
||||||
|
delete user.usrrol
|
||||||
|
delete user.usrpwd
|
||||||
|
delete user.usrorg
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
...user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginWithBiometric(database, biometric_id) {
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', `loginWithBiometric() start for biometric_id=${biometric_id}`)
|
||||||
|
|
||||||
|
let sql = ''
|
||||||
|
let params = []
|
||||||
|
|
||||||
|
sql = `
|
||||||
|
SELECT usrid, usrnam, usrrol
|
||||||
|
FROM ${database}.usrmst
|
||||||
|
WHERE biometric_id = $1
|
||||||
|
`
|
||||||
|
params = [biometric_id]
|
||||||
|
const rows = await this.generalService.executeQueryParam(database, sql, params)
|
||||||
|
if (rows.length === 0) {
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', 'no biometric found')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = rows[0]
|
||||||
|
const token = generateToken({
|
||||||
|
id: user.usrid,
|
||||||
|
name: user.usrnam,
|
||||||
|
role: user.usrrol,
|
||||||
|
organization: database
|
||||||
|
})
|
||||||
|
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', 'biometric token generated')
|
||||||
|
return { token, user }
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerBiometric(database, usrid, biometric_id) {
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', `registerBiometric() start user=${usrid}`)
|
||||||
|
|
||||||
|
|
||||||
|
let sql = ''
|
||||||
|
let params = []
|
||||||
|
|
||||||
|
sql = `
|
||||||
|
UPDATE ${database}.usrmst
|
||||||
|
SET biometric_id = $1
|
||||||
|
WHERE usrid = $2
|
||||||
|
`
|
||||||
|
params = [biometric_id, usrid]
|
||||||
|
await this.generalService.executeQueryParam(database, sql, params)
|
||||||
|
|
||||||
|
this.generalService.devhint(2, 'loginservice.js', 'biometric registered')
|
||||||
|
return { message: 'Biometric registered successfully' }
|
||||||
|
}
|
||||||
|
}
|
||||||
17
exthernal-login-api/src/services/otpservice.js
Normal file
17
exthernal-login-api/src/services/otpservice.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { generateOTP } from '../utils/otp.js'
|
||||||
|
import { sendMockOtpMail } from '../utils/mailer.js'
|
||||||
|
import { saveOtp, verifyOtp, removeOtp } from '../utils/redis.js'
|
||||||
|
import { sendError } from '../utils/response.js'
|
||||||
|
|
||||||
|
export class OtpService {
|
||||||
|
async sendOtp(email) {
|
||||||
|
try {
|
||||||
|
const otp = generateOTP()
|
||||||
|
await saveOtp(email, otp)
|
||||||
|
await sendMockOtpMail(email, otp)
|
||||||
|
return { email, otp}
|
||||||
|
} catch (error) {
|
||||||
|
return sendError('ไม่สามารถส่ง OTP ได้', 'Failed to send OTP')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
exthernal-login-api/src/services/otpverifyservice.js
Normal file
29
exthernal-login-api/src/services/otpverifyservice.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import Redis from 'ioredis';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { sendError } from '../utils/response.js';
|
||||||
|
import { GeneralService } from '../share/generalservice.js';
|
||||||
|
|
||||||
|
export class OTPVerifyService {
|
||||||
|
constructor() {
|
||||||
|
this.redis = new Redis();
|
||||||
|
this.generalService = new GeneralService();
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyOtp(email, otp) {
|
||||||
|
const storedOtp = await this.redis.get(`otp:${email}`);
|
||||||
|
if (!storedOtp || storedOtp !== otp) {
|
||||||
|
throw sendError('รหัส OTP ไม่ถูกต้องหรือหมดอายุ', 'Invalid OTP');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.redis.del(`otp:${email}`);
|
||||||
|
|
||||||
|
const resetToken = crypto.randomBytes(32).toString('hex');
|
||||||
|
await this.redis.set(`reset:${email}`, resetToken, 'EX', 600); // TTL 10 นาที
|
||||||
|
|
||||||
|
this.generalService.devhint(1, 'otpverifyservice.js', `OTP Verified → Reset Token issued (${email})`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
resetToken:resetToken
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
88
exthernal-login-api/src/services/registerservice.js
Normal file
88
exthernal-login-api/src/services/registerservice.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import Redis from 'ioredis';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import { GeneralService } from '../share/generalservice.js';
|
||||||
|
import { sendError } from '../utils/response.js';
|
||||||
|
|
||||||
|
export class RegisterService {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.redis = new Redis();
|
||||||
|
this.generalService = new GeneralService();
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestRegistration(database, email, fname, lname, password) {
|
||||||
|
let result = [];
|
||||||
|
try {
|
||||||
|
let sql = `
|
||||||
|
SELECT usrseq FROM ${database}.usrmst WHERE usrnam = $1
|
||||||
|
`;
|
||||||
|
let param = [email];
|
||||||
|
const userCheck = await this.generalService.executeQueryParam(database, sql, param);
|
||||||
|
|
||||||
|
if (userCheck.length > 0) {
|
||||||
|
this.generalService.devhint(1, 'registerservice.js', `❌ Duplicate email (${email})`);
|
||||||
|
throw sendError('อีเมลนี้ถูกใช้แล้ว', 'Email already registered');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPwd = await bcrypt.hash(password, 10);
|
||||||
|
const token = crypto.randomBytes(32).toString('hex');
|
||||||
|
|
||||||
|
|
||||||
|
const payload = JSON.stringify({ fname, lname, hashedPwd, token, database });
|
||||||
|
await this.redis.set(`verify:${email}`, payload, 'EX', 86400); // 24h
|
||||||
|
|
||||||
|
|
||||||
|
const verifyUrl = `http://49.231.182.24:1012/api/verify-email?token=${token}&email=${encodeURIComponent(email)}&organization=${database}`;
|
||||||
|
await this.sendVerifyEmail(email, verifyUrl);
|
||||||
|
|
||||||
|
this.generalService.devhint(2, 'registerservice.js', `✅ Verify link sent to ${email}`);
|
||||||
|
|
||||||
|
result = {
|
||||||
|
code: '200',
|
||||||
|
message_th: 'ส่งลิงก์ยืนยันอีเมลแล้ว',
|
||||||
|
data: {}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.generalService.devhint(1, 'registerservice.js', '❌ Registration Error', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendVerifyEmail(email, verifyUrl) {
|
||||||
|
try {
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
service: 'gmail',
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER,
|
||||||
|
pass: process.env.SMTP_PASS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div style="font-family: sans-serif;">
|
||||||
|
<h2>ยืนยันการสมัครสมาชิก</h2>
|
||||||
|
<p>กรุณากดยืนยันภายใน 24 ชั่วโมง เพื่อเปิดใช้งานบัญชีของคุณ</p>
|
||||||
|
<a href="${verifyUrl}"
|
||||||
|
style="display:inline-block;background:#0078d4;color:white;
|
||||||
|
padding:10px 20px;text-decoration:none;border-radius:5px;">ยืนยันอีเมล</a>
|
||||||
|
<p style="margin-top:16px;font-size:13px;color:#555;">หากคุณไม่ได้สมัคร โปรดละเว้นอีเมลนี้</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: `"System" <${process.env.SMTP_USER}>`,
|
||||||
|
to: email,
|
||||||
|
subject: '📩 ยืนยันอีเมลสำหรับสมัครสมาชิก',
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.generalService.devhint(2, 'registerservice.js', `📤 Verification email sent (${email})`);
|
||||||
|
} catch (error) {
|
||||||
|
this.generalService.devhint(1, 'registerservice.js', '❌ Email Send Failed', error.message);
|
||||||
|
throw sendError('ไม่สามารถส่งอีเมลได้', 'Email send failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
exthernal-login-api/src/services/resetpasswordservice.js
Normal file
38
exthernal-login-api/src/services/resetpasswordservice.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import Redis from 'ioredis';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import { sendError } from '../utils/response.js';
|
||||||
|
import { GeneralService } from '../share/generalservice.js';
|
||||||
|
|
||||||
|
export class ResetPasswordService {
|
||||||
|
constructor() {
|
||||||
|
this.redis = new Redis();
|
||||||
|
this.generalService = new GeneralService();
|
||||||
|
}
|
||||||
|
|
||||||
|
async resetPassword(email, token, newPassword) {
|
||||||
|
let database = '';
|
||||||
|
|
||||||
|
const storedToken = await this.redis.get(`reset:${email}`);
|
||||||
|
if (!storedToken || storedToken !== token) {
|
||||||
|
throw sendError('Token ไม่ถูกต้องหรือหมดอายุ', 'Invalid or expired token');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.redis.del(`reset:${email}`);
|
||||||
|
|
||||||
|
// อัปเดตรหัสผ่านในฐานข้อมูลจริง
|
||||||
|
const hashedPwd = await bcrypt.hash(newPassword, 10);
|
||||||
|
let sql = `
|
||||||
|
UPDATE usrmst SET usrpwd = $1 WHERE usrnam = $2
|
||||||
|
`
|
||||||
|
let param = [hashedPwd, email];
|
||||||
|
await this.generalService.executeQueryParam(database, sql, param);
|
||||||
|
|
||||||
|
this.generalService.devhint(1, 'resetpasswordservice.js', `Password reset successful (${email})`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: '200',
|
||||||
|
message: 'successful',
|
||||||
|
message_th: 'รีเซ็ตรหัสผ่านสำเร็จ'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
13
exthernal-login-api/src/services/userservice.js
Normal file
13
exthernal-login-api/src/services/userservice.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { executeQueryParam } from '../share/generalservice.js'
|
||||||
|
|
||||||
|
export const userService = {
|
||||||
|
async createUser(database, usrnam, usreml) {
|
||||||
|
const sql = `
|
||||||
|
SELECT * FROM ${database}.usrmst
|
||||||
|
WHERE usrnam = $1 OR usreml = $2
|
||||||
|
`
|
||||||
|
const params = [usrnam, usreml]
|
||||||
|
const result = await executeQueryParam(sql, database, params)
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
}
|
||||||
66
exthernal-login-api/src/services/verifyemailservice.js
Normal file
66
exthernal-login-api/src/services/verifyemailservice.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import Redis from 'ioredis';
|
||||||
|
import { GeneralService } from '../share/generalservice.js';
|
||||||
|
import { sendError } from '../utils/response.js';
|
||||||
|
|
||||||
|
export class VerifyEmailService {
|
||||||
|
constructor() {
|
||||||
|
this.redis = new Redis();
|
||||||
|
this.generalService = new GeneralService();
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyAndCreate({ email, token, schema = 'nuttakit' }) {
|
||||||
|
// ✅ STEP 1: โหลด payload จาก Redis
|
||||||
|
const key = `verify:${email}`;
|
||||||
|
const stored = await this.redis.get(key);
|
||||||
|
if (!stored) {
|
||||||
|
throw sendError('ลิงก์หมดอายุหรือไม่ถูกต้อง', 'Verification link expired or invalid', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(stored);
|
||||||
|
} catch (ex) {
|
||||||
|
await this.redis.del(key).catch(() => {});
|
||||||
|
throw sendError('ข้อมูลการยืนยันไม่ถูกต้อง', 'Invalid verify payload', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ STEP 2: ตรวจสอบ token
|
||||||
|
if (parsed.token !== token) {
|
||||||
|
throw sendError('Token ไม่ถูกต้อง', 'Invalid token', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ STEP 3: ตรวจสอบว่าอีเมลนี้เคยถูกสร้างใน schema แล้วหรือยัง
|
||||||
|
const checkSql = `
|
||||||
|
SELECT usrseq FROM \${database}.usrmst WHERE usrnam = $1
|
||||||
|
`;
|
||||||
|
const checkResult = await this.generalService.executeQueryParam(schema, checkSql, [email]);
|
||||||
|
|
||||||
|
if (checkResult && checkResult.length > 0) {
|
||||||
|
await this.redis.del(key).catch(() => {});
|
||||||
|
throw sendError('อีเมลนี้ถูกใช้แล้วในองค์กรนี้', 'Email already registered in this organization', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ STEP 4: Insert ข้อมูลลงในตารางจริง
|
||||||
|
const insertSql = `
|
||||||
|
INSERT INTO ${database}.usrmst (usrnam, usrthinam, usrthilstnam, usrpwd, usrrol)
|
||||||
|
VALUES ($1, $2, $3, $4, 'U')
|
||||||
|
`;
|
||||||
|
const params = [email, parsed.fname, parsed.lname, parsed.hashedPwd];
|
||||||
|
await this.generalService.executeQueryParam(schema, insertSql, params);
|
||||||
|
|
||||||
|
// ✅ STEP 5: ลบ Redis Key (เคลียร์ payload)
|
||||||
|
await this.redis.del(key).catch(() => {});
|
||||||
|
|
||||||
|
this.generalService.devhint(2, 'verifyemailservice.js', `✅ Account verified (${email})`);
|
||||||
|
|
||||||
|
// ✅ STEP 6: ส่งผลลัพธ์กลับ
|
||||||
|
return {
|
||||||
|
code: '200',
|
||||||
|
message_th: 'ยืนยันอีเมลสำเร็จ บัญชีถูกสร้างแล้ว',
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
schema,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
177
exthernal-login-api/src/share/generalservice.js
Normal file
177
exthernal-login-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-login-api/src/utils/errorList.js
Normal file
40
exthernal-login-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-login-api/src/utils/mailer.js
Normal file
62
exthernal-login-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-login-api/src/utils/oftenError.js
Normal file
41
exthernal-login-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-login-api/src/utils/otp.js
Normal file
3
exthernal-login-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-login-api/src/utils/redis.js
Normal file
28
exthernal-login-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-login-api/src/utils/response.js
Normal file
24
exthernal-login-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
|
||||||
|
}
|
||||||
|
}
|
||||||
15
exthernal-login-api/src/utils/token.js
Normal file
15
exthernal-login-api/src/utils/token.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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 {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
11
exthernal-login-api/src/utils/trim.js
Normal file
11
exthernal-login-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
|
||||||
|
}
|
||||||
19
exthernal-mobile-api/.env
Normal file
19
exthernal-mobile-api/.env
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#project
|
||||||
|
PJ_NAME=exthernal-mobile-api
|
||||||
|
|
||||||
|
# database
|
||||||
|
PG_HOST=localhost
|
||||||
|
PG_USER=postgres
|
||||||
|
PG_PASS=1234
|
||||||
|
PG_DB=postgres
|
||||||
|
PG_PORT=5432
|
||||||
|
|
||||||
|
# JWT-TOKENS
|
||||||
|
JWT_SECRET=MY_SUPER_SECRET
|
||||||
|
|
||||||
|
# DEV_HINT
|
||||||
|
DEVHINT=true
|
||||||
|
DEVHINT_LEVEL=3
|
||||||
|
|
||||||
|
#PORT
|
||||||
|
PORT=4000
|
||||||
29
exthernal-mobile-api/.vscode/launch.json
vendored
Normal file
29
exthernal-mobile-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-mobile-api/package.json
Normal file
22
exthernal-mobile-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-mobile-api/src/app.js
Normal file
32
exthernal-mobile-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 { validateJsonFormat } from './middlewares/validate.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((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" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(process.env.PORT, () => {
|
||||||
|
console.log(`✅ ${process.env.PJ_NAME} running on port ${process.env.PORT}`)
|
||||||
|
})
|
||||||
13
exthernal-mobile-api/src/config/db.js
Normal file
13
exthernal-mobile-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,
|
||||||
|
})
|
||||||
41
exthernal-mobile-api/src/controllers/userController.js
Normal file
41
exthernal-mobile-api/src/controllers/userController.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { userService } from '../services/userservice.js'
|
||||||
|
import { trim_all_array } from '../utils/trim.js'
|
||||||
|
import { sendResponse } from '../utils/response.js'
|
||||||
|
import { devhint } from '../share/generalservice.js'
|
||||||
|
|
||||||
|
export function userController() {
|
||||||
|
|
||||||
|
async function onNavigate(req, res) {
|
||||||
|
const database = req.body.organization
|
||||||
|
if (!database) {return sendResponse(res, 400, 'ไม่พบบริษัท', 'Missing organization')}
|
||||||
|
const prommis = await onUserController(req, res, database)
|
||||||
|
return prommis
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUserController(req, res, database) {
|
||||||
|
try {
|
||||||
|
var idx = -1
|
||||||
|
let result = []
|
||||||
|
result = await userService.createUser(database, usrnam, usreml);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
idx = 1
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (idx === 1) {
|
||||||
|
return sendResponse(res, 400, 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error')
|
||||||
|
}
|
||||||
|
|
||||||
|
trim_all_array(result)
|
||||||
|
const array_diy = {
|
||||||
|
result,
|
||||||
|
count: result.length,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_diy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { onNavigate }
|
||||||
|
}
|
||||||
24
exthernal-mobile-api/src/middlewares/validate.js
Normal file
24
exthernal-mobile-api/src/middlewares/validate.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { sendResponse } 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 sendResponse(res, 400, 'รูปแบบ บอร์ดี้ ไม่ถูกต้อง', '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()
|
||||||
|
// }
|
||||||
14
exthernal-mobile-api/src/routes/route.js
Normal file
14
exthernal-mobile-api/src/routes/route.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import express from 'express'
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import { userController } from '../controllers/userController.js'
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
const controller_user_post = userController()
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
router.post('/user', async (req, res) => { const data = await controller_user_post.onNavigate(req, res); if (data) return sendResponse(res, 200, null, null, data)})
|
||||||
|
|
||||||
|
export default router
|
||||||
13
exthernal-mobile-api/src/services/userservice.js
Normal file
13
exthernal-mobile-api/src/services/userservice.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { executeQueryParam } from '../share/generalservice.js'
|
||||||
|
|
||||||
|
export const userService = {
|
||||||
|
async createUser(database, usrnam, usreml) {
|
||||||
|
const sql = `
|
||||||
|
SELECT * FROM ${database}.usrmst
|
||||||
|
WHERE usrnam = $1 OR usreml = $2
|
||||||
|
`
|
||||||
|
const params = [usrnam, usreml]
|
||||||
|
const result = await executeQueryParam(sql, database, params)
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
}
|
||||||
179
exthernal-mobile-api/src/share/generalservice.js
Normal file
179
exthernal-mobile-api/src/share/generalservice.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { connection } from '../config/db.js'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
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)
|
||||||
|
|
||||||
|
if (!isEnabled || level > currentLevel) return
|
||||||
|
|
||||||
|
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)
|
||||||
|
} 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)
|
||||||
|
|
||||||
|
// 🧩 แสดงเฉพาะเมื่อ DEVHINT_LEVEL >= 2
|
||||||
|
devhint(2, 'executeQueryConditions', `📤 Executing Query`, {
|
||||||
|
database,
|
||||||
|
sql: formattedSQL,
|
||||||
|
params
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await connection.query(formattedSQL, params)
|
||||||
|
|
||||||
|
devhint(2, 'executeQueryConditions', `✅ Query Success (${result.rowCount} rows)`)
|
||||||
|
return result.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================
|
||||||
|
// ✅ 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 }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * ✅ 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-mobile-api/src/utils/errorList.js
Normal file
40
exthernal-mobile-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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
41
exthernal-mobile-api/src/utils/oftenError.js
Normal file
41
exthernal-mobile-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";
|
||||||
|
}
|
||||||
|
}
|
||||||
29
exthernal-mobile-api/src/utils/response.js
Normal file
29
exthernal-mobile-api/src/utils/response.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
// ✅ ถ้าไม่ใช่ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(status).json(response)
|
||||||
|
}
|
||||||
11
exthernal-mobile-api/src/utils/trim.js
Normal file
11
exthernal-mobile-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
|
||||||
|
}
|
||||||
16
node_modules/.bin/acorn
generated
vendored
Normal file
16
node_modules/.bin/acorn
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../acorn/bin/acorn" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../acorn/bin/acorn" "$@"
|
||||||
|
fi
|
||||||
17
node_modules/.bin/acorn.cmd
generated
vendored
Normal file
17
node_modules/.bin/acorn.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\acorn\bin\acorn" %*
|
||||||
28
node_modules/.bin/acorn.ps1
generated
vendored
Normal file
28
node_modules/.bin/acorn.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../acorn/bin/acorn" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../acorn/bin/acorn" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../acorn/bin/acorn" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../acorn/bin/acorn" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
node_modules/.bin/crc32
generated
vendored
Normal file
16
node_modules/.bin/crc32
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../crc-32/bin/crc32.njs" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../crc-32/bin/crc32.njs" "$@"
|
||||||
|
fi
|
||||||
17
node_modules/.bin/crc32.cmd
generated
vendored
Normal file
17
node_modules/.bin/crc32.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\crc-32\bin\crc32.njs" %*
|
||||||
28
node_modules/.bin/crc32.ps1
generated
vendored
Normal file
28
node_modules/.bin/crc32.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
node_modules/.bin/eslint
generated
vendored
Normal file
16
node_modules/.bin/eslint
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../eslint/bin/eslint.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../eslint/bin/eslint.js" "$@"
|
||||||
|
fi
|
||||||
17
node_modules/.bin/eslint.cmd
generated
vendored
Normal file
17
node_modules/.bin/eslint.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\eslint\bin\eslint.js" %*
|
||||||
28
node_modules/.bin/eslint.ps1
generated
vendored
Normal file
28
node_modules/.bin/eslint.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../eslint/bin/eslint.js" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../eslint/bin/eslint.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../eslint/bin/eslint.js" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../eslint/bin/eslint.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
node_modules/.bin/js-yaml
generated
vendored
Normal file
16
node_modules/.bin/js-yaml
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../js-yaml/bin/js-yaml.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../js-yaml/bin/js-yaml.js" "$@"
|
||||||
|
fi
|
||||||
17
node_modules/.bin/js-yaml.cmd
generated
vendored
Normal file
17
node_modules/.bin/js-yaml.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\js-yaml\bin\js-yaml.js" %*
|
||||||
28
node_modules/.bin/js-yaml.ps1
generated
vendored
Normal file
28
node_modules/.bin/js-yaml.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../js-yaml/bin/js-yaml.js" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../js-yaml/bin/js-yaml.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../js-yaml/bin/js-yaml.js" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../js-yaml/bin/js-yaml.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
node_modules/.bin/mime
generated
vendored
Normal file
16
node_modules/.bin/mime
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../mime/cli.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../mime/cli.js" "$@"
|
||||||
|
fi
|
||||||
17
node_modules/.bin/mime.cmd
generated
vendored
Normal file
17
node_modules/.bin/mime.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mime\cli.js" %*
|
||||||
28
node_modules/.bin/mime.ps1
generated
vendored
Normal file
28
node_modules/.bin/mime.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../mime/cli.js" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../mime/cli.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../mime/cli.js" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../mime/cli.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user