-api เส้น register

-เครื่องมือต่างๆ (ไม่สมบูร)
This commit is contained in:
x2Skyz
2025-11-21 12:09:43 +07:00
parent 4cb135d251
commit f8344e7afc
13 changed files with 561 additions and 139 deletions

View File

@@ -25,7 +25,7 @@ app.use((err, req, res, next) => {
next()
})
app.use('/api', router)
app.use('/api/login', router)
app.listen(process.env.PORT, () => {
console.log(`${process.env.PJ_NAME} running on port ${process.env.PORT}`)

View File

@@ -33,35 +33,7 @@ export class loginController {
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
if (!result) return sendError('ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง', 'Invalid credentials');
if(result) { return result }
return null
return result
}
}
// 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,26 +1,72 @@
import { RegisterService } from '../services/registerservice.js';
import { sendError } from '../utils/response.js';
import { RegisterService } from '../services/registerservice.js'
import { sendError } from '../utils/response.js'
import { GeneralService } from '../share/generalservice.js';
import bcrypt from 'bcrypt'
import { Interface } from '../interfaces/Interface.js';
import { validateSave } from '../utils/validate.js'; // import เข้ามา
export class RegisterController {
constructor() {
this.registerService = new RegisterService();
this.generalService = new GeneralService();
this.registerService = new RegisterService();
this.Interface = new Interface();
}
async onNavigate(req, res) {
let idx = -1, result = [];
this.generalService.devhint(1, 'registercontroller.js', 'onNavigate() start');
let organization = req.body.organization;
const prommis = await this.onRegisterController(req, res, organization);
return prommis;
}
async onRegisterController(req, res, database) {
let idx = -1
let result = []
try {
const { organization, request } = req.body;
const { email, fname, lname, password } = request;
result = await this.registerService.requestRegistration(organization, email, fname, lname, password);
// 1. ดึง Sequence ล่าสุดจาก Service (เพื่อเอามา +1)
const Seq = await this.registerService.genNum(database);
// 2. Hash Password
let passwordRaw = req.body.request.password;
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(passwordRaw, saltRounds);
// 3. เรียก makeArySave เพื่อเตรียมข้อมูลและบันทึกผ่าน saveInterface
// ส่ง nextSeq และ hashedPassword เข้าไป
result = await this.makeArySave(req, Seq, hashedPassword);
this.generalService.devhint(1, 'registercontroller.js', 'Register success');
} catch (error) {
idx = 1;
this.generalService.devhint(1, 'registercontroller.js', 'Jumpout', error.message);
result = error; // จะถูก Global Handler จัด format
} finally {
if (idx === 1) return result;
return result;
} finally {
if (idx === 1) return sendError('เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', 'Unexpected error');
if (!result) return sendError('ไม่สามารถลงทะเบียนได้', 'Registration failed');
return result
}
}
}
async makeArySave(req, seq, hashedPassword) {
// Map ข้อมูลเข้า Field ตามตาราง usrmst
let arysave = {
methods: 'post', // สั่งให้ saveInterface ทำการ INSERT
usrseq: seq + 1, // PK: integer
usrnam: validateSave(req.body.request.username, 'username'),
usrpwd: hashedPassword, // character varying(255)
usreml: validateSave(req.body.request.email, 'Email'), // character varying(50)
usrthinam: validateSave(req.body.request.firstname, 'firstname'), // character varying(100)
usrthilstnam: validateSave(req.body.request.lastname, 'lastname'), // character varying(100)
// Field อื่นๆ ที่อาจต้อง Default ค่าไว้ก่อน (ตาม Schema)
usrrol: 'U', // Default User Role
adpdte: '', // Approved Date (รออนุมัติ)
expdte: '', // Expire Date
lstlgn: '', // Last Login
usrorg: req.body.request.organization, // ถ้ามี field นี้
}
// เรียก saveInterface ผ่าน generalService (ระบุชื่อตาราง 'usrmst')
return this.Interface.saveInterface('usrmst', arysave, req);
}
}

View File

@@ -0,0 +1,70 @@
import jwt from 'jsonwebtoken'
import { BdgmstInterface } from './table/bdgmstInterface.js'
import { ActmstInterface } from './table/actmstInterface.js'
import { UsrmstInterface } from './table/usrmstInterface.js'
import { sendError } from '../utils/response.js'
// -------------------------------
// GLOBAL FILE
// -----------------------------
export class Interface {
constructor() {
this.map = {
bdgmst: new BdgmstInterface(),
actmst: new ActmstInterface(),
usrmst: new UsrmstInterface(),
}
}
// ===============================================================
// saveInterface → แกะ token เอง และ route ไปยัง interface เฉพาะ table
// ===============================================================
async saveInterface(tableName, data, req) {
// ------------------------------
// 1) จับ Interface ที่ตรงกับ table
// ------------------------------
const handler = this.map[tableName.toLowerCase()]
if (!handler) {
return new sendError(`Interface not found for table: ${tableName}`)
}
let schema;
// ------------------------------
// 2) ตรวจสอบเงื่อนไข (Exception for usrmst)
// ------------------------------
if (tableName.toLowerCase() === 'usrmst') {
// กรณี usrmst (เช่น Register/Login) ไม่บังคับ Token
// เราต้องกำหนด schema เอง เพราะไม่มี token ให้แกะ
schema = 'nuttakit'
} else {
// ------------------------------
// 3) ตารางอื่น ๆ บังคับ Token ตามปกติ
// ------------------------------
const token = req.headers.authorization?.split(' ')[1]
if (!token) {
return new sendError('ไม่พบการยืนยันตัวตน' ,'Missing token in request header')
}
let decoded
try {
decoded = jwt.verify(token, process.env.JWT_SECRET)
} catch (err) {
return new sendError('Invalid token: ' + err.message)
}
schema = decoded.organization
}
if (!schema) return new sendError("Token missing 'organization' field or Schema undefined")
// ------------------------------
// ✔ 4) ส่งงานไปยัง interface ของ table นั้น ๆ
// ------------------------------
return await handler.saveInterface(schema, data)
}
}

View File

@@ -0,0 +1,86 @@
import { GeneralService } from '../../share/generalservice.js'
export class ActmstInterface {
constructor() {
this.general = new GeneralService()
this.table = 'actmst'
this.pk = ['actseq', 'actnum'] // ⭐ PK ตาม CREATE TABLE
}
async saveInterface(database, data) {
const method = data.methods.toLowerCase()
const { methods, ...payload } = data
if (method === 'put') return this.update(database, payload)
if (method === 'post') return this.insert(database, payload)
if (method === 'delete') return this.remove(database, payload)
throw new Error(`Unknown method: ${method}`)
}
async insert(database, payload) {
const cols = Object.keys(payload)
const vals = Object.values(payload)
const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ')
const sql = `
INSERT INTO ${database}.${this.table} (${cols.join(', ')})
VALUES (${placeholders})
RETURNING *
`
return await this.general.executeQueryParam(database, sql, vals)
}
async update(database, payload) {
const where = {}
const update = {}
for (const col in payload) {
if (this.pk.includes(col)) where[col] = payload[col]
else update[col] = payload[col]
}
const setCols = Object.keys(update)
.map((col, i) => `${col} = $${i + 1}`)
.join(', ')
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1 + Object.keys(update).length}`)
.join(' AND ')
const params = [...Object.values(update), ...Object.values(where)]
const sql = `
UPDATE ${database}.${this.table}
SET ${setCols}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
async remove(database, payload) {
const where = {}
this.pk.forEach(pk => {
if (!payload[pk]) throw new Error(`Missing PK: ${pk}`)
where[pk] = payload[pk]
})
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1}`)
.join(' AND ')
const params = Object.values(where)
const sql = `
DELETE FROM ${database}.${this.table}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
}

View File

@@ -0,0 +1,86 @@
import { GeneralService } from '../../share/generalservice.js'
export class BdgmstInterface {
constructor() {
this.general = new GeneralService()
this.table = 'bdgmst'
this.pk = ['bdgseq']
}
async saveInterface(database, data) {
const method = data.method.toLowerCase()
const payload = { ...data }
delete payload.method
if (method === 'put') return this.update(database, payload)
if (method === 'post') return this.insert(database, payload)
if (method === 'delete') return this.remove(database, payload)
throw new Error(`Unknown method: ${method}`)
}
async insert(database, payload) {
const cols = Object.keys(payload)
const vals = Object.values(payload)
const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ')
const sql = `
INSERT INTO ${database}.${this.table} (${cols.join(', ')})
VALUES (${placeholders})
RETURNING *
`
return await this.general.executeQueryParam(database, sql, vals)
}
async update(database, payload) {
const where = {}
const update = {}
for (const col in payload) {
if (this.pk.includes(col)) where[col] = payload[col]
else update[col] = payload[col]
}
const setCols = Object.keys(update)
.map((col, i) => `${col} = $${i + 1}`)
.join(', ')
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1 + Object.keys(update).length}`)
.join(' AND ')
const params = [...Object.values(update), ...Object.values(where)]
const sql = `
UPDATE ${database}.${this.table}
SET ${setCols}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
async remove(database, payload) {
const where = {}
this.pk.forEach(pk => {
if (!payload[pk]) throw new Error(`Missing PK: ${pk}`)
where[pk] = payload[pk]
})
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1}`)
.join(' AND ')
const params = Object.values(where)
const sql = `
DELETE FROM ${database}.${this.table}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
}

View File

@@ -0,0 +1,86 @@
import { GeneralService } from '../../share/generalservice.js'
export class UsrmstInterface {
constructor() {
this.general = new GeneralService()
this.table = 'usrmst'
this.pk = ['usrseq'] // ⭐ PK ตาม CREATE TABLE
}
async saveInterface(database, data) {
const method = data.methods.toLowerCase()
const { methods, ...payload } = data
if (method === 'put') return this.update(database, payload)
if (method === 'post') return this.insert(database, payload)
if (method === 'delete') return this.remove(database, payload)
throw new Error(`Unknown method: ${method}`)
}
async insert(database, payload) {
const cols = Object.keys(payload)
const vals = Object.values(payload)
const placeholders = cols.map((_, i) => `$${i + 1}`).join(', ')
const sql = `
INSERT INTO ${database}.${this.table} (${cols.join(', ')})
VALUES (${placeholders})
RETURNING *
`
return await this.general.executeQueryParam(database, sql, vals)
}
async update(database, payload) {
const where = {}
const update = {}
for (const col in payload) {
if (this.pk.includes(col)) where[col] = payload[col]
else update[col] = payload[col]
}
const setCols = Object.keys(update)
.map((col, i) => `${col} = $${i + 1}`)
.join(', ')
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1 + Object.keys(update).length}`)
.join(' AND ')
const params = [...Object.values(update), ...Object.values(where)]
const sql = `
UPDATE ${database}.${this.table}
SET ${setCols}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
async remove(database, payload) {
const where = {}
this.pk.forEach(pk => {
if (!payload[pk]) throw new Error(`Missing PK: ${pk}`)
where[pk] = payload[pk]
})
const whereCols = Object.keys(where)
.map((col, i) => `${col} = $${i + 1}`)
.join(' AND ')
const params = Object.values(where)
const sql = `
DELETE FROM ${database}.${this.table}
WHERE ${whereCols}
RETURNING *
`
return await this.general.executeQueryParam(database, sql, params)
}
}

View File

@@ -18,18 +18,18 @@ const controller_login_post = new loginController()
const otpController = new OtpController()
const otpVerifyController = new OtpVerifyController()
router.post('/login/login', async (req, res) => {const result = await controller_login_post.onNavigate(req, res); return res.json(result)})
router.post('/login', async (req, res) => {const result = await controller_login_post.onNavigate(req, res); return res.json(result)})
router.post('/login/otp/send', async (req, res) => {
router.post('/otp/send', async (req, res) => {
const result = await otpController.onSendOtp(req, res)
if (result) return res.json(result)
})
router.post('/login/register', async (req, res) => {
router.post('/register', async (req, res) => {
const result = await registerController.onNavigate(req, res);
if (result) return res.json(result);
});
router.get('/login/verify-email', async (req, res) => {
router.get('/verify-email', async (req, res) => {
const result = await verifyEmailController.onVerifyEmail(req, res);
if (result?.code && result.code !== '200') return res.status(400).send(result.message_th);
// ถ้า controller ส่ง HTML string กลับมา → render ตรง ๆ
@@ -38,13 +38,13 @@ router.get('/login/verify-email', async (req, res) => {
});
router.post('/login/reset-password', async (req, res) => {
router.post('/reset-password', async (req, res) => {
const result = await resetPasswordController.onNavigate(req, res);
if (result) return res.json(result);
});
router.post('/login/otp/verify', async (req, res) => {
router.post('/otp/verify', async (req, res) => {
const result = await otpVerifyController.onNavigate(req, res)
if (result) return res.json(result)
})

View File

@@ -6,7 +6,7 @@ export class LoginService {
constructor() {
this.generalService = new GeneralService()
}
async verifyLogin(database, username, password) {
async verifyLogin(database, username) {
this.generalService.devhint(2, 'loginservice.js', `verifyLogin() start for username=${username}`)
let user = null
@@ -15,7 +15,7 @@ export class LoginService {
let sql = `
SELECT usrseq, usrnam, usrorg, usrrol, usrpwd, usrthinam, usrthilstnam
FROM nuttakit.usrmst
WHERE usrnam = $1
WHERE usrnam = $1 OR (usreml = $1)
`
let params = [username]
const rows = await this.generalService.executeQueryParam(database, sql, params)

View File

@@ -0,0 +1,88 @@
import bcrypt from 'bcrypt';
import crypto from 'crypto';
import nodemailer from 'nodemailer';
import { GeneralService } from '../share/generalservice.js';
import { sendError } from '../utils/response.js';
import redis from '../utils/redis.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 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

@@ -1,88 +1,57 @@
import bcrypt from 'bcrypt';
import crypto from 'crypto';
import nodemailer from 'nodemailer';
import { GeneralService } from '../share/generalservice.js';
import { sendError } from '../utils/response.js';
import redis from '../utils/redis.js';
import { GeneralService } from '../share/generalservice.js'
import bcrypt from 'bcrypt'
export class RegisterService {
constructor() {
// this.redis = new Redis();
this.generalService = new GeneralService();
this.generalService = new GeneralService()
}
async requestRegistration(database, email, fname, lname, password) {
let result = [];
async createUser(database, userData) {
// 1. ทำการ Hash Password
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(userData.password, saltRounds);
// 2. เตรียม SQL
const sql = `
INSERT INTO ${database}.usrmst
(username, password, email, firstname, lastname, created_at)
VALUES (?, ?, ?, ?, ?, NOW())
`
// 3. ใช้ hashedPassword แทน password เดิม
const params = [
userData.username,
hashedPassword, // ส่งตัวที่ Hash แล้วเข้า DB
userData.email,
userData.firstname,
userData.lastname
]
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 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: {}
};
const result = await this.generalService.executeQueryParam(database, sql, params);
// เช็คผลลัพธ์ตาม Structure ของ GeneralService
// สมมติว่าถ้า Error ตัว executeQueryParam อาจจะ throw หรือ return null
return { status: true, message: 'Registration successful' };
} 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');
console.error('Register Service Error:', error);
return null;
}
}
}
async genNum(database) {
const sql = `
SELECT
MAX(usrseq) as max_seq
FROM nuttakit.usrmst
`
const params = []
const aryResult = await this.generalService.executeQueryParam(database, sql, params);
const lastSeq = aryResult[0]?.max_seq || 0;
return lastSeq + 1;
}
}

View File

@@ -1,19 +1,13 @@
// ===================================================
// ⚙️ Nuttakit Response Layer vFinal++++++
// ===================================================
export function sendError(thMsg = 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', enMsg = 'Unexpected error', code = 400) {
export function sendError(thMsg = 'เกิดข้อผิดพลาดไม่คาดคิดเกิดขึ้น', enMsg = 'Unexpected error', code = 400, data = []) {
return {
code: String(code),
message: enMsg,
message_th: thMsg,
data: []
data: data
}
}
// ===================================================
// 🔹 Auto Success Response (ใช้โดย Global Handler เท่านั้น)
// ===================================================
export function formatSuccessResponse(data) {
return {
code: "200",
@@ -21,4 +15,4 @@ export function formatSuccessResponse(data) {
message_th: "ดำเนินการสำเร็จ",
data: data || null
}
}
}

View File

@@ -0,0 +1,25 @@
import { sendError } from './response.js';
export const validateSave = (value, columnName) => {
// เช็คว่าค่าเป็น null, undefined หรือ empty string
if (value === undefined || value === null || value === '') {
// สร้างก้อน data ที่จะบอกว่า column ไหนหายไป
// ตามโจทย์: data: { "email": "ไม่พบข้อมูล" }
const errorDetail = {};
errorDetail[columnName] = "ไม่พบข้อมูล";
// เรียก sendError ใส่ message และ errorDetail ลงไปใน parameter ตัวที่ 4
sendError(
'ข้อมูลพารามิเตอร์ ไม่ถูกต้อง', // thMsg
'Invalid Parameter', // enMsg
400, // code
errorDetail // data
);
// ปาลูกระเบิดออกไปให้ Controller รับ
// throw errorObj;
}
return value;
}