-web-service

This commit is contained in:
2025-11-11 15:11:56 +07:00
parent 9ad26fa5ef
commit fcf59ce5db
21 changed files with 537 additions and 253 deletions

View File

@@ -16,4 +16,4 @@ DEVHINT=true
DEVHINT_LEVEL=3
#PORT
PORT=1012
PORT=1013

View File

@@ -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}`)

View File

@@ -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() }
}
}

View File

@@ -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()

View 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()
}

View File

@@ -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()
}

View 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('เกิดข้อผิดพลาดในระบบ');
}
}

View File

@@ -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

View File

@@ -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

View 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')
}
}
}

View 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
};
}
}

View 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://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 = `
<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');
}
}
}

View 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: 'รีเซ็ตรหัสผ่านสำเร็จ'
};
}
}

View 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
},
}

View 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,
},
};
}
}

View File

@@ -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 หรืออื่นๆ เรียกใช้ได้ด้วย
// ===================================================

View 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
})
}

View File

@@ -0,0 +1,3 @@
export function generateOTP(length = 6) {
return Math.floor(100000 + Math.random() * 900000).toString()
}

View 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

View File

@@ -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
}
}

View File

@@ -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}`);