From fcf59ce5db3dc4c59a31e8afee4dc3d99ae5db07 Mon Sep 17 00:00:00 2001 From: x2Skyz Date: Tue, 11 Nov 2025 15:11:56 +0700 Subject: [PATCH] -web-service --- exthernal-accountingwep-api/.env | 2 +- exthernal-accountingwep-api/src/app.js | 32 +++---- .../src/controllers/logincontroller.js | 85 ------------------ .../src/middlewares/auth.js | 6 +- .../src/middlewares/responseHandler.js | 20 +++++ .../src/middlewares/validate.js | 4 +- .../src/middlewares/verifyEmailHandler.js | 37 ++++++++ .../src/routes/route.js | 69 +++++++-------- .../src/services/loginservice.js | 46 ++++------ .../src/services/otpservice.js | 17 ++++ .../src/services/otpverifyservice.js | 29 ++++++ .../src/services/registerservice.js | 88 +++++++++++++++++++ .../src/services/resetpasswordservice.js | 38 ++++++++ .../src/services/userservice.js | 13 +++ .../src/services/verifyemailservice.js | 66 ++++++++++++++ .../src/share/generalservice.js | 82 +++++++++-------- .../src/utils/mailer.js | 62 +++++++++++++ exthernal-accountingwep-api/src/utils/otp.js | 3 + .../src/utils/redis.js | 28 ++++++ .../src/utils/response.js | 61 +++++-------- .../src/services/registerservice.js | 2 +- 21 files changed, 537 insertions(+), 253 deletions(-) delete mode 100644 exthernal-accountingwep-api/src/controllers/logincontroller.js create mode 100644 exthernal-accountingwep-api/src/middlewares/responseHandler.js create mode 100644 exthernal-accountingwep-api/src/middlewares/verifyEmailHandler.js create mode 100644 exthernal-accountingwep-api/src/services/otpservice.js create mode 100644 exthernal-accountingwep-api/src/services/otpverifyservice.js create mode 100644 exthernal-accountingwep-api/src/services/registerservice.js create mode 100644 exthernal-accountingwep-api/src/services/resetpasswordservice.js create mode 100644 exthernal-accountingwep-api/src/services/userservice.js create mode 100644 exthernal-accountingwep-api/src/services/verifyemailservice.js create mode 100644 exthernal-accountingwep-api/src/utils/mailer.js create mode 100644 exthernal-accountingwep-api/src/utils/otp.js create mode 100644 exthernal-accountingwep-api/src/utils/redis.js 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 = ` +
+
+ Logo +

รหัส 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}`);