backend: Mod

This commit is contained in:
2025-10-04 23:55:15 +07:00
parent d2a7a3e478
commit 71fc7eee13
72 changed files with 6978 additions and 7135 deletions

View File

@@ -1,4 +1,4 @@
[/dms] [/dms]
max_log = 496206 max_log = 498246
number = 3 number = 4
finish = 1 finish = 1

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

146
backend/src/db/sequelize.js Normal file → Executable file
View File

@@ -5,11 +5,7 @@
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
import { config } from "../config.js"; import { config } from "../config.js";
export const sequelize = new Sequelize( export const sequelize = new Sequelize(config.DB.NAME, config.DB.USER, config.DB.PASS, {
config.DB.NAME,
config.DB.USER,
config.DB.PASS,
{
host: config.DB.HOST, host: config.DB.HOST,
port: config.DB.PORT, port: config.DB.PORT,
dialect: "mariadb", dialect: "mariadb",
@@ -17,79 +13,91 @@ export const sequelize = new Sequelize(
dialectOptions: { timezone: "Z" }, dialectOptions: { timezone: "Z" },
define: { freezeTableName: true, underscored: false, timestamps: false }, define: { freezeTableName: true, underscored: false, timestamps: false },
pool: { max: 10, min: 0, idle: 10000 }, pool: { max: 10, min: 0, idle: 10000 },
} });
);
// --- 1. ประกาศตัวแปรสำหรับ Export Model ทั้งหมด --- // --- 1. ประกาศตัวแปรสำหรับ Export Model ทั้งหมด ---
export let User = null; export let User, Role, Permission, Organization, Project, UserRole, RolePermission,
export let Role = null; UserProjectRole, Correspondence, CorrespondenceVersion, Document, CorrDocumentMap,
export let Permission = null; Drawing, DrawingRevision, FileObject, RFA, RFARevision, RfaDrawingMap,
export let UserRole = null; Transmittal, TransmittalItem, Volume, ContractDwg, SubCategory;
export let RolePermission = null;
export let Project = null; // <-- เพิ่มเข้ามา
export let UserProjectRole = null; // <-- เพิ่มเข้ามา
if (process.env.ENABLE_SEQUELIZE === "1") { if (process.env.ENABLE_SEQUELIZE === "1") {
// --- 2. โหลดโมเดลทั้งหมดแบบ on-demand --- // --- 2. สร้าง Object ของ Models ทั้งหมดที่จะโหลด ---
const mdlUser = await import("./models/User.js").catch(() => null); const modelsToLoad = {
const mdlRole = await import("./models/Role.js").catch(() => null); User: await import("./models/User.js").catch(() => null),
const mdlPerm = await import("./models/Permission.js").catch(() => null); Role: await import("./models/Role.js").catch(() => null),
const mdlUR = await import("./models/UserRole.js").catch(() => null); Permission: await import("./models/Permission.js").catch(() => null),
const mdlRP = await import("./models/RolePermission.js").catch(() => null); Organization: await import("./models/Organization.js").catch(() => null),
const mdlProj = await import("./models/Project.js").catch(() => null); // <-- เพิ่มเข้ามา Project: await import("./models/Project.js").catch(() => null),
const mdlUPR = await import("./models/UserProjectRole.js").catch(() => null); // <-- เพิ่มเข้ามา UserRole: await import("./models/UserRole.js").catch(() => null),
RolePermission: await import("./models/RolePermission.js").catch(() => null),
UserProjectRole: await import("./models/UserProjectRole.js").catch(() => null),
Correspondence: await import("./models/Correspondence.js").catch(() => null),
CorrespondenceVersion: await import("./models/CorrespondenceVersion.js").catch(() => null),
Document: await import("./models/Document.js").catch(() => null),
CorrDocumentMap: await import("./models/CorrDocumentMap.js").catch(() => null),
Drawing: await import("./models/Drawing.js").catch(() => null),
DrawingRevision: await import("./models/DrawingRevision.js").catch(() => null),
FileObject: await import("./models/FileObject.js").catch(() => null),
RFA: await import("./models/RFA.js").catch(() => null),
RFARevision: await import("./models/RFARevision.js").catch(() => null),
RfaDrawingMap: await import("./models/RfaDrawingMap.js").catch(() => null),
Transmittal: await import("./models/Transmittal.js").catch(() => null),
TransmittalItem: await import("./models/TransmittalItem.js").catch(() => null),
Volume: await import("./models/Volume.js").catch(() => null),
ContractDwg: await import("./models/ContractDwg.js").catch(() => null),
SubCategory: await import("./models/SubCategory.js").catch(() => null),
};
// --- 3. Initialize Model ทั้งหมด --- // --- 3. Initialize Model ทั้งหมด ---
if (mdlUser?.default) User = mdlUser.default(sequelize); User = modelsToLoad.User?.default ? modelsToLoad.User.default(sequelize) : null;
if (mdlRole?.default) Role = mdlRole.default(sequelize); Role = modelsToLoad.Role?.default ? modelsToLoad.Role.default(sequelize) : null;
if (mdlPerm?.default) Permission = mdlPerm.default(sequelize); Permission = modelsToLoad.Permission?.default ? modelsToLoad.Permission.default(sequelize) : null;
if (mdlUR?.default) UserRole = mdlUR.default(sequelize); Organization = modelsToLoad.Organization?.default ? modelsToLoad.Organization.default(sequelize) : null;
if (mdlRP?.default) RolePermission = mdlRP.default(sequelize); Project = modelsToLoad.Project?.default ? modelsToLoad.Project.default(sequelize) : null;
if (mdlProj?.default) Project = mdlProj.default(sequelize); // <-- เพิ่มเข้ามา UserRole = modelsToLoad.UserRole?.default ? modelsToLoad.UserRole.default(sequelize) : null;
if (mdlUPR?.default) UserProjectRole = mdlUPR.default(sequelize); // <-- เพิ่มเข้ามา RolePermission = modelsToLoad.RolePermission?.default ? modelsToLoad.RolePermission.default(sequelize) : null;
UserProjectRole = modelsToLoad.UserProjectRole?.default ? modelsToLoad.UserProjectRole.default(sequelize) : null;
Correspondence = modelsToLoad.Correspondence?.default ? modelsToLoad.Correspondence.default(sequelize) : null;
CorrespondenceVersion = modelsToLoad.CorrespondenceVersion?.default ? modelsToLoad.CorrespondenceVersion.default(sequelize) : null;
Document = modelsToLoad.Document?.default ? modelsToLoad.Document.default(sequelize) : null;
CorrDocumentMap = modelsToLoad.CorrDocumentMap?.default ? modelsToLoad.CorrDocumentMap.default(sequelize) : null;
Drawing = modelsToLoad.Drawing?.default ? modelsToLoad.Drawing.default(sequelize) : null;
DrawingRevision = modelsToLoad.DrawingRevision?.default ? modelsToLoad.DrawingRevision.default(sequelize) : null;
FileObject = modelsToLoad.FileObject?.default ? modelsToLoad.FileObject.default(sequelize) : null;
RFA = modelsToLoad.RFA?.default ? modelsToLoad.RFA.default(sequelize) : null;
RFARevision = modelsToLoad.RFARevision?.default ? modelsToLoad.RFARevision.default(sequelize) : null;
RfaDrawingMap = modelsToLoad.RfaDrawingMap?.default ? modelsToLoad.RfaDrawingMap.default(sequelize) : null;
Transmittal = modelsToLoad.Transmittal?.default ? modelsToLoad.Transmittal.default(sequelize) : null;
TransmittalItem = modelsToLoad.TransmittalItem?.default ? modelsToLoad.TransmittalItem.default(sequelize) : null;
Volume = modelsToLoad.Volume?.default ? modelsToLoad.Volume.default(sequelize) : null;
ContractDwg = modelsToLoad.ContractDwg?.default ? modelsToLoad.ContractDwg.default(sequelize) : null;
SubCategory = modelsToLoad.SubCategory?.default ? modelsToLoad.SubCategory.default(sequelize) : null;
// --- 4. สร้างความสัมพันธ์ (Associations) ทั้งหมด ---
if (User && Role && Permission && UserRole && RolePermission) {
// ความสัมพันธ์ระดับระบบ (System-level)
User.belongsToMany(Role, { through: UserRole, foreignKey: "user_id" });
Role.belongsToMany(User, { through: UserRole, foreignKey: "role_id" });
Role.belongsToMany(Permission, {
through: RolePermission,
foreignKey: "role_id",
});
Permission.belongsToMany(Role, {
through: RolePermission,
foreignKey: "permission_id",
});
}
// ความสัมพันธ์ระดับโปรเจกต์ (Project-level) - ส่วนที่เพิ่มเข้ามา // --- 4. สร้างความสัมพันธ์ (Associations) ---
if (User && Project && Role && UserProjectRole) { const loadedModels = { User, Role, Permission, Organization, Project, UserRole, RolePermission,
// ทำให้ UserProjectRole เป็นตัวกลางเชื่อม 3 ตาราง UserProjectRole, Correspondence, CorrespondenceVersion, Document, CorrDocumentMap,
UserProjectRole.belongsTo(User, { foreignKey: "user_id" }); Drawing, DrawingRevision, FileObject, RFA, RFARevision, RfaDrawingMap,
UserProjectRole.belongsTo(Project, { foreignKey: "project_id" }); Transmittal, TransmittalItem, Volume, ContractDwg, SubCategory };
UserProjectRole.belongsTo(Role, { foreignKey: "role_id" });
// ทำให้สามารถ include ข้อมูลที่เกี่ยวข้องได้ง่ายขึ้น for (const modelName in loadedModels) {
User.hasMany(UserProjectRole, { foreignKey: "user_id" }); if (loadedModels[modelName] && loadedModels[modelName].associate) {
Project.hasMany(UserProjectRole, { foreignKey: "project_id" }); loadedModels[modelName].associate(loadedModels);
Role.hasMany(UserProjectRole, { foreignKey: "role_id" }); }
} }
} }
/**
* ฟังก์ชันสำหรับทดสอบการเชื่อมต่อ Sequelize
*/
export async function dbReady() { export async function dbReady() {
if (process.env.ENABLE_SEQUELIZE !== "1") { if (process.env.ENABLE_SEQUELIZE !== "1") {
console.log("Sequelize is disabled."); console.log("Sequelize is disabled.");
return Promise.resolve(); return Promise.resolve();
} }
try { try {
await sequelize.authenticate(); await sequelize.authenticate();
console.log("Sequelize connection has been established successfully."); console.log("Sequelize connection has been established successfully.");
} catch (error) { } catch (error) {
console.error("Unable to connect to the database via Sequelize:", error); console.error("Unable to connect to the database via Sequelize:", error);
return Promise.reject(error); return Promise.reject(error);
} }
} }

View File

@@ -0,0 +1,61 @@
// FILE: backend/src/middleware/auth.js
import jwt from "jsonwebtoken";
import { config } from "../config.js";
import { User, Role, UserRole } from "../db/sequelize.js";
export function signAccessToken(payload) {
return jwt.sign(payload, config.JWT.SECRET, {
expiresIn: config.JWT.EXPIRES_IN,
});
}
export function signRefreshToken(payload) {
return jwt.sign(payload, config.JWT.REFRESH_SECRET, {
expiresIn: config.JWT.REFRESH_EXPIRES_IN,
});
}
export function extractToken(req) {
// ให้คุกกี้มาก่อน แล้วค่อย Bearer (รองรับทั้งสองทาง)
const cookieTok = req.cookies?.access_token || null;
if (cookieTok) return cookieTok;
const hdr = req.headers.authorization || "";
return hdr.startsWith("Bearer ") ? hdr.slice(7) : null;
}
export function requireAuth(req, res, next) {
if (req.path === "/health") return next(); // อนุญาต health เสมอ
const token = extractToken(req);
if (!token) return res.status(401).json({ error: "Missing token" });
try {
req.user = jwt.verify(token, config.JWT.SECRET);
next();
} catch {
return res.status(401).json({ error: "Invalid/Expired token" });
}
}
// ใช้กับเส้นทางที่ login แล้วจะ enrich ต่อได้ แต่ไม่บังคับ
export function optionalAuth(req, _res, next) {
const token = extractToken(req);
if (!token) return next();
try {
req.user = jwt.verify(token, config.JWT.SECRET);
} catch {}
next();
}
export async function enrichRoles(req, _res, next) {
if (!req.user?.user_id) return next();
const rows = await UserRole.findAll({
where: { user_id: req.user.user_id },
include: [{ model: Role }],
}).catch(() => []);
req.user.roles = rows.map((r) => r.role?.role_name).filter(Boolean);
next();
}
export function hasPerm(req, perm) {
const set = new Set(req?.user?.permissions || []);
return set.has(perm);
}

0
backend/src/middleware/auth.js Normal file → Executable file
View File

6
backend/src/middleware/index.js Normal file → Executable file
View File

@@ -1,7 +1,7 @@
// File: backend/src/middleware/index.js // File: backend/src/middleware/index.js
import * as authJwt from "./authJwt.js";
import * as abac from "./abac.js"; import * as abac from "./abac.js";
import * as auth from "./auth.js"; import * as auth from "./auth.js";
import * as authJwt from "./authJwt.js";
import * as errorHandler from "./errorHandler.js"; import * as errorHandler from "./errorHandler.js";
import * as loadPrincipal from "./loadPrincipal.js"; import * as loadPrincipal from "./loadPrincipal.js";
import * as permGuard from "./permGuard.js"; import * as permGuard from "./permGuard.js";
@@ -12,9 +12,9 @@ import * as requirePerm from "./requirePerm.js";
// Export ทุกอย่างออกมาเป็น named exports // Export ทุกอย่างออกมาเป็น named exports
// เพื่อให้สามารถ import แบบ `import { authJwt, permGuard } from '../middleware';` ได้ // เพื่อให้สามารถ import แบบ `import { authJwt, permGuard } from '../middleware';` ได้
export { export {
authJwt,
abac, abac,
auth, auth,
authJwt,
errorHandler, errorHandler,
loadPrincipal, loadPrincipal,
permGuard, permGuard,
@@ -25,9 +25,9 @@ export {
// (Optional) สร้าง default export สำหรับกรณีที่ต้องการ import ทั้งหมดใน object เดียว // (Optional) สร้าง default export สำหรับกรณีที่ต้องการ import ทั้งหมดใน object เดียว
const middleware = { const middleware = {
authJwt,
abac, abac,
auth, auth,
authJwt,
errorHandler, errorHandler,
loadPrincipal, loadPrincipal,
permGuard, permGuard,

0
backend/src/middleware/loadPrincipal.js Normal file → Executable file
View File

72
backend/src/routes/dashboard.js Normal file → Executable file
View File

@@ -1,23 +1,63 @@
import { Router } from "express"; // backend/src/routes/dashboard.js
import { User } from "../db/index.js"; import { Router } from 'express';
import { authJwt } from "../middleware/index.js"; import { Op } from 'sequelize';
// 1. Import Middleware ที่ถูกต้อง
import { authJwt } from '../middleware/authJwt.js';
import { loadPrincipalMw } from '../middleware/loadPrincipal.js';
// 2. Import Sequelize Models จาก `sequelize.js` ไม่ใช่ `index.js`
import { Correspondence, Document, RFA, User } from '../db/sequelize.js';
const router = Router(); const router = Router();
router.use(authJwt.verifyToken); // 3. ใช้ Middleware Chain ที่ถูกต้อง 100%
router.use(authJwt(), loadPrincipalMw());
router.get("/users/summary", async (req, res, next) => {
try { // === API สำหรับ User Management Widget ===
const totalUsers = await User.count(); router.get('/users/summary', async (req, res, next) => {
const activeUsers = await User.count({ where: { is_active: true } }); try {
res.json({ // ตรวจสอบว่า Model ถูกโหลดแล้วหรือยัง (จำเป็นสำหรับโหมด lazy-load)
total: totalUsers, if (!User) {
active: activeUsers, return res.status(503).json({ message: 'Database models not available. Is ENABLE_SEQUELIZE=1 set?' });
inactive: totalUsers - activeUsers, }
}); const totalUsers = await User.count();
} catch (error) { const activeUsers = await User.count({ where: { is_active: true } });
next(error);
} res.json({
total: totalUsers,
active: activeUsers,
inactive: totalUsers - activeUsers,
});
} catch (error) {
next(error);
}
}); });
// === API อื่นๆ สำหรับ Dashboard ที่เราคุยกันไว้ก่อนหน้า ===
router.get('/stats', async (req, res, next) => {
try {
if (!Document || !RFA) {
return res.status(503).json({ message: 'Database models not available. Is ENABLE_SEQUELIZE=1 set?' });
}
const sevenDaysAgo = new Date(new Date().setDate(new Date().getDate() - 7));
const totalDocuments = await Document.count();
const newThisWeek = await Document.count({ where: { createdAt: { [Op.gte]: sevenDaysAgo } } });
const pendingRfas = await RFA.count({ where: { status: 'pending' } }); // สมมติตาม status
res.json({
totalDocuments,
newThisWeek,
pendingRfas
});
} catch (error) {
next(error);
}
});
export default router; export default router;

128
backend/src/routes/rbac_admin.js Normal file → Executable file
View File

@@ -1,144 +1,88 @@
// FILE: backend/src/routes/rbac_admin.js // FILE: backend/src/routes/rbac_admin.js
import { Router } from "express"; import { Router } from "express";
import { Role, Permission, UserProjectRole, Project } from "../db/sequelize.js"; import { Role, Permission, UserProjectRole, Project } from "../db/sequelize.js";
import { authJwt, permGuard } from "../middleware/index.js"; import { authJwt } from "../middleware/authJwt.js";
import { loadPrincipalMw } from "../middleware/loadPrincipal.js"; // แก้ไข: import ให้ถูกต้อง
import { requirePerm } from "../middleware/requirePerm.js";
const router = Router(); const router = Router();
// กำหนดให้ทุก route ในไฟล์นี้ต้องมีสิทธิ์ 'manage_rbac' // Middleware Chain ที่ถูกต้อง 100% ตามสถาปัตยกรรมของคุณ
router.use(authJwt.verifyToken, permGuard("manage_rbac")); router.use(authJwt(), loadPrincipalMw());
// == ROLES Management == // == ROLES Management ==
router.get("/roles", async (req, res, next) => { router.get("/roles", requirePerm("roles.manage"), async (req, res, next) => {
try { try {
const roles = await Role.findAll({ const roles = await Role.findAll({
include: [ include: [{ model: Permission, attributes: ["id", "name"], through: { attributes: [] } }],
{
model: Permission,
attributes: ["id", "name"],
through: { attributes: [] },
},
],
order: [["name", "ASC"]], order: [["name", "ASC"]],
}); });
res.json(roles); res.json(roles);
} catch (error) { } catch (error) { next(error); }
next(error);
}
}); });
router.post("/roles", async (req, res, next) => { router.post("/roles", requirePerm("roles.manage"), async (req, res, next) => {
const { name, description } = req.body;
if (!name) return res.status(400).json({ message: "Role name is required." });
try { try {
const { name, description } = req.body;
if (!name)
return res.status(400).json({ message: "Role name is required." });
const newRole = await Role.create({ name, description }); const newRole = await Role.create({ name, description });
res.status(201).json(newRole); res.status(201).json(newRole);
} catch (error) { } catch (error) {
if (error.name === "SequelizeUniqueConstraintError") { if (error.name === "SequelizeUniqueConstraintError") {
return res return res.status(409).json({ message: `Role '${name}' already exists.` });
.status(409)
.json({ message: `Role '${name}' already exists.` });
} }
next(error); next(error);
} }
}); });
router.put("/roles/:id/permissions", async (req, res, next) => { router.put("/roles/:id/permissions", requirePerm("roles.manage"), async (req, res, next) => {
try {
const { permissionIds } = req.body; const { permissionIds } = req.body;
if (!Array.isArray(permissionIds)) if (!Array.isArray(permissionIds)) return res.status(400).json({ message: "permissionIds must be an array." });
return res try {
.status(400) const role = await Role.findByPk(req.params.id);
.json({ message: "permissionIds must be an array." }); if (!role) return res.status(404).json({ message: "Role not found." });
await role.setPermissions(permissionIds);
const role = await Role.findByPk(req.params.id); const updatedRole = await Role.findByPk(req.params.id, {
if (!role) return res.status(404).json({ message: "Role not found." }); include: [{ model: Permission, attributes: ['id', 'name'], through: { attributes: [] } }]
});
await role.setPermissions(permissionIds); res.json(updatedRole);
const updatedRole = await Role.findByPk(req.params.id, { } catch (error) { next(error); }
include: [
{
model: Permission,
attributes: ["id", "name"],
through: { attributes: [] },
},
],
});
res.json(updatedRole);
} catch (error) {
next(error);
}
});
// == PERMISSIONS Management ==
router.get("/permissions", async (req, res, next) => {
try {
const permissions = await Permission.findAll({ order: [["name", "ASC"]] });
res.json(permissions);
} catch (error) {
next(error);
}
}); });
// == USER-PROJECT-ROLES Management == // == USER-PROJECT-ROLES Management ==
router.get("/user-project-roles", async (req, res, next) => { router.get("/user-project-roles", requirePerm("users.manage"), async (req, res, next) => {
const { userId } = req.query; const { userId } = req.query;
if (!userId) if (!userId) return res.status(400).json({ message: "userId query parameter is required." });
return res
.status(400)
.json({ message: "userId query parameter is required." });
try { try {
const assignments = await UserProjectRole.findAll({ const assignments = await UserProjectRole.findAll({
where: { user_id: userId }, where: { user_id: userId },
include: [ include: [ { model: Project, attributes: ["id", "name"] }, { model: Role, attributes: ["id", "name"] } ],
{ model: Project, attributes: ["id", "name"] },
{ model: Role, attributes: ["id", "name"] },
],
}); });
res.json(assignments); res.json(assignments);
} catch (error) { } catch (error) { next(error); }
next(error);
}
}); });
router.post("/user-project-roles", async (req, res, next) => { router.post("/user-project-roles", requirePerm("users.manage"), async (req, res, next) => {
const { userId, projectId, roleId } = req.body; const { userId, projectId, roleId } = req.body;
if (!userId || !projectId || !roleId) if (!userId || !projectId || !roleId) return res.status(400).json({ message: "userId, projectId, and roleId are required." });
return res
.status(400)
.json({ message: "userId, projectId, and roleId are required." });
try { try {
const [assignment, created] = await UserProjectRole.findOrCreate({ const [assignment, created] = await UserProjectRole.findOrCreate({
where: { user_id: userId, project_id: projectId, role_id: roleId }, where: { user_id: userId, project_id: projectId, role_id: roleId },
defaults: { user_id: userId, project_id: projectId, role_id: roleId }, defaults: { user_id: userId, project_id: projectId, role_id: roleId },
}); });
if (!created) if (!created) return res.status(409).json({ message: "This assignment already exists." });
return res
.status(409)
.json({ message: "This assignment already exists." });
res.status(201).json(assignment); res.status(201).json(assignment);
} catch (error) { } catch (error) { next(error); }
next(error);
}
}); });
router.delete("/user-project-roles", async (req, res, next) => { router.delete("/user-project-roles", requirePerm("users.manage"), async (req, res, next) => {
const { userId, projectId, roleId } = req.body; const { userId, projectId, roleId } = req.body;
if (!userId || !projectId || !roleId) if (!userId || !projectId || !roleId) return res.status(400).json({ message: "userId, projectId, and roleId are required." });
return res
.status(400)
.json({ message: "userId, projectId, and roleId are required." });
try { try {
const deletedCount = await UserProjectRole.destroy({ const deletedCount = await UserProjectRole.destroy({ where: { user_id: userId, project_id: projectId, role_id: roleId } });
where: { user_id: userId, project_id: projectId, role_id: roleId }, if (deletedCount === 0) return res.status(404).json({ message: 'Assignment not found.' });
});
if (deletedCount === 0)
return res.status(404).json({ message: "Assignment not found." });
res.status(204).send(); res.status(204).send();
} catch (error) { } catch (error) { next(error); }
next(error);
}
}); });
export default router; export default router;

203
backend/src/routes/users.js Normal file → Executable file
View File

@@ -1,136 +1,99 @@
// File: backend/src/routes/users.js // File: backend/src/routes/users.js
import { Router } from "express"; import { Router } from 'express';
import { User, Role } from "../db/sequelize.js"; import { User, Role } from '../db/sequelize.js';
import { authJwt, permGuard } from "../middleware/index.js"; import { authJwt } from "../middleware/authJwt.js";
import { hashPassword } from "../utils/passwords.js"; import { loadPrincipalMw } from "../middleware/loadPrincipal.js"; // แก้ไข: import ให้ถูกต้อง
import { requirePerm } from '../middleware/requirePerm.js';
import { hashPassword } from '../utils/passwords.js';
const router = Router(); const router = Router();
// Middleware สำหรับทุก route ในไฟล์นี้ // Middleware Chain ที่ถูกต้อง 100%
router.use(authJwt.verifyToken); router.use(authJwt(), loadPrincipalMw());
// GET /api/users - ดึงรายชื่อผู้ใช้ทั้งหมด // GET /api/users
router.get( router.get('/', requirePerm('users.view'), async (req, res, next) => {
"/",
permGuard("manage_users"), // ตรวจสอบสิทธิ์
async (req, res, next) => {
try { try {
const users = await User.findAll({ const users = await User.findAll({
attributes: { exclude: ["password_hash"] }, // **สำคัญมาก: ห้ามส่ง password hash ออกไป** attributes: { exclude: ['password_hash'] },
include: [ include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }],
{ order: [['username', 'ASC']]
model: Role, });
attributes: ["id", "name"], res.json(users);
through: { attributes: [] }, // ไม่ต้องเอาข้อมูลจากตาราง join (UserRoles) มา } catch (error) { next(error); }
}, });
],
order: [["username", "ASC"]], // POST /api/users
}); router.post('/', requirePerm('users.manage'), async (req, res, next) => {
res.json(users); const { username, email, password, first_name, last_name, is_active, roles } = req.body;
if (!username || !email || !password) {
return res.status(400).json({ message: 'Username, email, and password are required' });
}
try {
const password_hash = await hashPassword(password);
const newUser = await User.create({
username, email, password_hash, first_name, last_name, is_active: is_active !== false,
created_by: req.principal.user_id,
updated_by: req.principal.user_id,
org_id: req.principal.org_ids[0] || null,
});
if (roles && roles.length > 0) {
await newUser.setRoles(roles);
}
const userWithRoles = await User.findByPk(newUser.id, {
attributes: { exclude: ['password_hash'] },
include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }]
});
res.status(201).json(userWithRoles);
} catch (error) { } catch (error) {
next(error); if (error.name === 'SequelizeUniqueConstraintError') {
return res.status(409).json({ message: 'Username or email already exists.' });
}
next(error);
} }
}
);
// POST /api/users - สร้างผู้ใช้ใหม่
router.post("/", permGuard("manage_users"), async (req, res, next) => {
const { username, email, password, first_name, last_name, is_active, roles } =
req.body;
if (!username || !email || !password) {
return res
.status(400)
.json({ message: "Username, email, and password are required" });
}
try {
const password_hash = await hashPassword(password);
const newUser = await User.create({
username,
email,
password_hash,
first_name,
last_name,
is_active: is_active !== false,
});
if (roles && roles.length > 0) {
await newUser.setRoles(roles);
}
const userWithRoles = await User.findByPk(newUser.id, {
attributes: { exclude: ["password_hash"] },
include: [
{
model: Role,
attributes: ["id", "name"],
through: { attributes: [] },
},
],
});
res.status(201).json(userWithRoles);
} catch (error) {
if (error.name === "SequelizeUniqueConstraintError") {
return res
.status(409)
.json({ message: "Username or email already exists." });
}
next(error);
}
}); });
// PUT /api/users/:id - อัปเดตข้อมูลผู้ใช้ // PUT /api/users/:id
router.put("/:id", permGuard("manage_users"), async (req, res, next) => { router.put('/:id', requirePerm('users.manage'), async (req, res, next) => {
const { id } = req.params; const { id } = req.params;
const { email, first_name, last_name, is_active, roles } = req.body; const { email, first_name, last_name, is_active, roles } = req.body;
try {
const user = await User.findByPk(id);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
user.email = email ?? user.email;
user.first_name = first_name ?? user.first_name;
user.last_name = last_name ?? user.last_name;
user.is_active = is_active ?? user.is_active;
user.updated_by = req.principal.user_id;
await user.save();
try { if (roles) {
const user = await User.findByPk(id); await user.setRoles(roles);
if (!user) { }
return res.status(404).json({ message: "User not found" }); const updatedUser = await User.findByPk(id, {
} attributes: { exclude: ['password_hash'] },
include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }]
user.email = email ?? user.email; });
user.first_name = first_name ?? user.first_name; res.json(updatedUser);
user.last_name = last_name ?? user.last_name; } catch (error) { next(error); }
user.is_active = is_active ?? user.is_active;
await user.save();
if (roles) {
await user.setRoles(roles);
}
const updatedUser = await User.findByPk(id, {
attributes: { exclude: ["password_hash"] },
include: [
{
model: Role,
attributes: ["id", "name"],
through: { attributes: [] },
},
],
});
res.json(updatedUser);
} catch (error) {
next(error);
}
}); });
// DELETE /api/users/:id - ลบผู้ใช้ (Soft Delete) // DELETE /api/users/:id
router.delete("/:id", permGuard("manage_users"), async (req, res, next) => { router.delete('/:id', requirePerm('users.manage'), async (req, res, next) => {
try { try {
const user = await User.findByPk(req.params.id); const user = await User.findByPk(req.params.id);
if (!user) { if (!user) {
return res.status(404).json({ message: "User not found" }); return res.status(404).json({ message: 'User not found' });
} }
user.is_active = false; // Soft Delete user.is_active = false;
await user.save(); user.updated_by = req.principal.user_id;
res.status(204).send(); await user.save();
} catch (error) { res.status(204).send();
next(error); } catch (error) { next(error); }
}
}); });
export default router; export default router;

38
frontend/lib/auth copy.js Executable file
View File

@@ -0,0 +1,38 @@
// frontend/lib/auth.js
import { cookies } from "next/headers";
const COOKIE_NAME = "access_token";
/**
* Server-side session fetcher (ใช้ใน Server Components/Layouts)
* - อ่านคุกกี้แบบ async: await cookies()
* - ถ้าไม่มี token → return null
* - ถ้ามี → เรียก /api/auth/me ที่ backend เพื่อตรวจสอบ
*/
export async function getSession() {
// ✅ ต้อง await
const cookieStore = await cookies();
const token = cookieStore.get(COOKIE_NAME)?.value;
if (!token) return null;
// เรียก backend ตรวจ session (ปรับ endpoint ให้ตรงของคุณ)
const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE}/api/auth/me`, {
// ส่งต่อคุกกี้ไป backend (เลือกอย่างใดอย่างหนึ่ง)
// วิธี A: ส่ง header Cookie โดยตรง
headers: { Cookie: `${COOKIE_NAME}=${token}` },
// วิธี B: ถ้า proxy ผ่าน nginx ในโดเมนเดียวกัน ใช้ credentials รวมคุกกี้อัตโนมัติได้
// credentials: "include",
cache: "no-store",
});
if (!res.ok) return null;
const data = await res.json();
// คาดหวังโครงสร้าง { user, permissions } จาก backend
return {
user: data.user,
permissions: data.permissions || [],
token,
};
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,6 @@
1 1
/var/lib/postgresql/data /var/lib/postgresql/data
1759373409 1759595341
5432 5432
/var/run/postgresql /var/run/postgresql
* *

Binary file not shown.

View File

@@ -1,99 +0,0 @@
# ------------------------------------------------------------
# git.np-dms.work
# ------------------------------------------------------------
map $scheme $hsts_header {
https "max-age=63072000; preload";
}
server {
set $forward_scheme http;
set $server "gitea";
set $port 3000;
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
server_name git.np-dms.work;
http2 on;
# Let's Encrypt SSL
include conf.d/include/letsencrypt-acme-challenge.conf;
include conf.d/include/ssl-cache.conf;
include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/npm-10/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/npm-10/privkey.pem;
# Force SSL
include conf.d/include/force-ssl.conf;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
access_log /data/logs/proxy-host-5_access.log proxy;
error_log /data/logs/proxy-host-5_error.log warn;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization $http_authorization;
proxy_connect_timeout 30s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
send_timeout 300s;
client_max_body_size 512m;
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
# Proxy!
include conf.d/include/proxy.conf;
}
# Custom
include /data/nginx/custom/server_proxy[.]conf;
}

View File

@@ -1,5 +1,5 @@
# ------------------------------------------------------------ # ------------------------------------------------------------
# pma.np-dms.work # git.np-dms.work
# ------------------------------------------------------------ # ------------------------------------------------------------
@@ -10,8 +10,8 @@ map $scheme $hsts_header {
server { server {
set $forward_scheme http; set $forward_scheme http;
set $server "phpmyadmin"; set $server "gitea";
set $port 80; set $port 3000;
listen 80; listen 80;
listen [::]:80; listen [::]:80;
@@ -20,16 +20,17 @@ listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
server_name pma.np-dms.work; server_name git.np-dms.work;
http2 off;
http2 on;
# Let's Encrypt SSL # Let's Encrypt SSL
include conf.d/include/letsencrypt-acme-challenge.conf; include conf.d/include/letsencrypt-acme-challenge.conf;
include conf.d/include/ssl-cache.conf; include conf.d/include/ssl-cache.conf;
include conf.d/include/ssl-ciphers.conf; include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/npm-11/fullchain.pem; ssl_certificate /etc/letsencrypt/live/npm-10/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/npm-11/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/npm-10/privkey.pem;
@@ -45,28 +46,31 @@ http2 off;
# Force SSL
include conf.d/include/force-ssl.conf;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection; proxy_set_header Connection $http_connection;
proxy_http_version 1.1; proxy_http_version 1.1;
access_log /data/logs/proxy-host-6_access.log proxy; access_log /data/logs/proxy-host-5_access.log proxy;
error_log /data/logs/proxy-host-6_error.log warn; error_log /data/logs/proxy-host-5_error.log warn;
client_max_body_size 128m;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization $http_authorization;
proxy_buffering off; proxy_connect_timeout 30s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
send_timeout 300s;
client_max_body_size 512m;
@@ -81,6 +85,8 @@ proxy_buffering off;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection; proxy_set_header Connection $http_connection;
proxy_http_version 1.1; proxy_http_version 1.1;

View File

@@ -1 +0,0 @@
PMA_token |s:32:"6537407a672f376761784c234d79666a"; HMAC_secret |s:16:";_tF,Ps<jX+#Z=f>";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"34347471654523645a3b4a32556f6c2c"; HMAC_secret |s:16:"cxqQ@%S|9[H3Y6^%";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"263a746d69672f6a57305b5842603462"; HMAC_secret |s:16:".e@/Y$FkQ[hrnThJ";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"53754e49395b233f312a3e444e604c6c"; HMAC_secret |s:16:"'W%y<SKRJnU;}^!|";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"4156236d5e362168327e646d41463b24"; HMAC_secret |s:16:"0JzUDdkie;5^OD$x";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"397472635b712b6d2a40624244354f3f"; HMAC_secret |s:16:"MIW6@(E`(8hUZPvn";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"59692f53212c545c55273d71446d4a3a"; HMAC_secret |s:16:"-+x:]'T{Gq%(y'M)";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"4239422850276e464b7e426d556b5277"; HMAC_secret |s:16:"q]Ldh$];4JH,{.MX";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"24545877502f6d7d7967723b5e6c3a38"; HMAC_secret |s:16:"B%B#Rs(y)DGW?uv6";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"222b524d303848785047625636644350"; HMAC_secret |s:16:"r>gT.Yji&;>+(WBB";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"2f5e6c66684a7b414659303778513a25"; HMAC_secret |s:16:"32szCS_&Uq+0r$jk";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"637c736e2b30345b2f5e34516d2a5279"; HMAC_secret |s:16:"dwf#X!d2CIQn#30!";browser_access_time|a:0:{}encryption_key|s:32:"<22>S];W<>

View File

@@ -1 +0,0 @@
PMA_token |s:32:"32295c5e38423a7e2a4a237221505b3d"; HMAC_secret |s:16:"&:))STa2pQ/P6Z"h";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"2f472d2e70326a4f667d725e6e6c2e34"; HMAC_secret |s:16:"mAV2g;|NA4_6~ZyR";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"7057757d4e6d696d4e592a7b37584c55"; HMAC_secret |s:16:"A;qa<:i@!s2_vvR>";errors|a:0:{}

View File

@@ -1 +0,0 @@
PMA_token |s:32:"34247c3c42743a5d50762a54336f2355"; HMAC_secret |s:16:"vj[cT;Q$S4x)t]Qi";errors|a:0:{}