
ต่อไปนี้คือบทความแบบทำตามได้ทีละขั้นตอน สำหรับสร้าง API ด้วย Express.js + MySQL เก็บไฟล์ขึ้น AWS S3 ใช้ ULID เป็นไอดีหลัก และพร้อมใช้งานจริงได้
เป้าหมาย
- สมัครพนักงานและล็อกอินพนักงาน
- ล็อกอินแอดมิน + สร้างแอดมินใหม่
- ข้อมูลบริษัทและแผนกให้เลือกตอนสมัคร
- โปรไฟล์พนักงาน: ดูและแก้ไข
- แอดมิน: ดูรายชื่อพนักงานทั้งหมด, ลบพนักงาน
- อัปโหลดรูปโปรไฟล์ → resize ด้วย sharp → เก็บใน S3
- JWT access/refresh + rate limit + validate inputs + Helmet + CORS
- Sequelize + MySQL พร้อมรองรับ SSL ของ Azure
- ใช้ ULID เป็นคีย์หลัก
1) แพ็กเกจที่ใช้ และใช้ทำอะไร
aws-sdk/client-s3
SDK V3 สำหรับเรียก S3aws-sdk/lib-storage
อัปโหลดแบบ multipart สำหรับไฟล์ใหญ่bcryptjs
แฮ็ชรหัสผ่านcors
อนุญาต cross-origin จากFRONTEND_URL
dotenv
โหลดตัวแปรจาก.env
express-rate-limit
จำกัดความพยายาม เช่น ล็อกอินexpress-validator
ตรวจรูปแบบอินพุตhelmet
เฮดเดอร์ความปลอดภัยjsonwebtoken
ออกและตรวจ JWTmulter
รับไฟล์จากmultipart/form-data
mysql2
ไดรเวอร์ MySQLsequelize
ORM จัดการโมเดลและคำสั่ง SQLulid
สร้างไอดีเรียงตามเวลา อ่านง่ายกว่า UUIDxlsx
ส่งออกข้อมูลเป็นไฟล์ Excel ได้ภายหลังsharp
แปลงและย่อภาพก่อนอัปโหลด S3
2) โครงสร้างโปรเจกต์
/certs/ # เก็บไฟล์ CA ถ้าใช้ MySQL SSL (เช่น Azure G2)
DigiCertGlobalRootG2.crt.pem
/config/
db.js # ตั้งค่า Sequelize + SSL
/controllers/
admin.controller.js
auth.controller.js
employee.controller.js
/middleware/
auth.js # ตรวจ JWT + บทบาท
rateLimit.js # จำกัดความถี่
validate.js # wrapper express-validator
/migrations/ # ถ้าใช้ sequelize-cli
/models/
index.js # รวมโมเดลและ associate
company.model.js
department.model.js
employee.model.js
admin.model.js
refreshToken.model.js
/routes/
admin.routes.js
auth.routes.js
employee.routes.js
/seeders/
000-admin.seed.js
001-company-dept.seed.js
/utils/
jwt.js
s3.js
ulid.js
passwords.js
logger.js
errors.js
index.js # บูทเซิร์ฟเวอร์
3) ตัวอย่าง .env
ที่จำเป็น
NODE_ENV=development
DB_HOST=127.0.0.1
DB_NAME=appdb
DB_USERNAME=root
DB_PASSWORD=secret
DB_PORT=3306
DB_SSL=false
AZURE_MYSQL_SSL_CA=./certs/DigiCertGlobalRootG2.crt.pem
AZURE_MYSQL_RSA_CA= # ไม่ต้องใช้ถ้าไม่ได้บังคับ
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
AWS_REGION=ap-southeast-1
AWS_ENDPOINT=
AWS_S3_BUCKET=my-app-bucket
MAIN_FOLDER=uploads
FRONTEND_URL=http://localhost:5173
MAX_LOGIN_ATTEMPTS=5
LOGIN_ATTEMPT_TIMEOUT=5 # นาที
PORT=3000
TRUST_PROXY=1
SQL_LOG=0
JWT_ACCESS_SECRET=replace_me_access
JWT_REFRESH_SECRET=replace_me_refresh
JWT_ACCESS_TTL=15m
JWT_REFRESH_TTL=30d
จำเป็นจริง: DB_*
, JWT_*
, PORT
, FRONTEND_URL
, AWS_*
, MAIN_FOLDER
.DB_SSL
, AZURE_*
ใช้เมื่อ MySQL บังคับ SSL เท่านั้น.
4) ติดตั้งและสคริปต์
npm init -y
npm i express cors helmet dotenv express-rate-limit express-validator jsonwebtoken bcryptjs multer sharp xlsx ulid
npm i sequelize mysql2
npm i @aws-sdk/client-s3 @aws-sdk/lib-storage
npm i -D nodemon
package.json
scripts:
{
"scripts": {
"dev": "nodemon index.js",
"start": "node index.js"
}
}
5) ตั้งค่า DB และ Sequelize
const { Sequelize } = require('sequelize');
const fs = require('fs');
require('dotenv').config();
const sslEnabled = String(process.env.DB_SSL).toLowerCase() === 'true';
const dialectOptions = {};
if (sslEnabled) {
const caPath = process.env.AZURE_MYSQL_SSL_CA;
if (caPath) {
dialectOptions.ssl = {
require: true,
ca: fs.readFileSync(caPath, 'utf8'),
};
} else {
dialectOptions.ssl = { require: true, rejectUnauthorized: true };
}
}
const sequelize = new Sequelize(
process.env.DB_NAME,
process.env.DB_USERNAME,
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT || 3306),
dialect: 'mysql',
logging: Number(process.env.SQL_LOG || 0) ? console.log : false,
dialectOptions,
define: { underscored: true, freezeTableName: true },
}
);
module.exports = { sequelize };
const { sequelize } = require('../config/db');
const Company = require('./company.model');
const Department = require('./department.model');
const Employee = require('./employee.model');
const Admin = require('./admin.model');
const RefreshToken = require('./refreshToken.model');
// /models/index.js
Company.hasMany(Department, { foreignKey: 'company_id', onDelete: 'RESTRICT', onUpdate: 'CASCADE' });
Department.belongsTo(Company, { foreignKey: 'company_id' });
Company.hasMany(Employee, { foreignKey: 'company_id', onDelete: 'RESTRICT', onUpdate: 'CASCADE' });
Department.hasMany(Employee,{ foreignKey: 'department_id', onDelete: 'RESTRICT', onUpdate: 'CASCADE' });
Employee.belongsTo(Company, { foreignKey: 'company_id' });
Employee.belongsTo(Department,{ foreignKey: 'department_id' });
module.exports = {
sequelize, Company, Department, Employee, Admin, RefreshToken
};
const { ulid } = require('ulid');
module.exports = { newId: () => ulid() };
โมเดลหลัก
const { DataTypes, Model } = require('sequelize');
const { sequelize } = require('../config/db');
class Company extends Model {}
Company.init({
id: { type: DataTypes.STRING(26), primaryKey: true },
name: { type: DataTypes.STRING(200), allowNull: false, unique: true },
}, { sequelize, modelName: 'company' });
module.exports = Company;
const { DataTypes, Model } = require('sequelize');
const { sequelize } = require('../config/db');
class Department extends Model {}
Department.init({
id: { type: DataTypes.STRING(26), primaryKey: true },
company_id: { type: DataTypes.STRING(26), allowNull: false },
name: { type: DataTypes.STRING(200), allowNull: false },
}, { sequelize, modelName: 'department' });
module.exports = Department;
const { DataTypes, Model } = require('sequelize');
const { sequelize } = require('../config/db');
class Employee extends Model {}
Employee.init(
{
id: { type: DataTypes.STRING(26), primaryKey: true },
employee_code: { type: DataTypes.STRING(32), unique: true, allowNull: false },
first_name: { type: DataTypes.STRING(120), allowNull: false },
last_name: { type: DataTypes.STRING(120), allowNull: false },
company_id: {
type: DataTypes.STRING(26),
allowNull: false,
references: { model: 'company', key: 'id' } // ชื่อตาราง
// ถ้าอยากอ้างอิงด้วยคลาสโมเดล: references: { model: Company, key: 'id' }
},
department_id: {
type: DataTypes.STRING(26),
allowNull: false,
references: { model: 'department', key: 'id' }
},
password_hash: { type: DataTypes.STRING(200), allowNull: false },
avatar_url: { type: DataTypes.STRING(500) }
},
{
sequelize,
modelName: 'employee',
tableName: 'employee' // ใส่เพื่อชัวร์ ถ้าไม่ได้ใช้ freezeTableName global
}
);
module.exports = Employee;
const { DataTypes, Model } = require('sequelize');
const { sequelize } = require('../config/db');
class Admin extends Model {}
Admin.init({
id: { type: DataTypes.STRING(26), primaryKey: true },
email: { type: DataTypes.STRING(200), unique: true, allowNull: false },
password_hash: { type: DataTypes.STRING(200), allowNull: false },
}, { sequelize, modelName: 'admin' });
module.exports = Admin;
const { DataTypes, Model } = require('sequelize');
const { sequelize } = require('../config/db');
class RefreshToken extends Model {}
RefreshToken.init({
id: { type: DataTypes.STRING(26), primaryKey: true },
user_id: { type: DataTypes.STRING(26), allowNull: false },
user_role: { type: DataTypes.ENUM('employee','admin'), allowNull: false },
token: { type: DataTypes.STRING(500), allowNull: false, unique: true },
revoked: { type: DataTypes.BOOLEAN, defaultValue: false },
expires_at: { type: DataTypes.DATE, allowNull: false },
}, { sequelize, modelName: 'refresh_token' });
module.exports = RefreshToken;
6) Utilities
const bcrypt = require('bcryptjs');
const SALT_ROUNDS = 12;
const hash = (plain) => bcrypt.hash(plain, SALT_ROUNDS);
const compare = (plain, hashv) => bcrypt.compare(plain, hashv);
module.exports = { hash, compare };
const jwt = require('jsonwebtoken');
require('dotenv').config();
const signAccess = (payload) =>
jwt.sign(payload, process.env.JWT_ACCESS_SECRET, { expiresIn: process.env.JWT_ACCESS_TTL || '15m' });
const signRefresh = (payload) =>
jwt.sign(payload, process.env.JWT_REFRESH_SECRET, { expiresIn: process.env.JWT_REFRESH_TTL || '30d' });
const verifyAccess = (token) =>
jwt.verify(token, process.env.JWT_ACCESS_SECRET);
const verifyRefresh = (token) =>
jwt.verify(token, process.env.JWT_REFRESH_SECRET);
module.exports = { signAccess, signRefresh, verifyAccess, verifyRefresh };
const { S3Client, HeadObjectCommand } = require('@aws-sdk/client-s3');
const { Upload } = require('@aws-sdk/lib-storage');
require('dotenv').config();
const s3 = new S3Client({
region: process.env.AWS_REGION,
endpoint: process.env.AWS_ENDPOINT || undefined,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
},
forcePathStyle: !!process.env.AWS_ENDPOINT // สำหรับ S3-compatible
});
async function putObject({ Key, Body, ContentType }) {
const upload = new Upload({
client: s3,
params: { Bucket: process.env.AWS_S3_BUCKET, Key, Body, ContentType, ACL: 'private' }
});
await upload.done();
return `s3://${process.env.AWS_S3_BUCKET}/${Key}`;
}
async function exists(Key) {
try {
await s3.send(new HeadObjectCommand({ Bucket: process.env.AWS_S3_BUCKET, Key }));
return true;
} catch {
return false;
}
}
module.exports = { putObject, exists };
const { validationResult } = require('express-validator');
module.exports = (req, res, next) => {
const result = validationResult(req);
if (result.isEmpty()) return next();
return res.status(422).json({ errors: result.array() });
};
const { verifyAccess } = require('../utils/jwt');
function requireAuth(req, res, next) {
const h = req.headers.authorization || '';
const token = h.startsWith('Bearer ') ? h.slice(7) : null;
if (!token) return res.status(401).json({ message: 'missing token' });
try {
const payload = verifyAccess(token);
req.user = payload;
return next();
} catch {
return res.status(401).json({ message: 'invalid token' });
}
}
function requireRole(role) {
return (req, res, next) => {
if (!req.user || req.user.role !== role) return res.status(403).json({ message: 'forbidden' });
next();
};
}
module.exports = { requireAuth, requireRole };
const rateLimit = require('express-rate-limit');
// global limiter
const globalLimiter = rateLimit({
windowMs: 60_000,
max: 300,
standardHeaders: true,
legacyHeaders: false
});
// per-username login limiter (พื้นฐาน)
const attempts = new Map(); // key: userKey, value: { count, until }
function loginGuard(key, max, timeoutMin) {
const now = Date.now();
const rec = attempts.get(key);
if (rec && rec.until > now) {
if (rec.count >= max) return { blocked: true, until: rec.until };
} else {
attempts.delete(key);
}
return { blocked: false };
}
function recordFail(key, max, timeoutMin) {
const now = Date.now();
const rec = attempts.get(key) || { count: 0, until: now };
rec.count += 1;
if (rec.count >= max) rec.until = now + timeoutMin * 60_000;
attempts.set(key, rec);
}
function resetAttempts(key) { attempts.delete(key); }
module.exports = { globalLimiter, loginGuard, recordFail, resetAttempts };
7) Controllers
const { newId } = require('../utils/ulid');
const { hash, compare } = require('../utils/passwords');
const { signAccess, signRefresh, verifyRefresh } = require('../utils/jwt');
const { Company, Department, Employee, Admin, RefreshToken } = require('../models');
const { Op } = require('sequelize');
const { addMinutes } = require('date-fns');
const { loginGuard, recordFail, resetAttempts } = require('../middleware/rateLimit');
const MAX = Number(process.env.MAX_LOGIN_ATTEMPTS || 5);
const TIMEOUT = Number(process.env.LOGIN_ATTEMPT_TIMEOUT || 5);
exports.registerEmployee = async (req, res) => {
const { employee_code, first_name, last_name, company_id, department_id, password } = req.body;
const empExists = await Employee.findOne({ where: { employee_code } });
if (empExists) return res.status(409).json({ message: 'employee_code exists' });
const ckCompany = await Company.findByPk(company_id);
const ckDept = await Department.findOne({ where: { id: department_id, company_id } });
if (!ckCompany || !ckDept) return res.status(400).json({ message: 'invalid company/department' });
const password_hash = await hash(password);
const emp = await Employee.create({
id: newId(), employee_code, first_name, last_name, company_id, department_id, password_hash
});
return res.status(201).json({ id: emp.id });
};
exports.loginEmployee = async (req, res) => {
const { employee_code, password } = req.body;
const key = `emp:${employee_code}`;
const check = loginGuard(key, MAX, TIMEOUT);
if (check.blocked) return res.status(429).json({ message: 'too many attempts' });
const emp = await Employee.findOne({ where: { employee_code } });
if (!emp || !(await compare(password, emp.password_hash))) {
recordFail(key, MAX, TIMEOUT);
return res.status(400).json({ message: 'invalid credentials' });
}
resetAttempts(key);
const access = signAccess({ sub: emp.id, role: 'employee' });
const refresh = signRefresh({ sub: emp.id, role: 'employee' });
await RefreshToken.create({
id: newId(), user_id: emp.id, user_role: 'employee', token: refresh,
expires_at: addMinutes(new Date(), 60 * 24 * 30)
});
res.json({ access, refresh });
};
exports.loginAdmin = async (req, res) => {
const { email, password } = req.body;
const key = `admin:${email}`;
const check = loginGuard(key, MAX, TIMEOUT);
if (check.blocked) return res.status(429).json({ message: 'too many attempts' });
const admin = await Admin.findOne({ where: { email: { [Op.eq]: email } } });
if (!admin || !(await compare(password, admin.password_hash))) {
recordFail(key, MAX, TIMEOUT);
return res.status(400).json({ message: 'invalid credentials' });
}
resetAttempts(key);
const access = signAccess({ sub: admin.id, role: 'admin' });
const refresh = signRefresh({ sub: admin.id, role: 'admin' });
await RefreshToken.create({
id: newId(), user_id: admin.id, user_role: 'admin', token: refresh,
expires_at: addMinutes(new Date(), 60 * 24 * 30)
});
res.json({ access, refresh });
};
exports.createAdmin = async (req, res) => {
const { email, password } = req.body;
const exists = await Admin.findOne({ where: { email } });
if (exists) return res.status(409).json({ message: 'email exists' });
const admin = await Admin.create({
id: newId(),
email,
password_hash: await hash(password)
});
res.status(201).json({ id: admin.id });
};
const { Employee } = require('../models');
const sharp = require('sharp');
const { putObject } = require('../utils/s3');
exports.getProfile = async (req, res) => {
const me = await Employee.findByPk(req.user.sub, {
attributes: ['id','employee_code','first_name','last_name','company_id','department_id','avatar_url']
});
if (!me) return res.status(404).json({ message: 'not found' });
res.json(me);
};
exports.updateProfile = async (req, res) => {
if (req.params.id !== req.user.sub && req.user.role !== 'admin') {
return res.status(403).json({ message: 'forbidden' });
}
const emp = await Employee.findByPk(req.params.id);
if (!emp) return res.status(404).json({ message: 'not found' });
const { first_name, last_name } = req.body;
if (typeof first_name === 'string') emp.first_name = first_name;
if (typeof last_name === 'string') emp.last_name = last_name;
if (req.file) {
const buf = await sharp(req.file.buffer).resize(512, 512, { fit: 'cover' }).webp({ quality: 82 }).toBuffer();
const key = `${process.env.MAIN_FOLDER}/avatars/${emp.id}.webp`;
await putObject({ Key: key, Body: buf, ContentType: 'image/webp' });
emp.avatar_url = key; // เก็บ key; ตอนเสิร์ฟให้ frontend ทำ pre-signed URL ฝั่งเซิร์ฟ
}
await emp.save();
res.json({ ok: true });
};
const { Employee } = require('../models');
exports.listEmployees = async (req, res) => {
const page = Math.max(1, Number(req.query.page || 1));
const size = Math.min(100, Math.max(1, Number(req.query.size || 20)));
const { rows, count } = await Employee.findAndCountAll({
offset: (page - 1) * size, limit: size, order: [['created_at','DESC']],
attributes: ['id','employee_code','first_name','last_name','company_id','department_id','avatar_url']
});
res.json({ data: rows, total: count, page, size });
};
exports.deleteEmployee = async (req, res) => {
const id = req.params.id;
const n = await Employee.destroy({ where: { id } });
if (!n) return res.status(404).json({ message: 'not found' });
res.json({ ok: true });
};
8) Routes + Validators + Multer
const router = require('express').Router();
const { body } = require('express-validator');
const validate = require('../middleware/validate');
const ctrl = require('../controllers/auth.controller');
router.post('/employee/register',
body('employee_code').isLength({ min: 3 }),
body('first_name').notEmpty(),
body('last_name').notEmpty(),
body('company_id').isLength({ min: 10 }),
body('department_id').isLength({ min: 10 }),
body('password').isStrongPassword({ minLength: 8, minSymbols: 0 }),
validate,
ctrl.registerEmployee
);
router.post('/employee/login',
body('employee_code').notEmpty(),
body('password').notEmpty(),
validate,
ctrl.loginEmployee
);
router.post('/admin/login',
body('email').isEmail(),
body('password').notEmpty(),
validate,
ctrl.loginAdmin
);
router.post('/admin/create',
body('email').isEmail(),
body('password').isStrongPassword({ minLength: 10, minSymbols: 0 }),
validate,
ctrl.createAdmin
);
module.exports = router;
const router = require('express').Router();
const { requireAuth, requireRole } = require('../middleware/auth');
const { body } = require('express-validator');
const validate = require('../middleware/validate');
const ctrl = require('../controllers/employee.controller');
const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024 } });
router.get('/employee/profile', requireAuth, requireRole('employee'), ctrl.getProfile);
router.patch('/employee/profile/edit/:id',
requireAuth,
upload.single('avatar'),
body('first_name').optional().isLength({ min: 1 }),
body('last_name').optional().isLength({ min: 1 }),
validate,
ctrl.updateProfile
);
module.exports = router;
const router = require('express').Router();
const { requireAuth, requireRole } = require('../middleware/auth');
const ctrl = require('../controllers/admin.controller');
router.get('/admin/employee/all', requireAuth, requireRole('admin'), ctrl.listEmployees);
router.delete('/admin/employee/delete/:id', requireAuth, requireRole('admin'), ctrl.deleteEmployee);
module.exports = router;
9) Seeders อย่างง่าย
const { Admin } = require('../models');
const { newId } = require('../utils/ulid');
const { hash } = require('../utils/passwords');
async function seedAdmin() {
const email = 'root@example.com';
const exists = await Admin.findOne({ where: { email } });
if (!exists) {
await Admin.create({ id: newId(), email, password_hash: await hash('ChangeMe1234') });
console.log('seeded admin:', email);
}
}
module.exports = { seedAdmin };
const { Company, Department } = require('../models');
const { newId } = require('../utils/ulid');
async function seedOrg() {
const cid = newId();
const did = newId();
const c = await Company.findOne({ where: { name: 'Sample Co., Ltd.' } });
if (!c) {
await Company.create({ id: cid, name: 'Sample Co., Ltd.' });
await Department.create({ id: did, company_id: cid, name: 'IT' });
console.log('seeded company+department');
}
}
module.exports = { seedOrg };
10) บูทเซิร์ฟเวอร์
require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const { globalLimiter } = require('./middleware/rateLimit');
const { sequelize } = require('./models');
const authRoutes = require('./routes/auth.routes');
const employeeRoutes = require('./routes/employee.routes');
const adminRoutes = require('./routes/admin.routes');
const app = express();
if (Number(process.env.TRUST_PROXY || 0)) app.set('trust proxy', true);
app.use(helmet());
app.use(cors({ origin: process.env.FRONTEND_URL, credentials: true }));
app.use(globalLimiter);
app.use(express.json({ limit: '2mb' }));
app.use('/api', authRoutes);
app.use('/api', employeeRoutes);
app.use('/api', adminRoutes);
app.get('/health', (req, res) => res.json({ ok: true }));
(async () => {
await sequelize.sync(); // โปรดใช้ migrations ใน production
// seed
await require('./seeders/000-admin.seed').seedAdmin();
await require('./seeders/001-company-dept.seed').seedOrg();
const port = Number(process.env.PORT || 3000);
app.listen(port, () => console.log(`API on :${port}`));
})();
11) ตัวอย่างการเรียกใช้งาน
curl -X POST http://localhost:3000/api/employee/register \
-H "Content-Type: application/json" \
-d '{"employee_code":"E1001","first_name":"Som","last_name":"chai","company_id":"<cid>","department_id":"<did>","password":"StrongPass123"}'
curl -X POST http://localhost:3000/api/employee/login \
-H "Content-Type: application/json" \
-d '{"employee_code":"E1001","password":"StrongPass123"}'
curl http://localhost:3000/api/employee/profile -H "Authorization: Bearer <access>"
curl -X PATCH http://localhost:3000/api/employee/profile/edit/<employee_id> \
-H "Authorization: Bearer <access>" \
-F "first_name=NewName" \
-F "avatar=@./me.jpg"
curl -X POST http://localhost:3000/api/admin/login \
-H "Content-Type: application/json" \
-d '{"email":"root@example.com","password":"ChangeMe1234"}'
curl "http://localhost:3000/api/admin/employee/all?page=1&size=20" \
-H "Authorization: Bearer <admin_access>"
curl -X DELETE http://localhost:3000/api/admin/employee/delete/<employee_id> \
-H "Authorization: Bearer <admin_access>"
12) ความปลอดภัยที่ควรใช้จริง
- ใช้ HTTPS เสมอ และตั้ง
TRUST_PROXY=1
เมื่ออยู่หลัง reverse proxy - เปิด
DB_SSL=true
เมื่อใช้คลาวด์ DB ที่บังคับ SSL และใส่ CA ให้ถูกต้อง - เก็บไฟล์บน S3 แบบ private แล้วสร้าง pre-signed URL ตอนดาวน์โหลด แทนการเปิด public
- แยก access และ refresh token, จัดเก็บ refresh ในฐานข้อมูลและทำ rotation เมื่อรีเฟรช
- จำกัดขนาดไฟล์และชนิดไฟล์ใน
multer
และsharp
- เปิด
helmet()
ทั้งหมด และตั้ง CORS ชี้เฉพาะโดเมนหน้าเว็บ - บันทึก audit log เมื่อสร้าง/ลบพนักงาน
- ใช้
sequelize-cli
+ migrations ใน production แทนsync()
- สำรองฐานข้อมูลอย่างสม่ำเสมอ
13) เช็กลิสต์สั้น
- เติม
.env
ให้ครบ โดยเฉพาะDB_*
,AWS_*
,JWT_*
- สร้างบัคเก็ต S3 และกำหนดสิทธิ์ IAM ให้เขียนได้
- ใส่ CA ไฟล์ใน
/certs
ถ้าใช้ Azure MySQL แบบ SSL npm run dev
- เรียก
/health
ดูสถานะ - ใช้ seed แอดมินเริ่มต้น แล้วเปลี่ยนรหัสทันที
ต้องการเพิ่ม Excel export, refresh token rotation endpoint, หรือ pre-signed URL เสิร์ฟรูป แจ้งได้ ฉันจะเติมโค้ดให้ครบชุด.