diff --git a/exthernal-accountingwep-api/.env b/exthernal-accountingwep-api/.env
index 18c79d3..b5d922a 100644
--- a/exthernal-accountingwep-api/.env
+++ b/exthernal-accountingwep-api/.env
@@ -16,4 +16,4 @@ DEVHINT=true
DEVHINT_LEVEL=3
#PORT
-PORT=1012
+PORT=1013
diff --git a/exthernal-accountingwep-api/src/app.js b/exthernal-accountingwep-api/src/app.js
index 28d6e29..48ea973 100644
--- a/exthernal-accountingwep-api/src/app.js
+++ b/exthernal-accountingwep-api/src/app.js
@@ -2,30 +2,30 @@ import express from 'express'
import cors from 'cors'
import dotenv from 'dotenv'
import router from './routes/route.js'
-import { validateJsonFormat } from './middlewares/validate.js'
+import { globalResponseHandler } from './middlewares/responseHandler.js'
dotenv.config()
const app = express()
app.use(cors())
-
-// ✅ ตรวจจับ JSON format error ก่อน parser
app.use(express.json({ limit: '10mb' }))
-app.use(validateJsonFormat)
+
+app.use(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)
-// 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}`)
diff --git a/exthernal-accountingwep-api/src/controllers/logincontroller.js b/exthernal-accountingwep-api/src/controllers/logincontroller.js
deleted file mode 100644
index e55e2a8..0000000
--- a/exthernal-accountingwep-api/src/controllers/logincontroller.js
+++ /dev/null
@@ -1,85 +0,0 @@
-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() }
- }
-}
diff --git a/exthernal-accountingwep-api/src/middlewares/auth.js b/exthernal-accountingwep-api/src/middlewares/auth.js
index 739ee12..d99d548 100644
--- a/exthernal-accountingwep-api/src/middlewares/auth.js
+++ b/exthernal-accountingwep-api/src/middlewares/auth.js
@@ -1,13 +1,13 @@
import { verifyToken } from '../utils/token.js'
-import { sendResponse } from '../utils/response.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 sendResponse(res, 401, 'ไม่พบ Token', 'Missing token')
+ if (!token) return sendError('ไม่พบ Token', 'Missing token', 401)
const decoded = verifyToken(token)
- if (!decoded) return sendResponse(res, 403, 'Token ไม่ถูกต้อง', 'Invalid token')
+ if (!decoded) return sendError('Token ไม่ถูกต้อง', 'Invalid token', 403)
req.user = decoded
next()
diff --git a/exthernal-accountingwep-api/src/middlewares/responseHandler.js b/exthernal-accountingwep-api/src/middlewares/responseHandler.js
new file mode 100644
index 0000000..ce05cab
--- /dev/null
+++ b/exthernal-accountingwep-api/src/middlewares/responseHandler.js
@@ -0,0 +1,20 @@
+import { formatSuccessResponse } from '../utils/response.js'
+
+export function globalResponseHandler(req, res, next) {
+ const oldJson = res.json.bind(res)
+
+ res.json = (data) => {
+ if (!data) return oldJson(formatSuccessResponse(null))
+
+ // ถ้า code ไม่ใช่ 200 → ตั้ง HTTP status ให้ตรงกับ code
+ if (data?.code && String(data.code) !== '200') {
+ res.status(Number(data.code) || 400)
+ return oldJson(data)
+ }
+
+ res.status(200)
+ return oldJson(formatSuccessResponse(data))
+ }
+
+ next()
+}
diff --git a/exthernal-accountingwep-api/src/middlewares/validate.js b/exthernal-accountingwep-api/src/middlewares/validate.js
index a0d69bb..e4993e3 100644
--- a/exthernal-accountingwep-api/src/middlewares/validate.js
+++ b/exthernal-accountingwep-api/src/middlewares/validate.js
@@ -1,4 +1,4 @@
-import { sendResponse } from '../utils/response.js'
+import { sendError } from '../utils/response.js'
/**
* ✅ Middleware สำหรับตรวจสอบความถูกต้องของ JSON body
@@ -7,7 +7,7 @@ import { sendResponse } from '../utils/response.js'
export function validateJsonFormat(err, req, res, next) {
if (err instanceof SyntaxError && 'body' in err) {
console.error('[Invalid JSON Format]', err.message)
- return sendResponse(res, 400, 'รูปแบบ บอร์ดี้ ไม่ถูกต้อง', 'Invalid Body format')
+ return sendError('รูปแบบ บอร์ดี้ ไม่ถูกต้อง', 'Invalid Body format')
}
next()
}
diff --git a/exthernal-accountingwep-api/src/middlewares/verifyEmailHandler.js b/exthernal-accountingwep-api/src/middlewares/verifyEmailHandler.js
new file mode 100644
index 0000000..caa253f
--- /dev/null
+++ b/exthernal-accountingwep-api/src/middlewares/verifyEmailHandler.js
@@ -0,0 +1,37 @@
+import Redis from 'ioredis';
+import { GeneralService } from '../share/generalservice.js';
+// import { sendError } from './response.js';
+
+export async function verifyEmailHandler(req, res) {
+ const redis = new Redis();
+ const generalService = new GeneralService();
+
+ try {
+ const { email, token } = req.query;
+ const schema = req.body?.organization || 'nuttakit'; // 🧩 ใช้ schema ตาม org
+ const storedData = await redis.get(`verify:${email}`);
+
+ if (!storedData) {
+ return res.status(400).send('ลิงก์หมดอายุหรือไม่ถูกต้อง');
+ }
+
+ const { fname, lname, hashedPwd, token: storedToken } = JSON.parse(storedData);
+ if (token !== storedToken) {
+ return res.status(400).send('Token ไม่ถูกต้อง');
+ }
+
+ let sql = `
+ INSERT INTO ${schema}.usrmst (usrnam, usrthinam, usrthilstnam, usrpwd, usrrol)
+ VALUES ($1, $2, $3, $4, 'U')
+ `;
+ let param = [email, fname, lname, hashedPwd];
+ await generalService.executeQueryParam(sql, param);
+
+ await redis.del(`verify:${email}`);
+
+ res.send(`
✅ ยืนยันอีเมลสำเร็จ บัญชีของคุณถูกสร้างแล้ว (${schema})
`);
+ } catch (error) {
+ console.error('❌ [Verify Email Error]', error);
+ res.status(500).send('เกิดข้อผิดพลาดในระบบ');
+ }
+}
diff --git a/exthernal-accountingwep-api/src/routes/route.js b/exthernal-accountingwep-api/src/routes/route.js
index 4772a27..dc61da5 100644
--- a/exthernal-accountingwep-api/src/routes/route.js
+++ b/exthernal-accountingwep-api/src/routes/route.js
@@ -1,48 +1,45 @@
-// ===================================================
-// ⚙️ 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'
+// 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()
+// 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)
-})
+// 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 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)
-})
+// // ===================================================
+// // 🔹 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)
-})
+// // ===================================================
+// // 🔹 TOKEN RENEW (ต่ออายุ Token)
+// // ===================================================
+// router.post('/token/renew', authMiddleware, async (req, res) => {
+// const data = await controller_login_post.onRenewToken(req, res)
+// if (data)
+// return sendResponse(res, 200, 'ออก Token ใหม่สำเร็จ', 'Token renewed', data)
+// })
export default router
diff --git a/exthernal-accountingwep-api/src/services/loginservice.js b/exthernal-accountingwep-api/src/services/loginservice.js
index f33dea4..8f355f5 100644
--- a/exthernal-accountingwep-api/src/services/loginservice.js
+++ b/exthernal-accountingwep-api/src/services/loginservice.js
@@ -1,34 +1,23 @@
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
+ SELECT usrseq, usrnam, usrorg, usrrol, usrpwd, usrthinam, usrthilstnam
+ FROM nuttakit.usrmst
WHERE usrnam = $1
`
- let params = [username] // ✅ ห้ามลืมเด็ดขาด
+ let params = [username]
const rows = await this.generalService.executeQueryParam(database, sql, params)
this.generalService.devhint(3, 'loginservice.js', `query done, found=${rows.length}`)
@@ -37,41 +26,41 @@ export class LoginService {
return null
}
- // Zone 3️⃣ : Validate password
user = rows[0]
const match = await bcrypt.compare(password, user.usrpwd)
- if (!match) {
+ if (match === false) {
this.generalService.devhint(2, 'loginservice.js', 'password mismatch')
return null
}
- // Zone 4️⃣ : Generate JWT Token
token = generateToken({
id: user.usrseq,
name: user.usrnam,
+ realname: user.usrthinam,
+ lastname: user.usrthilstnam,
role: user.usrrol,
- organization: database
+ organization: user.usrorg
})
this.generalService.devhint(2, 'loginservice.js', 'token generated successfully')
- // Zone 5️⃣ : Return Raw Result
+
+ delete user.usrseq
+ delete user.usrnam
+ delete user.usrrol
+ delete user.usrpwd
+ delete user.usrorg
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
@@ -84,7 +73,6 @@ export class LoginService {
return null
}
- // Zone 3️⃣ : Generate Token
const user = rows[0]
const token = generateToken({
id: user.usrid,
@@ -97,17 +85,13 @@ export class LoginService {
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
diff --git a/exthernal-accountingwep-api/src/services/otpservice.js b/exthernal-accountingwep-api/src/services/otpservice.js
new file mode 100644
index 0000000..4ce4834
--- /dev/null
+++ b/exthernal-accountingwep-api/src/services/otpservice.js
@@ -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')
+ }
+ }
+}
diff --git a/exthernal-accountingwep-api/src/services/otpverifyservice.js b/exthernal-accountingwep-api/src/services/otpverifyservice.js
new file mode 100644
index 0000000..7bf59f6
--- /dev/null
+++ b/exthernal-accountingwep-api/src/services/otpverifyservice.js
@@ -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
+ };
+ }
+}
diff --git a/exthernal-accountingwep-api/src/services/registerservice.js b/exthernal-accountingwep-api/src/services/registerservice.js
new file mode 100644
index 0000000..2b93131
--- /dev/null
+++ b/exthernal-accountingwep-api/src/services/registerservice.js
@@ -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://localhost:1012/login/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 = `
+
+
ยืนยันการสมัครสมาชิก
+
กรุณากดยืนยันภายใน 24 ชั่วโมง เพื่อเปิดใช้งานบัญชีของคุณ
+
ยืนยันอีเมล
+
หากคุณไม่ได้สมัคร โปรดละเว้นอีเมลนี้
+
+ `;
+
+ 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');
+ }
+ }
+}
diff --git a/exthernal-accountingwep-api/src/services/resetpasswordservice.js b/exthernal-accountingwep-api/src/services/resetpasswordservice.js
new file mode 100644
index 0000000..bfaecfd
--- /dev/null
+++ b/exthernal-accountingwep-api/src/services/resetpasswordservice.js
@@ -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: 'รีเซ็ตรหัสผ่านสำเร็จ'
+ };
+ }
+}
diff --git a/exthernal-accountingwep-api/src/services/userservice.js b/exthernal-accountingwep-api/src/services/userservice.js
new file mode 100644
index 0000000..fb5fcd6
--- /dev/null
+++ b/exthernal-accountingwep-api/src/services/userservice.js
@@ -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
+ },
+}
diff --git a/exthernal-accountingwep-api/src/services/verifyemailservice.js b/exthernal-accountingwep-api/src/services/verifyemailservice.js
new file mode 100644
index 0000000..02a05f5
--- /dev/null
+++ b/exthernal-accountingwep-api/src/services/verifyemailservice.js
@@ -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,
+ },
+ };
+ }
+}
diff --git a/exthernal-accountingwep-api/src/share/generalservice.js b/exthernal-accountingwep-api/src/share/generalservice.js
index 652ef5e..c2cada3 100644
--- a/exthernal-accountingwep-api/src/share/generalservice.js
+++ b/exthernal-accountingwep-api/src/share/generalservice.js
@@ -1,11 +1,9 @@
import { connection } from '../config/db.js'
import dotenv from 'dotenv'
+import { sendError } from '../utils/response.js'
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)
@@ -14,12 +12,38 @@ export class GeneralService {
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)
+
+ // 🔹 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)
}
// ===================================================
- // ✅ executeQueryConditions()
+ // ✅ 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')
@@ -30,7 +54,6 @@ export class GeneralService {
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()
@@ -48,39 +71,22 @@ export class GeneralService {
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 จะจับได้
+ 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 หรืออื่นๆ เรียกใช้ได้ด้วย
// ===================================================
diff --git a/exthernal-accountingwep-api/src/utils/mailer.js b/exthernal-accountingwep-api/src/utils/mailer.js
new file mode 100644
index 0000000..0237e29
--- /dev/null
+++ b/exthernal-accountingwep-api/src/utils/mailer.js
@@ -0,0 +1,62 @@
+import nodemailer from 'nodemailer'
+import dotenv from 'dotenv'
+dotenv.config()
+
+export async function sendMockOtpMail(to, otp) {
+ const transporter = nodemailer.createTransport({
+ service: 'gmail',
+ auth: {
+ user: process.env.SMTP_USER,
+ pass: process.env.SMTP_PASS
+ }
+ })
+
+const html = `
+
+
+

+
รหัส OTP สำหรับเปลี่ยนรหัสผ่าน
+
กรุณาใส่รหัสยืนยันด้านล่างนี้
+
${otp}
+
รหัสนี้จะหมดอายุใน 5 นาที
+
+
หากคุณไม่ได้ร้องขอการเปลี่ยนรหัสผ่าน กรุณาละเว้นอีเมลนี้
+
+
+`
+
+ await transporter.sendMail({
+ from: `"Support" <${process.env.SMTP_USER}>`,
+ to,
+ subject: 'OTP สำหรับเปลี่ยนรหัสผ่าน',
+ html
+ })
+}
diff --git a/exthernal-accountingwep-api/src/utils/otp.js b/exthernal-accountingwep-api/src/utils/otp.js
new file mode 100644
index 0000000..78185b7
--- /dev/null
+++ b/exthernal-accountingwep-api/src/utils/otp.js
@@ -0,0 +1,3 @@
+export function generateOTP(length = 6) {
+ return Math.floor(100000 + Math.random() * 900000).toString()
+}
diff --git a/exthernal-accountingwep-api/src/utils/redis.js b/exthernal-accountingwep-api/src/utils/redis.js
new file mode 100644
index 0000000..fd9023c
--- /dev/null
+++ b/exthernal-accountingwep-api/src/utils/redis.js
@@ -0,0 +1,28 @@
+import Redis from 'ioredis'
+import dotenv from 'dotenv'
+dotenv.config()
+
+const redis = new Redis({
+ host: process.env.REDIS_HOST,
+ port: process.env.REDIS_PORT
+})
+
+export async function saveOtp(email, otp) {
+ const key = `otp:${email}`
+ const ttl = parseInt(process.env.OTP_TTL_SECONDS || '300')
+ await redis.setex(key, ttl, otp)
+}
+
+export async function verifyOtp(email, otp) {
+ const key = `otp:${email}`
+ const stored = await redis.get(key)
+ if (!stored) return false
+ return stored === otp
+}
+
+export async function removeOtp(email) {
+ const key = `otp:${email}`
+ await redis.del(key)
+}
+
+export default redis
diff --git a/exthernal-accountingwep-api/src/utils/response.js b/exthernal-accountingwep-api/src/utils/response.js
index 560a84b..ed277ac 100644
--- a/exthernal-accountingwep-api/src/utils/response.js
+++ b/exthernal-accountingwep-api/src/utils/response.js
@@ -1,43 +1,24 @@
-/**
- * sendResponse
- * ----------------------------------------------
- * ส่ง response แบบมาตรฐาน รองรับข้อความ 2 ภาษา
- * ----------------------------------------------
- * @param {object} res - Express response object
- * @param {number} status - HTTP Status code (200, 400, 500, etc.)
- * @param {string} msg_th - ข้อความภาษาไทย
- * @param {string} msg_en - ข้อความภาษาอังกฤษ
- * @param {any} [data=null] - optional data
- */
+// ===================================================
+// ⚙️ Nuttakit Response Layer vFinal++++++
+// ===================================================
-// ===================================================
-// 🧩 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]'
+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
}
}
diff --git a/exthernal-login-api/src/services/registerservice.js b/exthernal-login-api/src/services/registerservice.js
index de8a96e..2b93131 100644
--- a/exthernal-login-api/src/services/registerservice.js
+++ b/exthernal-login-api/src/services/registerservice.js
@@ -34,7 +34,7 @@ export class RegisterService {
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}`;
+ const verifyUrl = `http://localhost:1012/login/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}`);