This commit is contained in:
2025-10-05 11:57:43 +07:00
parent 754e494e7f
commit 670228b76e
52 changed files with 39264 additions and 331 deletions

4
.qsync/meta/meta.conf Normal file
View File

@@ -0,0 +1,4 @@
[/dms]
max_log = 502986
number = 3
finish = 1

10988
.qsync/meta/qmeta0 Normal file

File diff suppressed because it is too large Load Diff

11488
.qsync/meta/qmeta1 Normal file

File diff suppressed because it is too large Load Diff

8799
.qsync/meta/qmeta2 Normal file

File diff suppressed because it is too large Load Diff

150
backend/src/db/sequelize.js Normal file → Executable file
View 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);
}
}

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
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
View File

74
backend/src/routes/dashboard.js Normal file → Executable file
View 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
View 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
View 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;

View File

@@ -1,22 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": false,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

View File

@@ -1,3 +1,4 @@
<<<<<<< HEAD
// frontend/lib/auth.js
import { cookies } from "next/headers";
@@ -36,3 +37,43 @@ export async function getSession() {
token,
};
}
=======
// 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,
};
}
>>>>>>> 71fc7eee (backend: Mod)

7509
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,12 +7,12 @@ services:
user: "${UID:-1000}:${GID:-1000}"
command: >
sh -c "
echo '📦 Installing dependencies...' &&
echo '...installing dependencies...' &&
npm install &&
echo '🎨 Initializing shadcn/ui...' &&
echo '...initiallizing shadcn/ui...' &&
npx shadcn@latest init -y -d &&
echo '📥 Adding components...' &&
npx shadcn@latest add -y alert-dialog dialog checkbox scroll-area button label input card badge tabs progress dropdown-menu tooltip switch &&
echo '...adding components...' &&
npx shadcn@latest add -y alert dialog checkbox scroll-area button label input card badge tabs progress dropdown-menu tooltip switch &&
echo '✅ Done! Check components/ui/ directory'
"

Binary file not shown.

Binary file not shown.

BIN
n8n-postgres/base/16384/1247 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/1247_vm Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/1249 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/1249_fsm Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/1249_vm Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/1259 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/1259_vm Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2608 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2608_vm Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2619 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2619_fsm Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2658 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2658_fsm Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2659 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2659_fsm Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2662 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2663 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2673 Executable file

Binary file not shown.

Binary file not shown.

BIN
n8n-postgres/base/16384/2674 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2696 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2703 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2704 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2840 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/2841 Executable file

Binary file not shown.

BIN
n8n-postgres/base/16384/3455 Executable file

Binary file not shown.

Binary file not shown.

BIN
n8n-postgres/global/pg_control Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
n8n-postgres/pg_xact/0000 Executable file

Binary file not shown.

View File

@@ -0,0 +1,8 @@
1
/var/lib/postgresql/data
1759595341
5432
/var/run/postgresql
*
117512122 0
ready

BIN
npm/data/database.sqlite Normal file

Binary file not shown.

View File

@@ -0,0 +1,102 @@
# ------------------------------------------------------------
# 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;
# Block Exploits
include conf.d/include/block-exploits.conf;
# 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;
}