backend: Mod
This commit is contained in:
		
							
								
								
									
										150
									
								
								backend/src/db/sequelize.js
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										150
									
								
								backend/src/db/sequelize.js
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -5,11 +5,7 @@ | ||||
| import { Sequelize } from "sequelize"; | ||||
| import { config } from "../config.js"; | ||||
|  | ||||
| export const sequelize = new Sequelize( | ||||
|   config.DB.NAME, | ||||
|   config.DB.USER, | ||||
|   config.DB.PASS, | ||||
|   { | ||||
| export const sequelize = new Sequelize(config.DB.NAME, config.DB.USER, config.DB.PASS, { | ||||
|     host: config.DB.HOST, | ||||
|     port: config.DB.PORT, | ||||
|     dialect: "mariadb", | ||||
| @@ -17,79 +13,91 @@ export const sequelize = new Sequelize( | ||||
|     dialectOptions: { timezone: "Z" }, | ||||
|     define: { freezeTableName: true, underscored: false, timestamps: false }, | ||||
|     pool: { max: 10, min: 0, idle: 10000 }, | ||||
|   } | ||||
| ); | ||||
| }); | ||||
|  | ||||
| // --- 1. ประกาศตัวแปรสำหรับ Export Model ทั้งหมด --- | ||||
| export let User = null; | ||||
| export let Role = null; | ||||
| export let Permission = null; | ||||
| export let UserRole = null; | ||||
| export let RolePermission = null; | ||||
| export let Project = null; // <-- เพิ่มเข้ามา | ||||
| export let UserProjectRole = null; // <-- เพิ่มเข้ามา | ||||
| export let User, Role, Permission, Organization, Project, UserRole, RolePermission,  | ||||
|     UserProjectRole, Correspondence, CorrespondenceVersion, Document, CorrDocumentMap,  | ||||
|     Drawing, DrawingRevision, FileObject, RFA, RFARevision, RfaDrawingMap,  | ||||
|     Transmittal, TransmittalItem, Volume, ContractDwg, SubCategory; | ||||
|  | ||||
| if (process.env.ENABLE_SEQUELIZE === "1") { | ||||
|   // --- 2. โหลดโมเดลทั้งหมดแบบ on-demand --- | ||||
|   const mdlUser = await import("./models/User.js").catch(() => null); | ||||
|   const mdlRole = await import("./models/Role.js").catch(() => null); | ||||
|   const mdlPerm = await import("./models/Permission.js").catch(() => null); | ||||
|   const mdlUR = await import("./models/UserRole.js").catch(() => null); | ||||
|   const mdlRP = await import("./models/RolePermission.js").catch(() => null); | ||||
|   const mdlProj = await import("./models/Project.js").catch(() => null); // <-- เพิ่มเข้ามา | ||||
|   const mdlUPR = await import("./models/UserProjectRole.js").catch(() => null); // <-- เพิ่มเข้ามา | ||||
|     // --- 2. สร้าง Object ของ Models ทั้งหมดที่จะโหลด --- | ||||
|     const modelsToLoad = { | ||||
|         User: await import("./models/User.js").catch(() => null), | ||||
|         Role: await import("./models/Role.js").catch(() => null), | ||||
|         Permission: await import("./models/Permission.js").catch(() => null), | ||||
|         Organization: await import("./models/Organization.js").catch(() => null), | ||||
|         Project: await import("./models/Project.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 ทั้งหมด --- | ||||
|   if (mdlUser?.default) User = mdlUser.default(sequelize); | ||||
|   if (mdlRole?.default) Role = mdlRole.default(sequelize); | ||||
|   if (mdlPerm?.default) Permission = mdlPerm.default(sequelize); | ||||
|   if (mdlUR?.default) UserRole = mdlUR.default(sequelize); | ||||
|   if (mdlRP?.default) RolePermission = mdlRP.default(sequelize); | ||||
|   if (mdlProj?.default) Project = mdlProj.default(sequelize); // <-- เพิ่มเข้ามา | ||||
|   if (mdlUPR?.default) UserProjectRole = mdlUPR.default(sequelize); // <-- เพิ่มเข้ามา | ||||
|     // --- 3. Initialize Model ทั้งหมด --- | ||||
|     User = modelsToLoad.User?.default ? modelsToLoad.User.default(sequelize) : null; | ||||
|     Role = modelsToLoad.Role?.default ? modelsToLoad.Role.default(sequelize) : null; | ||||
|     Permission = modelsToLoad.Permission?.default ? modelsToLoad.Permission.default(sequelize) : null; | ||||
|     Organization = modelsToLoad.Organization?.default ? modelsToLoad.Organization.default(sequelize) : null; | ||||
|     Project = modelsToLoad.Project?.default ? modelsToLoad.Project.default(sequelize) : null; | ||||
|     UserRole = modelsToLoad.UserRole?.default ? modelsToLoad.UserRole.default(sequelize) : null; | ||||
|     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) - ส่วนที่เพิ่มเข้ามา | ||||
|   if (User && Project && Role && UserProjectRole) { | ||||
|     // ทำให้ UserProjectRole เป็นตัวกลางเชื่อม 3 ตาราง | ||||
|     UserProjectRole.belongsTo(User, { foreignKey: "user_id" }); | ||||
|     UserProjectRole.belongsTo(Project, { foreignKey: "project_id" }); | ||||
|     UserProjectRole.belongsTo(Role, { foreignKey: "role_id" }); | ||||
|  | ||||
|     // ทำให้สามารถ include ข้อมูลที่เกี่ยวข้องได้ง่ายขึ้น | ||||
|     User.hasMany(UserProjectRole, { foreignKey: "user_id" }); | ||||
|     Project.hasMany(UserProjectRole, { foreignKey: "project_id" }); | ||||
|     Role.hasMany(UserProjectRole, { foreignKey: "role_id" }); | ||||
|   } | ||||
|     // --- 4. สร้างความสัมพันธ์ (Associations) --- | ||||
|     const loadedModels = { User, Role, Permission, Organization, Project, UserRole, RolePermission,  | ||||
|         UserProjectRole, Correspondence, CorrespondenceVersion, Document, CorrDocumentMap,  | ||||
|         Drawing, DrawingRevision, FileObject, RFA, RFARevision, RfaDrawingMap,  | ||||
|         Transmittal, TransmittalItem, Volume, ContractDwg, SubCategory }; | ||||
|          | ||||
|     for (const modelName in loadedModels) { | ||||
|         if (loadedModels[modelName] && loadedModels[modelName].associate) { | ||||
|             loadedModels[modelName].associate(loadedModels); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * ฟังก์ชันสำหรับทดสอบการเชื่อมต่อ Sequelize | ||||
|  */ | ||||
| export async function dbReady() { | ||||
|   if (process.env.ENABLE_SEQUELIZE !== "1") { | ||||
|     console.log("Sequelize is disabled."); | ||||
|     return Promise.resolve(); | ||||
|   } | ||||
|   try { | ||||
|     await sequelize.authenticate(); | ||||
|     console.log("Sequelize connection has been established successfully."); | ||||
|   } catch (error) { | ||||
|     console.error("Unable to connect to the database via Sequelize:", error); | ||||
|     return Promise.reject(error); | ||||
|   } | ||||
| } | ||||
|     if (process.env.ENABLE_SEQUELIZE !== "1") { | ||||
|         console.log("Sequelize is disabled."); | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
|     try { | ||||
|         await sequelize.authenticate(); | ||||
|         console.log("Sequelize connection has been established successfully."); | ||||
|     } catch (error) { | ||||
|         console.error("Unable to connect to the database via Sequelize:", error); | ||||
|         return Promise.reject(error); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										61
									
								
								backend/src/middleware/auth copy.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										61
									
								
								backend/src/middleware/auth copy.js
									
									
									
									
									
										Executable 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
									
								
							
							
						
						
									
										0
									
								
								backend/src/middleware/auth.js
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										6
									
								
								backend/src/middleware/index.js
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										6
									
								
								backend/src/middleware/index.js
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -1,7 +1,7 @@ | ||||
| // File: backend/src/middleware/index.js | ||||
| import * as authJwt from "./authJwt.js"; | ||||
| import * as abac from "./abac.js"; | ||||
| import * as auth from "./auth.js"; | ||||
| import * as authJwt from "./authJwt.js"; | ||||
| import * as errorHandler from "./errorHandler.js"; | ||||
| import * as loadPrincipal from "./loadPrincipal.js"; | ||||
| import * as permGuard from "./permGuard.js"; | ||||
| @@ -12,9 +12,9 @@ import * as requirePerm from "./requirePerm.js"; | ||||
| // Export ทุกอย่างออกมาเป็น named exports | ||||
| // เพื่อให้สามารถ import แบบ `import { authJwt, permGuard } from '../middleware';` ได้ | ||||
| export { | ||||
|   authJwt, | ||||
|   abac, | ||||
|   auth, | ||||
|   authJwt, | ||||
|   errorHandler, | ||||
|   loadPrincipal, | ||||
|   permGuard, | ||||
| @@ -25,9 +25,9 @@ export { | ||||
|  | ||||
| // (Optional) สร้าง default export สำหรับกรณีที่ต้องการ import ทั้งหมดใน object เดียว | ||||
| const middleware = { | ||||
|   authJwt, | ||||
|   abac, | ||||
|   auth, | ||||
|   authJwt, | ||||
|   errorHandler, | ||||
|   loadPrincipal, | ||||
|   permGuard, | ||||
|   | ||||
							
								
								
									
										0
									
								
								backend/src/middleware/loadPrincipal.js
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								backend/src/middleware/loadPrincipal.js
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
								
								
									
										74
									
								
								backend/src/routes/dashboard.js
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										74
									
								
								backend/src/routes/dashboard.js
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -1,23 +1,63 @@ | ||||
| import { Router } from "express"; | ||||
| import { User } from "../db/index.js"; | ||||
| import { authJwt } from "../middleware/index.js"; | ||||
| // backend/src/routes/dashboard.js | ||||
| import { Router } from 'express'; | ||||
| 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(); | ||||
|  | ||||
| router.use(authJwt.verifyToken); | ||||
| // 3. ใช้ Middleware Chain ที่ถูกต้อง 100% | ||||
| router.use(authJwt(), loadPrincipalMw()); | ||||
|  | ||||
| router.get("/users/summary", async (req, res, next) => { | ||||
|   try { | ||||
|     const totalUsers = await User.count(); | ||||
|     const activeUsers = await User.count({ where: { is_active: true } }); | ||||
|     res.json({ | ||||
|       total: totalUsers, | ||||
|       active: activeUsers, | ||||
|       inactive: totalUsers - activeUsers, | ||||
|     }); | ||||
|   } catch (error) { | ||||
|     next(error); | ||||
|   } | ||||
|  | ||||
| // === API สำหรับ User Management Widget === | ||||
| router.get('/users/summary', async (req, res, next) => { | ||||
|     try { | ||||
|         // ตรวจสอบว่า Model ถูกโหลดแล้วหรือยัง (จำเป็นสำหรับโหมด lazy-load) | ||||
|         if (!User) { | ||||
|             return res.status(503).json({ message: 'Database models not available. Is ENABLE_SEQUELIZE=1 set?' }); | ||||
|         } | ||||
|         const totalUsers = await User.count(); | ||||
|         const activeUsers = await User.count({ where: { is_active: true } }); | ||||
|          | ||||
|         res.json({ | ||||
|             total: totalUsers, | ||||
|             active: activeUsers, | ||||
|             inactive: totalUsers - activeUsers, | ||||
|         }); | ||||
|     } catch (error) { | ||||
|         next(error); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| export default router; | ||||
|  | ||||
| // === 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; | ||||
							
								
								
									
										130
									
								
								backend/src/routes/rbac_admin.js
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										130
									
								
								backend/src/routes/rbac_admin.js
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -1,144 +1,88 @@ | ||||
| // FILE: backend/src/routes/rbac_admin.js | ||||
| import { Router } from "express"; | ||||
| 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(); | ||||
|  | ||||
| // กำหนดให้ทุก route ในไฟล์นี้ต้องมีสิทธิ์ 'manage_rbac' | ||||
| router.use(authJwt.verifyToken, permGuard("manage_rbac")); | ||||
| // Middleware Chain ที่ถูกต้อง 100% ตามสถาปัตยกรรมของคุณ | ||||
| router.use(authJwt(), loadPrincipalMw()); | ||||
|  | ||||
| // == ROLES Management == | ||||
| router.get("/roles", async (req, res, next) => { | ||||
| router.get("/roles", requirePerm("roles.manage"), async (req, res, next) => { | ||||
|   try { | ||||
|     const roles = await Role.findAll({ | ||||
|       include: [ | ||||
|         { | ||||
|           model: Permission, | ||||
|           attributes: ["id", "name"], | ||||
|           through: { attributes: [] }, | ||||
|         }, | ||||
|       ], | ||||
|       include: [{ model: Permission, attributes: ["id", "name"], through: { attributes: [] } }], | ||||
|       order: [["name", "ASC"]], | ||||
|     }); | ||||
|     res.json(roles); | ||||
|   } catch (error) { | ||||
|     next(error); | ||||
|   } | ||||
|   } catch (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 { | ||||
|     const { name, description } = req.body; | ||||
|     if (!name) | ||||
|       return res.status(400).json({ message: "Role name is required." }); | ||||
|     const newRole = await Role.create({ name, description }); | ||||
|     res.status(201).json(newRole); | ||||
|   } catch (error) { | ||||
|     if (error.name === "SequelizeUniqueConstraintError") { | ||||
|       return res | ||||
|         .status(409) | ||||
|         .json({ message: `Role '${name}' already exists.` }); | ||||
|       return res.status(409).json({ message: `Role '${name}' already exists.` }); | ||||
|     } | ||||
|     next(error); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| router.put("/roles/:id/permissions", async (req, res, next) => { | ||||
|   try { | ||||
| router.put("/roles/:id/permissions", requirePerm("roles.manage"), async (req, res, next) => { | ||||
|     const { permissionIds } = req.body; | ||||
|     if (!Array.isArray(permissionIds)) | ||||
|       return res | ||||
|         .status(400) | ||||
|         .json({ message: "permissionIds must be an array." }); | ||||
|  | ||||
|     const role = await Role.findByPk(req.params.id); | ||||
|     if (!role) return res.status(404).json({ message: "Role not found." }); | ||||
|  | ||||
|     await role.setPermissions(permissionIds); | ||||
|     const updatedRole = await Role.findByPk(req.params.id, { | ||||
|       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); | ||||
|   } | ||||
|     if (!Array.isArray(permissionIds)) return res.status(400).json({ message: "permissionIds must be an array." }); | ||||
|     try { | ||||
|         const role = await Role.findByPk(req.params.id); | ||||
|         if (!role) return res.status(404).json({ message: "Role not found." }); | ||||
|         await role.setPermissions(permissionIds); | ||||
|         const updatedRole = await Role.findByPk(req.params.id, { | ||||
|             include: [{ model: Permission, attributes: ['id', 'name'], through: { attributes: [] } }] | ||||
|         }); | ||||
|         res.json(updatedRole); | ||||
|     } catch (error) { next(error); } | ||||
| }); | ||||
|  | ||||
| // == 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; | ||||
|   if (!userId) | ||||
|     return res | ||||
|       .status(400) | ||||
|       .json({ message: "userId query parameter is required." }); | ||||
|   if (!userId) return res.status(400).json({ message: "userId query parameter is required." }); | ||||
|   try { | ||||
|     const assignments = await UserProjectRole.findAll({ | ||||
|       where: { user_id: userId }, | ||||
|       include: [ | ||||
|         { model: Project, attributes: ["id", "name"] }, | ||||
|         { model: Role, attributes: ["id", "name"] }, | ||||
|       ], | ||||
|       include: [ { model: Project, attributes: ["id", "name"] }, { model: Role, attributes: ["id", "name"] } ], | ||||
|     }); | ||||
|     res.json(assignments); | ||||
|   } catch (error) { | ||||
|     next(error); | ||||
|   } | ||||
|   } catch (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; | ||||
|   if (!userId || !projectId || !roleId) | ||||
|     return res | ||||
|       .status(400) | ||||
|       .json({ message: "userId, projectId, and roleId are required." }); | ||||
|   if (!userId || !projectId || !roleId) return res.status(400).json({ message: "userId, projectId, and roleId are required." }); | ||||
|   try { | ||||
|     const [assignment, created] = await UserProjectRole.findOrCreate({ | ||||
|       where: { user_id: userId, project_id: projectId, role_id: roleId }, | ||||
|       defaults: { user_id: userId, project_id: projectId, role_id: roleId }, | ||||
|     }); | ||||
|     if (!created) | ||||
|       return res | ||||
|         .status(409) | ||||
|         .json({ message: "This assignment already exists." }); | ||||
|     if (!created) return res.status(409).json({ message: "This assignment already exists." }); | ||||
|     res.status(201).json(assignment); | ||||
|   } catch (error) { | ||||
|     next(error); | ||||
|   } | ||||
|   } catch (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; | ||||
|   if (!userId || !projectId || !roleId) | ||||
|     return res | ||||
|       .status(400) | ||||
|       .json({ message: "userId, projectId, and roleId are required." }); | ||||
|   if (!userId || !projectId || !roleId) return res.status(400).json({ message: "userId, projectId, and roleId are required." }); | ||||
|   try { | ||||
|     const deletedCount = await UserProjectRole.destroy({ | ||||
|       where: { user_id: userId, project_id: projectId, role_id: roleId }, | ||||
|     }); | ||||
|     if (deletedCount === 0) | ||||
|       return res.status(404).json({ message: "Assignment not found." }); | ||||
|     const deletedCount = await UserProjectRole.destroy({ where: { user_id: userId, project_id: projectId, role_id: roleId } }); | ||||
|     if (deletedCount === 0) return res.status(404).json({ message: 'Assignment not found.' }); | ||||
|     res.status(204).send(); | ||||
|   } catch (error) { | ||||
|     next(error); | ||||
|   } | ||||
|   } catch (error) { next(error); } | ||||
| }); | ||||
|  | ||||
| export default router; | ||||
| export default router; | ||||
							
								
								
									
										205
									
								
								backend/src/routes/users.js
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										205
									
								
								backend/src/routes/users.js
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @@ -1,136 +1,99 @@ | ||||
| // File: backend/src/routes/users.js | ||||
| import { Router } from "express"; | ||||
| import { User, Role } from "../db/sequelize.js"; | ||||
| import { authJwt, permGuard } from "../middleware/index.js"; | ||||
| import { hashPassword } from "../utils/passwords.js"; | ||||
| import { Router } from 'express'; | ||||
| import { User, Role } from '../db/sequelize.js'; | ||||
| import { authJwt } from "../middleware/authJwt.js"; | ||||
| import { loadPrincipalMw } from "../middleware/loadPrincipal.js"; // แก้ไข: import ให้ถูกต้อง | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import { hashPassword } from '../utils/passwords.js'; | ||||
|  | ||||
| const router = Router(); | ||||
|  | ||||
| // Middleware สำหรับทุก route ในไฟล์นี้ | ||||
| router.use(authJwt.verifyToken); | ||||
| // Middleware Chain ที่ถูกต้อง 100% | ||||
| router.use(authJwt(), loadPrincipalMw()); | ||||
|  | ||||
| // GET /api/users - ดึงรายชื่อผู้ใช้ทั้งหมด | ||||
| router.get( | ||||
|   "/", | ||||
|   permGuard("manage_users"), // ตรวจสอบสิทธิ์ | ||||
|   async (req, res, next) => { | ||||
| // GET /api/users | ||||
| router.get('/', requirePerm('users.view'), async (req, res, next) => { | ||||
|     try { | ||||
|       const users = await User.findAll({ | ||||
|         attributes: { exclude: ["password_hash"] }, // **สำคัญมาก: ห้ามส่ง password hash ออกไป** | ||||
|         include: [ | ||||
|           { | ||||
|             model: Role, | ||||
|             attributes: ["id", "name"], | ||||
|             through: { attributes: [] }, // ไม่ต้องเอาข้อมูลจากตาราง join (UserRoles) มา | ||||
|           }, | ||||
|         ], | ||||
|         order: [["username", "ASC"]], | ||||
|       }); | ||||
|       res.json(users); | ||||
|         const users = await User.findAll({ | ||||
|             attributes: { exclude: ['password_hash'] }, | ||||
|             include: [{ model: Role, attributes: ['id', 'name'], through: { attributes: [] } }], | ||||
|             order: [['username', 'ASC']] | ||||
|         }); | ||||
|         res.json(users); | ||||
|     } catch (error) { next(error); } | ||||
| }); | ||||
|  | ||||
| // POST /api/users | ||||
| router.post('/', requirePerm('users.manage'), 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, | ||||
|             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) { | ||||
|       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 - อัปเดตข้อมูลผู้ใช้ | ||||
| router.put("/:id", permGuard("manage_users"), async (req, res, next) => { | ||||
|   const { id } = req.params; | ||||
|   const { email, first_name, last_name, is_active, roles } = req.body; | ||||
| // PUT /api/users/:id | ||||
| router.put('/:id', requirePerm('users.manage'), async (req, res, next) => { | ||||
|     const { id } = req.params; | ||||
|     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 { | ||||
|     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; | ||||
|     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); | ||||
|   } | ||||
|         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) | ||||
| router.delete("/:id", permGuard("manage_users"), async (req, res, next) => { | ||||
|   try { | ||||
|     const user = await User.findByPk(req.params.id); | ||||
|     if (!user) { | ||||
|       return res.status(404).json({ message: "User not found" }); | ||||
|     } | ||||
|     user.is_active = false; // Soft Delete | ||||
|     await user.save(); | ||||
|     res.status(204).send(); | ||||
|   } catch (error) { | ||||
|     next(error); | ||||
|   } | ||||
| // DELETE /api/users/:id | ||||
| router.delete('/:id', requirePerm('users.manage'), async (req, res, next) => { | ||||
|     try { | ||||
|         const user = await User.findByPk(req.params.id); | ||||
|         if (!user) { | ||||
|             return res.status(404).json({ message: 'User not found' }); | ||||
|         } | ||||
|         user.is_active = false; | ||||
|         user.updated_by = req.principal.user_id; | ||||
|         await user.save(); | ||||
|         res.status(204).send(); | ||||
|     } catch (error) { next(error); } | ||||
| }); | ||||
|  | ||||
| export default router; | ||||
| export default router; | ||||
		Reference in New Issue
	
	Block a user