fix: tailwind v4 postcss, auth-server session, eslint cleanups

This commit is contained in:
2025-10-09 15:47:56 +07:00
parent 670228b76e
commit bbfbc5b910
117 changed files with 4005 additions and 3414 deletions

BIN
backend/backend_tree.txt Executable file

Binary file not shown.

77
backend/docker-compose.yml Executable file
View File

@@ -0,0 +1,77 @@
# File: backend/docker-compose.yml
# DMS Container v0_8_0 แยก service/ lcbp3-backend
x-restart: &restart_policy
restart: unless-stopped
x-logging: &default_logging
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
volumes:
backend_node_modules:
services:
backend:
<<: [*restart_policy, *default_logging]
image: dms-backend:dev
# pull_policy: never # <-- FINAL FIX ADDED HERE
container_name: dms_backend
stdin_open: true
tty: true
#user: "node"
user: "1000:1000"
working_dir: /app
deploy:
resources:
limits:
cpus: "2.0"
memory: 1G
reservations:
cpus: "0.25"
memory: 256M
environment:
TZ: "Asia/Bangkok"
CHOKIDAR_USEPOLLING: "1"
CHOKIDAR_INTERVAL: "300"
WATCHPACK_POLLING: "true"
# NODE_ENV: "production"
NODE_ENV: "development"
PORT: "3001"
DB_HOST: "mariadb"
DB_PORT: "3306"
DB_USER: "center"
DB_PASSWORD: "Center#2025"
DB_NAME: "dms"
JWT_SECRET: "9a6d8705a6695ab9bae4ca1cd46c72a6379aa72404b96e2c5b59af881bb55c639dd583afdce5a885c68e188da55ce6dbc1fb4aa9cd4055ceb51507e56204e4ca"
JWT_ACCESS_SECRET: "9a6d8705a6695ab9bae4ca1cd46c72a6379aa72404b96e2c5b59af881bb55c639dd583afdce5a885c68e188da55ce6dbc1fb4aa9cd4055ceb51507e56204e4ca"
JWT_REFRESH_SECRET: "743e798bb10d6aba168bf68fc3cf8eff103c18bd34f1957a3906dc87987c0df139ab72498f2fe20d6c4c580f044ccba7d7bfa4393ee6035b73ba038f28d7480c"
ACCESS_TTL_MS: "900000"
REFRESH_TTL_MS: "604800000"
JWT_EXPIRES_IN: "12h"
PASSWORD_SALT_ROUNDS: "10"
FRONTEND_ORIGIN: "https://lcbp3.np-dms.work"
CORS_ORIGINS: "https://lcbp3.np-dms.work,http://localhost:3000,http://127.0.0.1:3000"
COOKIE_DOMAIN: ".np-dms.work"
RATE_LIMIT_WINDOW_MS: "900000"
RATE_LIMIT_MAX: "200"
BACKEND_LOG_DIR: "/app/logs"
networks:
lcbp3: {}
volumes:
- "/share/Container/dms/backend/src:/app/src:rw"
# - "/share/Container/dms/backend/package.json:/app/package.json"
# - "/share/Container/dms/backend/package-lock.json:/app/package-lock.json"
- "/share/dms-data:/share/dms-data:rw"
- "/share/Container/dms/logs/backend:/app/logs:rw"
# - "/share/Container/dms/backend/node_modules:/app/node_modules"
- "backend_node_modules:/app/node_modules"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3001/health"]
interval: 10s
timeout: 5s
retries: 30
networks:
lcbp3:
external: true

View File

@@ -5,7 +5,11 @@
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",
@@ -13,91 +17,55 @@ export const sequelize = new Sequelize(config.DB.NAME, config.DB.USER, config.DB
dialectOptions: { timezone: "Z" },
define: { freezeTableName: true, underscored: false, timestamps: false },
pool: { max: 10, min: 0, idle: 10000 },
});
}
);
// --- 1. ประกาศตัวแปรสำหรับ Export Model ทั้งหมด ---
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;
export let User = null;
export let Role = null;
export let Permission = null;
export let UserRole = null;
export let RolePermission = null;
if (process.env.ENABLE_SEQUELIZE === "1") {
// --- 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),
};
// โหลดโมเดลแบบ on-demand เพื่อลดความเสี่ยง runtime หากไฟล์โมเดลไม่มี
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);
// --- 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;
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 (User && Role && Permission && UserRole && RolePermission) {
User.belongsToMany(Role, {
through: UserRole,
foreignKey: "user_id",
otherKey: "role_id",
});
Role.belongsToMany(User, {
through: UserRole,
foreignKey: "role_id",
otherKey: "user_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);
}
}
Role.belongsToMany(Permission, {
through: RolePermission,
foreignKey: "role_id",
otherKey: "permission_id",
});
Permission.belongsToMany(Role, {
through: RolePermission,
foreignKey: "permission_id",
otherKey: "role_id",
});
}
}
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);
}
}
// โหมดเบา ๆ: แค่ทดสอบเชื่อมต่อ
await sequelize.authenticate();
}

View File

@@ -1,8 +1,8 @@
// FILE: backend/src/index.js (ESM) ไฟล์ฉบับ “Bearer-only”
// FILE: src/index.js (ESM)
import fs from "node:fs";
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser"; // added
import sql from "./db/index.js";
import healthRouter from "./routes/health.js";
@@ -10,9 +10,6 @@ import { authJwt } from "./middleware/authJwt.js";
import { loadPrincipalMw } from "./middleware/loadPrincipal.js";
// ROUTES
import usersRoutes from "./routes/users.js";
import rbacAdminRoutes from "./routes/rbac_admin.js";
import dashboardRoutes from "./routes/dashboard.js";
import authRoutes from "./routes/auth.js";
import lookupRoutes from "./routes/lookup.js";
import organizationsRoutes from "./routes/organizations.js";
@@ -26,6 +23,7 @@ import contractDwgRoutes from "./routes/contract_dwg.js";
import categoriesRoutes from "./routes/categories.js";
import volumesRoutes from "./routes/volumes.js";
import uploadsRoutes from "./routes/uploads.js";
import usersRoutes from "./routes/users.js";
import permissionsRoutes from "./routes/permissions.js";
const PORT = Number(process.env.PORT || 3001);
@@ -39,9 +37,7 @@ const ALLOW_ORIGINS = [
"http://127.0.0.1:3000",
FRONTEND_ORIGIN,
...(process.env.CORS_ALLOWLIST
? process.env.CORS_ALLOWLIST.split(",")
.map((x) => x.trim())
.filter(Boolean)
? process.env.CORS_ALLOWLIST.split(",").map((x) => x.trim()).filter(Boolean)
: []),
].filter(Boolean);
@@ -78,10 +74,6 @@ app.use(
exposedHeaders: ["Content-Disposition", "Content-Length"],
})
);
// parse cookies สำหรับ access_token / refresh_token
app.use(cookieParser()); // added
app.options(
"*",
cors({
@@ -113,12 +105,8 @@ app.get("/health", async (_req, res) => {
});
app.get("/livez", (_req, res) => res.send("ok"));
app.get("/readyz", async (_req, res) => {
try {
await sql.query("SELECT 1");
res.send("ready");
} catch {
res.status(500).send("not-ready");
}
try { await sql.query("SELECT 1"); res.send("ready"); }
catch { res.status(500).send("not-ready"); }
});
app.get("/info", (_req, res) =>
res.json({
@@ -150,8 +138,6 @@ app.use("/api/volumes", volumesRoutes);
app.use("/api/uploads", uploadsRoutes);
app.use("/api/users", usersRoutes);
app.use("/api/permissions", permissionsRoutes);
app.use("/api/rbac", rbacAdminRoutes);
app.use("/api/dashboard", dashboardRoutes);
// 404 / error
app.use((req, res) =>
@@ -173,9 +159,7 @@ async function shutdown(signal) {
try {
console.log(`[SHUTDOWN] ${signal} received`);
await new Promise((resolve) => server.close(resolve));
try {
await sql.end();
} catch {}
try { await sql.end(); } catch {}
console.log("[SHUTDOWN] complete");
process.exit(0);
} catch (e) {

View File

@@ -3,118 +3,41 @@
// - Project-scoped access control base on user_project_roles + permissions
// - Requires req.user.roles and req.user.permissions to be populated (e.g. via auth.js with enrichment)
// - Uses UserProjectRole model to check project membership
// Helper ABAC เสริมบางเคส (ถ้าต้องการฟิลเตอร์/บังคับ project_id ตรง ๆ)
// หมายเหตุ: โดยหลักแล้วคุณควรใช้ requirePerm() ที่บังคับ ABAC อัตโนมัติจาก permissions.scope_level
import { sequelize } from "../db/sequelize.js";
import UPRModel from "../db/models/UserProjectRole.js";
/**
* ดึง project_id ที่ผู้ใช้เข้าถึงได้ (จาก user_project_roles)
*/
export async function getUserProjectIds(user_id) {
const UPR = UPRModel(sequelize);
const rows = await UPR.findAll({ where: { user_id } });
return [...new Set(rows.map((r) => r.project_id))];
}
/**
* projectScopedView(moduleName) -> middleware
* - ต้องมี permission '<module>:view' หรือ
* - เป็นสมาชิกของโปรเจ็กต์ (ผ่าน user_project_roles)
* Behavior:
* - ถ้า query ไม่มี project_id และผู้ใช้ไม่ใช่ Admin:
* จำกัดผลลัพธ์ให้เฉพาะโปรเจ็กต์ที่ผู้ใช้เป็นสมาชิก
* - ถ้ามี project_id: บังคับตรวจสิทธิ์การเป็นสมาชิกของโปรเจ็กต์นั้น (เว้นแต่เป็น Admin)
*/
export function projectScopedView(moduleName) {
export function projectScopedViewFallback(moduleName) {
// ใช้ในเคส legacy เท่านั้น
return async (req, res, next) => {
const roles = req.user?.roles || [];
const isAdmin = roles.includes("Admin");
const permName = `${moduleName}:view`;
const hasViewPerm = (req.user?.permissions || []).includes(permName);
const p = req.principal;
if (!p) return res.status(401).json({ error: "Unauthenticated" });
// Admin ผ่านได้เสมอ
if (isAdmin) return next();
const hasViewPerm = p.can?.(`${moduleName}.view`) || p.permissions?.has?.(`${moduleName}.view`);
if (p.is_superadmin) return next();
const qProjectId = req.query?.project_id
? Number(req.query.project_id)
: null;
const memberProjects = await getUserProjectIds(req.user?.user_id);
const qProjectId = req.query?.project_id ? Number(req.query.project_id) : null;
if (qProjectId) {
// ต้องเป็นสมาชิกโปรเจ็กต์นั้น หรือมี perm view
if (hasViewPerm || memberProjects.includes(qProjectId)) return next();
return res
.status(403)
.json({ error: "Forbidden: not a member of project" });
if (hasViewPerm || p.inProject(qProjectId)) return next();
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
} else {
// ไม่มี project_id: ถ้ามี perm view → อนุญาตทั้งหมด
// ถ้าไม่มี perm view → จำกัดด้วยรายการโปรเจ็กต์ที่เป็นสมาชิก (บันทึกไว้ใน req.abac.filterProjectIds)
if (hasViewPerm) return next();
if (!memberProjects.length)
return res
.status(403)
.json({ error: "Forbidden: no accessible projects" });
if (!p.project_ids?.length) return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
req.abac = req.abac || {};
req.abac.filterProjectIds = memberProjects;
req.abac.filterProjectIds = p.project_ids;
return next();
}
};
}
/**
* บังคับเป็นสมาชิกโปรเจ็กต์จากค่า project_id ใน body
* ใช้กับ create endpoints
*/
export function requireProjectMembershipFromBody() {
return async (req, res, next) => {
const roles = req.user?.roles || [];
const isAdmin = roles.includes("Admin");
if (isAdmin) return next();
const pid = Number(req.body?.project_id);
if (!pid) return res.status(400).json({ error: "project_id required" });
const memberProjects = await getUserProjectIds(req.user?.user_id);
if (!memberProjects.includes(pid))
return res.status(403).json({ error: "Forbidden: not a project member" });
next();
};
}
/**
* บังคับเป็นสมาชิกโปรเจ็กต์โดยอ้างอิงจากเรคคอร์ด (ใช้กับ update/delete)
* opts: { modelLoader: (sequelize)=>Model, idParam: 'id', projectField: 'project_id' }
*/
export function requireProjectMembershipByRecord(opts) {
const { modelLoader, idParam = "id", projectField = "project_id" } = opts;
return async (req, res, next) => {
const roles = req.user?.roles || [];
const isAdmin = roles.includes("Admin");
if (isAdmin) return next();
const id = Number(req.params[idParam]);
if (!id) return res.status(400).json({ error: "Invalid id" });
const Model = modelLoader(sequelize);
const row = await Model.findByPk(id);
if (!row) return res.status(404).json({ error: "Not found" });
const pid = Number(row[projectField]);
const memberProjects = await getUserProjectIds(req.user?.user_id);
if (!memberProjects.includes(pid))
return res.status(403).json({ error: "Forbidden: not a project member" });
next();
};
}
/**
* บังคับให้ view ทุกอันต้องส่ง project_id (ยกเว้น Admin)
*/
export function requireProjectIdQuery() {
return async (req, res, next) => {
const roles = req.user?.roles || [];
const isAdmin = roles.includes("Admin");
if (isAdmin) return next();
const qProjectId = req.query?.project_id
? Number(req.query.project_id)
: null;
if (!qProjectId)
return res.status(400).json({ error: "project_id query required" });
return (req, res, next) => {
const p = req.principal;
if (!p) return res.status(401).json({ error: "Unauthenticated" });
if (p.is_superadmin) return next();
const qProjectId = req.query?.project_id ? Number(req.query.project_id) : null;
if (!qProjectId) return res.status(400).json({ error: "project_id query required" });
next();
};
}

View File

@@ -1,61 +1,30 @@
// FILE: backend/src/middleware/auth.js
// (ถ้ายังใช้อยู่) ปรับให้สอดคล้อง Bearer + principal
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,
});
const { JWT_SECRET = "dev-secret", JWT_EXPIRES_IN = "30m" } = process.env;
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN, issuer: "dms-backend" });
}
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;
const { JWT_REFRESH_SECRET = "dev-refresh", JWT_REFRESH_EXPIRES_IN = "30d" } = process.env;
return jwt.sign({ ...payload, t: "refresh" }, JWT_REFRESH_SECRET, { expiresIn: JWT_REFRESH_EXPIRES_IN, issuer: "dms-backend" });
}
// ถ้าจะใช้ standalone (ไม่แนะนำถ้ามี authJwt แล้ว)
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" });
const h = req.headers.authorization || "";
const m = /^Bearer\s+(.+)$/i.exec(h || "");
if (!m) return res.status(401).json({ error: "Missing token" });
try {
req.user = jwt.verify(token, config.JWT.SECRET);
const { JWT_SECRET = "dev-secret" } = process.env;
const payload = jwt.verify(m[1], JWT_SECRET, { issuer: "dms-backend" });
req.auth = { user_id: payload.user_id, username: payload.username };
req.user = req.user || {};
req.user.user_id = payload.user_id;
req.user.username = payload.username;
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);
}
}

View File

@@ -25,6 +25,10 @@ export function authJwt() {
// แนบข้อมูลขั้นต่ำให้ middleware ถัดไป
req.auth = { user_id: payload.user_id, username: payload.username };
//req.user = { user_id: payload.user_id, username: payload.username };
// เผื่อโค้ดเก่าอ้างอิง req.user
req.user = req.user || {};
req.user.user_id = payload.user_id;
req.user.username = payload.username;
next();
} catch (e) {
return res.status(401).json({ error: "Unauthenticated" });

View File

@@ -5,15 +5,90 @@
// - Uses rbac.js utility to load principal info
// - Attaches to req.principal
// - Requires req.user.user_id to be populated (e.g. via auth.js or authJwt.js)
// โหลด principal จาก DB แล้วแนบไว้ใน req.principal
// NOTE: ตรงนี้สมมุติว่าคุณมี service/query ฝั่ง DB อยู่แล้ว (เช่น sql/Sequelize)
// ถ้าคุณมีฟังก์ชันโหลด principal อยู่ที่อื่น ให้แทน logic DB ตรง FIXME ด้านล่าง
// ใช้ req.auth.user_id และตั้ง req.principal ให้ครบ (RBAC + ABAC)
import { loadPrincipal } from "../utils/rbac.js";
import sql from "../db/index.js";
export function loadPrincipalMw() {
return async (req, res, next) => {
try {
if (!req.user?.user_id)
return res.status(401).json({ error: "Unauthenticated" });
req.principal = await loadPrincipal(req.user.user_id);
const uid = req?.auth?.user_id || req?.user?.user_id;
if (!uid) return res.status(401).json({ error: "Unauthenticated" });
// --- 1) users (รวม org_id)
const [[u]] = await sql.query(
`SELECT user_id, username, email, first_name, last_name, org_id, is_active
FROM users WHERE user_id=? LIMIT 1`,
[uid]
);
if (!u || u.is_active === 0) return res.status(401).json({ error: "Unauthenticated" });
// --- 2) roles (global)
const [roleRows] = await sql.query(
`SELECT r.role_id, r.role_code, r.role_name
FROM user_roles ur
JOIN roles r ON r.role_id = ur.role_id
WHERE ur.user_id=?`,
[uid]
);
const roleCodes = new Set(roleRows.map(r => r.role_code));
const is_superadmin = roleCodes.has("SUPER_ADMIN");
// --- 3) permissions (ผ่าน role_permissions)
const [permRows] = await sql.query(
`SELECT DISTINCT p.perm_code
FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.permission_id = rp.permission_id AND p.is_active=1
WHERE ur.user_id=?`,
[uid]
);
const permSet = new Set(permRows.map(x => x.perm_code));
// --- 4) project scope (user_project_roles)
const [projRows] = await sql.query(
`SELECT DISTINCT project_id FROM user_project_roles WHERE user_id=?`,
[uid]
);
const project_ids = projRows.map(r => r.project_id);
// --- 5) org scope: users.org_id + orgs จาก project_parties ของโปรเจ็คที่เข้าถึง
const baseOrgIds = u.org_id ? [u.org_id] : [];
let projOrgIds = [];
if (project_ids.length) {
const [rows] = await sql.query(
`SELECT DISTINCT org_id FROM project_parties WHERE project_id IN (?)`,
[project_ids]
);
projOrgIds = rows.map(r => r.org_id);
}
const org_ids = Array.from(new Set([...baseOrgIds, ...projOrgIds]));
req.principal = {
user_id: u.user_id,
username: u.username,
email: u.email,
first_name: u.first_name,
last_name: u.last_name,
org_id: u.org_id || null,
roles: roleRows.map(r => ({ role_id: r.role_id, role_code: r.role_code, role_name: r.role_name })),
permissions: permSet, // Set ของ perm_code
project_ids,
org_ids,
is_superadmin,
// helpers
can: (code) => is_superadmin || permSet.has(code),
canAny: (codes=[]) => is_superadmin || codes.some(c => permSet.has(c)),
canAll: (codes=[]) => is_superadmin || codes.every(c => permSet.has(c)),
inProject: (pid) => is_superadmin || project_ids.includes(Number(pid)),
inOrg: (oid) => is_superadmin || org_ids.includes(Number(oid)),
};
next();
} catch (err) {
console.error("loadPrincipal error", err);

View File

@@ -2,16 +2,14 @@
// Permission guard middleware
// - Checks if user has required permissions
// - Requires req.user.permissions to be populated (e.g. via auth.js or authJwt.js with enrichment)
// เปลี่ยนให้เป็น wrapper ที่เรียก req.principal (ทางเก่ายังใช้ได้)**
/**
* requirePerm('rfa:create') => ตรวจว่ามี permission นี้ใน req.user.permissions
* ต้องแน่ใจว่าเรียก enrichPermissions() มาก่อน หรือคำนวณที่จุดเข้าใช้งาน
*/
export function requirePerm(...allowedPerms) {
return (req, res, next) => {
const perms = req.user?.permissions || [];
const ok = perms.some((p) => allowedPerms.includes(p));
if (!ok) return res.status(403).json({ error: "Forbidden" });
const p = req.principal;
if (!p) return res.status(401).json({ error: "Unauthenticated" });
const ok = p.is_superadmin || allowedPerms.some((code) => p.permissions?.has?.(code));
if (!ok) return res.status(403).json({ error: "FORBIDDEN", need_any_of: allowedPerms });
next();
};
}
}

View File

@@ -2,39 +2,40 @@
// Permission calculation and enrichment middleware
// - Computes effective permissions for a user based on their roles
// - Attaches permissions to req.user.permissions
// ใช้เฉพาะกรณีที่คุณยังมี stack Sequelize เดิมอยู่ และอยาก enrich จาก Role/Permission model
// โดยทั่วไป ถ้าคุณใช้ loadPrincipalMw() อยู่แล้ว สามารถไม่ใช้ไฟล์นี้ได้
import { Role, Permission, UserRole, RolePermission } from "../db/sequelize.js";
import { Permission, UserRole, RolePermission } from "../db/sequelize.js";
/**
* คืนชุด permission (string[]) ของ user_id
*/
export async function computeEffectivePermissions(user_id) {
// ดึง roles ของผู้ใช้
const userRoles = await UserRole.findAll({ where: { user_id } });
const roleIds = userRoles.map((r) => r.role_id);
if (!roleIds.length) return [];
// ดึง permission ผ่าน role_permissions
const rp = await RolePermission.findAll({ where: { role_id: roleIds } });
const permIds = [...new Set(rp.map((x) => x.permission_id))];
if (!permIds.length) return [];
const perms = await Permission.findAll({ where: { permission_id: permIds } });
return [...new Set(perms.map((p) => p.permission_name))];
// ใช้ perm_code ให้สอดคล้อง seed
return [...new Set(perms.map((p) => p.perm_code))];
}
/**
* middleware: เติม permissions ลง req.user.permissions
*/
export function enrichPermissions() {
return async (req, _res, next) => {
if (!req.user?.user_id) return next();
const uid = req?.auth?.user_id || req?.user?.user_id;
if (!uid) return next();
try {
const perms = await computeEffectivePermissions(req.user.user_id);
const perms = await computeEffectivePermissions(uid);
// อัปเดตทั้ง req.principal และ req.user (เผื่อโค้ดเก่า)
req.principal = req.principal || {};
req.principal.permissions = new Set(perms);
req.user = req.user || {};
req.user.permissions = perms;
} catch (e) {
req.user.permissions = [];
} catch {
if (req.principal) req.principal.permissions = new Set();
if (req.user) req.user.permissions = [];
}
next();
};
}
}

View File

@@ -5,18 +5,19 @@
export function requireRole(...allowed) {
return (req, res, next) => {
const roles = req.user?.roles || [];
const ok = roles.some((r) => allowed.includes(r));
if (!ok) return res.status(403).json({ error: "Forbidden" });
const roles = (req.principal?.roles || []).map(r => r.role_code);
const ok = roles.some((r) => allowed.includes(r)) || req.principal?.is_superadmin;
if (!ok) return res.status(403).json({ error: "FORBIDDEN_ROLE", need_any_of: allowed });
next();
};
}
export function requirePermission(...allowedPerms) {
export function requirePermissionCode(...codes) {
return (req, res, next) => {
const perms = req.user?.permissions || [];
const ok = perms.some((p) => allowedPerms.includes(p));
if (!ok) return res.status(403).json({ error: "Forbidden" });
const p = req.principal;
if (!p) return res.status(401).json({ error: "Unauthenticated" });
const ok = p.is_superadmin || codes.some((c) => p.permissions?.has?.(c));
if (!ok) return res.status(403).json({ error: "FORBIDDEN", need_any_of: codes });
next();
};
}

View File

@@ -0,0 +1,18 @@
// FILE: src/middleware/requireBearer.js
import jwt from "jsonwebtoken";
import { findUserById } from "../db/models/users.js";
export async function requireBearer(req, res, next) {
const hdr = req.get("Authorization") || "";
const m = hdr.match(/^Bearer\s+(.+)$/i);
if (!m) return res.status(401).json({ error: "Unauthenticated" });
try {
const payload = jwt.verify(m[1], process.env.JWT_ACCESS_SECRET, { issuer: "dms-backend" });
const user = await findUserById(payload.user_id);
if (!user) return res.status(401).json({ error: "Unauthenticated" });
req.user = { user_id: user.user_id, username: user.username, email: user.email, first_name: user.first_name, last_name: user.last_name };
next();
} catch {
return res.status(401).json({ error: "Unauthenticated" });
}
}

View File

@@ -6,32 +6,59 @@
// - Uses canPerform() utility from rbac.js
// - Supports global, org, and project scopes
// - Requires req.principal to be populated (e.g. via loadPrincipal middleware)
// เช็คตาม perm_code + ABAC อัตโนมัติจาก permissions.scope_level
import sql from "../db/index.js";
import { canPerform } from "../utils/rbac.js";
let _permMap = null;
let _loadedAt = 0;
const TTL_MS = 60_000;
async function getPermRegistry() {
const now = Date.now();
if (_permMap && now - _loadedAt < TTL_MS) return _permMap;
const [rows] = await sql.query(
`SELECT perm_code, scope_level FROM permissions WHERE is_active=1`
);
_permMap = new Map(rows.map(r => [r.perm_code, r.scope_level])); // GLOBAL | ORG | PROJECT
_loadedAt = now;
return _permMap;
}
/**
* requirePerm('correspondence.create', { scope: 'org', getOrgId: req => ... })
* scope: 'global' | 'org' | 'project'
* requirePerm('rfas.view', { projectParam: 'project_id', orgParam: 'org_id' })
* - GLOBAL: แค่มี perm ก็ผ่าน
* - ORG: ต้องมี perm + อยู่ใน org scope (อ่าน org_id จาก param หากระบุ; ไม่ระบุจะใช้ req.principal.org_id)
* - PROJECT:ต้องมี perm + อยู่ใน project scope (อ่าน project_id จาก param)
*/
export function requirePerm(
permCode,
{ scope = "global", getOrgId = null, getProjectId = null } = {}
) {
export function requirePerm(permCode, { projectParam, orgParam } = {}) {
return async (req, res, next) => {
try {
const orgId = getOrgId ? await getOrgId(req) : null;
const projectId = getProjectId ? await getProjectId(req) : null;
const p = req.principal;
if (!p) return res.status(401).json({ error: "Unauthenticated" });
if (canPerform(req.principal, permCode, { scope, orgId, projectId }))
return next();
return res.status(403).json({
error: "FORBIDDEN",
message: `Require ${permCode} (${scope}-scoped)`,
});
} catch (e) {
console.error("requirePerm error", e);
res.status(500).json({ error: "Permission check error" });
if (!(p.is_superadmin || p.permissions?.has?.(permCode))) {
return res.status(403).json({ error: "FORBIDDEN", need: permCode });
}
const registry = await getPermRegistry();
const scope = registry.get(permCode) || "GLOBAL";
const readParam = (name) => req.params?.[name] ?? req.query?.[name] ?? req.body?.[name];
if (scope === "PROJECT") {
const pid = Number(projectParam ? readParam(projectParam) : undefined);
if (!p.is_superadmin) {
if (!pid || !p.inProject(pid)) {
return res.status(403).json({ error: "FORBIDDEN_PROJECT", project_id: pid || null });
}
}
} else if (scope === "ORG") {
const oid = Number(orgParam ? readParam(orgParam) : p.org_id);
if (!p.is_superadmin) {
if (!oid || !p.inOrg(oid)) {
return res.status(403).json({ error: "FORBIDDEN_ORG", org_id: oid || null });
}
}
}
next();
};
}
}

View File

@@ -1,4 +1,6 @@
// FILE: src/routes/categories.js
// อ่าน: ใช้ organizations.view (GLOBAL)
// สร้าง/แก้/ลบ: ใช้ settings.manage (GLOBAL)
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";

View File

@@ -1,4 +1,6 @@
// FILE: src/routes/contract_dwg.js
// ใน seed ยังไม่มี contract_dwg.* → ผูกชั่วคราวกับสิทธิ์กลุ่ม drawings:
// read → drawings.view, create/update/delete → drawings.upload/delete (PROJECT scope)
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";

View File

@@ -1,4 +1,7 @@
// FILE: src/routes/contracts.js
// ไม่มี contract.* ใน seed → map เป็นงานดูแลองค์กร/โปรเจ็กต์:
// list/get → projects.view (ORG)
// create/update/delete → projects.manage (ORG)
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";

View File

@@ -1,4 +1,5 @@
// FILE: backend/src/routes/mvp.js
// (generic entity maps — ใช้ projects.view อ่าน และ projects.manage เขียน/ลบ)
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";

View File

@@ -1,99 +1,55 @@
// File: backend/src/routes/users.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';
// FILE: backend/src/routes/users.js
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const router = Router();
const r = Router();
// Middleware Chain ที่ถูกต้อง 100%
router.use(authJwt(), loadPrincipalMw());
// GET /api/users
router.get('/', requirePerm('users.view'), async (req, res, next) => {
try {
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); }
// ME (ทุกคน)
r.get("/me", async (req, res) => {
const p = req.principal;
const [[u]] = await sql.query(
`SELECT user_id, username, email, first_name, last_name, org_id FROM users WHERE user_id=?`,
[p.user_id]
);
if (!u) return res.status(404).json({ error: "User not found" });
const [roles] = await sql.query(
`SELECT r.role_code, r.role_name, ur.org_id, ur.project_id
FROM user_roles ur JOIN roles r ON r.role_id = ur.role_id
WHERE ur.user_id=?`,
[p.user_id]
);
res.json({
...u,
roles,
role_codes: roles.map((r) => r.role_code),
permissions: [...(p.permissions || [])],
project_ids: p.project_ids,
org_ids: p.org_ids,
is_superadmin: p.is_superadmin,
});
});
// 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' });
// USERS LIST (ORG scope) — admin.access
r.get(
"/",
requirePerm("admin.access", { orgParam: "org_id" }),
async (req, res) => {
const P = req.principal;
let rows = [];
if (P.is_superadmin) {
[rows] = await sql.query(
"SELECT user_id, username, email, org_id FROM users ORDER BY user_id DESC LIMIT 500"
);
} else if (P.org_ids?.length) {
const inSql = P.org_ids.map(() => "?").join(",");
[rows] = await sql.query(
`SELECT user_id, username, email, org_id FROM users WHERE org_id IN (${inSql}) ORDER BY user_id DESC LIMIT 500`,
P.org_ids
);
}
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,
});
res.json(rows);
}
);
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', 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();
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
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 r;