Apply .gitignore cleanup

This commit is contained in:
admin
2025-10-05 09:21:04 +07:00
parent d2a7a3e478
commit 3448594bc5
3515 changed files with 20582 additions and 1501448 deletions

View File

@@ -1,60 +1,60 @@
// FILE: src/config/permissions.js
// Reference enum สำหรับฝั่งโค้ด/ฟรอนต์เท่านั้น
// แหล่งจริงของ scope อ่านจาก DB: permissions.scope_level (ผ่าน requirePerm())
const PERM = {
organizations: {
view: "organizations.view",
manage: "organizations.manage",
},
projects: {
view: "projects.view",
manage: "projects.manage",
partiesManage: "project_parties.manage",
},
drawings: {
view: "drawings.view",
upload: "drawings.upload",
delete: "drawings.delete",
},
documents: {
view: "documents.view",
manage: "documents.manage",
},
materials: {
view: "materials.view",
manage: "materials.manage",
},
ms: {
view: "ms.view",
manage: "ms.manage",
},
rfas: {
view: "rfas.view",
create: "rfas.create",
respond: "rfas.respond",
delete: "rfas.delete",
},
correspondences: {
view: "corr.view",
manage: "corr.manage",
},
transmittals: {
manage: "transmittals.manage",
},
circulations: {
manage: "cirs.manage",
},
admin: {
access: "admin.access",
},
reports: {
view: "reports.view",
},
settings: {
manage: "settings.manage",
},
};
export { PERM };
export default PERM;
// FILE: src/config/permissions.js
// Reference enum สำหรับฝั่งโค้ด/ฟรอนต์เท่านั้น
// แหล่งจริงของ scope อ่านจาก DB: permissions.scope_level (ผ่าน requirePerm())
const PERM = {
organizations: {
view: "organizations.view",
manage: "organizations.manage",
},
projects: {
view: "projects.view",
manage: "projects.manage",
partiesManage: "project_parties.manage",
},
drawings: {
view: "drawings.view",
upload: "drawings.upload",
delete: "drawings.delete",
},
documents: {
view: "documents.view",
manage: "documents.manage",
},
materials: {
view: "materials.view",
manage: "materials.manage",
},
ms: {
view: "ms.view",
manage: "ms.manage",
},
rfas: {
view: "rfas.view",
create: "rfas.create",
respond: "rfas.respond",
delete: "rfas.delete",
},
correspondences: {
view: "corr.view",
manage: "corr.manage",
},
transmittals: {
manage: "transmittals.manage",
},
circulations: {
manage: "cirs.manage",
},
admin: {
access: "admin.access",
},
reports: {
view: "reports.view",
},
settings: {
manage: "settings.manage",
},
};
export { PERM };
export default PERM;

View File

@@ -1,39 +1,39 @@
// FILE: backend/src/db/index.js (ESM)
import mysql from "mysql2/promise";
const {
DB_HOST = "mariadb",
DB_PORT = "3306",
DB_USER = "center",
DB_PASSWORD = "Center#2025",
DB_NAME = "dms",
DB_CONN_LIMIT = "10",
} = process.env;
const pool = mysql.createPool({
host: DB_HOST,
port: Number(DB_PORT),
user: DB_USER,
password: DB_PASSWORD,
database: DB_NAME,
connectionLimit: Number(DB_CONN_LIMIT),
waitForConnections: true,
namedPlaceholders: true,
dateStrings: true, // คงวันที่เป็น string
timezone: "Z", // ใช้ UTC
});
/**
* เรียก Stored Procedure แบบง่าย
* @param {string} procName ชื่อโปรซีเยอร์ เช่น "sp_rfa_create_with_items"
* @param {Array<any>} params ลำดับพารามิเตอร์
* @returns {Promise<any>} rows จาก CALL
*/
export async function callProc(procName, params = []) {
const placeholders = params.map(() => "?").join(",");
const sql = `CALL ${procName}(${placeholders})`;
const [rows] = await pool.query(sql, params);
return rows;
}
export default pool; // ใช้ sql.query(...) ได้ตามที่ routes เรียกอยู่
// FILE: backend/src/db/index.js (ESM)
import mysql from "mysql2/promise";
const {
DB_HOST = "mariadb",
DB_PORT = "3306",
DB_USER = "center",
DB_PASSWORD = "Center#2025",
DB_NAME = "dms",
DB_CONN_LIMIT = "10",
} = process.env;
const pool = mysql.createPool({
host: DB_HOST,
port: Number(DB_PORT),
user: DB_USER,
password: DB_PASSWORD,
database: DB_NAME,
connectionLimit: Number(DB_CONN_LIMIT),
waitForConnections: true,
namedPlaceholders: true,
dateStrings: true, // คงวันที่เป็น string
timezone: "Z", // ใช้ UTC
});
/**
* เรียก Stored Procedure แบบง่าย
* @param {string} procName ชื่อโปรซีเยอร์ เช่น "sp_rfa_create_with_items"
* @param {Array<any>} params ลำดับพารามิเตอร์
* @returns {Promise<any>} rows จาก CALL
*/
export async function callProc(procName, params = []) {
const placeholders = params.map(() => "?").join(",");
const sql = `CALL ${procName}(${placeholders})`;
const [rows] = await pool.query(sql, params);
return rows;
}
export default pool; // ใช้ sql.query(...) ได้ตามที่ routes เรียกอยู่

View File

@@ -1,33 +1,33 @@
// FILE: src/middleware/authJwt.js
// 03.2 4) เพิ่ม middleware authJwt (ใหม่)
// นำ middleware นี้ไปใส่ หลัง verify JWT เสมอ เช่น app.use('/api', authJwt(), routes)
// Simple JWT authentication middleware example
// - For demonstration or simple use cases
// - Not as feature-rich as auth.js (no role/permission enrichment)
// - Can be used standalone or alongside auth.js
// authJwt.js สมมติคุณมี JWT อยู่แล้ว (ปรับ verify ตามที่ใช้จริง)
// - ตรวจ token และเติม req.user
// - ไม่ได้เติม roles/permissions (ถ้าต้องการให้ใช้ auth.js แทนหรือร่วมกัน)
import jwt from "jsonwebtoken";
export function authJwt() {
const { JWT_SECRET = "dev-secret" } = process.env;
return (req, res, next) => {
const h = req.headers.authorization || "";
// const token = h.startsWith("Bearer ") ? h.slice(7) : null;
const m = /^Bearer\s+(.+)$/i.exec(h || "");
//if (!token) return res.status(401).json({ error: "Unauthenticated" });
if (!m) return res.status(401).json({ error: "Unauthenticated" });
try {
//const payload = jwt.verify(token, JWT_SECRET);
const payload = jwt.verify(m[1], JWT_SECRET, { issuer: "dms-backend" });
// แนบข้อมูลขั้นต่ำให้ middleware ถัดไป
req.auth = { user_id: payload.user_id, username: payload.username };
//req.user = { user_id: payload.user_id, username: payload.username };
next();
} catch (e) {
return res.status(401).json({ error: "Unauthenticated" });
}
};
}
// FILE: src/middleware/authJwt.js
// 03.2 4) เพิ่ม middleware authJwt (ใหม่)
// นำ middleware นี้ไปใส่ หลัง verify JWT เสมอ เช่น app.use('/api', authJwt(), routes)
// Simple JWT authentication middleware example
// - For demonstration or simple use cases
// - Not as feature-rich as auth.js (no role/permission enrichment)
// - Can be used standalone or alongside auth.js
// authJwt.js สมมติคุณมี JWT อยู่แล้ว (ปรับ verify ตามที่ใช้จริง)
// - ตรวจ token และเติม req.user
// - ไม่ได้เติม roles/permissions (ถ้าต้องการให้ใช้ auth.js แทนหรือร่วมกัน)
import jwt from "jsonwebtoken";
export function authJwt() {
const { JWT_SECRET = "dev-secret" } = process.env;
return (req, res, next) => {
const h = req.headers.authorization || "";
// const token = h.startsWith("Bearer ") ? h.slice(7) : null;
const m = /^Bearer\s+(.+)$/i.exec(h || "");
//if (!token) return res.status(401).json({ error: "Unauthenticated" });
if (!m) return res.status(401).json({ error: "Unauthenticated" });
try {
//const payload = jwt.verify(token, JWT_SECRET);
const payload = jwt.verify(m[1], JWT_SECRET, { issuer: "dms-backend" });
// แนบข้อมูลขั้นต่ำให้ middleware ถัดไป
req.auth = { user_id: payload.user_id, username: payload.username };
//req.user = { user_id: payload.user_id, username: payload.username };
next();
} catch (e) {
return res.status(401).json({ error: "Unauthenticated" });
}
};
}

View File

@@ -1,23 +1,23 @@
// FILE: src/middleware/loadPrincipal.js
// 03.2 4) เพิ่ม middleware loadPrincipal (ใหม่)
// นำ middleware นี้ไปใส่ หลัง verify JWT เสมอ เช่น app.use('/api', authJwt(), loadPrincipalMw(), routes)
// Load principal (roles + permissions) middleware
// - 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)
import { loadPrincipal } from "../utils/rbac.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);
next();
} catch (err) {
console.error("loadPrincipal error", err);
res.status(500).json({ error: "Failed to load principal" });
}
};
}
// FILE: src/middleware/loadPrincipal.js
// 03.2 4) เพิ่ม middleware loadPrincipal (ใหม่)
// นำ middleware นี้ไปใส่ หลัง verify JWT เสมอ เช่น app.use('/api', authJwt(), loadPrincipalMw(), routes)
// Load principal (roles + permissions) middleware
// - 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)
import { loadPrincipal } from "../utils/rbac.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);
next();
} catch (err) {
console.error("loadPrincipal error", err);
res.status(500).json({ error: "Failed to load principal" });
}
};
}

View File

@@ -1,37 +1,37 @@
// FILE: src/middleware/requirePerm.js
// 03.2 4) เพิ่ม middleware requirePerm (ใหม่)
// นำ middleware นี้ไปใส่ หลัง loadPrincipal เสมอ เช่น app.use('/api', authJwt(), loadPrincipalMw(), requirePerm('correspondence.create', {scope:'org', getOrgId: req=>...}), routes)
// หรือใส่ใน route เดี่ยวๆ ก็ได้ เช่น router.post('/', requirePerm('correspondence.create', {scope:'org', getOrgId: req=>...}), (req,res)=>{...})
// Permission requirement middleware with scope support
// - Uses canPerform() utility from rbac.js
// - Supports global, org, and project scopes
// - Requires req.principal to be populated (e.g. via loadPrincipal middleware)
import { canPerform } from "../utils/rbac.js";
/**
* requirePerm('correspondence.create', { scope: 'org', getOrgId: req => ... })
* scope: 'global' | 'org' | 'project'
*/
export function requirePerm(
permCode,
{ scope = "global", getOrgId = null, getProjectId = null } = {}
) {
return async (req, res, next) => {
try {
const orgId = getOrgId ? await getOrgId(req) : null;
const projectId = getProjectId ? await getProjectId(req) : null;
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" });
}
};
}
// FILE: src/middleware/requirePerm.js
// 03.2 4) เพิ่ม middleware requirePerm (ใหม่)
// นำ middleware นี้ไปใส่ หลัง loadPrincipal เสมอ เช่น app.use('/api', authJwt(), loadPrincipalMw(), requirePerm('correspondence.create', {scope:'org', getOrgId: req=>...}), routes)
// หรือใส่ใน route เดี่ยวๆ ก็ได้ เช่น router.post('/', requirePerm('correspondence.create', {scope:'org', getOrgId: req=>...}), (req,res)=>{...})
// Permission requirement middleware with scope support
// - Uses canPerform() utility from rbac.js
// - Supports global, org, and project scopes
// - Requires req.principal to be populated (e.g. via loadPrincipal middleware)
import { canPerform } from "../utils/rbac.js";
/**
* requirePerm('correspondence.create', { scope: 'org', getOrgId: req => ... })
* scope: 'global' | 'org' | 'project'
*/
export function requirePerm(
permCode,
{ scope = "global", getOrgId = null, getProjectId = null } = {}
) {
return async (req, res, next) => {
try {
const orgId = getOrgId ? await getOrgId(req) : null;
const projectId = getProjectId ? await getProjectId(req) : null;
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" });
}
};
}

View File

@@ -1,94 +1,94 @@
// FILE: src/routes/admin.js
import { Router } from "express";
import os from "node:os";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
/**
* GET /api/admin/sysinfo
* perm: admin.access (ORG scope) ใช้สิทธิ์กลุ่ม admin
*/
r.get(
"/sysinfo",
requirePerm("admin.access", { orgParam: "org_id" }),
async (_req, res) => {
try {
await sql.query("SELECT 1");
res.json({
now: new Date().toISOString(),
node: process.version,
platform: os.platform(),
arch: os.arch(),
cpus: os.cpus()?.length,
uptime_sec: os.uptime(),
loadavg: os.loadavg(),
memory: { total: os.totalmem(), free: os.freemem() },
env: {
NODE_ENV: process.env.NODE_ENV,
APP_VERSION: process.env.APP_VERSION,
},
});
} catch (e) {
res.status(500).json({ error: "SYSINFO_FAIL", message: e?.message });
}
}
);
/**
* POST /api/admin/maintenance/reindex
* perm: settings.manage (GLOBAL) งานดูแลระบบ
*/
r.post(
"/maintenance/reindex",
requirePerm("settings.manage"),
async (_req, res) => {
try {
// ปรับตามตารางจริงของคุณ
await sql.query("ANALYZE TABLE correspondences, rfas, drawings");
res.json({ ok: 1 });
} catch (e) {
res.status(500).json({ error: "MAINT_FAIL", message: e?.message });
}
}
);
/**
* GET /api/admin/perm-matrix?format=json
* perm: admin.access (ORG)
*/
r.get(
"/perm-matrix",
requirePerm("admin.access", { orgParam: "org_id" }),
async (req, res) => {
const format = String(req.query.format || "json").toLowerCase();
const [roles] = await sql.query(
`SELECT r.role_id, r.role_code, r.role_name,
GROUP_CONCAT(p.perm_code ORDER BY p.perm_code SEPARATOR ', ') AS perm_codes
FROM roles r
LEFT JOIN role_permissions rp ON rp.role_id = r.role_id
LEFT JOIN permissions p ON p.permission_id = rp.permission_id
GROUP BY r.role_id, r.role_code, r.role_name
ORDER BY r.role_code`
);
if (format === "json") return res.json({ roles });
// markdown แบบง่าย
const lines = [
`# Permission Matrix`,
`_Generated at: ${new Date().toISOString()}_`,
`| # | Role Code | Role Name | Permissions |`,
`|---:|:---------|:----------|:------------|`,
...roles.map(
(r, i) =>
`| ${i + 1} | \`${r.role_code}\` | ${r.role_name || ""} | ${
r.perm_codes || ""
} |`
),
];
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
res.send(lines.join("\n"));
}
);
export default r;
// FILE: src/routes/admin.js
import { Router } from "express";
import os from "node:os";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
/**
* GET /api/admin/sysinfo
* perm: admin.access (ORG scope) ใช้สิทธิ์กลุ่ม admin
*/
r.get(
"/sysinfo",
requirePerm("admin.access", { orgParam: "org_id" }),
async (_req, res) => {
try {
await sql.query("SELECT 1");
res.json({
now: new Date().toISOString(),
node: process.version,
platform: os.platform(),
arch: os.arch(),
cpus: os.cpus()?.length,
uptime_sec: os.uptime(),
loadavg: os.loadavg(),
memory: { total: os.totalmem(), free: os.freemem() },
env: {
NODE_ENV: process.env.NODE_ENV,
APP_VERSION: process.env.APP_VERSION,
},
});
} catch (e) {
res.status(500).json({ error: "SYSINFO_FAIL", message: e?.message });
}
}
);
/**
* POST /api/admin/maintenance/reindex
* perm: settings.manage (GLOBAL) งานดูแลระบบ
*/
r.post(
"/maintenance/reindex",
requirePerm("settings.manage"),
async (_req, res) => {
try {
// ปรับตามตารางจริงของคุณ
await sql.query("ANALYZE TABLE correspondences, rfas, drawings");
res.json({ ok: 1 });
} catch (e) {
res.status(500).json({ error: "MAINT_FAIL", message: e?.message });
}
}
);
/**
* GET /api/admin/perm-matrix?format=json
* perm: admin.access (ORG)
*/
r.get(
"/perm-matrix",
requirePerm("admin.access", { orgParam: "org_id" }),
async (req, res) => {
const format = String(req.query.format || "json").toLowerCase();
const [roles] = await sql.query(
`SELECT r.role_id, r.role_code, r.role_name,
GROUP_CONCAT(p.perm_code ORDER BY p.perm_code SEPARATOR ', ') AS perm_codes
FROM roles r
LEFT JOIN role_permissions rp ON rp.role_id = r.role_id
LEFT JOIN permissions p ON p.permission_id = rp.permission_id
GROUP BY r.role_id, r.role_code, r.role_name
ORDER BY r.role_code`
);
if (format === "json") return res.json({ roles });
// markdown แบบง่าย
const lines = [
`# Permission Matrix`,
`_Generated at: ${new Date().toISOString()}_`,
`| # | Role Code | Role Name | Permissions |`,
`|---:|:---------|:----------|:------------|`,
...roles.map(
(r, i) =>
`| ${i + 1} | \`${r.role_code}\` | ${r.role_name || ""} | ${
r.perm_codes || ""
} |`
),
];
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
res.send(lines.join("\n"));
}
);
export default r;

View File

@@ -1,60 +1,60 @@
// FILE: src/routes/categories.js
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// Categories
r.get("/categories", requirePerm("organizations.view"), async (_req, res) => {
const [rows] = await sql.query(
"SELECT * FROM categories ORDER BY cat_id DESC"
);
res.json(rows);
});
r.post("/categories", requirePerm("settings.manage"), async (req, res) => {
const { cat_code, cat_name } = req.body || {};
if (!cat_code || !cat_name)
return res.status(400).json({ error: "cat_code and cat_name required" });
const [rs] = await sql.query(
"INSERT INTO categories (cat_code, cat_name) VALUES (?,?)",
[cat_code, cat_name]
);
res.json({ cat_id: rs.insertId });
});
r.put("/categories/:id", requirePerm("settings.manage"), async (req, res) => {
const id = Number(req.params.id);
const { cat_name } = req.body || {};
await sql.query("UPDATE categories SET cat_name=? WHERE cat_id=?", [
cat_name,
id,
]);
res.json({ ok: 1 });
});
r.delete(
"/categories/:id",
requirePerm("settings.manage"),
async (req, res) => {
const id = Number(req.params.id);
await sql.query("DELETE FROM categories WHERE cat_id=?", [id]);
res.json({ ok: 1 });
}
);
// Subcategories
r.get("/subcategories", requirePerm("organizations.view"), async (req, res) => {
const { cat_id } = req.query;
const params = [];
let where = "";
if (cat_id) {
where = " WHERE cat_id=?";
params.push(Number(cat_id));
}
const [rows] = await sql.query(
`SELECT * FROM subcategories${where} ORDER BY sub_cat_id DESC`,
params
);
res.json(rows);
});
export default r;
// FILE: src/routes/categories.js
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// Categories
r.get("/categories", requirePerm("organizations.view"), async (_req, res) => {
const [rows] = await sql.query(
"SELECT * FROM categories ORDER BY cat_id DESC"
);
res.json(rows);
});
r.post("/categories", requirePerm("settings.manage"), async (req, res) => {
const { cat_code, cat_name } = req.body || {};
if (!cat_code || !cat_name)
return res.status(400).json({ error: "cat_code and cat_name required" });
const [rs] = await sql.query(
"INSERT INTO categories (cat_code, cat_name) VALUES (?,?)",
[cat_code, cat_name]
);
res.json({ cat_id: rs.insertId });
});
r.put("/categories/:id", requirePerm("settings.manage"), async (req, res) => {
const id = Number(req.params.id);
const { cat_name } = req.body || {};
await sql.query("UPDATE categories SET cat_name=? WHERE cat_id=?", [
cat_name,
id,
]);
res.json({ ok: 1 });
});
r.delete(
"/categories/:id",
requirePerm("settings.manage"),
async (req, res) => {
const id = Number(req.params.id);
await sql.query("DELETE FROM categories WHERE cat_id=?", [id]);
res.json({ ok: 1 });
}
);
// Subcategories
r.get("/subcategories", requirePerm("organizations.view"), async (req, res) => {
const { cat_id } = req.query;
const params = [];
let where = "";
if (cat_id) {
where = " WHERE cat_id=?";
params.push(Number(cat_id));
}
const [rows] = await sql.query(
`SELECT * FROM subcategories${where} ORDER BY sub_cat_id DESC`,
params
);
res.json(rows);
});
export default r;

View File

@@ -1,141 +1,141 @@
// FILE: src/routes/contract_dwg.js
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// LIST (PROJECT scope enforce via params ifส่ง project_id, ไม่ส่งจะถูก filter ด้วย scope ภายใน)
r.get(
"/",
requirePerm("drawings.view", { projectParam: "project_id" }),
async (req, res) => {
const { project_id, org_id, condwg_no, limit = 50, offset = 0 } = req.query;
const p = req.principal;
const params = [];
const cond = [];
// ABAC filter ฝั่ง server กันหลุดขอบเขต
if (!p.is_superadmin) {
if (project_id) {
if (!p.inProject(Number(project_id)))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
cond.push("m.project_id=?");
params.push(Number(project_id));
} else if (p.project_ids?.length) {
cond.push(
`m.project_id IN (${p.project_ids.map(() => "?").join(",")})`
);
params.push(...p.project_ids);
}
} else if (project_id) {
cond.push("m.project_id=?");
params.push(Number(project_id));
}
if (org_id) {
cond.push("m.org_id=?");
params.push(Number(org_id));
}
if (condwg_no) {
cond.push("m.condwg_no=?");
params.push(condwg_no);
}
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
const [rows] = await sql.query(
`SELECT m.* FROM contract_dwg m ${where} ORDER BY m.id DESC LIMIT ? OFFSET ?`,
[...params, Number(limit), Number(offset)]
);
res.json(rows);
}
);
// GET item (ตรวจ ABAC หลังอ่านเรคคอร์ด)
r.get("/:id", requirePerm("drawings.view"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM contract_dwg WHERE id=?", [
id,
]);
if (!row) return res.status(404).json({ error: "Not found" });
const p = req.principal;
if (!p.is_superadmin && !p.inProject(row.project_id))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
res.json(row);
});
// CREATE
r.post(
"/",
requirePerm("drawings.upload", { projectParam: "project_id" }),
async (req, res) => {
const {
org_id,
project_id,
condwg_no,
title,
drawing_id,
volume_id,
sub_cat_id,
sub_no,
remark,
} = req.body || {};
if (!project_id || !condwg_no)
return res
.status(400)
.json({ error: "project_id and condwg_no required" });
const [rs] = await sql.query(
`INSERT INTO contract_dwg (org_id, project_id, condwg_no, title, drawing_id, volume_id, sub_cat_id, sub_no, remark, created_by)
VALUES (?,?,?,?,?,?,?,?,?,?)`,
[
org_id || null,
project_id,
condwg_no,
title || null,
drawing_id || null,
volume_id || null,
sub_cat_id || null,
sub_no || null,
remark || null,
req.principal.user_id,
]
);
res.json({ id: rs.insertId });
}
);
// UPDATE
r.put("/:id", requirePerm("drawings.upload"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM contract_dwg WHERE id=?", [
id,
]);
if (!row) return res.status(404).json({ error: "Not found" });
const p = req.principal;
if (!p.is_superadmin && !p.inProject(row.project_id))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
const { title, remark } = req.body || {};
await sql.query("UPDATE contract_dwg SET title=?, remark=? WHERE id=?", [
title ?? row.title,
remark ?? row.remark,
id,
]);
res.json({ ok: 1 });
});
// DELETE
r.delete("/:id", requirePerm("drawings.delete"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM contract_dwg WHERE id=?", [
id,
]);
if (!row) return res.status(404).json({ error: "Not found" });
const p = req.principal;
if (!p.is_superadmin && !p.inProject(row.project_id))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
await sql.query("DELETE FROM contract_dwg WHERE id=?", [id]);
res.json({ ok: 1 });
});
export default r;
// FILE: src/routes/contract_dwg.js
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// LIST (PROJECT scope enforce via params ifส่ง project_id, ไม่ส่งจะถูก filter ด้วย scope ภายใน)
r.get(
"/",
requirePerm("drawings.view", { projectParam: "project_id" }),
async (req, res) => {
const { project_id, org_id, condwg_no, limit = 50, offset = 0 } = req.query;
const p = req.principal;
const params = [];
const cond = [];
// ABAC filter ฝั่ง server กันหลุดขอบเขต
if (!p.is_superadmin) {
if (project_id) {
if (!p.inProject(Number(project_id)))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
cond.push("m.project_id=?");
params.push(Number(project_id));
} else if (p.project_ids?.length) {
cond.push(
`m.project_id IN (${p.project_ids.map(() => "?").join(",")})`
);
params.push(...p.project_ids);
}
} else if (project_id) {
cond.push("m.project_id=?");
params.push(Number(project_id));
}
if (org_id) {
cond.push("m.org_id=?");
params.push(Number(org_id));
}
if (condwg_no) {
cond.push("m.condwg_no=?");
params.push(condwg_no);
}
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
const [rows] = await sql.query(
`SELECT m.* FROM contract_dwg m ${where} ORDER BY m.id DESC LIMIT ? OFFSET ?`,
[...params, Number(limit), Number(offset)]
);
res.json(rows);
}
);
// GET item (ตรวจ ABAC หลังอ่านเรคคอร์ด)
r.get("/:id", requirePerm("drawings.view"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM contract_dwg WHERE id=?", [
id,
]);
if (!row) return res.status(404).json({ error: "Not found" });
const p = req.principal;
if (!p.is_superadmin && !p.inProject(row.project_id))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
res.json(row);
});
// CREATE
r.post(
"/",
requirePerm("drawings.upload", { projectParam: "project_id" }),
async (req, res) => {
const {
org_id,
project_id,
condwg_no,
title,
drawing_id,
volume_id,
sub_cat_id,
sub_no,
remark,
} = req.body || {};
if (!project_id || !condwg_no)
return res
.status(400)
.json({ error: "project_id and condwg_no required" });
const [rs] = await sql.query(
`INSERT INTO contract_dwg (org_id, project_id, condwg_no, title, drawing_id, volume_id, sub_cat_id, sub_no, remark, created_by)
VALUES (?,?,?,?,?,?,?,?,?,?)`,
[
org_id || null,
project_id,
condwg_no,
title || null,
drawing_id || null,
volume_id || null,
sub_cat_id || null,
sub_no || null,
remark || null,
req.principal.user_id,
]
);
res.json({ id: rs.insertId });
}
);
// UPDATE
r.put("/:id", requirePerm("drawings.upload"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM contract_dwg WHERE id=?", [
id,
]);
if (!row) return res.status(404).json({ error: "Not found" });
const p = req.principal;
if (!p.is_superadmin && !p.inProject(row.project_id))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
const { title, remark } = req.body || {};
await sql.query("UPDATE contract_dwg SET title=?, remark=? WHERE id=?", [
title ?? row.title,
remark ?? row.remark,
id,
]);
res.json({ ok: 1 });
});
// DELETE
r.delete("/:id", requirePerm("drawings.delete"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM contract_dwg WHERE id=?", [
id,
]);
if (!row) return res.status(404).json({ error: "Not found" });
const p = req.principal;
if (!p.is_superadmin && !p.inProject(row.project_id))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
await sql.query("DELETE FROM contract_dwg WHERE id=?", [id]);
res.json({ ok: 1 });
});
export default r;

View File

@@ -1,138 +1,138 @@
// FILE: src/routes/contracts.js
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// LIST
r.get(
"/",
requirePerm("projects.view", { orgParam: "org_id" }),
async (req, res) => {
const {
project_id,
org_id,
contract_no,
q,
limit = 50,
offset = 0,
} = req.query;
const p = req.principal;
const params = [];
const cond = [];
if (!p.is_superadmin) {
if (org_id) {
if (!p.inOrg(Number(org_id)))
return res.status(403).json({ error: "FORBIDDEN_ORG" });
cond.push("c.org_id=?");
params.push(Number(org_id));
} else if (p.org_ids?.length) {
cond.push(`c.org_id IN (${p.org_ids.map(() => "?").join(",")})`);
params.push(...p.org_ids);
}
} else if (org_id) {
cond.push("c.org_id=?");
params.push(Number(org_id));
}
if (project_id) {
cond.push("c.project_id=?");
params.push(Number(project_id));
}
if (contract_no) {
cond.push("c.contract_no=?");
params.push(contract_no);
}
if (q) {
cond.push("(c.contract_no LIKE ? OR c.title LIKE ?)");
params.push(`%${q}%`, `%${q}%`);
}
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
const [rows] = await sql.query(
`SELECT c.* FROM contracts c ${where} ORDER BY c.id DESC LIMIT ? OFFSET ?`,
[...params, Number(limit), Number(offset)]
);
res.json(rows);
}
);
// GET
r.get(
"/:id",
requirePerm("projects.view", { orgParam: "org_id" }),
async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM contracts WHERE id=?", [id]);
if (!row) return res.status(404).json({ error: "Not found" });
const p = req.principal;
if (!p.is_superadmin && !p.inOrg(row.org_id))
return res.status(403).json({ error: "FORBIDDEN_ORG" });
res.json(row);
}
);
// CREATE
r.post(
"/",
requirePerm("projects.manage", { orgParam: "org_id" }),
async (req, res) => {
const { org_id, project_id, contract_no, title, status } = req.body || {};
if (!org_id || !project_id || !contract_no)
return res
.status(400)
.json({ error: "org_id, project_id, contract_no required" });
const [rs] = await sql.query(
`INSERT INTO contracts (org_id, project_id, contract_no, title, status, created_by) VALUES (?,?,?,?,?,?)`,
[
org_id,
project_id,
contract_no,
title || null,
status || null,
req.principal.user_id,
]
);
res.json({ id: rs.insertId });
}
);
// UPDATE
r.put(
"/:id",
requirePerm("projects.manage", { orgParam: "org_id" }),
async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM contracts WHERE id=?", [id]);
if (!row) return res.status(404).json({ error: "Not found" });
const p = req.principal;
if (!p.is_superadmin && !p.inOrg(row.org_id))
return res.status(403).json({ error: "FORBIDDEN_ORG" });
const { title, status } = req.body || {};
await sql.query("UPDATE contracts SET title=?, status=? WHERE id=?", [
title ?? row.title,
status ?? row.status,
id,
]);
res.json({ ok: 1 });
}
);
// DELETE
r.delete(
"/:id",
requirePerm("projects.manage", { orgParam: "org_id" }),
async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM contracts WHERE id=?", [id]);
if (!row) return res.status(404).json({ error: "Not found" });
const p = req.principal;
if (!p.is_superadmin && !p.inOrg(row.org_id))
return res.status(403).json({ error: "FORBIDDEN_ORG" });
await sql.query("DELETE FROM contracts WHERE id=?", [id]);
res.json({ ok: 1 });
}
);
export default r;
// FILE: src/routes/contracts.js
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// LIST
r.get(
"/",
requirePerm("projects.view", { orgParam: "org_id" }),
async (req, res) => {
const {
project_id,
org_id,
contract_no,
q,
limit = 50,
offset = 0,
} = req.query;
const p = req.principal;
const params = [];
const cond = [];
if (!p.is_superadmin) {
if (org_id) {
if (!p.inOrg(Number(org_id)))
return res.status(403).json({ error: "FORBIDDEN_ORG" });
cond.push("c.org_id=?");
params.push(Number(org_id));
} else if (p.org_ids?.length) {
cond.push(`c.org_id IN (${p.org_ids.map(() => "?").join(",")})`);
params.push(...p.org_ids);
}
} else if (org_id) {
cond.push("c.org_id=?");
params.push(Number(org_id));
}
if (project_id) {
cond.push("c.project_id=?");
params.push(Number(project_id));
}
if (contract_no) {
cond.push("c.contract_no=?");
params.push(contract_no);
}
if (q) {
cond.push("(c.contract_no LIKE ? OR c.title LIKE ?)");
params.push(`%${q}%`, `%${q}%`);
}
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
const [rows] = await sql.query(
`SELECT c.* FROM contracts c ${where} ORDER BY c.id DESC LIMIT ? OFFSET ?`,
[...params, Number(limit), Number(offset)]
);
res.json(rows);
}
);
// GET
r.get(
"/:id",
requirePerm("projects.view", { orgParam: "org_id" }),
async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM contracts WHERE id=?", [id]);
if (!row) return res.status(404).json({ error: "Not found" });
const p = req.principal;
if (!p.is_superadmin && !p.inOrg(row.org_id))
return res.status(403).json({ error: "FORBIDDEN_ORG" });
res.json(row);
}
);
// CREATE
r.post(
"/",
requirePerm("projects.manage", { orgParam: "org_id" }),
async (req, res) => {
const { org_id, project_id, contract_no, title, status } = req.body || {};
if (!org_id || !project_id || !contract_no)
return res
.status(400)
.json({ error: "org_id, project_id, contract_no required" });
const [rs] = await sql.query(
`INSERT INTO contracts (org_id, project_id, contract_no, title, status, created_by) VALUES (?,?,?,?,?,?)`,
[
org_id,
project_id,
contract_no,
title || null,
status || null,
req.principal.user_id,
]
);
res.json({ id: rs.insertId });
}
);
// UPDATE
r.put(
"/:id",
requirePerm("projects.manage", { orgParam: "org_id" }),
async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM contracts WHERE id=?", [id]);
if (!row) return res.status(404).json({ error: "Not found" });
const p = req.principal;
if (!p.is_superadmin && !p.inOrg(row.org_id))
return res.status(403).json({ error: "FORBIDDEN_ORG" });
const { title, status } = req.body || {};
await sql.query("UPDATE contracts SET title=?, status=? WHERE id=?", [
title ?? row.title,
status ?? row.status,
id,
]);
res.json({ ok: 1 });
}
);
// DELETE
r.delete(
"/:id",
requirePerm("projects.manage", { orgParam: "org_id" }),
async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM contracts WHERE id=?", [id]);
if (!row) return res.status(404).json({ error: "Not found" });
const p = req.principal;
if (!p.is_superadmin && !p.inOrg(row.org_id))
return res.status(403).json({ error: "FORBIDDEN_ORG" });
await sql.query("DELETE FROM contracts WHERE id=?", [id]);
res.json({ ok: 1 });
}
);
export default r;

View File

@@ -1,16 +1,16 @@
// FILE: backend/src/routes/permissions.js
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// GLOBAL: settings.manage จึงเห็นได้ทั้งหมด
r.get("/", requirePerm("settings.manage"), async (_req, res) => {
const [rows] = await sql.query(
"SELECT permission_id, perm_code AS permission_code, scope_level, description FROM permissions ORDER BY perm_code"
);
res.json(rows);
});
export default r;
// FILE: backend/src/routes/permissions.js
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// GLOBAL: settings.manage จึงเห็นได้ทั้งหมด
r.get("/", requirePerm("settings.manage"), async (_req, res) => {
const [rows] = await sql.query(
"SELECT permission_id, perm_code AS permission_code, scope_level, description FROM permissions ORDER BY perm_code"
);
res.json(rows);
});
export default r;

View File

@@ -1,91 +1,91 @@
// FILE: backend/src/routes/rfa.js
// RFA: create + update-status ผ่าน stored procedures
import { Router } from "express";
import sql, { callProc } from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// CREATE (PROJECT scope) -> rfas.create
r.post(
"/create",
requirePerm("rfas.create", { projectParam: "project_id" }),
async (req, res, next) => {
try {
const {
project_id,
cor_status_id,
cor_no,
title,
originator_id,
recipient_id,
keywords = null,
pdf_path = null,
item_doc_ids = [],
} = req.body || {};
if (!project_id || !title) {
return res.status(400).json({ error: "project_id and title required" });
}
const json = JSON.stringify((item_doc_ids || []).map(Number));
await callProc("sp_rfa_create_with_items", [
req.principal.user_id,
project_id,
cor_status_id ?? null,
cor_no ?? null,
title,
originator_id ?? null,
recipient_id ?? null,
keywords,
pdf_path,
json,
null,
]);
res.status(201).json({ ok: true });
} catch (e) {
next(e);
}
}
);
// UPDATE STATUS (PROJECT scope) -> rfas.respond
r.post(
"/update-status",
requirePerm("rfas.respond"),
async (req, res, next) => {
try {
const { rfa_corr_id, status_id, set_issue = 0 } = req.body || {};
if (!rfa_corr_id || !status_id) {
return res
.status(400)
.json({ error: "rfa_corr_id and status_id required" });
}
// enforce ABAC: find project_id of the RFA
const [[ref]] = await sql.query(
"SELECT project_id FROM rfas WHERE id=? LIMIT 1",
[Number(rfa_corr_id)]
);
if (!ref) return res.status(404).json({ error: "RFA not found" });
if (
!req.principal.is_superadmin &&
!req.principal.inProject(ref.project_id)
) {
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
}
await callProc("sp_rfa_update_status", [
req.principal.user_id,
rfa_corr_id,
status_id,
set_issue ? 1 : 0,
]);
res.json({ ok: true });
} catch (e) {
next(e);
}
}
);
export default r;
// FILE: backend/src/routes/rfa.js
// RFA: create + update-status ผ่าน stored procedures
import { Router } from "express";
import sql, { callProc } from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// CREATE (PROJECT scope) -> rfas.create
r.post(
"/create",
requirePerm("rfas.create", { projectParam: "project_id" }),
async (req, res, next) => {
try {
const {
project_id,
cor_status_id,
cor_no,
title,
originator_id,
recipient_id,
keywords = null,
pdf_path = null,
item_doc_ids = [],
} = req.body || {};
if (!project_id || !title) {
return res.status(400).json({ error: "project_id and title required" });
}
const json = JSON.stringify((item_doc_ids || []).map(Number));
await callProc("sp_rfa_create_with_items", [
req.principal.user_id,
project_id,
cor_status_id ?? null,
cor_no ?? null,
title,
originator_id ?? null,
recipient_id ?? null,
keywords,
pdf_path,
json,
null,
]);
res.status(201).json({ ok: true });
} catch (e) {
next(e);
}
}
);
// UPDATE STATUS (PROJECT scope) -> rfas.respond
r.post(
"/update-status",
requirePerm("rfas.respond"),
async (req, res, next) => {
try {
const { rfa_corr_id, status_id, set_issue = 0 } = req.body || {};
if (!rfa_corr_id || !status_id) {
return res
.status(400)
.json({ error: "rfa_corr_id and status_id required" });
}
// enforce ABAC: find project_id of the RFA
const [[ref]] = await sql.query(
"SELECT project_id FROM rfas WHERE id=? LIMIT 1",
[Number(rfa_corr_id)]
);
if (!ref) return res.status(404).json({ error: "RFA not found" });
if (
!req.principal.is_superadmin &&
!req.principal.inProject(ref.project_id)
) {
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
}
await callProc("sp_rfa_update_status", [
req.principal.user_id,
rfa_corr_id,
status_id,
set_issue ? 1 : 0,
]);
res.json({ ok: true });
} catch (e) {
next(e);
}
}
);
export default r;

View File

@@ -1,124 +1,124 @@
// FILE: backend/src/routes/technicaldocs.js
// แมปเป็นเอกสารประเภทหนึ่ง → ใช้สิทธิ์ documents.view/manage (PROJECT)
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// LIST
r.get(
"/",
requirePerm("documents.view", { projectParam: "project_id" }),
async (req, res) => {
const { project_id, status, q, limit = 50, offset = 0 } = req.query;
const P = req.principal;
const cond = [];
const params = [];
if (!P.is_superadmin) {
if (project_id) {
const pid = Number(project_id);
if (!P.inProject(pid))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
cond.push("t.project_id=?");
params.push(pid);
} else if (P.project_ids?.length) {
cond.push(
`t.project_id IN (${P.project_ids.map(() => "?").join(",")})`
);
params.push(...P.project_ids);
}
} else if (project_id) {
cond.push("t.project_id=?");
params.push(Number(project_id));
}
if (status) {
cond.push("t.status=?");
params.push(status);
}
if (q) {
cond.push("(t.doc_no LIKE ? OR t.title LIKE ?)");
params.push(`%${q}%`, `%${q}%`);
}
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
const [rows] = await sql.query(
`SELECT t.* FROM technicaldocs t ${where} ORDER BY t.id DESC LIMIT ? OFFSET ?`,
[...params, Number(limit), Number(offset)]
);
res.json(rows);
}
);
// GET
r.get("/:id", requirePerm("documents.view"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM technicaldocs WHERE id=?", [
id,
]);
if (!row) return res.status(404).json({ error: "Not found" });
const P = req.principal;
if (!P.is_superadmin && !P.inProject(row.project_id))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
res.json(row);
});
// CREATE
r.post(
"/",
requirePerm("documents.manage", { projectParam: "project_id" }),
async (req, res) => {
const { org_id, project_id, doc_no, title, status } = req.body || {};
if (!project_id || !doc_no)
return res.status(400).json({ error: "project_id and doc_no required" });
const [rs] = await sql.query(
`INSERT INTO technicaldocs (org_id, project_id, doc_no, title, status, created_by)
VALUES (?,?,?,?,?,?)`,
[
org_id ?? null,
project_id,
doc_no,
title ?? null,
status ?? null,
req.principal.user_id,
]
);
res.status(201).json({ id: rs.insertId });
}
);
// UPDATE
r.put("/:id", requirePerm("documents.manage"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM technicaldocs WHERE id=?", [
id,
]);
if (!row) return res.status(404).json({ error: "Not found" });
const P = req.principal;
if (!P.is_superadmin && !P.inProject(row.project_id))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
const { title, status } = req.body || {};
await sql.query("UPDATE technicaldocs SET title=?, status=? WHERE id=?", [
title ?? row.title,
status ?? row.status,
id,
]);
res.json({ ok: 1 });
});
// DELETE
r.delete("/:id", requirePerm("documents.manage"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM technicaldocs WHERE id=?", [
id,
]);
if (!row) return res.status(404).json({ error: "Not found" });
const P = req.principal;
if (!P.is_superadmin && !P.inProject(row.project_id))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
await sql.query("DELETE FROM technicaldocs WHERE id=?", [id]);
res.json({ ok: 1 });
});
export default r;
// FILE: backend/src/routes/technicaldocs.js
// แมปเป็นเอกสารประเภทหนึ่ง → ใช้สิทธิ์ documents.view/manage (PROJECT)
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// LIST
r.get(
"/",
requirePerm("documents.view", { projectParam: "project_id" }),
async (req, res) => {
const { project_id, status, q, limit = 50, offset = 0 } = req.query;
const P = req.principal;
const cond = [];
const params = [];
if (!P.is_superadmin) {
if (project_id) {
const pid = Number(project_id);
if (!P.inProject(pid))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
cond.push("t.project_id=?");
params.push(pid);
} else if (P.project_ids?.length) {
cond.push(
`t.project_id IN (${P.project_ids.map(() => "?").join(",")})`
);
params.push(...P.project_ids);
}
} else if (project_id) {
cond.push("t.project_id=?");
params.push(Number(project_id));
}
if (status) {
cond.push("t.status=?");
params.push(status);
}
if (q) {
cond.push("(t.doc_no LIKE ? OR t.title LIKE ?)");
params.push(`%${q}%`, `%${q}%`);
}
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
const [rows] = await sql.query(
`SELECT t.* FROM technicaldocs t ${where} ORDER BY t.id DESC LIMIT ? OFFSET ?`,
[...params, Number(limit), Number(offset)]
);
res.json(rows);
}
);
// GET
r.get("/:id", requirePerm("documents.view"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM technicaldocs WHERE id=?", [
id,
]);
if (!row) return res.status(404).json({ error: "Not found" });
const P = req.principal;
if (!P.is_superadmin && !P.inProject(row.project_id))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
res.json(row);
});
// CREATE
r.post(
"/",
requirePerm("documents.manage", { projectParam: "project_id" }),
async (req, res) => {
const { org_id, project_id, doc_no, title, status } = req.body || {};
if (!project_id || !doc_no)
return res.status(400).json({ error: "project_id and doc_no required" });
const [rs] = await sql.query(
`INSERT INTO technicaldocs (org_id, project_id, doc_no, title, status, created_by)
VALUES (?,?,?,?,?,?)`,
[
org_id ?? null,
project_id,
doc_no,
title ?? null,
status ?? null,
req.principal.user_id,
]
);
res.status(201).json({ id: rs.insertId });
}
);
// UPDATE
r.put("/:id", requirePerm("documents.manage"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM technicaldocs WHERE id=?", [
id,
]);
if (!row) return res.status(404).json({ error: "Not found" });
const P = req.principal;
if (!P.is_superadmin && !P.inProject(row.project_id))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
const { title, status } = req.body || {};
await sql.query("UPDATE technicaldocs SET title=?, status=? WHERE id=?", [
title ?? row.title,
status ?? row.status,
id,
]);
res.json({ ok: 1 });
});
// DELETE
r.delete("/:id", requirePerm("documents.manage"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM technicaldocs WHERE id=?", [
id,
]);
if (!row) return res.status(404).json({ error: "Not found" });
const P = req.principal;
if (!P.is_superadmin && !P.inProject(row.project_id))
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
await sql.query("DELETE FROM technicaldocs WHERE id=?", [id]);
res.json({ ok: 1 });
});
export default r;

View File

@@ -1,100 +1,100 @@
// FILE: backend/src/routes/view.js
// Saved Views: อ่านด้วย reports.view (GLOBAL); เขียนด้วย settings.manage
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// LIST (ทุกคนที่มี reports.view)
r.get("/", requirePerm("reports.view"), async (req, res) => {
const { project_id, shared = "1", q, limit = 50, offset = 0 } = req.query;
const p = req.principal;
const cond = [];
const params = [];
// ให้เห็นของตัวเองเสมอ + shared
cond.push("(v.is_shared=1 OR v.owner_user_id=?)");
params.push(p.user_id);
if (project_id) {
cond.push("v.project_id=?");
params.push(Number(project_id));
}
if (q) {
cond.push("v.name LIKE ?");
params.push(`%${q}%`);
}
if (shared === "0") {
cond.push("v.is_shared=0");
}
const where = `WHERE ${cond.join(" AND ")}`;
const [rows] = await sql.query(
`SELECT v.* FROM saved_views v ${where} ORDER BY v.id DESC LIMIT ? OFFSET ?`,
[...params, Number(limit), Number(offset)]
);
res.json(rows);
});
// GET
r.get("/:id", requirePerm("reports.view"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM saved_views WHERE id=?", [id]);
if (!row) return res.status(404).json({ error: "Not found" });
if (
!(
row.is_shared ||
row.owner_user_id === req.principal.user_id ||
req.principal.is_superadmin
)
) {
return res.status(403).json({ error: "FORBIDDEN" });
}
res.json(row);
});
// CREATE / UPDATE / DELETE (ต้องมี settings.manage)
r.post("/", requirePerm("settings.manage"), async (req, res) => {
const {
org_id,
project_id,
name,
payload_json,
is_shared = 0,
} = req.body || {};
const [rs] = await sql.query(
`INSERT INTO saved_views (org_id, project_id, name, payload_json, is_shared, owner_user_id)
VALUES (?,?,?,?,?,?)`,
[
org_id ?? null,
project_id ?? null,
name ?? "",
JSON.stringify(payload_json ?? {}),
Number(is_shared) ? 1 : 0,
req.principal.user_id,
]
);
res.status(201).json({ id: rs.insertId });
});
r.put("/:id", requirePerm("settings.manage"), async (req, res) => {
const id = Number(req.params.id);
const { name, payload_json, is_shared } = req.body || {};
await sql.query(
"UPDATE saved_views SET name=?, payload_json=?, is_shared=? WHERE id=?",
[
name ?? null,
JSON.stringify(payload_json ?? {}),
Number(is_shared) ? 1 : 0,
id,
]
);
res.json({ ok: 1 });
});
r.delete("/:id", requirePerm("settings.manage"), async (req, res) => {
const id = Number(req.params.id);
await sql.query("DELETE FROM saved_views WHERE id=?", [id]);
res.json({ ok: 1 });
});
export default r;
// FILE: backend/src/routes/view.js
// Saved Views: อ่านด้วย reports.view (GLOBAL); เขียนด้วย settings.manage
import { Router } from "express";
import sql from "../db/index.js";
import { requirePerm } from "../middleware/requirePerm.js";
const r = Router();
// LIST (ทุกคนที่มี reports.view)
r.get("/", requirePerm("reports.view"), async (req, res) => {
const { project_id, shared = "1", q, limit = 50, offset = 0 } = req.query;
const p = req.principal;
const cond = [];
const params = [];
// ให้เห็นของตัวเองเสมอ + shared
cond.push("(v.is_shared=1 OR v.owner_user_id=?)");
params.push(p.user_id);
if (project_id) {
cond.push("v.project_id=?");
params.push(Number(project_id));
}
if (q) {
cond.push("v.name LIKE ?");
params.push(`%${q}%`);
}
if (shared === "0") {
cond.push("v.is_shared=0");
}
const where = `WHERE ${cond.join(" AND ")}`;
const [rows] = await sql.query(
`SELECT v.* FROM saved_views v ${where} ORDER BY v.id DESC LIMIT ? OFFSET ?`,
[...params, Number(limit), Number(offset)]
);
res.json(rows);
});
// GET
r.get("/:id", requirePerm("reports.view"), async (req, res) => {
const id = Number(req.params.id);
const [[row]] = await sql.query("SELECT * FROM saved_views WHERE id=?", [id]);
if (!row) return res.status(404).json({ error: "Not found" });
if (
!(
row.is_shared ||
row.owner_user_id === req.principal.user_id ||
req.principal.is_superadmin
)
) {
return res.status(403).json({ error: "FORBIDDEN" });
}
res.json(row);
});
// CREATE / UPDATE / DELETE (ต้องมี settings.manage)
r.post("/", requirePerm("settings.manage"), async (req, res) => {
const {
org_id,
project_id,
name,
payload_json,
is_shared = 0,
} = req.body || {};
const [rs] = await sql.query(
`INSERT INTO saved_views (org_id, project_id, name, payload_json, is_shared, owner_user_id)
VALUES (?,?,?,?,?,?)`,
[
org_id ?? null,
project_id ?? null,
name ?? "",
JSON.stringify(payload_json ?? {}),
Number(is_shared) ? 1 : 0,
req.principal.user_id,
]
);
res.status(201).json({ id: rs.insertId });
});
r.put("/:id", requirePerm("settings.manage"), async (req, res) => {
const id = Number(req.params.id);
const { name, payload_json, is_shared } = req.body || {};
await sql.query(
"UPDATE saved_views SET name=?, payload_json=?, is_shared=? WHERE id=?",
[
name ?? null,
JSON.stringify(payload_json ?? {}),
Number(is_shared) ? 1 : 0,
id,
]
);
res.json({ ok: 1 });
});
r.delete("/:id", requirePerm("settings.manage"), async (req, res) => {
const id = Number(req.params.id);
await sql.query("DELETE FROM saved_views WHERE id=?", [id]);
res.json({ ok: 1 });
});
export default r;

View File

@@ -1,107 +1,107 @@
// FILE: backend/src/utils/rbac.js
// 03.2 2) เพิ่มตัวช่วย RBAC (ใหม่)
// Role-Based Access Control (RBAC) utilities
// - loadPrincipal(userId) to load user's roles, permissions, orgs, projects
// - canPerform(principal, permCode, {scope, orgId, projectId}) to check permission
// - Uses raw SQL queries via db/index.js
// - Permissions can be global, org-scoped, or project-scoped
// - Admin roles have special handling for org/project scope
// - SUPER_ADMIN bypasses all checks
import sql from "../db/index.js";
/**
* โหลด principal (บทบาท, องค์กร, โปรเจกต์) ให้ผู้ใช้
*/
export async function loadPrincipal(userId) {
const [rolesRows] = await sql.query(
/*sql*/ `
SELECT ur.user_id, r.role_code, ur.org_id, ur.project_id
FROM user_roles ur
JOIN roles r ON r.role_id = ur.role_id
WHERE ur.user_id = ?
`,
[userId]
);
const [permRows] = await sql.query(
/*sql*/ `
SELECT ur.user_id, r.role_code, p.permission_code, ur.org_id, ur.project_id
FROM user_roles ur
JOIN roles r ON r.role_id = ur.role_id
JOIN role_permissions rp ON rp.role_id = r.role_id
JOIN permissions p ON p.permission_id = rp.permission_id
WHERE ur.user_id = ?
`,
[userId]
);
const roleCodes = new Set(rolesRows.map((r) => r.role_code));
const isSuperAdmin = roleCodes.has("SUPER_ADMIN");
// set องค์กรที่ผู้ใช้อยู่ (ใช้สำหรับ ADMIN scope)
const orgIds = new Set(
rolesRows.filter((r) => r.org_id).map((r) => r.org_id)
);
const projectIds = new Set(
rolesRows.filter((r) => r.project_id).map((r) => r.project_id)
);
// map สิทธิเป็น: permCode -> { orgIds:Set, projectIds:Set }
const perms = new Map();
for (const r of permRows) {
const key = r.permission_code;
if (!perms.has(key))
perms.set(key, { orgIds: new Set(), projectIds: new Set() });
if (r.org_id) perms.get(key).orgIds.add(r.org_id);
if (r.project_id) perms.get(key).projectIds.add(r.project_id);
}
return {
userId,
roleCodes, // Set<role_code>
isSuperAdmin, // SUPER_ADMIN = true
orgIds, // องค์กรของผู้ใช้ (จาก mapping)
projectIds, // โปรเจกต์ของผู้ใช้ (จาก mapping)
perms, // Map<permission_code, {orgIds:Set, projectIds:Set}>
};
}
/**
* ตรวจสิทธิ์ตามกติกา:
* - SUPER_ADMIN: ผ่านทุกอย่าง (ข้าม org/project)
* - ADMIN: ผ่านได้ "ภายใน org ของตัวเอง" เท่านั้น
* - อื่น ๆ: ต้องถือ permission_code และเข้า scope ที่ถูกต้อง
*/
export function canPerform(
principal,
permCode,
{ scope = "global", orgId = null, projectId = null } = {}
) {
if (!principal) return false;
if (principal.isSuperAdmin) return true;
const hasAdminRole = principal.roleCodes.has("ADMIN");
if (scope === "global") return !!principal.perms.get(permCode);
if (scope === "org") {
if (!orgId) return false;
if (hasAdminRole && principal.orgIds.has(orgId))
return !!principal.perms.get(permCode);
const entry = principal.perms.get(permCode);
return !!entry && (entry.orgIds.has(orgId) || entry.orgIds.size === 0);
}
if (scope === "project") {
if (!projectId) return false;
if (hasAdminRole && principal.projectIds.has(projectId))
return !!principal.perms.get(permCode);
const entry = principal.perms.get(permCode);
return (
!!entry &&
(entry.projectIds.has(projectId) || entry.projectIds.size === 0)
);
}
return false;
}
// FILE: backend/src/utils/rbac.js
// 03.2 2) เพิ่มตัวช่วย RBAC (ใหม่)
// Role-Based Access Control (RBAC) utilities
// - loadPrincipal(userId) to load user's roles, permissions, orgs, projects
// - canPerform(principal, permCode, {scope, orgId, projectId}) to check permission
// - Uses raw SQL queries via db/index.js
// - Permissions can be global, org-scoped, or project-scoped
// - Admin roles have special handling for org/project scope
// - SUPER_ADMIN bypasses all checks
import sql from "../db/index.js";
/**
* โหลด principal (บทบาท, องค์กร, โปรเจกต์) ให้ผู้ใช้
*/
export async function loadPrincipal(userId) {
const [rolesRows] = await sql.query(
/*sql*/ `
SELECT ur.user_id, r.role_code, ur.org_id, ur.project_id
FROM user_roles ur
JOIN roles r ON r.role_id = ur.role_id
WHERE ur.user_id = ?
`,
[userId]
);
const [permRows] = await sql.query(
/*sql*/ `
SELECT ur.user_id, r.role_code, p.permission_code, ur.org_id, ur.project_id
FROM user_roles ur
JOIN roles r ON r.role_id = ur.role_id
JOIN role_permissions rp ON rp.role_id = r.role_id
JOIN permissions p ON p.permission_id = rp.permission_id
WHERE ur.user_id = ?
`,
[userId]
);
const roleCodes = new Set(rolesRows.map((r) => r.role_code));
const isSuperAdmin = roleCodes.has("SUPER_ADMIN");
// set องค์กรที่ผู้ใช้อยู่ (ใช้สำหรับ ADMIN scope)
const orgIds = new Set(
rolesRows.filter((r) => r.org_id).map((r) => r.org_id)
);
const projectIds = new Set(
rolesRows.filter((r) => r.project_id).map((r) => r.project_id)
);
// map สิทธิเป็น: permCode -> { orgIds:Set, projectIds:Set }
const perms = new Map();
for (const r of permRows) {
const key = r.permission_code;
if (!perms.has(key))
perms.set(key, { orgIds: new Set(), projectIds: new Set() });
if (r.org_id) perms.get(key).orgIds.add(r.org_id);
if (r.project_id) perms.get(key).projectIds.add(r.project_id);
}
return {
userId,
roleCodes, // Set<role_code>
isSuperAdmin, // SUPER_ADMIN = true
orgIds, // องค์กรของผู้ใช้ (จาก mapping)
projectIds, // โปรเจกต์ของผู้ใช้ (จาก mapping)
perms, // Map<permission_code, {orgIds:Set, projectIds:Set}>
};
}
/**
* ตรวจสิทธิ์ตามกติกา:
* - SUPER_ADMIN: ผ่านทุกอย่าง (ข้าม org/project)
* - ADMIN: ผ่านได้ "ภายใน org ของตัวเอง" เท่านั้น
* - อื่น ๆ: ต้องถือ permission_code และเข้า scope ที่ถูกต้อง
*/
export function canPerform(
principal,
permCode,
{ scope = "global", orgId = null, projectId = null } = {}
) {
if (!principal) return false;
if (principal.isSuperAdmin) return true;
const hasAdminRole = principal.roleCodes.has("ADMIN");
if (scope === "global") return !!principal.perms.get(permCode);
if (scope === "org") {
if (!orgId) return false;
if (hasAdminRole && principal.orgIds.has(orgId))
return !!principal.perms.get(permCode);
const entry = principal.perms.get(permCode);
return !!entry && (entry.orgIds.has(orgId) || entry.orgIds.size === 0);
}
if (scope === "project") {
if (!projectId) return false;
if (hasAdminRole && principal.projectIds.has(projectId))
return !!principal.perms.get(permCode);
const entry = principal.perms.get(permCode);
return (
!!entry &&
(entry.projectIds.has(projectId) || entry.projectIds.size === 0)
);
}
return false;
}

View File

@@ -1,98 +1,98 @@
// FILE: backend/src/utils/scope.js
// 03.2 5) เพิ่ม utils/scope.js (ใหม่)
// - ใช้ร่วมกับ requirePerm() และ loadPrincipal()
// - สำหรับสร้าง SQL WHERE clause ในการ list entities ตาม scope ของผู้ใช้
// Scope and permission utilities
// - Functions to build SQL WHERE clauses based on user principal and permissions
// - Used for filtering list queries according to user's
// roles, permissions, and associated orgs/projects
// - Works with rbac.js loadPrincipal() output
// - Supports SUPER_ADMIN, ADMIN, and scoped permissions
/**
* สร้าง WHERE fragment + params สำหรับ list ตาม principal
* - SUPER_ADMIN: ไม่จำกัด
* - ADMIN: จำกัดใน org/project ที่ตนสังกัด
* - อื่น ๆ: จำกัดตาม permission scope ที่มี
*
* @param {object} principal - จาก loadPrincipal()
* @param {object} opts
* tableAlias: ชื่อ alias ของตารางหลัก (เช่น 'c' สำหรับ correspondences)
* orgColumn: ระบุคอลัมน์ org_id (เช่น 'c.org_id')
* projectColumn: ระบุคอลัมน์ project_id (เช่น 'c.project_id')
* permCode: permission_code ที่ใช้สำหรับ read list (เช่น 'correspondence.read')
* preferProject: true -> บังคับต้อง match project scope ก่อน (ถ้ามี)
*/
export function buildScopeWhere(
principal,
{ tableAlias, orgColumn, projectColumn, permCode, preferProject = false }
) {
if (principal.isSuperAdmin) return { where: "1=1", params: {} };
const perm = principal.perms.get(permCode);
const orgIds = new Set(principal.orgIds);
const projectIds = new Set(principal.projectIds);
// กรณี ADMIN: ให้ดูภายใน org/project ตัวเองได้ทั้งหมด (แต่ต้องถือ permCode)
if (principal.roleCodes.has("ADMIN") && perm) {
const orgList = [...orgIds];
const prjList = [...projectIds];
if (preferProject && prjList.length > 0) {
return {
where: `${projectColumn} IN (:prjList)`,
params: { prjList },
};
}
if (orgList.length > 0) {
return {
where: `${orgColumn} IN (:orgList)`,
params: { orgList },
};
}
// ถ้าไม่มี mapping เลย ให้ไม่เห็นอะไร
return { where: "1=0", params: {} };
}
// บทบาทอื่น: อิงตาม perm scope
if (!perm) return { where: "1=0", params: {} };
const permOrg = [...perm.orgIds];
const permPrj = [...perm.projectIds];
if (preferProject && permPrj.length > 0) {
return { where: `${projectColumn} IN (:permPrj)`, params: { permPrj } };
}
if (permOrg.length > 0) {
return { where: `${orgColumn} IN (:permOrg)`, params: { permOrg } };
}
// ถ้า perm ไม่มี scope ผูก (global grant) ให้ผ่านทั้งหมด
return { where: "1=1", params: {} };
}
/**
* owner resolvers: อ่าน org_id/project_id จาก DB ด้วย id
* ใช้กับ requirePerm(getOrgId/getProjectId) เสมอ
*/
export function ownerResolvers(sql, mainTable, idColumn = "id") {
return {
async getOrgIdById(req) {
const id = Number(req.params.id ?? req.body?.id);
if (!id) return null;
const [[row]] = await sql.query(
`SELECT org_id FROM ${mainTable} WHERE ${idColumn}=?`,
[id]
);
return row?.org_id ?? null;
},
async getProjectIdById(req) {
const id = Number(req.params.id ?? req.body?.id);
if (!id) return null;
const [[row]] = await sql.query(
`SELECT project_id FROM ${mainTable} WHERE ${idColumn}=?`,
[id]
);
return row?.project_id ?? null;
},
};
}
// FILE: backend/src/utils/scope.js
// 03.2 5) เพิ่ม utils/scope.js (ใหม่)
// - ใช้ร่วมกับ requirePerm() และ loadPrincipal()
// - สำหรับสร้าง SQL WHERE clause ในการ list entities ตาม scope ของผู้ใช้
// Scope and permission utilities
// - Functions to build SQL WHERE clauses based on user principal and permissions
// - Used for filtering list queries according to user's
// roles, permissions, and associated orgs/projects
// - Works with rbac.js loadPrincipal() output
// - Supports SUPER_ADMIN, ADMIN, and scoped permissions
/**
* สร้าง WHERE fragment + params สำหรับ list ตาม principal
* - SUPER_ADMIN: ไม่จำกัด
* - ADMIN: จำกัดใน org/project ที่ตนสังกัด
* - อื่น ๆ: จำกัดตาม permission scope ที่มี
*
* @param {object} principal - จาก loadPrincipal()
* @param {object} opts
* tableAlias: ชื่อ alias ของตารางหลัก (เช่น 'c' สำหรับ correspondences)
* orgColumn: ระบุคอลัมน์ org_id (เช่น 'c.org_id')
* projectColumn: ระบุคอลัมน์ project_id (เช่น 'c.project_id')
* permCode: permission_code ที่ใช้สำหรับ read list (เช่น 'correspondence.read')
* preferProject: true -> บังคับต้อง match project scope ก่อน (ถ้ามี)
*/
export function buildScopeWhere(
principal,
{ tableAlias, orgColumn, projectColumn, permCode, preferProject = false }
) {
if (principal.isSuperAdmin) return { where: "1=1", params: {} };
const perm = principal.perms.get(permCode);
const orgIds = new Set(principal.orgIds);
const projectIds = new Set(principal.projectIds);
// กรณี ADMIN: ให้ดูภายใน org/project ตัวเองได้ทั้งหมด (แต่ต้องถือ permCode)
if (principal.roleCodes.has("ADMIN") && perm) {
const orgList = [...orgIds];
const prjList = [...projectIds];
if (preferProject && prjList.length > 0) {
return {
where: `${projectColumn} IN (:prjList)`,
params: { prjList },
};
}
if (orgList.length > 0) {
return {
where: `${orgColumn} IN (:orgList)`,
params: { orgList },
};
}
// ถ้าไม่มี mapping เลย ให้ไม่เห็นอะไร
return { where: "1=0", params: {} };
}
// บทบาทอื่น: อิงตาม perm scope
if (!perm) return { where: "1=0", params: {} };
const permOrg = [...perm.orgIds];
const permPrj = [...perm.projectIds];
if (preferProject && permPrj.length > 0) {
return { where: `${projectColumn} IN (:permPrj)`, params: { permPrj } };
}
if (permOrg.length > 0) {
return { where: `${orgColumn} IN (:permOrg)`, params: { permOrg } };
}
// ถ้า perm ไม่มี scope ผูก (global grant) ให้ผ่านทั้งหมด
return { where: "1=1", params: {} };
}
/**
* owner resolvers: อ่าน org_id/project_id จาก DB ด้วย id
* ใช้กับ requirePerm(getOrgId/getProjectId) เสมอ
*/
export function ownerResolvers(sql, mainTable, idColumn = "id") {
return {
async getOrgIdById(req) {
const id = Number(req.params.id ?? req.body?.id);
if (!id) return null;
const [[row]] = await sql.query(
`SELECT org_id FROM ${mainTable} WHERE ${idColumn}=?`,
[id]
);
return row?.org_id ?? null;
},
async getProjectIdById(req) {
const id = Number(req.params.id ?? req.body?.id);
if (!id) return null;
const [[row]] = await sql.query(
`SELECT project_id FROM ${mainTable} WHERE ${idColumn}=?`,
[id]
);
return row?.project_id ?? null;
},
};
}