Update frontend login page.jsx และ backend
This commit is contained in:
		| @@ -1,4 +1,12 @@ | ||||
| // src/routes/admin.js | ||||
| // FILE: src/routes/admin.js | ||||
| // Admin routes | ||||
| // - System info  (GET /api/admin/sysinfo) | ||||
| // - Maintenance tasks (POST /api/admin/maintenance/reindex) | ||||
| // - Permission matrix (GET /api/admin/perm-matrix?format=md|json) | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses global scope for all permissions | ||||
| // - admin.read, admin.maintain | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import os from "node:os"; | ||||
| import { dbReady, sequelize, Role, Permission } from "../db/sequelize.js"; | ||||
|   | ||||
| @@ -1,62 +1,79 @@ | ||||
| // src/routes/auth.js | ||||
| // FILE: src/routes/auth.js (ESM) — Bearer only, refresh via header/body, forgot/reset password | ||||
| // 03.2 เพิ่ม auth.js และ lookup.js ให้สอดคล้อง RBAC/permission_code | ||||
| // ตาม src/config/permissions.js) และอ่าน scope จาก DB เสมอ | ||||
| /*สมมติว่ามีตาราง password_resets สำหรับเก็บโทเคนรีเซ็ต: | ||||
| password_resets( | ||||
|   id BIGINT PK, user_id BIGINT, token_hash CHAR(64), | ||||
|   expires_at DATETIME, used_at DATETIME NULL, | ||||
|   created_at DATETIME DEFAULT CURRENT_TIMESTAMP | ||||
| ) | ||||
| INDEX(token_hash), INDEX(user_id), INDEX(expires_at) | ||||
| */ | ||||
| import { Router } from "express"; | ||||
| import jwt from "jsonwebtoken"; | ||||
| import sql from "../db/index.js"; | ||||
| import crypto from "node:crypto"; // ถ้าต้องการ timingSafeEqual | ||||
| import bcrypt from "bcryptjs"; // ถ้า password_hash เป็น bcrypt (แนะนำ) | ||||
| import bcrypt from "bcryptjs"; | ||||
| import crypto from "node:crypto"; | ||||
|  | ||||
| const r = Router(); | ||||
|  | ||||
| // ตั้งค่า JWT (อย่าใช้ .env ในโปรดักชันของคุณ → ใส่ผ่าน docker-compose) | ||||
| const JWT_ACCESS_SECRET = process.env.JWT_ACCESS_SECRET || "dev-access-secret"; | ||||
| const JWT_REFRESH_SECRET = | ||||
|   process.env.JWT_REFRESH_SECRET || "dev-refresh-secret"; | ||||
| const ACCESS_TTL_MS = 30 * 60 * 1000; // 30 นาที | ||||
| const REFRESH_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 วัน | ||||
|  | ||||
| function cookieOpts(maxAge) { | ||||
|   return { | ||||
|     httpOnly: true, | ||||
|     secure: true, // ใช้งานจริงหลัง HTTPS | ||||
|     sameSite: "lax", | ||||
|     path: "/", | ||||
|     maxAge, | ||||
|     // domain: ".np-dms.work", // ถ้าต้องการใช้ข้าม subdomain ให้เปิด | ||||
|   }; | ||||
| } | ||||
| /* ========================= | ||||
|  * CONFIG & HELPERS | ||||
|  * ========================= */ | ||||
| // ใช้ค่าเดียวกับ middleware authJwt() | ||||
| const JWT_SECRET = process.env.JWT_SECRET || "dev-secret"; | ||||
| const REFRESH_SECRET = process.env.REFRESH_SECRET || "dev-refresh-secret"; | ||||
| const ACCESS_TTL = process.env.ACCESS_TTL || "30m"; | ||||
| const REFRESH_TTL = process.env.REFRESH_TTL || "30d"; | ||||
| // อายุของ reset token (นาที) | ||||
| const RESET_TTL_MIN = Number(process.env.RESET_TTL_MIN || 30); | ||||
|  | ||||
| function signAccessToken(user) { | ||||
|   return jwt.sign( | ||||
|     { user_id: user.user_id, username: user.username }, | ||||
|     JWT_ACCESS_SECRET, | ||||
|     { expiresIn: "30m", issuer: "dms-backend" } | ||||
|     JWT_SECRET, | ||||
|     { expiresIn: ACCESS_TTL, issuer: "dms-backend" } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function signRefreshToken(user) { | ||||
|   return jwt.sign({ user_id: user.user_id }, JWT_REFRESH_SECRET, { | ||||
|     expiresIn: "30d", | ||||
|     issuer: "dms-backend", | ||||
|   }); | ||||
|   return jwt.sign( | ||||
|     { user_id: user.user_id, username: user.username, t: "refresh" }, | ||||
|     REFRESH_SECRET, | ||||
|     { expiresIn: REFRESH_TTL, issuer: "dms-backend" } | ||||
|   ); | ||||
| } | ||||
|  | ||||
| function getBearer(req) { | ||||
|   const h = req.headers.authorization || ""; | ||||
|   if (!h.startsWith("Bearer ")) return null; | ||||
|   const token = h.slice(7).trim(); | ||||
|   return token || null; | ||||
| } | ||||
|  | ||||
| async function findUserByUsername(username) { | ||||
|   const [rows] = await sql.query( | ||||
|     "SELECT user_id, username, email, password_hash FROM users WHERE username=? LIMIT 1", | ||||
|     `SELECT user_id, username, email, first_name, last_name, password_hash | ||||
|      FROM users WHERE username=? LIMIT 1`, | ||||
|     [username] | ||||
|   ); | ||||
|   return rows?.[0] || null; | ||||
| } | ||||
|  | ||||
| async function verifyPassword(plain, hash) { | ||||
|   // ถ้าใช้ bcrypt: | ||||
|   try { | ||||
|     return await bcrypt.compare(plain, hash); | ||||
|   } catch { | ||||
|     return false; | ||||
|   } | ||||
|   // ถ้าระบบคุณใช้ hash แบบอื่น ให้สลับมาใช้วิธีที่ตรงกับของจริง | ||||
| async function findUserByEmail(email) { | ||||
|   const [rows] = await sql.query( | ||||
|     `SELECT user_id, username, email, first_name, last_name, password_hash | ||||
|      FROM users WHERE email=? LIMIT 1`, | ||||
|     [email] | ||||
|   ); | ||||
|   return rows?.[0] || null; | ||||
| } | ||||
|  | ||||
| /* ========================= | ||||
|  * POST /api/auth/login | ||||
|  * - รับ username/password | ||||
|  * - ตรวจ bcrypt แล้วออก token+refresh_token (JSON) | ||||
|  * ========================= */ | ||||
| r.post("/login", async (req, res) => { | ||||
|   const { username, password } = req.body || {}; | ||||
|   if (!username || !password) { | ||||
| @@ -66,62 +83,169 @@ r.post("/login", async (req, res) => { | ||||
|   const user = await findUserByUsername(username); | ||||
|   if (!user) return res.status(401).json({ error: "INVALID_CREDENTIALS" }); | ||||
|  | ||||
|   const ok = await verifyPassword(password, user.password_hash); | ||||
|   const ok = await bcrypt.compare(password, user.password_hash || ""); | ||||
|   if (!ok) return res.status(401).json({ error: "INVALID_CREDENTIALS" }); | ||||
|  | ||||
|   const access = signAccessToken(user); | ||||
|   const refresh = signRefreshToken(user); | ||||
|  | ||||
|   res.cookie("access_token", access, cookieOpts(ACCESS_TTL_MS)); | ||||
|   res.cookie("refresh_token", refresh, cookieOpts(REFRESH_TTL_MS)); | ||||
|   const token = signAccessToken(user); | ||||
|   const refresh_token = signRefreshToken(user); | ||||
|  | ||||
|   return res.json({ | ||||
|     ok: true, | ||||
|     token, | ||||
|     refresh_token, | ||||
|     user: { | ||||
|       user_id: user.user_id, | ||||
|       username: user.username, | ||||
|       email: user.email, | ||||
|       first_name: user.first_name, | ||||
|       last_name: user.last_name, | ||||
|     }, | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| /* ========================= | ||||
|  * POST /api/auth/refresh | ||||
|  * - รองรับ refresh token จาก: | ||||
|  *   1) Authorization: Bearer <refresh_token> | ||||
|  *   2) req.body.refresh_token | ||||
|  * - ออก token ใหม่ + refresh ใหม่ (rotation) | ||||
|  * ========================= */ | ||||
| r.post("/refresh", async (req, res) => { | ||||
|   const refresh = req.cookies?.refresh_token || req.body?.refresh_token; | ||||
|   if (!refresh) | ||||
|   const fromHeader = getBearer(req); | ||||
|   const fromBody = (req.body || {}).refresh_token; | ||||
|   const refreshToken = fromHeader || fromBody; | ||||
|   if (!refreshToken) { | ||||
|     return res.status(400).json({ error: "REFRESH_TOKEN_REQUIRED" }); | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     const payload = jwt.verify(refresh, JWT_REFRESH_SECRET, { | ||||
|     const payload = jwt.verify(refreshToken, REFRESH_SECRET, { | ||||
|       issuer: "dms-backend", | ||||
|     }); | ||||
|     // TODO: (ถ้ามี) ตรวจ blacklist/rotation store ของ refresh token | ||||
|     if (payload.t !== "refresh") throw new Error("bad token type"); | ||||
|  | ||||
|     // คืน user จากฐานข้อมูลจริงตาม payload.user_id | ||||
|     const [rows] = await sql.query( | ||||
|       "SELECT user_id, username, email FROM users WHERE user_id=? LIMIT 1", | ||||
|     const [[user]] = await sql.query( | ||||
|       `SELECT user_id, username, email, first_name, last_name | ||||
|        FROM users WHERE user_id=? LIMIT 1`, | ||||
|       [payload.user_id] | ||||
|     ); | ||||
|     const user = rows?.[0]; | ||||
|     if (!user) return res.status(401).json({ error: "USER_NOT_FOUND" }); | ||||
|  | ||||
|     // rotation: ออก access+refresh ใหม่ | ||||
|     const access = signAccessToken(user); | ||||
|     const newRef = signRefreshToken(user); | ||||
|     // rotation | ||||
|     const token = signAccessToken(user); | ||||
|     const new_refresh = signRefreshToken(user); | ||||
|  | ||||
|     res.cookie("access_token", access, cookieOpts(ACCESS_TTL_MS)); | ||||
|     res.cookie("refresh_token", newRef, cookieOpts(REFRESH_TTL_MS)); | ||||
|     return res.json({ ok: true }); | ||||
|   } catch (e) { | ||||
|     return res.json({ | ||||
|       token, | ||||
|       refresh_token: new_refresh, | ||||
|       user: { | ||||
|         user_id: user.user_id, | ||||
|         username: user.username, | ||||
|         email: user.email, | ||||
|         first_name: user.first_name, | ||||
|         last_name: user.last_name, | ||||
|       }, | ||||
|     }); | ||||
|   } catch { | ||||
|     return res.status(401).json({ error: "INVALID_REFRESH_TOKEN" }); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| r.post("/logout", (req, res) => { | ||||
|   res.clearCookie("access_token", { path: "/" }); | ||||
|   res.clearCookie("refresh_token", { path: "/" }); | ||||
| /* ========================= | ||||
|  * POST /api/auth/forgot-password | ||||
|  * - รับ username หรือ email อย่างใดอย่างหนึ่ง | ||||
|  * - สร้าง reset token แบบสุ่ม, เก็บ hash ใน DB พร้อมหมดอายุ | ||||
|  * - ส่งเสมอ {ok:true} เพื่อลด user enumeration | ||||
|  * - การ “ส่งอีเมล/ลิงก์รีเซ็ต” ให้ทำนอกระบบนี้ (เช่น n8n) | ||||
|  * ========================= */ | ||||
| r.post("/forgot-password", async (req, res) => { | ||||
|   const { username, email } = req.body || {}; | ||||
|   // หา user จาก username หรือ email (ถ้ามีทั้งสอง จะให้ username มาก่อน) | ||||
|   let user = null; | ||||
|   if (username) user = await findUserByUsername(username); | ||||
|   if (!user && email) user = await findUserByEmail(email); | ||||
|  | ||||
|   // สร้างโทเคน “เหมือนจริง” เสมอ (แต่ถ้าไม่เจอ user ก็ไม่บอก) | ||||
|   if (user) { | ||||
|     const raw = crypto.randomBytes(32).toString("hex"); // โทเคนดิบ (ส่งทางอีเมล) | ||||
|     const hash = crypto.createHash("sha256").update(raw).digest("hex"); // เก็บใน DB | ||||
|     const expires = new Date(Date.now() + RESET_TTL_MIN * 60 * 1000); | ||||
|  | ||||
|     // ทำ invalid เก่า ๆ ของ user นี้ (optional) | ||||
|     await sql.query( | ||||
|       `UPDATE password_resets SET used_at=NOW() | ||||
|        WHERE user_id=? AND used_at IS NULL AND expires_at < NOW()`, | ||||
|       [user.user_id] | ||||
|     ); | ||||
|  | ||||
|     // บันทึก token ใหม่ | ||||
|     await sql.query( | ||||
|       `INSERT INTO password_resets (user_id, token_hash, expires_at) | ||||
|        VALUES (?,?,?)`, | ||||
|       [user.user_id, hash, expires] | ||||
|     ); | ||||
|  | ||||
|     // TODO: ส่ง “raw token” ไปช่องทางปลอดภัย (เช่น n8n ส่งอีเมล) | ||||
|     // ตัวอย่างลิงก์ที่ frontend จะใช้: | ||||
|     // https://<frontend-domain>/reset-password?token=<raw> | ||||
|     // คุณสามารถต่อ webhook ไป n8n ได้ที่นี่ถ้าต้องการ | ||||
|   } | ||||
|  | ||||
|   // ไม่บอกว่าเจอหรือไม่เจอ user | ||||
|   return res.json({ ok: true }); | ||||
| }); | ||||
|  | ||||
| /* ========================= | ||||
|  * POST /api/auth/reset-password | ||||
|  * - รับ token (จากลิงก์ในอีเมล) + new_password | ||||
|  * - ตรวจ token_hash กับ DB, หมดอายุหรือถูกใช้ไปแล้วหรือยัง | ||||
|  * - เปลี่ยนรหัสผ่าน/ปิดใช้ token | ||||
|  * ========================= */ | ||||
| r.post("/reset-password", async (req, res) => { | ||||
|   const { token, new_password } = req.body || {}; | ||||
|   if (!token || !new_password) { | ||||
|     return res.status(400).json({ error: "TOKEN_AND_NEW_PASSWORD_REQUIRED" }); | ||||
|   } | ||||
|  | ||||
|   const token_hash = crypto.createHash("sha256").update(token).digest("hex"); | ||||
|  | ||||
|   const [[row]] = await sql.query( | ||||
|     `SELECT id, user_id, expires_at, used_at | ||||
|      FROM password_resets | ||||
|      WHERE token_hash=? LIMIT 1`, | ||||
|     [token_hash] | ||||
|   ); | ||||
|  | ||||
|   if (!row) return res.status(400).json({ error: "INVALID_TOKEN" }); | ||||
|   if (row.used_at) return res.status(400).json({ error: "TOKEN_ALREADY_USED" }); | ||||
|   if (new Date(row.expires_at).getTime() < Date.now()) { | ||||
|     return res.status(400).json({ error: "TOKEN_EXPIRED" }); | ||||
|   } | ||||
|  | ||||
|   // เปลี่ยนรหัสผ่าน | ||||
|   const salt = await bcrypt.genSalt(10); | ||||
|   const hash = await bcrypt.hash(new_password, salt); | ||||
|   await sql.query(`UPDATE users SET password_hash=? WHERE user_id=?`, [ | ||||
|     hash, | ||||
|     row.user_id, | ||||
|   ]); | ||||
|  | ||||
|   // ปิดใช้ token นี้ | ||||
|   await sql.query(`UPDATE password_resets SET used_at=NOW() WHERE id=?`, [ | ||||
|     row.id, | ||||
|   ]); | ||||
|  | ||||
|   return res.json({ ok: true }); | ||||
| }); | ||||
|  | ||||
| /* ========================= | ||||
|  * POST /api/auth/logout — stateless | ||||
|  * - frontend ลบ token เอง | ||||
|  * ========================= */ | ||||
| r.post("/logout", (_req, res) => { | ||||
|   return res.json({ ok: true }); | ||||
| }); | ||||
|  | ||||
| export default r; | ||||
|  | ||||
| // หมายเหตุ: คุณอาจเพิ่ม /register, /forgot-password, /reset-password ตามต้องการ | ||||
| // แต่ในโปรเจกต์นี้จะให้แอดมินสร้างบัญชีผู้ใช้ผ่าน /api/users แทน | ||||
|   | ||||
| @@ -1,4 +1,8 @@ | ||||
| // src/routes/auth_extras.js | ||||
| // FILE: src/routes/auth_extras.js | ||||
| // Extra auth-related middleware | ||||
| // - Simple JWT auth from httpOnly cookie | ||||
| // - Basic role check middleware (for simple cases, use requirePerm for flexibility) | ||||
|  | ||||
| import jwt from "jsonwebtoken"; | ||||
|  | ||||
| const JWT_ACCESS_SECRET = process.env.JWT_ACCESS_SECRET || "dev-access-secret"; | ||||
|   | ||||
| @@ -1,66 +1,86 @@ | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/categories.js | ||||
| // Categories and Subcategories routes | ||||
| // - CRUD operations for categories and subcategories | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses global scope for all permissions | ||||
| // - category:read, category:create, category:update, category:delete | ||||
| // - Category fields: cat_id (PK), cat_code, cat_name | ||||
| // - Subcategory fields: sub_cat_id (PK), cat_id (FK), sub_cat_code, sub_cat_name | ||||
| // - cat_code and sub_cat_code are unique | ||||
| // - Basic validation: cat_code, cat_name required for category create; sub_cat_code, sub_cat_name, cat_id required for subcategory create | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
|  | ||||
|  | ||||
| // Category LIST (global master, not scoped) – still require permission | ||||
| r.get('/categories', | ||||
| requirePerm(PERM.category.read, { scope: 'global' }), | ||||
| async (req, res) => { | ||||
| const [rows] = await sql.query('SELECT * FROM categories ORDER BY cat_id DESC'); | ||||
| res.json(rows); | ||||
| } | ||||
| r.get( | ||||
|   "/categories", | ||||
|   requirePerm(PERM.category.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const [rows] = await sql.query( | ||||
|       "SELECT * FROM categories ORDER BY cat_id DESC" | ||||
|     ); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| r.post('/categories', | ||||
| requirePerm(PERM.category.create, { scope: 'global' }), | ||||
| async (req, res) => { | ||||
| const { cat_code, cat_name } = req.body; | ||||
| const [rs] = await sql.query('INSERT INTO categories (cat_code, cat_name) VALUES (?,?)', [cat_code, cat_name]); | ||||
| res.json({ cat_id: rs.insertId }); | ||||
| } | ||||
| r.post( | ||||
|   "/categories", | ||||
|   requirePerm(PERM.category.create, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const { cat_code, cat_name } = req.body; | ||||
|     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(PERM.category.update, { scope: 'global' }), | ||||
| 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.put( | ||||
|   "/categories/:id", | ||||
|   requirePerm(PERM.category.update, { scope: "global" }), | ||||
|   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(PERM.category.delete, { scope: 'global' }), | ||||
| async (req, res) => { | ||||
| const id = Number(req.params.id); | ||||
| await sql.query('DELETE FROM categories WHERE cat_id=?', [id]); | ||||
| res.json({ ok: 1 }); | ||||
| } | ||||
| r.delete( | ||||
|   "/categories/:id", | ||||
|   requirePerm(PERM.category.delete, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     await sql.query("DELETE FROM categories WHERE cat_id=?", [id]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| // Subcategories (belong to categories) | ||||
| r.get('/subcategories', | ||||
| requirePerm(PERM.category.read, { scope: 'global' }), | ||||
| async (req, res) => { | ||||
| const { cat_id } = req.query; | ||||
| let sqlText = 'SELECT * FROM subcategories'; | ||||
| const params = []; | ||||
| if (cat_id) { sqlText += ' WHERE cat_id=?'; params.push(Number(cat_id)); } | ||||
| sqlText += ' ORDER BY sub_cat_id DESC'; | ||||
| const [rows] = await sql.query(sqlText, params); | ||||
| res.json(rows); | ||||
| } | ||||
| r.get( | ||||
|   "/subcategories", | ||||
|   requirePerm(PERM.category.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const { cat_id } = req.query; | ||||
|     let sqlText = "SELECT * FROM subcategories"; | ||||
|     const params = []; | ||||
|     if (cat_id) { | ||||
|       sqlText += " WHERE cat_id=?"; | ||||
|       params.push(Number(cat_id)); | ||||
|     } | ||||
|     sqlText += " ORDER BY sub_cat_id DESC"; | ||||
|     const [rows] = await sql.query(sqlText, params); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| export default r; | ||||
| export default r; | ||||
|   | ||||
| @@ -1,74 +1,147 @@ | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import { buildScopeWhere, ownerResolvers } from '../utils/scope.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/contract_dwg.js | ||||
| // Contract Drawings routes | ||||
| // - CRUD operations for contract drawings | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses scope-based access control (global, org, project) via requirePerm | ||||
| // - contract_dwg:read, contract_dwg:create, contract_dwg:update, contract_dwg:delete | ||||
| // - contract_dwg fields: id (PK), org_id, project_id, condwg_no, title, drawing_id, volume_id, sub_cat_id, sub_no, remark, created_by | ||||
| // - Basic filtering on list endpoint by project_id, org_id, condwg_no | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import { buildScopeWhere, ownerResolvers } from "../utils/scope.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const OWN = ownerResolvers(sql, 'contract_dwg', 'id'); | ||||
| const OWN = ownerResolvers(sql, "contract_dwg", "id"); | ||||
|  | ||||
| // LIST mappings | ||||
| r.get('/', | ||||
| requirePerm(PERM.contract_dwg.read, { scope: 'global' }), | ||||
| async (req, res) => { | ||||
| const { project_id, org_id, condwg_no, limit=50, offset=0 } = req.query; | ||||
| const base = buildScopeWhere(req.principal, { tableAlias: 'm', orgColumn: 'm.org_id', projectColumn: 'm.project_id', permCode: PERM.contract_dwg.read, preferProject: true }); | ||||
| const extra = []; | ||||
| const params = { ...base.params, limit: Number(limit), offset: Number(offset) }; | ||||
| if (project_id) { extra.push('m.project_id = :project_id'); params.project_id = Number(project_id); } | ||||
| if (org_id) { extra.push('m.org_id = :org_id'); params.org_id = Number(org_id); } | ||||
| if (condwg_no) { extra.push('m.condwg_no = :condwg_no'); params.condwg_no = condwg_no; } | ||||
| const where = [base.where, ...extra].filter(Boolean).join(' AND '); | ||||
| const [rows] = await sql.query(`SELECT m.* FROM contract_dwg m WHERE ${where} ORDER BY m.id DESC LIMIT :limit OFFSET :offset`, params); | ||||
| res.json(rows); | ||||
| } | ||||
| r.get( | ||||
|   "/", | ||||
|   requirePerm(PERM.contract_dwg.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const { project_id, org_id, condwg_no, limit = 50, offset = 0 } = req.query; | ||||
|     const base = buildScopeWhere(req.principal, { | ||||
|       tableAlias: "m", | ||||
|       orgColumn: "m.org_id", | ||||
|       projectColumn: "m.project_id", | ||||
|       permCode: PERM.contract_dwg.read, | ||||
|       preferProject: true, | ||||
|     }); | ||||
|     const extra = []; | ||||
|     const params = { | ||||
|       ...base.params, | ||||
|       limit: Number(limit), | ||||
|       offset: Number(offset), | ||||
|     }; | ||||
|     if (project_id) { | ||||
|       extra.push("m.project_id = :project_id"); | ||||
|       params.project_id = Number(project_id); | ||||
|     } | ||||
|     if (org_id) { | ||||
|       extra.push("m.org_id = :org_id"); | ||||
|       params.org_id = Number(org_id); | ||||
|     } | ||||
|     if (condwg_no) { | ||||
|       extra.push("m.condwg_no = :condwg_no"); | ||||
|       params.condwg_no = condwg_no; | ||||
|     } | ||||
|     const where = [base.where, ...extra].filter(Boolean).join(" AND "); | ||||
|     const [rows] = await sql.query( | ||||
|       `SELECT m.* FROM contract_dwg m WHERE ${where} ORDER BY m.id DESC LIMIT :limit OFFSET :offset`, | ||||
|       params | ||||
|     ); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| // GET mapping | ||||
| r.get('/:id', | ||||
| requirePerm(PERM.contract_dwg.read, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| 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' }); | ||||
| res.json(row); | ||||
| } | ||||
| r.get( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.contract_dwg.read, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   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" }); | ||||
|     res.json(row); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| // CREATE mapping (1 drawing per contract or per rule) | ||||
| r.post('/', | ||||
| requirePerm(PERM.contract_dwg.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }), | ||||
| async (req, res) => { | ||||
| const { org_id, project_id, condwg_no, title, drawing_id, volume_id, sub_cat_id, sub_no, remark } = req.body; | ||||
| 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, project_id, condwg_no, title, drawing_id, volume_id, sub_cat_id, sub_no, remark, req.principal.userId]); | ||||
| res.json({ id: rs.insertId }); | ||||
| } | ||||
| r.post( | ||||
|   "/", | ||||
|   requirePerm(PERM.contract_dwg.create, { | ||||
|     scope: "org", | ||||
|     getOrgId: async (req) => req.body?.org_id ?? null, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const { | ||||
|       org_id, | ||||
|       project_id, | ||||
|       condwg_no, | ||||
|       title, | ||||
|       drawing_id, | ||||
|       volume_id, | ||||
|       sub_cat_id, | ||||
|       sub_no, | ||||
|       remark, | ||||
|     } = req.body; | ||||
|     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, | ||||
|         project_id, | ||||
|         condwg_no, | ||||
|         title, | ||||
|         drawing_id, | ||||
|         volume_id, | ||||
|         sub_cat_id, | ||||
|         sub_no, | ||||
|         remark, | ||||
|         req.principal.userId, | ||||
|       ] | ||||
|     ); | ||||
|     res.json({ id: rs.insertId }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| // UPDATE | ||||
| r.put('/:id', | ||||
| requirePerm(PERM.contract_dwg.update, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| async (req, res) => { | ||||
| const id = Number(req.params.id); | ||||
| const { title, remark } = req.body; | ||||
| await sql.query('UPDATE contract_dwg SET title=?, remark=? WHERE id=?', [title, remark, id]); | ||||
| res.json({ ok: 1 }); | ||||
| } | ||||
| r.put( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.contract_dwg.update, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     const { title, remark } = req.body; | ||||
|     await sql.query("UPDATE contract_dwg SET title=?, remark=? WHERE id=?", [ | ||||
|       title, | ||||
|       remark, | ||||
|       id, | ||||
|     ]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| // DELETE | ||||
| r.delete('/:id', | ||||
| requirePerm(PERM.contract_dwg.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| async (req, res) => { | ||||
| const id = Number(req.params.id); | ||||
| await sql.query('DELETE FROM contract_dwg WHERE id=?', [id]); | ||||
| res.json({ ok: 1 }); | ||||
| } | ||||
| r.delete( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.contract_dwg.delete, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     await sql.query("DELETE FROM contract_dwg WHERE id=?", [id]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| export default r; | ||||
| export default r; | ||||
|   | ||||
| @@ -1,72 +1,130 @@ | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import { buildScopeWhere, ownerResolvers } from '../utils/scope.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/contracts.js | ||||
| // Contracts routes | ||||
| // - CRUD operations for contracts | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses org scope for all permissions | ||||
| // - contract.read, contract.create, contract.update, contract.delete | ||||
| // - Contract fields: id (PK), org_id, project_id, contract_no, title, status, created_by | ||||
| // - Basic filtering on list endpoint by project_id, org_id, contract_no | ||||
| // - Uses async/await for asynchronous operations | ||||
| // - Middleware functions are used for permission checks | ||||
| // - Owner resolvers are used to fetch org_id for specific contract ids | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import { buildScopeWhere, ownerResolvers } from "../utils/scope.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const OWN = ownerResolvers(sql, 'contracts', 'id'); | ||||
| const OWN = ownerResolvers(sql, "contracts", "id"); | ||||
|  | ||||
|  | ||||
| r.get('/', | ||||
| requirePerm(PERM.contract.read, { scope: 'global' }), | ||||
| async (req, res) => { | ||||
| const { project_id, org_id, contract_no, q, limit = 50, offset = 0 } = req.query; | ||||
| const base = buildScopeWhere(req.principal, { tableAlias: 'c', orgColumn: 'c.org_id', projectColumn: 'c.project_id', permCode: PERM.contract.read, preferProject: true }); | ||||
| const extra = []; | ||||
| const params = { ...base.params, limit: Number(limit), offset: Number(offset) }; | ||||
| if (project_id) { extra.push('c.project_id = :project_id'); params.project_id = Number(project_id); } | ||||
| if (org_id) { extra.push('c.org_id = :org_id'); params.org_id = Number(org_id); } | ||||
| if (contract_no){ extra.push('c.contract_no = :contract_no'); params.contract_no = contract_no; } | ||||
| if (q) { extra.push('(c.contract_no LIKE :q OR c.title LIKE :q)'); params.q = `%${q}%`; } | ||||
| const where = [base.where, ...extra].filter(Boolean).join(' AND '); | ||||
| const [rows] = await sql.query(`SELECT c.* FROM contracts c WHERE ${where} ORDER BY c.id DESC LIMIT :limit OFFSET :offset`, params); | ||||
| res.json(rows); | ||||
| } | ||||
| r.get( | ||||
|   "/", | ||||
|   requirePerm(PERM.contract.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const { | ||||
|       project_id, | ||||
|       org_id, | ||||
|       contract_no, | ||||
|       q, | ||||
|       limit = 50, | ||||
|       offset = 0, | ||||
|     } = req.query; | ||||
|     const base = buildScopeWhere(req.principal, { | ||||
|       tableAlias: "c", | ||||
|       orgColumn: "c.org_id", | ||||
|       projectColumn: "c.project_id", | ||||
|       permCode: PERM.contract.read, | ||||
|       preferProject: true, | ||||
|     }); | ||||
|     const extra = []; | ||||
|     const params = { | ||||
|       ...base.params, | ||||
|       limit: Number(limit), | ||||
|       offset: Number(offset), | ||||
|     }; | ||||
|     if (project_id) { | ||||
|       extra.push("c.project_id = :project_id"); | ||||
|       params.project_id = Number(project_id); | ||||
|     } | ||||
|     if (org_id) { | ||||
|       extra.push("c.org_id = :org_id"); | ||||
|       params.org_id = Number(org_id); | ||||
|     } | ||||
|     if (contract_no) { | ||||
|       extra.push("c.contract_no = :contract_no"); | ||||
|       params.contract_no = contract_no; | ||||
|     } | ||||
|     if (q) { | ||||
|       extra.push("(c.contract_no LIKE :q OR c.title LIKE :q)"); | ||||
|       params.q = `%${q}%`; | ||||
|     } | ||||
|     const where = [base.where, ...extra].filter(Boolean).join(" AND "); | ||||
|     const [rows] = await sql.query( | ||||
|       `SELECT c.* FROM contracts c WHERE ${where} ORDER BY c.id DESC LIMIT :limit OFFSET :offset`, | ||||
|       params | ||||
|     ); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| r.get('/:id', | ||||
| requirePerm(PERM.contract.read, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| 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' }); | ||||
| res.json(row); | ||||
| } | ||||
| r.get( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.contract.read, { scope: "org", getOrgId: OWN.getOrgIdById }), | ||||
|   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" }); | ||||
|     res.json(row); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| r.post('/', | ||||
| requirePerm(PERM.contract.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }), | ||||
| async (req, res) => { | ||||
| const { org_id, project_id, contract_no, title, status } = req.body; | ||||
| 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, status, req.principal.userId]); | ||||
| res.json({ id: rs.insertId }); | ||||
| } | ||||
| r.post( | ||||
|   "/", | ||||
|   requirePerm(PERM.contract.create, { | ||||
|     scope: "org", | ||||
|     getOrgId: async (req) => req.body?.org_id ?? null, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const { org_id, project_id, contract_no, title, status } = req.body; | ||||
|     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, status, req.principal.userId] | ||||
|     ); | ||||
|     res.json({ id: rs.insertId }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| r.put('/:id', | ||||
| requirePerm(PERM.contract.update, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| async (req, res) => { | ||||
| const id = Number(req.params.id); | ||||
| const { title, status } = req.body; | ||||
| await sql.query('UPDATE contracts SET title=?, status=? WHERE id=?', [title, status, id]); | ||||
| res.json({ ok: 1 }); | ||||
| } | ||||
| r.put( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.contract.update, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     const { title, status } = req.body; | ||||
|     await sql.query("UPDATE contracts SET title=?, status=? WHERE id=?", [ | ||||
|       title, | ||||
|       status, | ||||
|       id, | ||||
|     ]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| r.delete('/:id', | ||||
| requirePerm(PERM.contract.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| async (req, res) => { | ||||
| const id = Number(req.params.id); | ||||
| await sql.query('DELETE FROM contracts WHERE id=?', [id]); | ||||
| res.json({ ok: 1 }); | ||||
| } | ||||
| r.delete( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.contract.delete, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     await sql.query("DELETE FROM contracts WHERE id=?", [id]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| export default r; | ||||
| export default r; | ||||
|   | ||||
| @@ -1,74 +1,124 @@ | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import { buildScopeWhere, ownerResolvers } from '../utils/scope.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/correspondences.js | ||||
| // 03.2 7) เพิ่ม routes/correspondences.js (ใหม่) | ||||
| // - ใช้ร่วมกับ requirePerm() และ buildScopeWhere() | ||||
| // - สำหรับจัดการ correspondences (ดู/เพิ่ม/แก้ไข/ลบ) ตามสิทธิ์ของผู้ใช้ | ||||
| // Correspondences routes | ||||
| // - CRUD operations for correspondences | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses org scope for all permissions | ||||
| // - correspondence:read, correspondence:create, correspondence:update, correspondence:delete | ||||
| // - Correspondence fields: id (PK), org_id, project_id, corr_no, subject, status, created_by | ||||
| // - Basic validation: org_id, corr_no, subject required for create | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import { buildScopeWhere, ownerResolvers } from "../utils/scope.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const OWN = ownerResolvers(sql, 'correspondences', 'id'); | ||||
| const OWN = ownerResolvers(sql, "correspondences", "id"); | ||||
|  | ||||
|  | ||||
| r.get('/', | ||||
| requirePerm(PERM.correspondence.read, { scope: 'global' }), | ||||
| async (req, res) => { | ||||
| const { project_id, org_id, q, limit = 50, offset = 0 } = req.query; | ||||
| const base = buildScopeWhere(req.principal, { | ||||
| tableAlias: 'c', orgColumn: 'c.org_id', projectColumn: 'c.project_id', | ||||
| permCode: PERM.correspondence.read, preferProject: true, | ||||
| }); | ||||
| const extra = []; | ||||
| const params = { ...base.params, limit: Number(limit), offset: Number(offset) }; | ||||
| if (project_id) { extra.push('c.project_id = :project_id'); params.project_id = Number(project_id); } | ||||
| if (org_id) { extra.push('c.org_id = :org_id'); params.org_id = Number(org_id); } | ||||
| if (q) { extra.push('(c.corr_no LIKE :q OR c.subject LIKE :q)'); params.q = `%${q}%`; } | ||||
| const where = [base.where, ...extra].join(' AND '); | ||||
| const [rows] = await sql.query(`SELECT c.* FROM correspondences c WHERE ${where} ORDER BY c.id DESC LIMIT :limit OFFSET :offset`, params); | ||||
| res.json(rows); | ||||
| } | ||||
| r.get( | ||||
|   "/", | ||||
|   requirePerm(PERM.correspondence.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const { project_id, org_id, q, limit = 50, offset = 0 } = req.query; | ||||
|     const base = buildScopeWhere(req.principal, { | ||||
|       tableAlias: "c", | ||||
|       orgColumn: "c.org_id", | ||||
|       projectColumn: "c.project_id", | ||||
|       permCode: PERM.correspondence.read, | ||||
|       preferProject: true, | ||||
|     }); | ||||
|     const extra = []; | ||||
|     const params = { | ||||
|       ...base.params, | ||||
|       limit: Number(limit), | ||||
|       offset: Number(offset), | ||||
|     }; | ||||
|     if (project_id) { | ||||
|       extra.push("c.project_id = :project_id"); | ||||
|       params.project_id = Number(project_id); | ||||
|     } | ||||
|     if (org_id) { | ||||
|       extra.push("c.org_id = :org_id"); | ||||
|       params.org_id = Number(org_id); | ||||
|     } | ||||
|     if (q) { | ||||
|       extra.push("(c.corr_no LIKE :q OR c.subject LIKE :q)"); | ||||
|       params.q = `%${q}%`; | ||||
|     } | ||||
|     const where = [base.where, ...extra].join(" AND "); | ||||
|     const [rows] = await sql.query( | ||||
|       `SELECT c.* FROM correspondences c WHERE ${where} ORDER BY c.id DESC LIMIT :limit OFFSET :offset`, | ||||
|       params | ||||
|     ); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| r.get('/:id', | ||||
| requirePerm(PERM.correspondence.read, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| async (req, res) => { | ||||
| const id = Number(req.params.id); | ||||
| const [[row]] = await sql.query('SELECT * FROM correspondences WHERE id=?', [id]); | ||||
| if (!row) return res.status(404).json({ error: 'Not found' }); | ||||
| res.json(row); | ||||
| } | ||||
| r.get( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.correspondence.read, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     const [[row]] = await sql.query( | ||||
|       "SELECT * FROM correspondences WHERE id=?", | ||||
|       [id] | ||||
|     ); | ||||
|     if (!row) return res.status(404).json({ error: "Not found" }); | ||||
|     res.json(row); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| r.post('/', | ||||
| requirePerm(PERM.correspondence.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }), | ||||
| async (req, res) => { | ||||
| const { org_id, project_id, corr_no, subject, status } = req.body; | ||||
| const [rs] = await sql.query(`INSERT INTO correspondences (org_id, project_id, corr_no, subject, status, created_by) VALUES (?,?,?,?,?,?)`, [org_id, project_id, corr_no, subject, status, req.principal.userId]); | ||||
| res.json({ id: rs.insertId }); | ||||
| } | ||||
| r.post( | ||||
|   "/", | ||||
|   requirePerm(PERM.correspondence.create, { | ||||
|     scope: "org", | ||||
|     getOrgId: async (req) => req.body?.org_id ?? null, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const { org_id, project_id, corr_no, subject, status } = req.body; | ||||
|     const [rs] = await sql.query( | ||||
|       `INSERT INTO correspondences (org_id, project_id, corr_no, subject, status, created_by) VALUES (?,?,?,?,?,?)`, | ||||
|       [org_id, project_id, corr_no, subject, status, req.principal.userId] | ||||
|     ); | ||||
|     res.json({ id: rs.insertId }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| r.put('/:id', | ||||
| requirePerm(PERM.correspondence.update, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| async (req, res) => { | ||||
| const id = Number(req.params.id); | ||||
| const { subject, status } = req.body; | ||||
| await sql.query('UPDATE correspondences SET subject=?, status=? WHERE id=?', [subject, status, id]); | ||||
| res.json({ ok: 1 }); | ||||
| } | ||||
| r.put( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.correspondence.update, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     const { subject, status } = req.body; | ||||
|     await sql.query( | ||||
|       "UPDATE correspondences SET subject=?, status=? WHERE id=?", | ||||
|       [subject, status, id] | ||||
|     ); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| r.delete('/:id', | ||||
| requirePerm(PERM.correspondence.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| async (req, res) => { | ||||
| const id = Number(req.params.id); | ||||
| await sql.query('DELETE FROM correspondences WHERE id=?', [id]); | ||||
| res.json({ ok: 1 }); | ||||
| } | ||||
| r.delete( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.correspondence.delete, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     await sql.query("DELETE FROM correspondences WHERE id=?", [id]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| export default r; | ||||
| export default r; | ||||
|   | ||||
| @@ -1,3 +1,12 @@ | ||||
| // FILE: src/routes/documents.js | ||||
| // Documents routes | ||||
| // - CRUD operations for documents | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses project scope for all permissions | ||||
| // - document:read, document:create, document:update, document:delete | ||||
| // - Document fields: document_id (PK), project_id, doc_no, title, category, status, created_by, updated_by | ||||
| // - Basic validation: project_id and doc_no required for create | ||||
|  | ||||
| import { Router } from 'express'; | ||||
| import { requireAuth } from '../middleware/auth.js'; | ||||
| import { enrichPermissions } from '../middleware/permissions.js'; | ||||
|   | ||||
| @@ -1,31 +1,63 @@ | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import { buildScopeWhere, ownerResolvers } from '../utils/scope.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/drawings.js | ||||
| // 03.2 9) เพิ่ม routes/drawings.js (ใหม่) | ||||
| // - ใช้ร่วมกับ requirePerm() และ buildScopeWhere() | ||||
| // - สำหรับจัดการ drawings (ดู/เพิ่ม/แก้ไข/ลบ) ตามสิทธิ์ของผู้ใช้ | ||||
| // Drawings routes | ||||
| // - CRUD operations for drawings | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses org scope for all permissions | ||||
| // - drawing:read, drawing:create, drawing:update, drawing:delete | ||||
| // - Drawing fields: id (PK), org_id, project_id, dwg_no, dwg_code, title, created_by | ||||
| // - Basic validation: org_id, dwg_no required for create | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import { buildScopeWhere, ownerResolvers } from "../utils/scope.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const OWN = ownerResolvers(sql, 'drawings', 'id'); | ||||
| const OWN = ownerResolvers(sql, "drawings", "id"); | ||||
|  | ||||
| // LIST | ||||
| r.get('/', | ||||
|   requirePerm('drawing.read', { scope: 'global' }), | ||||
| r.get( | ||||
|   "/", | ||||
|   requirePerm("drawing.read", { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const { project_id, org_id, code, q, limit = 50, offset = 0 } = req.query; | ||||
|  | ||||
|     const base = buildScopeWhere(req.principal, { | ||||
|       tableAlias: 'd', orgColumn: 'd.org_id', projectColumn: 'd.project_id', | ||||
|       permCode: 'drawing.read', preferProject: true, | ||||
|       tableAlias: "d", | ||||
|       orgColumn: "d.org_id", | ||||
|       projectColumn: "d.project_id", | ||||
|       permCode: "drawing.read", | ||||
|       preferProject: true, | ||||
|     }); | ||||
|  | ||||
|     const extra = []; | ||||
|     const params = { ...base.params, limit: Number(limit), offset: Number(offset) }; | ||||
|     if (project_id) { extra.push('d.project_id = :project_id'); params.project_id = Number(project_id); } | ||||
|     if (org_id)     { extra.push('d.org_id = :org_id');         params.org_id = Number(org_id); } | ||||
|     if (code)       { extra.push('d.dwg_code = :code');         params.code = code; } | ||||
|     if (q)          { extra.push('(d.dwg_no LIKE :q OR d.title LIKE :q)'); params.q = `%${q}%`; } | ||||
|     const params = { | ||||
|       ...base.params, | ||||
|       limit: Number(limit), | ||||
|       offset: Number(offset), | ||||
|     }; | ||||
|     if (project_id) { | ||||
|       extra.push("d.project_id = :project_id"); | ||||
|       params.project_id = Number(project_id); | ||||
|     } | ||||
|     if (org_id) { | ||||
|       extra.push("d.org_id = :org_id"); | ||||
|       params.org_id = Number(org_id); | ||||
|     } | ||||
|     if (code) { | ||||
|       extra.push("d.dwg_code = :code"); | ||||
|       params.code = code; | ||||
|     } | ||||
|     if (q) { | ||||
|       extra.push("(d.dwg_no LIKE :q OR d.title LIKE :q)"); | ||||
|       params.q = `%${q}%`; | ||||
|     } | ||||
|  | ||||
|     const where = [base.where, ...extra].filter(Boolean).join(' AND '); | ||||
|     const where = [base.where, ...extra].filter(Boolean).join(" AND "); | ||||
|  | ||||
|     const [rows] = await sql.query( | ||||
|       `SELECT d.* FROM drawings d WHERE ${where} | ||||
| @@ -37,19 +69,24 @@ r.get('/', | ||||
| ); | ||||
|  | ||||
| // GET | ||||
| r.get('/:id', | ||||
|   requirePerm('drawing.read', { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.get( | ||||
|   "/:id", | ||||
|   requirePerm("drawing.read", { scope: "org", getOrgId: OWN.getOrgIdById }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     const [[row]] = await sql.query('SELECT * FROM drawings WHERE id=?', [id]); | ||||
|     if (!row) return res.status(404).json({ error: 'Not found' }); | ||||
|     const [[row]] = await sql.query("SELECT * FROM drawings WHERE id=?", [id]); | ||||
|     if (!row) return res.status(404).json({ error: "Not found" }); | ||||
|     res.json(row); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // CREATE | ||||
| r.post('/', | ||||
|   requirePerm('drawing.create', { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }), | ||||
| r.post( | ||||
|   "/", | ||||
|   requirePerm("drawing.create", { | ||||
|     scope: "org", | ||||
|     getOrgId: async (req) => req.body?.org_id ?? null, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const { org_id, project_id, dwg_no, dwg_code, title } = req.body; | ||||
|     const [rs] = await sql.query( | ||||
| @@ -62,22 +99,24 @@ r.post('/', | ||||
| ); | ||||
|  | ||||
| // UPDATE | ||||
| r.put('/:id', | ||||
|   requirePerm('drawing.update', { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.put( | ||||
|   "/:id", | ||||
|   requirePerm("drawing.update", { scope: "org", getOrgId: OWN.getOrgIdById }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     const { title } = req.body; | ||||
|     await sql.query('UPDATE drawings SET title=? WHERE id=?', [title, id]); | ||||
|     await sql.query("UPDATE drawings SET title=? WHERE id=?", [title, id]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // DELETE | ||||
| r.delete('/:id', | ||||
|   requirePerm('drawing.delete', { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.delete( | ||||
|   "/:id", | ||||
|   requirePerm("drawing.delete", { scope: "org", getOrgId: OWN.getOrgIdById }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     await sql.query('DELETE FROM drawings WHERE id=?', [id]); | ||||
|     await sql.query("DELETE FROM drawings WHERE id=?", [id]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|   | ||||
| @@ -1,90 +1,150 @@ | ||||
| import { Router } from 'express'; | ||||
| import fs from 'fs'; | ||||
| import path from 'path'; | ||||
| import jwt from 'jsonwebtoken'; | ||||
| import { requireAuth } from '../middleware/auth.js'; | ||||
| import { enrichPermissions } from '../middleware/permissions.js'; | ||||
| import { requireRole } from '../middleware/rbac.js'; | ||||
| import { requirePerm } from '../middleware/permGuard.js'; | ||||
| import { sequelize } from '../db/sequelize.js'; | ||||
| import FileModel from '../db/models/FileObject.js'; | ||||
| import { config } from '../config.js'; | ||||
| // FILE: src/routes/files_extras.js | ||||
| // Extra file-related routes | ||||
| // - HEAD for file meta | ||||
| // - DELETE to delete a file (physical + record) | ||||
| // - POST to rename a file (meta only) | ||||
| // - POST to refresh signed download URL | ||||
| // - Requires authentication and appropriate permissions via requireAuth, requirePerm, and enrichPermissions middleware | ||||
| // - Uses project scope for file access permissions | ||||
| // - file:read, file:create, file:update, file:delete | ||||
| // - File fields: file_id (PK), module, ref_id, orig_name, disk_path, mime, size, created_by | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import fs from "fs"; | ||||
| import path from "path"; | ||||
| import jwt from "jsonwebtoken"; | ||||
| import { requireAuth } from "../middleware/auth.js"; | ||||
| import { enrichPermissions } from "../middleware/permissions.js"; | ||||
| import { requireRole } from "../middleware/rbac.js"; | ||||
| import { requirePerm } from "../middleware/permGuard.js"; | ||||
| import { sequelize } from "../db/sequelize.js"; | ||||
| import FileModel from "../db/models/FileObject.js"; | ||||
| import { config } from "../config.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const Files = FileModel(sequelize); | ||||
|  | ||||
| async function projectForFile(rec) { | ||||
|   const mod = rec.module; const refId = rec.ref_id; | ||||
|   const mod = rec.module; | ||||
|   const refId = rec.ref_id; | ||||
|   switch (mod) { | ||||
|     case 'rfa': { const M = (await import('../db/models/RFA.js')).default(sequelize); const row = await M.findByPk(refId); return row?.project_id||null; } | ||||
|     case 'correspondence': { const M = (await import('../db/models/Correspondence.js')).default(sequelize); const row = await M.findByPk(refId); return row?.project_id||null; } | ||||
|     case 'drawing': { const M = (await import('../db/models/Drawing.js')).default(sequelize); const row = await M.findByPk(refId); return row?.project_id||null; } | ||||
|     case 'document': { const M = (await import('../db/models/Document.js')).default(sequelize); const row = await M.findByPk(refId); return row?.project_id||null; } | ||||
|     case 'transmittal': { const M = (await import('../db/models/Transmittal.js')).default(sequelize); const row = await M.findByPk(refId); return row?.project_id||null; } | ||||
|     default: return null; | ||||
|     case "rfa": { | ||||
|       const M = (await import("../db/models/RFA.js")).default(sequelize); | ||||
|       const row = await M.findByPk(refId); | ||||
|       return row?.project_id || null; | ||||
|     } | ||||
|     case "correspondence": { | ||||
|       const M = (await import("../db/models/Correspondence.js")).default( | ||||
|         sequelize | ||||
|       ); | ||||
|       const row = await M.findByPk(refId); | ||||
|       return row?.project_id || null; | ||||
|     } | ||||
|     case "drawing": { | ||||
|       const M = (await import("../db/models/Drawing.js")).default(sequelize); | ||||
|       const row = await M.findByPk(refId); | ||||
|       return row?.project_id || null; | ||||
|     } | ||||
|     case "document": { | ||||
|       const M = (await import("../db/models/Document.js")).default(sequelize); | ||||
|       const row = await M.findByPk(refId); | ||||
|       return row?.project_id || null; | ||||
|     } | ||||
|     case "transmittal": { | ||||
|       const M = (await import("../db/models/Transmittal.js")).default( | ||||
|         sequelize | ||||
|       ); | ||||
|       const row = await M.findByPk(refId); | ||||
|       return row?.project_id || null; | ||||
|     } | ||||
|     default: | ||||
|       return null; | ||||
|   } | ||||
| } | ||||
|  | ||||
|  | ||||
| // HEAD meta only | ||||
| r.head('/files/:file_id', requireAuth, async (req, res) => { | ||||
| r.head("/files/:file_id", requireAuth, async (req, res) => { | ||||
|   const rec = await Files.findByPk(Number(req.params.file_id)); | ||||
|   if (!rec) return res.status(404).end(); | ||||
|   res.setHeader('Content-Type', rec.mime || 'application/octet-stream'); | ||||
|   res.setHeader('Content-Length', String(rec.size || 0)); | ||||
|   res.setHeader("Content-Type", rec.mime || "application/octet-stream"); | ||||
|   res.setHeader("Content-Length", String(rec.size || 0)); | ||||
|   res.status(200).end(); | ||||
| }); | ||||
|  | ||||
| // delete (soft delete is recommended; here we do physical delete + record delete) | ||||
| r.delete('/files/:file_id', requireAuth, enrichPermissions(), requirePerm('file:delete'), async (req, res) => { | ||||
|   const rec = await Files.findByPk(Number(req.params.file_id)); | ||||
|   if (!rec) return res.status(404).json({ error: 'Not found' }); | ||||
|   const pid = await projectForFile(rec); | ||||
|   const roles = req.user?.roles || []; | ||||
|   const isAdmin = roles.includes('Admin'); | ||||
|   if (!isAdmin) { | ||||
|     const { getUserProjectIds } = await import('../middleware/abac.js'); | ||||
|     const memberProjects = await getUserProjectIds(req.user?.user_id); | ||||
|     if (!memberProjects.includes(pid)) return res.status(403).json({ error: 'Forbidden: not a project member' }); | ||||
| r.delete( | ||||
|   "/files/:file_id", | ||||
|   requireAuth, | ||||
|   enrichPermissions(), | ||||
|   requirePerm("file:delete"), | ||||
|   async (req, res) => { | ||||
|     const rec = await Files.findByPk(Number(req.params.file_id)); | ||||
|     if (!rec) return res.status(404).json({ error: "Not found" }); | ||||
|     const pid = await projectForFile(rec); | ||||
|     const roles = req.user?.roles || []; | ||||
|     const isAdmin = roles.includes("Admin"); | ||||
|     if (!isAdmin) { | ||||
|       const { getUserProjectIds } = await import("../middleware/abac.js"); | ||||
|       const memberProjects = await getUserProjectIds(req.user?.user_id); | ||||
|       if (!memberProjects.includes(pid)) | ||||
|         return res | ||||
|           .status(403) | ||||
|           .json({ error: "Forbidden: not a project member" }); | ||||
|     } | ||||
|     try { | ||||
|       fs.unlinkSync(rec.disk_path); | ||||
|     } catch {} | ||||
|     await rec.destroy(); | ||||
|     res.json({ ok: true }); | ||||
|   } | ||||
|   try { fs.unlinkSync(rec.disk_path); } catch {} | ||||
|   await rec.destroy(); | ||||
|   res.json({ ok: true }); | ||||
| }); | ||||
| ); | ||||
|  | ||||
| // rename (meta only - keep disk file name) | ||||
| r.post('/files/:file_id/rename', requireAuth, enrichPermissions(), requirePerm('file:update'), async (req, res) => { | ||||
|   const rec = await Files.findByPk(Number(req.params.file_id)); | ||||
|   if (!rec) return res.status(404).json({ error: 'Not found' }); | ||||
|   const pid = await projectForFile(rec); | ||||
|   const roles = req.user?.roles || []; | ||||
|   const isAdmin = roles.includes('Admin'); | ||||
|   if (!isAdmin) { | ||||
|     const { getUserProjectIds } = await import('../middleware/abac.js'); | ||||
|     const memberProjects = await getUserProjectIds(req.user?.user_id); | ||||
|     if (!memberProjects.includes(pid)) return res.status(403).json({ error: 'Forbidden: not a project member' }); | ||||
| r.post( | ||||
|   "/files/:file_id/rename", | ||||
|   requireAuth, | ||||
|   enrichPermissions(), | ||||
|   requirePerm("file:update"), | ||||
|   async (req, res) => { | ||||
|     const rec = await Files.findByPk(Number(req.params.file_id)); | ||||
|     if (!rec) return res.status(404).json({ error: "Not found" }); | ||||
|     const pid = await projectForFile(rec); | ||||
|     const roles = req.user?.roles || []; | ||||
|     const isAdmin = roles.includes("Admin"); | ||||
|     if (!isAdmin) { | ||||
|       const { getUserProjectIds } = await import("../middleware/abac.js"); | ||||
|       const memberProjects = await getUserProjectIds(req.user?.user_id); | ||||
|       if (!memberProjects.includes(pid)) | ||||
|         return res | ||||
|           .status(403) | ||||
|           .json({ error: "Forbidden: not a project member" }); | ||||
|     } | ||||
|     const { orig_name } = req.body || {}; | ||||
|     if (!orig_name) | ||||
|       return res.status(400).json({ error: "orig_name required" }); | ||||
|     rec.orig_name = orig_name; | ||||
|     await rec.save(); | ||||
|     res.json({ ok: true }); | ||||
|   } | ||||
|   const { orig_name } = req.body || {}; | ||||
|   if (!orig_name) return res.status(400).json({ error: 'orig_name required' }); | ||||
|   rec.orig_name = orig_name; | ||||
|   await rec.save(); | ||||
|   res.json({ ok: true }); | ||||
| }); | ||||
| ); | ||||
|  | ||||
| // refresh signed download url | ||||
| r.post('/files/:file_id/refresh-url', requireAuth, async (req, res) => { | ||||
| r.post("/files/:file_id/refresh-url", requireAuth, async (req, res) => { | ||||
|   const rec = await Files.findByPk(Number(req.params.file_id)); | ||||
|   if (!rec) return res.status(404).json({ error: 'Not found' }); | ||||
|   if (!rec) return res.status(404).json({ error: "Not found" }); | ||||
|   const pid = await projectForFile(rec); | ||||
|   const roles = req.user?.roles || []; | ||||
|   const isAdmin = roles.includes('Admin'); | ||||
|   const isAdmin = roles.includes("Admin"); | ||||
|   if (!isAdmin) { | ||||
|     const { getUserProjectIds } = await import('../middleware/abac.js'); | ||||
|     const { getUserProjectIds } = await import("../middleware/abac.js"); | ||||
|     const memberProjects = await getUserProjectIds(req.user?.user_id); | ||||
|     if (!memberProjects.includes(pid)) return res.status(403).json({ error: 'Forbidden: not a project member' }); | ||||
|     if (!memberProjects.includes(pid)) | ||||
|       return res.status(403).json({ error: "Forbidden: not a project member" }); | ||||
|   } | ||||
|   const expSec = Number(process.env.FILE_URL_EXPIRES || 600); | ||||
|   const token = jwt.sign({ file_id: rec.file_id }, config.JWT.SECRET, { expiresIn: expSec }); | ||||
|   const token = jwt.sign({ file_id: rec.file_id }, config.JWT.SECRET, { | ||||
|     expiresIn: expSec, | ||||
|   }); | ||||
|   const download_url = `/api/v1/files/${rec.file_id}?token=${token}`; | ||||
|   res.json({ download_url, expires_in: expSec }); | ||||
| }); | ||||
|   | ||||
| @@ -1,13 +1,18 @@ | ||||
| import { Router } from 'express'; | ||||
| import { sequelize } from '../db/sequelize.js'; | ||||
| // FILE: src/routes/health.js | ||||
| // Health check route | ||||
| // - GET /health to check server and database status | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import { sequelize } from "../db/sequelize.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| r.get('/health', async (_req, res) => { | ||||
| r.get("/health", async (_req, res) => { | ||||
|   try { | ||||
|     await sequelize.query('SELECT 1 AS ok'); | ||||
|     res.status(200).json({ ok: true, db: 'up' }); | ||||
|     await sequelize.query("SELECT 1 AS ok"); | ||||
|     res.status(200).json({ ok: true, db: "up" }); | ||||
|   } catch (e) { | ||||
|     res.status(500).json({ ok: false, db: 'down', error: String(e) }); | ||||
|     res.status(500).json({ ok: false, db: "down", error: String(e) }); | ||||
|   } | ||||
| }); | ||||
| export default r; | ||||
|   | ||||
| @@ -1,9 +1,17 @@ | ||||
| // src/routes/lookup.js (ESM) | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import { buildScopeWhere } from '../utils/scope.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/lookup.js | ||||
| // Lookup route | ||||
| // - GET /api/lookup to fetch various lookup data (organizations, projects, categories, subcategories, volumes, permissions) | ||||
| // - Requires appropriate permissions for each data type  via requirePerm middleware | ||||
| // - Supports query parameter 'pick' to specify which data to include (comma-separated, e.g. ?pick=org,project) | ||||
| // - If 'pick' is not provided, returns all data types | ||||
| // - Organizations and Projects are scoped based on user's permissions | ||||
| // - Categories, Subcategories, Volumes, and Permissions are global master data | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import { buildScopeWhere } from "../utils/scope.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
|  | ||||
| @@ -13,34 +21,47 @@ const r = Router(); | ||||
| function parsePick(qs) { | ||||
|   if (!qs) return null; | ||||
|   return String(qs) | ||||
|     .split(',') | ||||
|     .map(s => s.trim().toLowerCase()) | ||||
|     .split(",") | ||||
|     .map((s) => s.trim().toLowerCase()) | ||||
|     .filter(Boolean); | ||||
| } | ||||
|  | ||||
| // GET /api/lookup?pick=org,project,category,subcategory,volume,permission | ||||
| r.get('/', | ||||
| r.get( | ||||
|   "/", | ||||
|   // ต้องเป็นผู้ใช้ที่ยืนยันตัวตนแล้ว (middleware authJwt+loadPrincipal อยู่ใน /api) | ||||
|   // ไม่ require permission เดียว เพราะเราจะเช็คทีละชุดด้านล่าง | ||||
|   async (req, res) => { | ||||
|     const pick = new Set(parsePick(req.query.pick) || [ | ||||
|       'org', 'project', 'category', 'subcategory', 'volume', 'permission' | ||||
|     ]); | ||||
|     const pick = new Set( | ||||
|       parsePick(req.query.pick) || [ | ||||
|         "org", | ||||
|         "project", | ||||
|         "category", | ||||
|         "subcategory", | ||||
|         "volume", | ||||
|         "permission", | ||||
|       ] | ||||
|     ); | ||||
|  | ||||
|     const result = {}; | ||||
|  | ||||
|     // 1) Organizations (scoped list) — require organization.read | ||||
|     if (pick.has('org')) { | ||||
|     if (pick.has("org")) { | ||||
|       // มีสิทธิ์ถึงจะดึง | ||||
|       const canOrg = req.principal.isSuperAdmin || req.principal.perms.has(PERM.organization.read); | ||||
|       const canOrg = | ||||
|         req.principal.isSuperAdmin || | ||||
|         req.principal.perms.has(PERM.organization.read); | ||||
|       if (canOrg) { | ||||
|         const { where, params } = buildScopeWhere(req.principal, { | ||||
|           tableAlias: 'o', | ||||
|           orgColumn: 'o.org_id', | ||||
|           projectColumn: 'NULL', | ||||
|           tableAlias: "o", | ||||
|           orgColumn: "o.org_id", | ||||
|           projectColumn: "NULL", | ||||
|           permCode: PERM.organization.read, | ||||
|         }); | ||||
|         const [rows] = await sql.query(`SELECT o.org_id, o.org_name FROM organizations o WHERE ${where} ORDER BY o.org_name`, params); | ||||
|         const [rows] = await sql.query( | ||||
|           `SELECT o.org_id, o.org_name FROM organizations o WHERE ${where} ORDER BY o.org_name`, | ||||
|           params | ||||
|         ); | ||||
|         result.organizations = rows; | ||||
|       } else { | ||||
|         result.organizations = []; | ||||
| @@ -48,13 +69,15 @@ r.get('/', | ||||
|     } | ||||
|  | ||||
|     // 2) Projects (scoped list) — require project.read | ||||
|     if (pick.has('project')) { | ||||
|       const canPrj = req.principal.isSuperAdmin || req.principal.perms.has(PERM.project.read); | ||||
|     if (pick.has("project")) { | ||||
|       const canPrj = | ||||
|         req.principal.isSuperAdmin || | ||||
|         req.principal.perms.has(PERM.project.read); | ||||
|       if (canPrj) { | ||||
|         const { where, params } = buildScopeWhere(req.principal, { | ||||
|           tableAlias: 'p', | ||||
|           orgColumn: 'p.org_id', | ||||
|           projectColumn: 'p.project_id', | ||||
|           tableAlias: "p", | ||||
|           orgColumn: "p.org_id", | ||||
|           projectColumn: "p.project_id", | ||||
|           permCode: PERM.project.read, | ||||
|           preferProject: true, | ||||
|         }); | ||||
| @@ -70,10 +93,14 @@ r.get('/', | ||||
|     } | ||||
|  | ||||
|     // 3) Categories (global master) — require category.read | ||||
|     if (pick.has('category')) { | ||||
|       const can = req.principal.isSuperAdmin || req.principal.perms.has(PERM.category.read); | ||||
|     if (pick.has("category")) { | ||||
|       const can = | ||||
|         req.principal.isSuperAdmin || | ||||
|         req.principal.perms.has(PERM.category.read); | ||||
|       if (can) { | ||||
|         const [rows] = await sql.query('SELECT cat_id, cat_code, cat_name FROM categories ORDER BY cat_name'); | ||||
|         const [rows] = await sql.query( | ||||
|           "SELECT cat_id, cat_code, cat_name FROM categories ORDER BY cat_name" | ||||
|         ); | ||||
|         result.categories = rows; | ||||
|       } else { | ||||
|         result.categories = []; | ||||
| @@ -81,10 +108,14 @@ r.get('/', | ||||
|     } | ||||
|  | ||||
|     // 4) Subcategories (global master) — require category.read | ||||
|     if (pick.has('subcategory')) { | ||||
|       const can = req.principal.isSuperAdmin || req.principal.perms.has(PERM.category.read); | ||||
|     if (pick.has("subcategory")) { | ||||
|       const can = | ||||
|         req.principal.isSuperAdmin || | ||||
|         req.principal.perms.has(PERM.category.read); | ||||
|       if (can) { | ||||
|         const [rows] = await sql.query('SELECT sub_cat_id, cat_id, sub_cat_name FROM subcategories ORDER BY sub_cat_name'); | ||||
|         const [rows] = await sql.query( | ||||
|           "SELECT sub_cat_id, cat_id, sub_cat_name FROM subcategories ORDER BY sub_cat_name" | ||||
|         ); | ||||
|         result.subcategories = rows; | ||||
|       } else { | ||||
|         result.subcategories = []; | ||||
| @@ -92,10 +123,13 @@ r.get('/', | ||||
|     } | ||||
|  | ||||
|     // 5) Volumes (global master) — require volume.read | ||||
|     if (pick.has('volume')) { | ||||
|       const can = req.principal.isSuperAdmin || req.principal.perms.has(PERM.volume.read); | ||||
|     if (pick.has("volume")) { | ||||
|       const can = | ||||
|         req.principal.isSuperAdmin || req.principal.perms.has(PERM.volume.read); | ||||
|       if (can) { | ||||
|         const [rows] = await sql.query('SELECT volume_id, volume_code, volume_name FROM volumes ORDER BY volume_code'); | ||||
|         const [rows] = await sql.query( | ||||
|           "SELECT volume_id, volume_code, volume_name FROM volumes ORDER BY volume_code" | ||||
|         ); | ||||
|         result.volumes = rows; | ||||
|       } else { | ||||
|         result.volumes = []; | ||||
| @@ -103,10 +137,14 @@ r.get('/', | ||||
|     } | ||||
|  | ||||
|     // 6) Permissions (global master) — require permission.read | ||||
|     if (pick.has('permission')) { | ||||
|       const can = req.principal.isSuperAdmin || req.principal.perms.has(PERM.permission.read); | ||||
|     if (pick.has("permission")) { | ||||
|       const can = | ||||
|         req.principal.isSuperAdmin || | ||||
|         req.principal.perms.has(PERM.permission.read); | ||||
|       if (can) { | ||||
|         const [rows] = await sql.query('SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code'); | ||||
|         const [rows] = await sql.query( | ||||
|           "SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code" | ||||
|         ); | ||||
|         result.permissions = rows; | ||||
|       } else { | ||||
|         result.permissions = []; | ||||
|   | ||||
| @@ -1,15 +1,23 @@ | ||||
| import { Router } from 'express'; | ||||
| import { requireAuth } from '../middleware/auth.js'; | ||||
| import { enrichPermissions } from '../middleware/permissions.js'; | ||||
| import { requireRole } from '../middleware/rbac.js'; | ||||
| import { requirePerm } from '../middleware/permGuard.js'; | ||||
| import { sequelize } from '../db/sequelize.js'; | ||||
| import RfaModel from '../db/models/RFA.js'; | ||||
| import DrawingModel from '../db/models/Drawing.js'; | ||||
| import RfaDrawMapModel from '../db/models/RfaDrawingMap.js'; | ||||
| import CorrModel from '../db/models/Correspondence.js'; | ||||
| import DocModel from '../db/models/Document.js'; | ||||
| import CorrDocMapModel from '../db/models/CorrDocumentMap.js'; | ||||
| // FILE: src/routes/maps.js | ||||
| // Maps routes | ||||
| // - Manage relationships between RFAs and Drawings, Correspondences and Documents | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses project scope for RFA-Drawing maps and Correspondence-Document maps | ||||
| // - rfa:update for RFA-Drawing maps | ||||
| // - correspondence:update for Correspondence-Document maps | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import { requireAuth } from "../middleware/auth.js"; | ||||
| import { enrichPermissions } from "../middleware/permissions.js"; | ||||
| import { requireRole } from "../middleware/rbac.js"; | ||||
| import { requirePerm } from "../middleware/permGuard.js"; | ||||
| import { sequelize } from "../db/sequelize.js"; | ||||
| import RfaModel from "../db/models/RFA.js"; | ||||
| import DrawingModel from "../db/models/Drawing.js"; | ||||
| import RfaDrawMapModel from "../db/models/RfaDrawingMap.js"; | ||||
| import CorrModel from "../db/models/Correspondence.js"; | ||||
| import DocModel from "../db/models/Document.js"; | ||||
| import CorrDocMapModel from "../db/models/CorrDocumentMap.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const RFA = RfaModel(sequelize); | ||||
| @@ -22,62 +30,121 @@ const CorrDoc = CorrDocMapModel(sequelize); | ||||
| async function ensureRfaMembership(req, res) { | ||||
|   const rfaId = Number(req.params.rfa_id); | ||||
|   const row = await RFA.findByPk(rfaId); | ||||
|   if (!row) { res.status(404).json({ error:'RFA not found' }); return false; } | ||||
|   const roles = req.user?.roles || []; const isAdmin = roles.includes('Admin'); | ||||
|   if (!row) { | ||||
|     res.status(404).json({ error: "RFA not found" }); | ||||
|     return false; | ||||
|   } | ||||
|   const roles = req.user?.roles || []; | ||||
|   const isAdmin = roles.includes("Admin"); | ||||
|   if (isAdmin) return true; | ||||
|   const { getUserProjectIds } = await import('../middleware/abac.js'); | ||||
|   const { getUserProjectIds } = await import("../middleware/abac.js"); | ||||
|   const memberProjects = await getUserProjectIds(req.user?.user_id); | ||||
|   if (!memberProjects.includes(Number(row.project_id))) { res.status(403).json({ error:'Forbidden: not a project member' }); return false; } | ||||
|   if (!memberProjects.includes(Number(row.project_id))) { | ||||
|     res.status(403).json({ error: "Forbidden: not a project member" }); | ||||
|     return false; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| async function ensureCorrMembership(req, res) { | ||||
|   const corrId = Number(req.params.corr_id); | ||||
|   const row = await Corr.findByPk(corrId); | ||||
|   if (!row) { res.status(404).json({ error:'Correspondence not found' }); return false; } | ||||
|   const roles = req.user?.roles || []; const isAdmin = roles.includes('Admin'); | ||||
|   if (!row) { | ||||
|     res.status(404).json({ error: "Correspondence not found" }); | ||||
|     return false; | ||||
|   } | ||||
|   const roles = req.user?.roles || []; | ||||
|   const isAdmin = roles.includes("Admin"); | ||||
|   if (isAdmin) return true; | ||||
|   const { getUserProjectIds } = await import('../middleware/abac.js'); | ||||
|   const { getUserProjectIds } = await import("../middleware/abac.js"); | ||||
|   const memberProjects = await getUserProjectIds(req.user?.user_id); | ||||
|   if (!memberProjects.includes(Number(row.project_id))) { res.status(403).json({ error:'Forbidden: not a project member' }); return false; } | ||||
|   if (!memberProjects.includes(Number(row.project_id))) { | ||||
|     res.status(403).json({ error: "Forbidden: not a project member" }); | ||||
|     return false; | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
|  | ||||
| // RFA <-> Drawing | ||||
| r.get('/maps/rfa/:rfa_id/drawings', requireAuth, async (req, res) => { | ||||
|   const rows = await RfaDraw.findAll({ where: { rfa_id: Number(req.params.rfa_id) } }); | ||||
| r.get("/maps/rfa/:rfa_id/drawings", requireAuth, async (req, res) => { | ||||
|   const rows = await RfaDraw.findAll({ | ||||
|     where: { rfa_id: Number(req.params.rfa_id) }, | ||||
|   }); | ||||
|   res.json(rows); | ||||
| }); | ||||
| r.post('/maps/rfa/:rfa_id/drawings/:drawing_id', requireAuth, enrichPermissions(), requirePerm('rfa:update'), async (req, res) => { | ||||
|   if (!(await ensureRfaMembership(req, res))) return; | ||||
|   const { rfa_id, drawing_id } = { rfa_id: Number(req.params.rfa_id), drawing_id: Number(req.params.drawing_id) }; | ||||
|   await RfaDraw.create({ rfa_id, drawing_id }); | ||||
|   res.status(201).json({ ok: true }); | ||||
| }); | ||||
| r.delete('/maps/rfa/:rfa_id/drawings/:drawing_id', requireAuth, enrichPermissions(), requirePerm('rfa:update'), async (req, res) => { | ||||
|   if (!(await ensureRfaMembership(req, res))) return; | ||||
|   const { rfa_id, drawing_id } = { rfa_id: Number(req.params.rfa_id), drawing_id: Number(req.params.drawing_id) }; | ||||
|   const count = await RfaDraw.destroy({ where: { rfa_id, drawing_id } }); | ||||
|   res.json({ ok: count > 0 }); | ||||
| }); | ||||
| r.post( | ||||
|   "/maps/rfa/:rfa_id/drawings/:drawing_id", | ||||
|   requireAuth, | ||||
|   enrichPermissions(), | ||||
|   requirePerm("rfa:update"), | ||||
|   async (req, res) => { | ||||
|     if (!(await ensureRfaMembership(req, res))) return; | ||||
|     const { rfa_id, drawing_id } = { | ||||
|       rfa_id: Number(req.params.rfa_id), | ||||
|       drawing_id: Number(req.params.drawing_id), | ||||
|     }; | ||||
|     await RfaDraw.create({ rfa_id, drawing_id }); | ||||
|     res.status(201).json({ ok: true }); | ||||
|   } | ||||
| ); | ||||
| r.delete( | ||||
|   "/maps/rfa/:rfa_id/drawings/:drawing_id", | ||||
|   requireAuth, | ||||
|   enrichPermissions(), | ||||
|   requirePerm("rfa:update"), | ||||
|   async (req, res) => { | ||||
|     if (!(await ensureRfaMembership(req, res))) return; | ||||
|     const { rfa_id, drawing_id } = { | ||||
|       rfa_id: Number(req.params.rfa_id), | ||||
|       drawing_id: Number(req.params.drawing_id), | ||||
|     }; | ||||
|     const count = await RfaDraw.destroy({ where: { rfa_id, drawing_id } }); | ||||
|     res.json({ ok: count > 0 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // Correspondence <-> Document | ||||
| r.get('/maps/correspondence/:corr_id/documents', requireAuth, async (req, res) => { | ||||
|   const rows = await CorrDoc.findAll({ where: { correspondence_id: Number(req.params.corr_id) } }); | ||||
|   res.json(rows); | ||||
| }); | ||||
| r.post('/maps/correspondence/:corr_id/documents/:doc_id', requireAuth, enrichPermissions(), requirePerm('correspondence:update'), async (req, res) => { | ||||
|   if (!(await ensureCorrMembership(req, res))) return; | ||||
|   const { corr_id, doc_id } = { corr_id: Number(req.params.corr_id), doc_id: Number(req.params.doc_id) }; | ||||
|   await CorrDoc.create({ correspondence_id: corr_id, document_id: doc_id }); | ||||
|   res.status(201).json({ ok: true }); | ||||
| }); | ||||
| r.delete('/maps/correspondence/:corr_id/documents/:doc_id', requireAuth, enrichPermissions(), requirePerm('correspondence:update'), async (req, res) => { | ||||
|   if (!(await ensureCorrMembership(req, res))) return; | ||||
|   const { corr_id, doc_id } = { corr_id: Number(req.params.corr_id), doc_id: Number(req.params.doc_id) }; | ||||
|   const count = await CorrDoc.destroy({ where: { correspondence_id: corr_id, document_id: doc_id } }); | ||||
|   res.json({ ok: count > 0 }); | ||||
| }); | ||||
| r.get( | ||||
|   "/maps/correspondence/:corr_id/documents", | ||||
|   requireAuth, | ||||
|   async (req, res) => { | ||||
|     const rows = await CorrDoc.findAll({ | ||||
|       where: { correspondence_id: Number(req.params.corr_id) }, | ||||
|     }); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
| r.post( | ||||
|   "/maps/correspondence/:corr_id/documents/:doc_id", | ||||
|   requireAuth, | ||||
|   enrichPermissions(), | ||||
|   requirePerm("correspondence:update"), | ||||
|   async (req, res) => { | ||||
|     if (!(await ensureCorrMembership(req, res))) return; | ||||
|     const { corr_id, doc_id } = { | ||||
|       corr_id: Number(req.params.corr_id), | ||||
|       doc_id: Number(req.params.doc_id), | ||||
|     }; | ||||
|     await CorrDoc.create({ correspondence_id: corr_id, document_id: doc_id }); | ||||
|     res.status(201).json({ ok: true }); | ||||
|   } | ||||
| ); | ||||
| r.delete( | ||||
|   "/maps/correspondence/:corr_id/documents/:doc_id", | ||||
|   requireAuth, | ||||
|   enrichPermissions(), | ||||
|   requirePerm("correspondence:update"), | ||||
|   async (req, res) => { | ||||
|     if (!(await ensureCorrMembership(req, res))) return; | ||||
|     const { corr_id, doc_id } = { | ||||
|       corr_id: Number(req.params.corr_id), | ||||
|       doc_id: Number(req.params.doc_id), | ||||
|     }; | ||||
|     const count = await CorrDoc.destroy({ | ||||
|       where: { correspondence_id: corr_id, document_id: doc_id }, | ||||
|     }); | ||||
|     res.json({ ok: count > 0 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| export default r; | ||||
|   | ||||
| @@ -1,16 +1,32 @@ | ||||
| import { Router } from 'express'; | ||||
| import { requireAuth } from '../middleware/auth.js'; | ||||
| import { sequelize } from '../db/sequelize.js'; | ||||
| import FileModel from '../db/models/FileObject.js'; | ||||
| // FILE: src/routes/module_files.js | ||||
| // Module files routes | ||||
| // - GET /:module(s)/:id/files to list files for various modules (rfa, correspondence, drawing, document, transmittal) | ||||
| // - Requires authentication via requireAuth middleware | ||||
| // - Uses project scope for file access permissions | ||||
| // - file:read permission required | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import { requireAuth } from "../middleware/auth.js"; | ||||
| import { sequelize } from "../db/sequelize.js"; | ||||
| import FileModel from "../db/models/FileObject.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const Files = FileModel(sequelize); | ||||
|  | ||||
| async function listBy(mod, ref_id) { | ||||
|   return Files.findAll({ where: { module: mod, ref_id }, order:[['created_at','DESC']] }); | ||||
|   return Files.findAll({ | ||||
|     where: { module: mod, ref_id }, | ||||
|     order: [["created_at", "DESC"]], | ||||
|   }); | ||||
| } | ||||
|  | ||||
| for (const mod of ['rfa','correspondence','drawing','document','transmittal']) { | ||||
| for (const mod of [ | ||||
|   "rfa", | ||||
|   "correspondence", | ||||
|   "drawing", | ||||
|   "document", | ||||
|   "transmittal", | ||||
| ]) { | ||||
|   r.get(`/${mod}s/:id/files`, requireAuth, async (req, res) => { | ||||
|     const items = await listBy(mod, Number(req.params.id)); | ||||
|     res.json(items); | ||||
|   | ||||
| @@ -1,36 +1,71 @@ | ||||
| // src/routes/map.js | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import { buildScopeWhere, ownerResolvers } from '../utils/scope.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/maps.js | ||||
| // Maps routes | ||||
| // - Manage relationships between RFAs and Drawings, Correspondences and Documents | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses project scope for RFA-Drawing maps and Correspondence-Document maps | ||||
| // - rfa:update for RFA-Drawing maps | ||||
| // - correspondence:update for Correspondence-Document maps | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import { buildScopeWhere, ownerResolvers } from "../utils/scope.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const OWN = ownerResolvers(sql, 'entity_maps', 'id'); | ||||
| const OWN = ownerResolvers(sql, "entity_maps", "id"); | ||||
|  | ||||
| // LIST | ||||
| r.get('/', | ||||
|   requirePerm(PERM.map.read, { scope: 'global' }), | ||||
| r.get( | ||||
|   "/", | ||||
|   requirePerm(PERM.map.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const { project_id, org_id, module, src_type, dst_type, limit = 100, offset = 0 } = req.query; | ||||
|     const { | ||||
|       project_id, | ||||
|       org_id, | ||||
|       module, | ||||
|       src_type, | ||||
|       dst_type, | ||||
|       limit = 100, | ||||
|       offset = 0, | ||||
|     } = req.query; | ||||
|  | ||||
|     const base = buildScopeWhere(req.principal, { | ||||
|       tableAlias: 'm', | ||||
|       orgColumn: 'm.org_id', | ||||
|       projectColumn: 'm.project_id', | ||||
|       tableAlias: "m", | ||||
|       orgColumn: "m.org_id", | ||||
|       projectColumn: "m.project_id", | ||||
|       permCode: PERM.map.read, | ||||
|       preferProject: true, | ||||
|     }); | ||||
|  | ||||
|     const extra = []; | ||||
|     const params = { ...base.params, limit: Number(limit), offset: Number(offset) }; | ||||
|     if (project_id) { extra.push('m.project_id = :project_id'); params.project_id = Number(project_id); } | ||||
|     if (org_id)     { extra.push('m.org_id = :org_id');         params.org_id = Number(org_id); } | ||||
|     if (module)     { extra.push('m.module = :module');         params.module = module; } | ||||
|     if (src_type)   { extra.push('m.src_type = :src_type');     params.src_type = src_type; } | ||||
|     if (dst_type)   { extra.push('m.dst_type = :dst_type');     params.dst_type = dst_type; } | ||||
|     const params = { | ||||
|       ...base.params, | ||||
|       limit: Number(limit), | ||||
|       offset: Number(offset), | ||||
|     }; | ||||
|     if (project_id) { | ||||
|       extra.push("m.project_id = :project_id"); | ||||
|       params.project_id = Number(project_id); | ||||
|     } | ||||
|     if (org_id) { | ||||
|       extra.push("m.org_id = :org_id"); | ||||
|       params.org_id = Number(org_id); | ||||
|     } | ||||
|     if (module) { | ||||
|       extra.push("m.module = :module"); | ||||
|       params.module = module; | ||||
|     } | ||||
|     if (src_type) { | ||||
|       extra.push("m.src_type = :src_type"); | ||||
|       params.src_type = src_type; | ||||
|     } | ||||
|     if (dst_type) { | ||||
|       extra.push("m.dst_type = :dst_type"); | ||||
|       params.dst_type = dst_type; | ||||
|     } | ||||
|  | ||||
|     const where = [base.where, ...extra].filter(Boolean).join(' AND '); | ||||
|     const where = [base.where, ...extra].filter(Boolean).join(" AND "); | ||||
|     const [rows] = await sql.query( | ||||
|       `SELECT m.* FROM entity_maps m | ||||
|        WHERE ${where} | ||||
| @@ -42,25 +77,49 @@ r.get('/', | ||||
| ); | ||||
|  | ||||
| // CREATE | ||||
| r.post('/', | ||||
|   requirePerm(PERM.map.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }), | ||||
| r.post( | ||||
|   "/", | ||||
|   requirePerm(PERM.map.create, { | ||||
|     scope: "org", | ||||
|     getOrgId: async (req) => req.body?.org_id ?? null, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const { org_id, project_id, module, src_type, src_id, dst_type, dst_id, remark } = req.body; | ||||
|     const { | ||||
|       org_id, | ||||
|       project_id, | ||||
|       module, | ||||
|       src_type, | ||||
|       src_id, | ||||
|       dst_type, | ||||
|       dst_id, | ||||
|       remark, | ||||
|     } = req.body; | ||||
|     const [rs] = await sql.query( | ||||
|       `INSERT INTO entity_maps (org_id, project_id, module, src_type, src_id, dst_type, dst_id, remark, created_by) | ||||
|        VALUES (?,?,?,?,?,?,?,?,?)`, | ||||
|       [org_id, project_id, module, src_type, Number(src_id), dst_type, Number(dst_id), remark ?? null, req.principal.userId] | ||||
|       [ | ||||
|         org_id, | ||||
|         project_id, | ||||
|         module, | ||||
|         src_type, | ||||
|         Number(src_id), | ||||
|         dst_type, | ||||
|         Number(dst_id), | ||||
|         remark ?? null, | ||||
|         req.principal.userId, | ||||
|       ] | ||||
|     ); | ||||
|     res.json({ id: rs.insertId }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // DELETE (by id) | ||||
| r.delete('/:id', | ||||
|   requirePerm(PERM.map.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.delete( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.map.delete, { scope: "org", getOrgId: OWN.getOrgIdById }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     await sql.query('DELETE FROM entity_maps WHERE id=?', [id]); | ||||
|     await sql.query("DELETE FROM entity_maps WHERE id=?", [id]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|   | ||||
| @@ -1,28 +1,34 @@ | ||||
| import { Router } from 'express'; | ||||
| import { sequelize } from '../db/sequelize.js'; | ||||
| import fs from 'fs'; | ||||
| import path from 'path'; | ||||
| // FILE: src/routes/ops.js | ||||
| // Ops routes | ||||
| // - GET /ready for readiness check (DB connection) | ||||
| // - GET /live for liveness check | ||||
| // - GET /version to get app version from package.json | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import { sequelize } from "../db/sequelize.js"; | ||||
| import fs from "fs"; | ||||
| import path from "path"; | ||||
|  | ||||
| const r = Router(); | ||||
|  | ||||
| r.get('/ready', async (_req, res) => { | ||||
| r.get("/ready", async (_req, res) => { | ||||
|   try { | ||||
|     await sequelize.query('SELECT 1'); | ||||
|     await sequelize.query("SELECT 1"); | ||||
|     return res.json({ ready: true }); | ||||
|   } catch { | ||||
|     return res.status(500).json({ ready: false }); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| r.get('/live', (_req, res) => res.json({ live: true })); | ||||
| r.get("/live", (_req, res) => res.json({ live: true })); | ||||
|  | ||||
| r.get('/version', (_req, res) => { | ||||
| r.get("/version", (_req, res) => { | ||||
|   try { | ||||
|     const pkgPath = path.resolve(process.cwd(), 'package.json'); | ||||
|     const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); | ||||
|     const pkgPath = path.resolve(process.cwd(), "package.json"); | ||||
|     const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); | ||||
|     res.json({ name: pkg.name, version: pkg.version }); | ||||
|   } catch { | ||||
|     res.json({ name: 'dms-backend', version: 'unknown' }); | ||||
|     res.json({ name: "dms-backend", version: "unknown" }); | ||||
|   } | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -1,18 +1,32 @@ | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import { buildScopeWhere } from '../utils/scope.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/organizations.js | ||||
| // 03.2 5) เพิ่ม routes/organizations.js (ใหม่) | ||||
| // - ใช้ร่วมกับ requirePerm() และ buildScopeWhere() | ||||
| // - สำหรับจัดการ organizations (ดู/เพิ่ม/แก้ไข/ลบ) ตามสิทธิ์ของผู้ใช้ | ||||
| // Organizations routes | ||||
| // - CRUD operations for organizations | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses org scope for all permissions | ||||
| // - organization:read, organization:create, organization:update, organization:delete | ||||
| // - Organization fields: org_id (PK), org_name | ||||
| // - Basic validation: org_name required for create | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import { buildScopeWhere } from "../utils/scope.js"; | ||||
|  | ||||
| const r = Router(); | ||||
|  | ||||
| // LIST (org) – ดูเฉพาะ org ใน scope | ||||
| r.get('/', | ||||
|   requirePerm('organization.read', { scope: 'global' }), | ||||
| r.get( | ||||
|   "/", | ||||
|   requirePerm("organization.read", { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const { where, params } = buildScopeWhere(req.principal, { | ||||
|       tableAlias: 'o', orgColumn: 'o.org_id', projectColumn: 'NULL', | ||||
|       permCode: 'organization.read', | ||||
|       tableAlias: "o", | ||||
|       orgColumn: "o.org_id", | ||||
|       projectColumn: "NULL", | ||||
|       permCode: "organization.read", | ||||
|     }); | ||||
|  | ||||
|     const [rows] = await sql.query( | ||||
| @@ -24,15 +38,19 @@ r.get('/', | ||||
| ); | ||||
|  | ||||
| // GET by id | ||||
| r.get('/:id', | ||||
|   requirePerm('organization.read', { | ||||
|     scope: 'org', | ||||
|     getOrgId: async req => Number(req.params.id), | ||||
| r.get( | ||||
|   "/:id", | ||||
|   requirePerm("organization.read", { | ||||
|     scope: "org", | ||||
|     getOrgId: async (req) => Number(req.params.id), | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     const [[row]] = await sql.query('SELECT * FROM organizations WHERE org_id=?', [id]); | ||||
|     if (!row) return res.status(404).json({ error: 'Not found' }); | ||||
|     const [[row]] = await sql.query( | ||||
|       "SELECT * FROM organizations WHERE org_id=?", | ||||
|       [id] | ||||
|     ); | ||||
|     if (!row) return res.status(404).json({ error: "Not found" }); | ||||
|     res.json(row); | ||||
|   } | ||||
| ); | ||||
|   | ||||
| @@ -1,14 +1,25 @@ | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/permissions.js | ||||
| // 03.2 12) เพิ่ม routes/permissions.js (ใหม่) | ||||
| // - ใช้ร่วมกับ requirePerm() | ||||
| // - สำหรับดูรายชื่อสิทธิ์ทั้งหมด | ||||
| // Permissions route | ||||
| // - GET /api/permissions to list all permissions (permission_id, permission_code, description) | ||||
| // - Requires global permission.read permission via requirePerm middleware | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
|  | ||||
| r.get('/', | ||||
|   requirePerm('permission.read', { scope: 'global' }), | ||||
| r.get( | ||||
|   "/", | ||||
|   requirePerm("permission.read", { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const [rows] = await sql.query('SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code'); | ||||
|     const [rows] = await sql.query( | ||||
|       "SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code" | ||||
|     ); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|   | ||||
| @@ -1,18 +1,34 @@ | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import { buildScopeWhere } from '../utils/scope.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/projects.js | ||||
| // 03.2 6) เพิ่ม routes/projects.js (ใหม่) | ||||
| // - ใช้ร่วมกับ requirePerm() และ buildScopeWhere() | ||||
| // - สำหรับจัดการ projects (ดู/เพิ่ม/แก้ไข/ลบ) ตามสิทธิ์ของผู้ใช้ | ||||
| // Projects routes | ||||
| // - CRUD operations for projects | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses org/project scope for all permissions | ||||
| // - project:read, project:create, project:update, project:delete | ||||
| // - Project fields: project_id (PK), org_id (FK), project_code, project_name | ||||
| // - project_code is unique | ||||
| // - Basic validation: org_id, project_code, project_name required for create | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import { buildScopeWhere } from "../utils/scope.js"; | ||||
|  | ||||
| const r = Router(); | ||||
|  | ||||
| // LIST – จำกัดตาม org/project scope ของผู้ใช้ | ||||
| r.get('/', | ||||
|   requirePerm('project.read', { scope: 'global' }), | ||||
| r.get( | ||||
|   "/", | ||||
|   requirePerm("project.read", { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const { where, params } = buildScopeWhere(req.principal, { | ||||
|       tableAlias: 'p', orgColumn: 'p.org_id', projectColumn: 'p.project_id', | ||||
|       permCode: 'project.read', preferProject: true, | ||||
|       tableAlias: "p", | ||||
|       orgColumn: "p.org_id", | ||||
|       projectColumn: "p.project_id", | ||||
|       permCode: "project.read", | ||||
|       preferProject: true, | ||||
|     }); | ||||
|     const [rows] = await sql.query( | ||||
|       `SELECT p.* FROM projects p WHERE ${where}`, | ||||
| @@ -23,29 +39,34 @@ r.get('/', | ||||
| ); | ||||
|  | ||||
| // GET | ||||
| r.get('/:id', | ||||
|   requirePerm('project.read', { | ||||
|     scope: 'project', | ||||
|     getProjectId: async req => Number(req.params.id), | ||||
| r.get( | ||||
|   "/:id", | ||||
|   requirePerm("project.read", { | ||||
|     scope: "project", | ||||
|     getProjectId: async (req) => Number(req.params.id), | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     const [[row]] = await sql.query('SELECT * FROM projects WHERE project_id=?', [id]); | ||||
|     if (!row) return res.status(404).json({ error: 'Not found' }); | ||||
|     const [[row]] = await sql.query( | ||||
|       "SELECT * FROM projects WHERE project_id=?", | ||||
|       [id] | ||||
|     ); | ||||
|     if (!row) return res.status(404).json({ error: "Not found" }); | ||||
|     res.json(row); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // CREATE | ||||
| r.post('/', | ||||
|   requirePerm('project.create', { | ||||
|     scope: 'org', | ||||
|     getOrgId: async req => req.body?.org_id ?? null, | ||||
| r.post( | ||||
|   "/", | ||||
|   requirePerm("project.create", { | ||||
|     scope: "org", | ||||
|     getOrgId: async (req) => req.body?.org_id ?? null, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const { org_id, project_code, project_name } = req.body; | ||||
|     const [rs] = await sql.query( | ||||
|       'INSERT INTO projects (org_id, project_code, project_name) VALUES (?,?,?)', | ||||
|       "INSERT INTO projects (org_id, project_code, project_name) VALUES (?,?,?)", | ||||
|       [org_id, project_code, project_name] | ||||
|     ); | ||||
|     res.json({ project_id: rs.insertId }); | ||||
| @@ -53,28 +74,33 @@ r.post('/', | ||||
| ); | ||||
|  | ||||
| // UPDATE | ||||
| r.put('/:id', | ||||
|   requirePerm('project.update', { | ||||
|     scope: 'project', | ||||
|     getProjectId: async req => Number(req.params.id), | ||||
| r.put( | ||||
|   "/:id", | ||||
|   requirePerm("project.update", { | ||||
|     scope: "project", | ||||
|     getProjectId: async (req) => Number(req.params.id), | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const { project_name } = req.body; | ||||
|     const id = Number(req.params.id); | ||||
|     await sql.query('UPDATE projects SET project_name=? WHERE project_id=?', [project_name, id]); | ||||
|     await sql.query("UPDATE projects SET project_name=? WHERE project_id=?", [ | ||||
|       project_name, | ||||
|       id, | ||||
|     ]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // DELETE | ||||
| r.delete('/:id', | ||||
|   requirePerm('project.delete', { | ||||
|     scope: 'project', | ||||
|     getProjectId: async req => Number(req.params.id), | ||||
| r.delete( | ||||
|   "/:id", | ||||
|   requirePerm("project.delete", { | ||||
|     scope: "project", | ||||
|     getProjectId: async (req) => Number(req.params.id), | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     await sql.query('DELETE FROM projects WHERE project_id=?', [id]); | ||||
|     await sql.query("DELETE FROM projects WHERE project_id=?", [id]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|   | ||||
| @@ -1,101 +1,136 @@ | ||||
| // src/routes/rbac_admin.js (ESM) | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/rbac_admin.js | ||||
| // RBAC Admin routes | ||||
| // - Manage roles, permissions, user-role assignments | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses global scope for all permissions | ||||
| // - rbac_admin.read, rbac_admin.assign_role, rbac_admin.grant_perm | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
|  | ||||
| /** LIST: roles */ | ||||
| r.get('/roles', | ||||
|   requirePerm(PERM.rbac_admin.read, { scope: 'global' }), | ||||
| r.get( | ||||
|   "/roles", | ||||
|   requirePerm(PERM.rbac_admin.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const [rows] = await sql.query('SELECT role_id, role_code, role_name, description FROM roles ORDER BY role_code'); | ||||
|     const [rows] = await sql.query( | ||||
|       "SELECT role_id, role_code, role_name, description FROM roles ORDER BY role_code" | ||||
|     ); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| /** LIST: permissions */ | ||||
| r.get('/permissions', | ||||
|   requirePerm(PERM.rbac_admin.read, { scope: 'global' }), | ||||
| r.get( | ||||
|   "/permissions", | ||||
|   requirePerm(PERM.rbac_admin.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const [rows] = await sql.query('SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code'); | ||||
|     const [rows] = await sql.query( | ||||
|       "SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code" | ||||
|     ); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| /** LIST: role→permissions */ | ||||
| r.get('/roles/:role_id/permissions', | ||||
|   requirePerm(PERM.rbac_admin.read, { scope: 'global' }), | ||||
| r.get( | ||||
|   "/roles/:role_id/permissions", | ||||
|   requirePerm(PERM.rbac_admin.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const role_id = Number(req.params.role_id); | ||||
|     const [rows] = await sql.query( | ||||
|       `SELECT p.permission_id, p.permission_code, p.description | ||||
|        FROM role_permissions rp | ||||
|        JOIN permissions p ON p.permission_id = rp.permission_id | ||||
|        WHERE rp.role_id=? ORDER BY p.permission_code`, [role_id]); | ||||
|        WHERE rp.role_id=? ORDER BY p.permission_code`, | ||||
|       [role_id] | ||||
|     ); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| /** MAP: role↔permission (grant/revoke) */ | ||||
| r.post('/roles/:role_id/permissions', | ||||
|   requirePerm(PERM.rbac_admin.grant_perm, { scope: 'global' }), | ||||
| r.post( | ||||
|   "/roles/:role_id/permissions", | ||||
|   requirePerm(PERM.rbac_admin.grant_perm, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const role_id = Number(req.params.role_id); | ||||
|     const { permission_id } = req.body || {}; | ||||
|     await sql.query('INSERT IGNORE INTO role_permissions (role_id, permission_id) VALUES (?,?)', | ||||
|       [role_id, Number(permission_id)]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| r.delete('/roles/:role_id/permissions/:permission_id', | ||||
|   requirePerm(PERM.rbac_admin.grant_perm, { scope: 'global' }), | ||||
|   async (req, res) => { | ||||
|     const role_id = Number(req.params.role_id); | ||||
|     const permission_id = Number(req.params.permission_id); | ||||
|     await sql.query('DELETE FROM role_permissions WHERE role_id=? AND permission_id=?', [role_id, permission_id]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| /** LIST: user→roles(+scope) */ | ||||
| r.get('/users/:user_id/roles', | ||||
|   requirePerm(PERM.rbac_admin.read, { scope: 'global' }), | ||||
|   async (req, res) => { | ||||
|     const user_id = Number(req.params.user_id); | ||||
|     const [rows] = await sql.query( | ||||
|       `SELECT ur.user_id, ur.role_id, 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=? ORDER BY r.role_code`, [user_id]); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| /** MAP: user↔role(+scope)  (assign / revoke) */ | ||||
| r.post('/users/:user_id/roles', | ||||
|   requirePerm(PERM.rbac_admin.assign_role, { scope: 'global' }), | ||||
|   async (req, res) => { | ||||
|     const user_id = Number(req.params.user_id); | ||||
|     const { role_id, org_id = null, project_id = null } = req.body || {}; | ||||
|     await sql.query( | ||||
|       'INSERT INTO user_roles (user_id, role_id, org_id, project_id) VALUES (?,?,?,?)', | ||||
|       [user_id, Number(role_id), org_id ? Number(org_id) : null, project_id ? Number(project_id) : null] | ||||
|       "INSERT IGNORE INTO role_permissions (role_id, permission_id) VALUES (?,?)", | ||||
|       [role_id, Number(permission_id)] | ||||
|     ); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| r.delete('/users/:user_id/roles', | ||||
|   requirePerm(PERM.rbac_admin.assign_role, { scope: 'global' }), | ||||
| r.delete( | ||||
|   "/roles/:role_id/permissions/:permission_id", | ||||
|   requirePerm(PERM.rbac_admin.grant_perm, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const role_id = Number(req.params.role_id); | ||||
|     const permission_id = Number(req.params.permission_id); | ||||
|     await sql.query( | ||||
|       "DELETE FROM role_permissions WHERE role_id=? AND permission_id=?", | ||||
|       [role_id, permission_id] | ||||
|     ); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| /** LIST: user→roles(+scope) */ | ||||
| r.get( | ||||
|   "/users/:user_id/roles", | ||||
|   requirePerm(PERM.rbac_admin.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const user_id = Number(req.params.user_id); | ||||
|     const [rows] = await sql.query( | ||||
|       `SELECT ur.user_id, ur.role_id, 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=? ORDER BY r.role_code`, | ||||
|       [user_id] | ||||
|     ); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| /** MAP: user↔role(+scope)  (assign / revoke) */ | ||||
| r.post( | ||||
|   "/users/:user_id/roles", | ||||
|   requirePerm(PERM.rbac_admin.assign_role, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const user_id = Number(req.params.user_id); | ||||
|     const { role_id, org_id = null, project_id = null } = req.body || {}; | ||||
|     await sql.query( | ||||
|       'DELETE FROM user_roles WHERE user_id=? AND role_id=? AND <=> org_id ? AND <=> project_id ?' | ||||
|       .replace('<=> org_id ?', (org_id === null ? 'org_id IS ?' : 'org_id=?')) | ||||
|       .replace('<=> project_id ?', (project_id === null ? 'project_id IS ?' : 'project_id=?')), | ||||
|       "INSERT INTO user_roles (user_id, role_id, org_id, project_id) VALUES (?,?,?,?)", | ||||
|       [ | ||||
|         user_id, | ||||
|         Number(role_id), | ||||
|         org_id ? Number(org_id) : null, | ||||
|         project_id ? Number(project_id) : null, | ||||
|       ] | ||||
|     ); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| r.delete( | ||||
|   "/users/:user_id/roles", | ||||
|   requirePerm(PERM.rbac_admin.assign_role, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const user_id = Number(req.params.user_id); | ||||
|     const { role_id, org_id = null, project_id = null } = req.body || {}; | ||||
|     await sql.query( | ||||
|       "DELETE FROM user_roles WHERE user_id=? AND role_id=? AND <=> org_id ? AND <=> project_id ?" | ||||
|         .replace("<=> org_id ?", org_id === null ? "org_id IS ?" : "org_id=?") | ||||
|         .replace( | ||||
|           "<=> project_id ?", | ||||
|           project_id === null ? "project_id IS ?" : "project_id=?" | ||||
|         ), | ||||
|       [user_id, Number(role_id), org_id, project_id] | ||||
|     ); | ||||
|     res.json({ ok: 1 }); | ||||
|   | ||||
| @@ -1,34 +1,74 @@ | ||||
| import { Router } from 'express'; | ||||
| import { requireAuth } from '../middleware/auth.js'; | ||||
| import { requirePermission } from '../middleware/perm.js'; | ||||
| import { callProc } from '../db/index.js'; | ||||
| // FILE: src/routes/rfa.js | ||||
| // RFA routes | ||||
| // - POST /create to create a new RFA with optional associated item documents | ||||
| // - POST /update-status to update the status of an existing RFA | ||||
| // - Requires authentication and appropriate permissions via requireAuth and requirePermission middleware | ||||
| // - Uses project scope for permissions | ||||
| // - RFA_CREATE permission required for creating RFAs | ||||
| // - RFA_STATUS_UPDATE permission required for updating RFA status | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import { requireAuth } from "../middleware/auth.js"; | ||||
| import { requirePermission } from "../middleware/perm.js"; | ||||
| import { callProc } from "../db/index.js"; | ||||
|  | ||||
| const router = Router(); | ||||
|  | ||||
| router.post('/create', | ||||
| router.post( | ||||
|   "/create", | ||||
|   requireAuth, | ||||
|   requirePermission(['RFA_CREATE'], { projectRequired: true }), | ||||
|   requirePermission(["RFA_CREATE"], { projectRequired: true }), | ||||
|   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 || {}; | ||||
|       const { | ||||
|         project_id, | ||||
|         cor_status_id, | ||||
|         cor_no, | ||||
|         title, | ||||
|         originator_id, | ||||
|         recipient_id, | ||||
|         keywords = null, | ||||
|         pdf_path = null, | ||||
|         item_doc_ids = [], | ||||
|       } = req.body || {}; | ||||
|       const json = JSON.stringify(item_doc_ids.map(Number)); | ||||
|       await callProc('sp_rfa_create_with_items', [ | ||||
|         req.user.user_id, project_id, cor_status_id, cor_no, title, originator_id, recipient_id, keywords, pdf_path, json, null | ||||
|       await callProc("sp_rfa_create_with_items", [ | ||||
|         req.user.user_id, | ||||
|         project_id, | ||||
|         cor_status_id, | ||||
|         cor_no, | ||||
|         title, | ||||
|         originator_id, | ||||
|         recipient_id, | ||||
|         keywords, | ||||
|         pdf_path, | ||||
|         json, | ||||
|         null, | ||||
|       ]); | ||||
|       res.status(201).json({ ok: true }); | ||||
|     } catch (e) { next(e); } | ||||
|     } catch (e) { | ||||
|       next(e); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
|  | ||||
| router.post('/update-status', | ||||
| router.post( | ||||
|   "/update-status", | ||||
|   requireAuth, | ||||
|   requirePermission(['RFA_STATUS_UPDATE'], { projectRequired: true }), | ||||
|   requirePermission(["RFA_STATUS_UPDATE"], { projectRequired: true }), | ||||
|   async (req, res, next) => { | ||||
|     try { | ||||
|       const { rfa_corr_id, status_id, set_issue = 0 } = req.body || {}; | ||||
|       await callProc('sp_rfa_update_status', [req.user.user_id, rfa_corr_id, status_id, set_issue ? 1 : 0]); | ||||
|       await callProc("sp_rfa_update_status", [ | ||||
|         req.user.user_id, | ||||
|         rfa_corr_id, | ||||
|         status_id, | ||||
|         set_issue ? 1 : 0, | ||||
|       ]); | ||||
|       res.json({ ok: true }); | ||||
|     } catch (e) { next(e); } | ||||
|     } catch (e) { | ||||
|       next(e); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
|  | ||||
|   | ||||
| @@ -1,28 +1,43 @@ | ||||
| // backend/src/routes/rfas.js  (merged) | ||||
| // FILE: src/routes/rfas.js | ||||
| // 03.2 8) แก้ไข routes/rfas.js (ใหม่) | ||||
| // - ผสมผสานระหว่าง rfas.js เดิม + ฟีเจอร์ list/sort/paging/overdue จาก rfas-1.js | ||||
| // - ใช้ร่วมกับ requirePerm() และ buildScopeWhere() | ||||
| // - สำหรับจัดการ RFAs (ดู/เพิ่ม/แก้ไข/ลบ) ตามสิทธิ์ของผู้ใช้ | ||||
| // RFAs routes | ||||
| // - Enhanced version of rfas.js with list/sort/paging/overdue from rfas-1.js | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses project scope for rfa.read, org scope for rfa.create/update/delete | ||||
| // - GET /api/rfas for listing with faceted filters, sorting, and paging | ||||
| // - GET /api/rfas/:id for fetching a single RFA | ||||
| // - POST /api/rfas for creating a new RFA | ||||
| // - PUT /api/rfas/:id for updating an existing RFA (full update) | ||||
| // - PATCH /api/rfas/:id for partial updates | ||||
| // - DELETE /api/rfas/:id for deleting an RFA | ||||
|  | ||||
| // Base: rfas.js + enhanced list/sort/paging/overdue from rfas-1.js | ||||
|  | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import { buildScopeWhere, ownerResolvers } from '../utils/scope.js'; | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import { buildScopeWhere, ownerResolvers } from "../utils/scope.js"; | ||||
| // import PERM from '../config/permissions.js'; // ถ้าไม่ได้ใช้ สามารถลบได้ | ||||
|  | ||||
| const r = Router(); | ||||
| const OWN = ownerResolvers(sql, 'rfas', 'id'); | ||||
| const OWN = ownerResolvers(sql, "rfas", "id"); | ||||
|  | ||||
| /* ----------------------------- Utilities ----------------------------- */ | ||||
| // Allow-list สำหรับการ sort ป้องกัน SQL injection | ||||
| const ALLOWED_SORT = new Map([ | ||||
|   ['updated_at', 'updated_at'], | ||||
|   ['due_date', 'due_date'], | ||||
|   ['created_at', 'created_at'], | ||||
|   ['id', 'id'] | ||||
|   ["updated_at", "updated_at"], | ||||
|   ["due_date", "due_date"], | ||||
|   ["created_at", "created_at"], | ||||
|   ["id", "id"], | ||||
| ]); | ||||
|  | ||||
| function parseSort(sort = 'updated_at:desc') { | ||||
|   const [colRaw, dirRaw] = String(sort).split(':'); | ||||
|   const col = ALLOWED_SORT.get(colRaw) || 'updated_at'; | ||||
|   const dir = (dirRaw || 'desc').toLowerCase() === 'asc' ? 'ASC' : 'DESC'; | ||||
| function parseSort(sort = "updated_at:desc") { | ||||
|   const [colRaw, dirRaw] = String(sort).split(":"); | ||||
|   const col = ALLOWED_SORT.get(colRaw) || "updated_at"; | ||||
|   const dir = (dirRaw || "desc").toLowerCase() === "asc" ? "ASC" : "DESC"; | ||||
|   return `\`${col}\` ${dir}`; | ||||
| } | ||||
|  | ||||
| @@ -36,15 +51,29 @@ function parsePaging({ page = 1, pageSize = 20 }) { | ||||
| function buildExtraFilters({ q, status, overdue, project_id, org_id }) { | ||||
|   const parts = []; | ||||
|   const params = {}; | ||||
|   if (project_id) { parts.push('r.project_id = :project_id'); params.project_id = Number(project_id); } | ||||
|   if (org_id)     { parts.push('r.org_id = :org_id');         params.org_id = Number(org_id); } | ||||
|   if (status)     { parts.push('r.status = :status');         params.status = status; } | ||||
|   if (q)          { parts.push('(r.rfa_no LIKE :q OR r.title LIKE :q OR r.code LIKE :q)'); params.q = `%${q}%`; } | ||||
|   if (String(overdue) === '1') { | ||||
|     // overdue: due_date < TODAY และสถานะยังไม่ปิด | ||||
|     parts.push("r.due_date IS NOT NULL AND r.due_date < CURDATE() AND r.status NOT IN ('Closed','Approved')"); | ||||
|   if (project_id) { | ||||
|     parts.push("r.project_id = :project_id"); | ||||
|     params.project_id = Number(project_id); | ||||
|   } | ||||
|   return { where: parts.join(' AND '), params }; | ||||
|   if (org_id) { | ||||
|     parts.push("r.org_id = :org_id"); | ||||
|     params.org_id = Number(org_id); | ||||
|   } | ||||
|   if (status) { | ||||
|     parts.push("r.status = :status"); | ||||
|     params.status = status; | ||||
|   } | ||||
|   if (q) { | ||||
|     parts.push("(r.rfa_no LIKE :q OR r.title LIKE :q OR r.code LIKE :q)"); | ||||
|     params.q = `%${q}%`; | ||||
|   } | ||||
|   if (String(overdue) === "1") { | ||||
|     // overdue: due_date < TODAY และสถานะยังไม่ปิด | ||||
|     parts.push( | ||||
|       "r.due_date IS NOT NULL AND r.due_date < CURDATE() AND r.status NOT IN ('Closed','Approved')" | ||||
|     ); | ||||
|   } | ||||
|   return { where: parts.join(" AND "), params }; | ||||
| } | ||||
|  | ||||
| /* -------------------------------- LIST -------------------------------- | ||||
| @@ -52,63 +81,71 @@ function buildExtraFilters({ q, status, overdue, project_id, org_id }) { | ||||
|  - คง requirePerm แบบ rfas.js (scope:global + project/org scope ผ่าน buildScopeWhere) | ||||
|  - เพิ่ม faceted filters/sort/paging/overdue จาก rfas-1.js | ||||
| ------------------------------------------------------------------------*/ | ||||
| r.get('/', | ||||
|   requirePerm('rfa.read', { scope: 'global' }), | ||||
|   async (req, res) => { | ||||
|     try { | ||||
|       const { q, status, overdue, sort, page, pageSize, project_id, org_id } = req.query; | ||||
|       const orderBy = parseSort(sort); | ||||
|       const { limit, offset, page: p, pageSize: ps } = parsePaging({ page, pageSize }); | ||||
| r.get("/", requirePerm("rfa.read", { scope: "global" }), async (req, res) => { | ||||
|   try { | ||||
|     const { q, status, overdue, sort, page, pageSize, project_id, org_id } = | ||||
|       req.query; | ||||
|     const orderBy = parseSort(sort); | ||||
|     const { | ||||
|       limit, | ||||
|       offset, | ||||
|       page: p, | ||||
|       pageSize: ps, | ||||
|     } = parsePaging({ page, pageSize }); | ||||
|  | ||||
|       // base scope จาก principal (org/project) | ||||
|       const base = buildScopeWhere(req.principal, { | ||||
|         tableAlias: 'r', orgColumn: 'r.org_id', projectColumn: 'r.project_id', | ||||
|         permCode: 'rfa.read', preferProject: true, | ||||
|       }); | ||||
|     // base scope จาก principal (org/project) | ||||
|     const base = buildScopeWhere(req.principal, { | ||||
|       tableAlias: "r", | ||||
|       orgColumn: "r.org_id", | ||||
|       projectColumn: "r.project_id", | ||||
|       permCode: "rfa.read", | ||||
|       preferProject: true, | ||||
|     }); | ||||
|  | ||||
|       // extra filters | ||||
|       const extra = buildExtraFilters({ q, status, overdue, project_id, org_id }); | ||||
|     // extra filters | ||||
|     const extra = buildExtraFilters({ q, status, overdue, project_id, org_id }); | ||||
|  | ||||
|       // รวม where | ||||
|       const where = [base.where, extra.where].filter(Boolean).join(' AND ') || '1=1'; | ||||
|       const params = { ...base.params, ...extra.params, limit, offset }; | ||||
|     // รวม where | ||||
|     const where = | ||||
|       [base.where, extra.where].filter(Boolean).join(" AND ") || "1=1"; | ||||
|     const params = { ...base.params, ...extra.params, limit, offset }; | ||||
|  | ||||
|       // total | ||||
|       const [[{ cnt: total }]] = await sql.query( | ||||
|         `SELECT COUNT(*) AS cnt FROM rfas r WHERE ${where}`, | ||||
|         params | ||||
|       ); | ||||
|     // total | ||||
|     const [[{ cnt: total }]] = await sql.query( | ||||
|       `SELECT COUNT(*) AS cnt FROM rfas r WHERE ${where}`, | ||||
|       params | ||||
|     ); | ||||
|  | ||||
|       // rows | ||||
|       const [rows] = await sql.query( | ||||
|         `SELECT r.id, r.code, r.rfa_no, r.title, r.status, r.discipline, r.due_date, r.owner_id, r.updated_at, r.project_id, r.org_id | ||||
|     // rows | ||||
|     const [rows] = await sql.query( | ||||
|       `SELECT r.id, r.code, r.rfa_no, r.title, r.status, r.discipline, r.due_date, r.owner_id, r.updated_at, r.project_id, r.org_id | ||||
|          FROM rfas r | ||||
|          WHERE ${where} | ||||
|          ORDER BY ${orderBy} | ||||
|          LIMIT :limit OFFSET :offset`, | ||||
|         params | ||||
|       ); | ||||
|       params | ||||
|     ); | ||||
|  | ||||
|       res.json({ data: rows, total: Number(total || 0), page: p, pageSize: ps }); | ||||
|     } catch (e) { | ||||
|       res.status(500).json({ error: e.message || 'rfas/list failed' }); | ||||
|     } | ||||
|     res.json({ data: rows, total: Number(total || 0), page: p, pageSize: ps }); | ||||
|   } catch (e) { | ||||
|     res.status(500).json({ error: e.message || "rfas/list failed" }); | ||||
|   } | ||||
| ); | ||||
| }); | ||||
|  | ||||
| /* ------------------------------- GET ONE ------------------------------ | ||||
| // ยึดรูปแบบตรวจสิทธิ์จาก rfas.js | ||||
| ------------------------------------------------------------------------*/ | ||||
| r.get('/:id', | ||||
|   requirePerm('rfa.read', { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.get( | ||||
|   "/:id", | ||||
|   requirePerm("rfa.read", { scope: "org", getOrgId: OWN.getOrgIdById }), | ||||
|   async (req, res) => { | ||||
|     try { | ||||
|       const id = Number(req.params.id); | ||||
|       const [[row]] = await sql.query('SELECT * FROM rfas WHERE id=?', [id]); | ||||
|       if (!row) return res.status(404).json({ error: 'Not found' }); | ||||
|       const [[row]] = await sql.query("SELECT * FROM rfas WHERE id=?", [id]); | ||||
|       if (!row) return res.status(404).json({ error: "Not found" }); | ||||
|       res.json(row); | ||||
|     } catch (e) { | ||||
|       res.status(500).json({ error: e.message || 'rfas/detail failed' }); | ||||
|       res.status(500).json({ error: e.message || "rfas/detail failed" }); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
| @@ -117,14 +154,19 @@ r.get('/:id', | ||||
| // ยึดรูปแบบฟิลด์ของ rfas.js (org_id, project_id, rfa_no, title, status) | ||||
| // เพิ่ม validation เบื้องต้น (title required) | ||||
| ------------------------------------------------------------------------*/ | ||||
| r.post('/', | ||||
|   requirePerm('rfa.create', { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }), | ||||
| r.post( | ||||
|   "/", | ||||
|   requirePerm("rfa.create", { | ||||
|     scope: "org", | ||||
|     getOrgId: async (req) => req.body?.org_id ?? null, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     try { | ||||
|       const { org_id, project_id, rfa_no, title, status } = req.body || {}; | ||||
|       if (!title?.trim()) return res.status(400).json({ error: 'title is required' }); | ||||
|       if (!title?.trim()) | ||||
|         return res.status(400).json({ error: "title is required" }); | ||||
|  | ||||
|       const st = String(status || '').trim() || 'draft'; | ||||
|       const st = String(status || "").trim() || "draft"; | ||||
|       const [rs] = await sql.query( | ||||
|         `INSERT INTO rfas (org_id, project_id, rfa_no, title, status, created_by, created_at, updated_at) | ||||
|          VALUES (?,?,?,?,?,?,NOW(),NOW())`, | ||||
| @@ -132,7 +174,7 @@ r.post('/', | ||||
|       ); | ||||
|       res.status(201).json({ id: rs.insertId }); | ||||
|     } catch (e) { | ||||
|       res.status(500).json({ error: e.message || 'rfas/create failed' }); | ||||
|       res.status(500).json({ error: e.message || "rfas/create failed" }); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
| @@ -141,64 +183,86 @@ r.post('/', | ||||
| // PUT: รูปแบบเดิมของ rfas.js (อัปเดต title, status) | ||||
| // PATCH: แบบยืดหยุ่น (บางฟิลด์) ผสานแนวทางจาก rfas-1.js | ||||
| ------------------------------------------------------------------------*/ | ||||
| r.put('/:id', | ||||
|   requirePerm('rfa.update', { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.put( | ||||
|   "/:id", | ||||
|   requirePerm("rfa.update", { scope: "org", getOrgId: OWN.getOrgIdById }), | ||||
|   async (req, res) => { | ||||
|     try { | ||||
|       const id = Number(req.params.id); | ||||
|       const { title, status } = req.body || {}; | ||||
|       await sql.query('UPDATE rfas SET title=?, status=?, updated_at=NOW() WHERE id=?', [title, status, id]); | ||||
|       await sql.query( | ||||
|         "UPDATE rfas SET title=?, status=?, updated_at=NOW() WHERE id=?", | ||||
|         [title, status, id] | ||||
|       ); | ||||
|       res.json({ ok: 1, id }); | ||||
|     } catch (e) { | ||||
|       res.status(500).json({ error: e.message || 'rfas/update failed' }); | ||||
|       res.status(500).json({ error: e.message || "rfas/update failed" }); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // PATCH แบบ partial fields | ||||
| r.patch('/:id', | ||||
|   requirePerm('rfa.update', { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.patch( | ||||
|   "/:id", | ||||
|   requirePerm("rfa.update", { scope: "org", getOrgId: OWN.getOrgIdById }), | ||||
|   async (req, res) => { | ||||
|     try { | ||||
|       const id = Number(req.params.id); | ||||
|       const allowed = ['code', 'rfa_no', 'title', 'discipline', 'due_date', 'description', 'status', 'owner_id']; | ||||
|       const allowed = [ | ||||
|         "code", | ||||
|         "rfa_no", | ||||
|         "title", | ||||
|         "discipline", | ||||
|         "due_date", | ||||
|         "description", | ||||
|         "status", | ||||
|         "owner_id", | ||||
|       ]; | ||||
|       const patch = {}; | ||||
|       for (const k of allowed) if (k in req.body) patch[k] = req.body[k]; | ||||
|  | ||||
|       if (Object.keys(patch).length === 0) { | ||||
|         return res.status(400).json({ error: 'no fields to update' }); | ||||
|         return res.status(400).json({ error: "no fields to update" }); | ||||
|       } | ||||
|  | ||||
|       if ('status' in patch) { | ||||
|       if ("status" in patch) { | ||||
|         const s = String(patch.status); | ||||
|         const ok = ['draft','submitted','Pending','Review','Approved','Closed'].includes(s); | ||||
|         if (!ok) return res.status(400).json({ error: 'invalid status' }); | ||||
|         const ok = [ | ||||
|           "draft", | ||||
|           "submitted", | ||||
|           "Pending", | ||||
|           "Review", | ||||
|           "Approved", | ||||
|           "Closed", | ||||
|         ].includes(s); | ||||
|         if (!ok) return res.status(400).json({ error: "invalid status" }); | ||||
|       } | ||||
|  | ||||
|       const sets = Object.keys(patch).map(k => `\`${k}\` = :${k}`); | ||||
|       const sets = Object.keys(patch).map((k) => `\`${k}\` = :${k}`); | ||||
|       patch.id = id; | ||||
|  | ||||
|       await sql.query( | ||||
|         `UPDATE rfas SET ${sets.join(', ')}, updated_at=NOW() WHERE id=:id`, | ||||
|         `UPDATE rfas SET ${sets.join(", ")}, updated_at=NOW() WHERE id=:id`, | ||||
|         patch | ||||
|       ); | ||||
|       res.json({ ok: 1, id }); | ||||
|     } catch (e) { | ||||
|       res.status(500).json({ error: e.message || 'rfas/patch failed' }); | ||||
|       res.status(500).json({ error: e.message || "rfas/patch failed" }); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
|  | ||||
| /* ------------------------------- DELETE ------------------------------- */ | ||||
| r.delete('/:id', | ||||
|   requirePerm('rfa.delete', { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.delete( | ||||
|   "/:id", | ||||
|   requirePerm("rfa.delete", { scope: "org", getOrgId: OWN.getOrgIdById }), | ||||
|   async (req, res) => { | ||||
|     try { | ||||
|       const id = Number(req.params.id); | ||||
|       await sql.query('DELETE FROM rfas WHERE id=?', [id]); | ||||
|       await sql.query("DELETE FROM rfas WHERE id=?", [id]); | ||||
|       res.json({ ok: 1, id }); | ||||
|     } catch (e) { | ||||
|       res.status(500).json({ error: e.message || 'rfas/delete failed' }); | ||||
|       res.status(500).json({ error: e.message || "rfas/delete failed" }); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
|   | ||||
| @@ -1,48 +1,93 @@ | ||||
| import { Router } from 'express'; | ||||
| import { requireAuth } from '../middleware/auth.js'; | ||||
| import { enrichPermissions } from '../middleware/permissions.js'; | ||||
| import { requireRole } from '../middleware/rbac.js'; | ||||
| import { requirePerm } from '../middleware/permGuard.js'; | ||||
| import { sequelize } from '../db/sequelize.js'; | ||||
| import SubCatModel from '../db/models/SubCategory.js'; | ||||
| // FILE: src/routes/subcategories.js | ||||
| // Subcategories routes | ||||
| // - CRUD operations for subcategories | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses project scope for all permissions | ||||
| // - lookup:edit | ||||
| // - Subcategory fields: sub_cat_id (PK), project_id (FK), sub_cat_name, parent_cat_id (FK), code | ||||
| // - Basic validation: project_id, sub_cat_name required for create | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import { requireAuth } from "../middleware/auth.js"; | ||||
| import { enrichPermissions } from "../middleware/permissions.js"; | ||||
| import { requireRole } from "../middleware/rbac.js"; | ||||
| import { requirePerm } from "../middleware/permGuard.js"; | ||||
| import { sequelize } from "../db/sequelize.js"; | ||||
| import SubCatModel from "../db/models/SubCategory.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const SubCat = SubCatModel(sequelize); | ||||
|  | ||||
| r.get('/sub_categories', requireAuth, async (req, res) => { | ||||
|   const { q, project_id, page=1, page_size=50 } = req.query; | ||||
|   const limit = Math.min(Number(page_size)||50, 200); | ||||
|   const offset = (Math.max(Number(page)||1,1)-1) * limit; | ||||
| r.get("/sub_categories", requireAuth, async (req, res) => { | ||||
|   const { q, project_id, page = 1, page_size = 50 } = req.query; | ||||
|   const limit = Math.min(Number(page_size) || 50, 200); | ||||
|   const offset = (Math.max(Number(page) || 1, 1) - 1) * limit; | ||||
|   const where = {}; | ||||
|   if (project_id) where.project_id = project_id; | ||||
|   if (q) where.sub_cat_name = sequelize.where(sequelize.fn('LOWER', sequelize.col('sub_cat_name')), 'LIKE', `%${String(q).toLowerCase()}%`); | ||||
|   const { rows, count } = await SubCat.findAndCountAll({ where, limit, offset, order:[['sub_cat_name','ASC']] }); | ||||
|   if (q) | ||||
|     where.sub_cat_name = sequelize.where( | ||||
|       sequelize.fn("LOWER", sequelize.col("sub_cat_name")), | ||||
|       "LIKE", | ||||
|       `%${String(q).toLowerCase()}%` | ||||
|     ); | ||||
|   const { rows, count } = await SubCat.findAndCountAll({ | ||||
|     where, | ||||
|     limit, | ||||
|     offset, | ||||
|     order: [["sub_cat_name", "ASC"]], | ||||
|   }); | ||||
|   res.json({ items: rows, total: count, page: Number(page), page_size: limit }); | ||||
| }); | ||||
|  | ||||
| r.post('/sub_categories', requireAuth, enrichPermissions(), requirePerm('lookup:edit'), async (req, res) => { | ||||
|   const { project_id, sub_cat_name, parent_cat_id, code } = req.body || {}; | ||||
|   if (!project_id || !sub_cat_name) return res.status(400).json({ error: 'project_id and sub_cat_name required' }); | ||||
|   const created = await SubCat.create({ project_id, sub_cat_name, parent_cat_id, code }); | ||||
|   res.status(201).json({ sub_cat_id: created.sub_cat_id }); | ||||
| }); | ||||
| r.post( | ||||
|   "/sub_categories", | ||||
|   requireAuth, | ||||
|   enrichPermissions(), | ||||
|   requirePerm("lookup:edit"), | ||||
|   async (req, res) => { | ||||
|     const { project_id, sub_cat_name, parent_cat_id, code } = req.body || {}; | ||||
|     if (!project_id || !sub_cat_name) | ||||
|       return res | ||||
|         .status(400) | ||||
|         .json({ error: "project_id and sub_cat_name required" }); | ||||
|     const created = await SubCat.create({ | ||||
|       project_id, | ||||
|       sub_cat_name, | ||||
|       parent_cat_id, | ||||
|       code, | ||||
|     }); | ||||
|     res.status(201).json({ sub_cat_id: created.sub_cat_id }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| r.patch('/sub_categories/:id', requireAuth, enrichPermissions(), requirePerm('lookup:edit'), async (req, res) => { | ||||
|   const row = await SubCat.findByPk(Number(req.params.id)); | ||||
|   if (!row) return res.status(404).json({ error: 'Not found' }); | ||||
|   const { sub_cat_name, parent_cat_id, code } = req.body || {}; | ||||
|   if (sub_cat_name !== undefined) row.sub_cat_name = sub_cat_name; | ||||
|   if (parent_cat_id !== undefined) row.parent_cat_id = parent_cat_id; | ||||
|   if (code !== undefined) row.code = code; | ||||
|   await row.save(); | ||||
|   res.json({ ok: true }); | ||||
| }); | ||||
| r.patch( | ||||
|   "/sub_categories/:id", | ||||
|   requireAuth, | ||||
|   enrichPermissions(), | ||||
|   requirePerm("lookup:edit"), | ||||
|   async (req, res) => { | ||||
|     const row = await SubCat.findByPk(Number(req.params.id)); | ||||
|     if (!row) return res.status(404).json({ error: "Not found" }); | ||||
|     const { sub_cat_name, parent_cat_id, code } = req.body || {}; | ||||
|     if (sub_cat_name !== undefined) row.sub_cat_name = sub_cat_name; | ||||
|     if (parent_cat_id !== undefined) row.parent_cat_id = parent_cat_id; | ||||
|     if (code !== undefined) row.code = code; | ||||
|     await row.save(); | ||||
|     res.json({ ok: true }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| r.delete('/sub_categories/:id', requireAuth, enrichPermissions(), requirePerm('lookup:edit'), async (req, res) => { | ||||
|   const row = await SubCat.findByPk(Number(req.params.id)); | ||||
|   if (!row) return res.status(404).json({ error: 'Not found' }); | ||||
|   await row.destroy(); | ||||
|   res.json({ ok: true }); | ||||
| }); | ||||
| r.delete( | ||||
|   "/sub_categories/:id", | ||||
|   requireAuth, | ||||
|   enrichPermissions(), | ||||
|   requirePerm("lookup:edit"), | ||||
|   async (req, res) => { | ||||
|     const row = await SubCat.findByPk(Number(req.params.id)); | ||||
|     if (!row) return res.status(404).json({ error: "Not found" }); | ||||
|     await row.destroy(); | ||||
|     res.json({ ok: true }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| export default r; | ||||
|   | ||||
| @@ -1,4 +1,36 @@ | ||||
| // src/routes/technicaldocs.js (ESM) | ||||
| // FILE: src/routes/technicaldocs.js | ||||
| // Technical Documents routes | ||||
| // - CRUD operations for technical documents | ||||
| // - Requires appropriate permissions via requirePerm middleware   | ||||
| // - Supports filtering and pagination on list endpoint | ||||
| // - Uses ownerResolvers utility to determine org ownership for permission checks | ||||
| // - Permissions required are defined in config/permissions.js | ||||
| //   - technicaldoc.read | ||||
| //   - technicaldoc.create | ||||
| //   - technicaldoc.update | ||||
| //   - technicaldoc.delete | ||||
| // - Scope can be 'global' (list), 'org' (get/create/update/delete) | ||||
| // - List endpoint supports filtering by project_id, org_id, status, and search query (q) | ||||
| // - Pagination via limit and offset query parameters | ||||
| // - Results ordered by id DESC | ||||
| // - Error handling for not found and no fields to update scenarios | ||||
| // - Uses async/await for asynchronous operations | ||||
| // - SQL queries use parameterized queries to prevent SQL injection | ||||
| // - Responses are in JSON format | ||||
| // - Middleware functions are used for permission checks | ||||
| // - Owner resolvers are used to fetch org_id for specific document ids | ||||
| // - Code is modular and organized for maintainability | ||||
| // - Comments are provided for clarity/documentation   | ||||
| // - Follows best practices for Express.js route handling | ||||
| // - Uses ES6+ features for cleaner code | ||||
| // - Assumes existence of technicaldocs table with appropriate columns | ||||
| // - Assumes existence of users table for created_by field | ||||
| // - Assumes existence of config/permissions.js with defined permission codes | ||||
| // - Assumes existence of utils/scope.js with buildScopeWhere and ownerResolvers functions | ||||
| // - Assumes existence of middleware/requirePerm.js for permission checks  | ||||
| // - Assumes existence of db/index.js for database connection/querying | ||||
| // - Assumes Express.js app is set up to use this router for /api/technicaldocs path | ||||
|  | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
|   | ||||
| @@ -1,32 +1,45 @@ | ||||
| // backend/src/routes/transmittals.js (merged) | ||||
| // FILE: src/routes/transmittals.js | ||||
| // Transmittals routes | ||||
| // - Enhanced version of transmittals.js with list/sort/paging from transmittals-1.js | ||||
| // - Supports GET /transmittals with filtering, sorting, and pagination | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - GET by id, POST (create), PUT (update), PATCH (partial update), DELETE | ||||
| // - RBAC/Scope | ||||
| //   - Global scope: list all transmittals user has access to (project/org scope applied) | ||||
| //   - Org scope: get/create/update/delete transmittals within a specific org | ||||
| // - Permissions required: | ||||
| //   - transmittal.read (global/org) for | ||||
| //     - GET /transmittals (list) | ||||
| //     - GET /transmittals/:id (get by id) | ||||
|  | ||||
| // Base: transmittals.js + list/sort/paging from transmittals-1.js | ||||
| // Notes: | ||||
| // - ยึด RBAC/Scope/Permissions แบบเดิมของ transmittals.js | ||||
| // - Faceted list -> ส่ง meta { data, total, page, pageSize } | ||||
| // - PATCH: อัปเดตบางฟิลด์อย่างปลอดภัย (ไม่บังคับให้มีคอลัมน์ใหม่ใน DB) | ||||
|  | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import { buildScopeWhere, ownerResolvers } from '../utils/scope.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import { buildScopeWhere, ownerResolvers } from "../utils/scope.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const OWN = ownerResolvers(sql, 'transmittals', 'id'); | ||||
| const OWN = ownerResolvers(sql, "transmittals", "id"); | ||||
|  | ||||
| /* ----------------------------- Utilities ----------------------------- */ | ||||
| // จำกัดฟิลด์ที่อนุญาตให้ sort เพื่อลดความเสี่ยง SQLi | ||||
| const ALLOWED_SORT = new Map([ | ||||
|   ['updated_at', 'updated_at'], | ||||
|   ['created_at', 'created_at'], | ||||
|   ['id', 'id'], | ||||
|   ['tr_no', 'tr_no'], | ||||
|   ['subject', 'subject'], | ||||
|   ["updated_at", "updated_at"], | ||||
|   ["created_at", "created_at"], | ||||
|   ["id", "id"], | ||||
|   ["tr_no", "tr_no"], | ||||
|   ["subject", "subject"], | ||||
| ]); | ||||
| function parseSort(sort = 'updated_at:desc') { | ||||
|   const [colRaw, dirRaw] = String(sort).split(':'); | ||||
|   const col = ALLOWED_SORT.get(colRaw) || 'updated_at'; | ||||
|   const dir = (dirRaw || 'desc').toLowerCase() === 'asc' ? 'ASC' : 'DESC'; | ||||
| function parseSort(sort = "updated_at:desc") { | ||||
|   const [colRaw, dirRaw] = String(sort).split(":"); | ||||
|   const col = ALLOWED_SORT.get(colRaw) || "updated_at"; | ||||
|   const dir = (dirRaw || "desc").toLowerCase() === "asc" ? "ASC" : "DESC"; | ||||
|   return `\`${col}\` ${dir}`; | ||||
| } | ||||
| function parsePaging({ page = 1, pageSize = 20 }) { | ||||
| @@ -38,15 +51,24 @@ function parsePaging({ page = 1, pageSize = 20 }) { | ||||
| function buildExtraFilters({ project_id, org_id, tr_no, q }) { | ||||
|   const extra = []; | ||||
|   const params = {}; | ||||
|   if (project_id) { extra.push('t.project_id = :project_id'); params.project_id = Number(project_id); } | ||||
|   if (org_id)     { extra.push('t.org_id = :org_id');         params.org_id = Number(org_id); } | ||||
|   if (tr_no)      { extra.push('t.tr_no = :tr_no');           params.tr_no = tr_no; } | ||||
|   if (project_id) { | ||||
|     extra.push("t.project_id = :project_id"); | ||||
|     params.project_id = Number(project_id); | ||||
|   } | ||||
|   if (org_id) { | ||||
|     extra.push("t.org_id = :org_id"); | ||||
|     params.org_id = Number(org_id); | ||||
|   } | ||||
|   if (tr_no) { | ||||
|     extra.push("t.tr_no = :tr_no"); | ||||
|     params.tr_no = tr_no; | ||||
|   } | ||||
|   if (q) { | ||||
|     // ใช้ฟิลด์พื้นฐานที่ transmittals.js มีแน่นอน (tr_no, subject) | ||||
|     extra.push('(t.tr_no LIKE :q OR t.subject LIKE :q)'); | ||||
|     extra.push("(t.tr_no LIKE :q OR t.subject LIKE :q)"); | ||||
|     params.q = `%${q}%`; | ||||
|   } | ||||
|   return { where: extra.join(' AND '), params }; | ||||
|   return { where: extra.join(" AND "), params }; | ||||
| } | ||||
|  | ||||
| /* -------------------------------- LIST -------------------------------- | ||||
| @@ -54,24 +76,31 @@ GET /transmittals | ||||
| - คง RBAC/Scope เดิม (global + project/org scope ผ่าน buildScopeWhere) | ||||
| - เพิ่ม sort/page/pageSize/q ตามสไตล์ transmittals-1.js และตอบ meta | ||||
| ------------------------------------------------------------------------*/ | ||||
| r.get('/', | ||||
|   requirePerm(PERM.transmittal.read, { scope: 'global' }), | ||||
| r.get( | ||||
|   "/", | ||||
|   requirePerm(PERM.transmittal.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     try { | ||||
|       const { project_id, org_id, tr_no, q, sort, page, pageSize } = req.query; | ||||
|       const orderBy = parseSort(sort); | ||||
|       const { limit, offset, page: p, pageSize: ps } = parsePaging({ page, pageSize }); | ||||
|       const { | ||||
|         limit, | ||||
|         offset, | ||||
|         page: p, | ||||
|         pageSize: ps, | ||||
|       } = parsePaging({ page, pageSize }); | ||||
|  | ||||
|       const base = buildScopeWhere(req.principal, { | ||||
|         tableAlias: 't', | ||||
|         orgColumn: 't.org_id', | ||||
|         projectColumn: 't.project_id', | ||||
|         tableAlias: "t", | ||||
|         orgColumn: "t.org_id", | ||||
|         projectColumn: "t.project_id", | ||||
|         permCode: PERM.transmittal.read, | ||||
|         preferProject: true | ||||
|         preferProject: true, | ||||
|       }); | ||||
|  | ||||
|       const extra = buildExtraFilters({ project_id, org_id, tr_no, q }); | ||||
|       const where = [base.where, extra.where].filter(Boolean).join(' AND ') || '1=1'; | ||||
|       const where = | ||||
|         [base.where, extra.where].filter(Boolean).join(" AND ") || "1=1"; | ||||
|       const params = { ...base.params, ...extra.params, limit, offset }; | ||||
|  | ||||
|       // total | ||||
| @@ -90,31 +119,48 @@ r.get('/', | ||||
|         params | ||||
|       ); | ||||
|  | ||||
|       res.json({ data: rows, total: Number(total || 0), page: p, pageSize: ps }); | ||||
|       res.json({ | ||||
|         data: rows, | ||||
|         total: Number(total || 0), | ||||
|         page: p, | ||||
|         pageSize: ps, | ||||
|       }); | ||||
|     } catch (e) { | ||||
|       res.status(500).json({ error: e.message || 'transmittals/list failed' }); | ||||
|       res.status(500).json({ error: e.message || "transmittals/list failed" }); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
|  | ||||
| /* ------------------------------- GET ONE ------------------------------ */ | ||||
| r.get('/:id', | ||||
|   requirePerm(PERM.transmittal.read, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.get( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.transmittal.read, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     try { | ||||
|       const id = Number(req.params.id); | ||||
|       const [[row]] = await sql.query('SELECT * FROM transmittals WHERE id=?', [id]); | ||||
|       if (!row) return res.status(404).json({ error: 'Not found' }); | ||||
|       const [[row]] = await sql.query("SELECT * FROM transmittals WHERE id=?", [ | ||||
|         id, | ||||
|       ]); | ||||
|       if (!row) return res.status(404).json({ error: "Not found" }); | ||||
|       res.json(row); | ||||
|     } catch (e) { | ||||
|       res.status(500).json({ error: e.message || 'transmittals/detail failed' }); | ||||
|       res | ||||
|         .status(500) | ||||
|         .json({ error: e.message || "transmittals/detail failed" }); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
|  | ||||
| /* -------------------------------- CREATE ------------------------------ */ | ||||
| r.post('/', | ||||
|   requirePerm(PERM.transmittal.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }), | ||||
| r.post( | ||||
|   "/", | ||||
|   requirePerm(PERM.transmittal.create, { | ||||
|     scope: "org", | ||||
|     getOrgId: async (req) => req.body?.org_id ?? null, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     try { | ||||
|       // ยึดสคีมาหลักจาก transmittals.js | ||||
| @@ -126,35 +172,50 @@ r.post('/', | ||||
|       ); | ||||
|       res.status(201).json({ id: rs.insertId }); | ||||
|     } catch (e) { | ||||
|       res.status(500).json({ error: e.message || 'transmittals/create failed' }); | ||||
|       res | ||||
|         .status(500) | ||||
|         .json({ error: e.message || "transmittals/create failed" }); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
|  | ||||
| /* -------------------------------- UPDATE ------------------------------ */ | ||||
| // PUT: รูปแบบเดิม (อัปเดต subject, status) | ||||
| r.put('/:id', | ||||
|   requirePerm(PERM.transmittal.update, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.put( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.transmittal.update, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     try { | ||||
|       const id = Number(req.params.id); | ||||
|       const { subject, status } = req.body; | ||||
|       await sql.query('UPDATE transmittals SET subject=?, status=? WHERE id=?', [subject, status, id]); | ||||
|       await sql.query( | ||||
|         "UPDATE transmittals SET subject=?, status=? WHERE id=?", | ||||
|         [subject, status, id] | ||||
|       ); | ||||
|       res.json({ ok: 1 }); | ||||
|     } catch (e) { | ||||
|       res.status(500).json({ error: e.message || 'transmittals/update failed' }); | ||||
|       res | ||||
|         .status(500) | ||||
|         .json({ error: e.message || "transmittals/update failed" }); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // PATCH: อัปเดตบางฟิลด์ (ไม่บังคับคอลัมน์ใหม่ใน DB — จะอัปเดตก็ต่อเมื่อ body ส่งมา) | ||||
| r.patch('/:id', | ||||
|   requirePerm(PERM.transmittal.update, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.patch( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.transmittal.update, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     try { | ||||
|       const id = Number(req.params.id); | ||||
|       // อนุญาตเฉพาะฟิลด์ที่คาดว่ามีในสคีมาหลัก | ||||
|       const allowed = ['tr_no', 'subject', 'status']; | ||||
|       const allowed = ["tr_no", "subject", "status"]; | ||||
|  | ||||
|       // ถ้าฐานข้อมูลของคุณมีคอลัมน์เพิ่ม เช่น to_party, sent_date, description | ||||
|       // และต้องการอัปเดตด้วย ให้เพิ่มชื่อคอลัมน์เข้าไปใน allowed ได้ | ||||
| @@ -164,39 +225,55 @@ r.patch('/:id', | ||||
|       for (const k of allowed) if (k in req.body) patch[k] = req.body[k]; | ||||
|  | ||||
|       if (Object.keys(patch).length === 0) { | ||||
|         return res.status(400).json({ error: 'no fields to update' }); | ||||
|         return res.status(400).json({ error: "no fields to update" }); | ||||
|       } | ||||
|  | ||||
|       if ('status' in patch) { | ||||
|       if ("status" in patch) { | ||||
|         const s = String(patch.status); | ||||
|         const ok = ['draft','submitted','Sent','Closed','Approved','Pending','Review'].includes(s); | ||||
|         if (!ok) return res.status(400).json({ error: 'invalid status' }); | ||||
|         const ok = [ | ||||
|           "draft", | ||||
|           "submitted", | ||||
|           "Sent", | ||||
|           "Closed", | ||||
|           "Approved", | ||||
|           "Pending", | ||||
|           "Review", | ||||
|         ].includes(s); | ||||
|         if (!ok) return res.status(400).json({ error: "invalid status" }); | ||||
|       } | ||||
|  | ||||
|       const sets = Object.keys(patch).map(k => `\`${k}\` = :${k}`); | ||||
|       const sets = Object.keys(patch).map((k) => `\`${k}\` = :${k}`); | ||||
|       patch.id = id; | ||||
|  | ||||
|       await sql.query( | ||||
|         `UPDATE transmittals SET ${sets.join(', ')}, updated_at = NOW() WHERE id = :id`, | ||||
|         `UPDATE transmittals SET ${sets.join( | ||||
|           ", " | ||||
|         )}, updated_at = NOW() WHERE id = :id`, | ||||
|         patch | ||||
|       ); | ||||
|       res.json({ ok: 1, id }); | ||||
|     } catch (e) { | ||||
|       res.status(500).json({ error: e.message || 'transmittals/patch failed' }); | ||||
|       res.status(500).json({ error: e.message || "transmittals/patch failed" }); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
|  | ||||
| /* -------------------------------- DELETE ------------------------------ */ | ||||
| r.delete('/:id', | ||||
|   requirePerm(PERM.transmittal.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.delete( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.transmittal.delete, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     try { | ||||
|       const id = Number(req.params.id); | ||||
|       await sql.query('DELETE FROM transmittals WHERE id=?', [id]); | ||||
|       await sql.query("DELETE FROM transmittals WHERE id=?", [id]); | ||||
|       res.json({ ok: 1 }); | ||||
|     } catch (e) { | ||||
|       res.status(500).json({ error: e.message || 'transmittals/delete failed' }); | ||||
|       res | ||||
|         .status(500) | ||||
|         .json({ error: e.message || "transmittals/delete failed" }); | ||||
|     } | ||||
|   } | ||||
| ); | ||||
|   | ||||
| @@ -1,67 +1,125 @@ | ||||
| import { Router } from 'express'; | ||||
| import multer from 'multer'; | ||||
| import fs from 'node:fs'; | ||||
| import path from 'node:path'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/uploads.js | ||||
| // 03.2 10) เพิ่ม routes/uploads.js (ใหม่) | ||||
| // - ใช้ร่วมกับ requirePerm() | ||||
| // - สำหรับอัพโหลดไฟล์แนบที่เกี่ยวข้องกับ module item ต่างๆ (เช่น correspondence, rfa, drawing) ตามสิทธิ์ของผู้ใช้ | ||||
| // Uploads routes | ||||
| // - POST /:module/:id/file to upload a file associated with a module item (e.g. correspondence, rfa, drawing) | ||||
| // - Uses multer for file handling | ||||
| // - Stores files in structured directories based on org_id, project_id, and creation date | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Supported modules: correspondences | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Permissions are mapped in PERM_UPLOAD | ||||
| // - Ensure req.user.permissions is populated (e.g. via auth.js or authJwt.js with enrichment) | ||||
| // - Requires req.user to have the upload permission for the specific module and project scope | ||||
| // - Example: POST /correspondences/123/file with form-data including 'file' field | ||||
| // - Environment variable UPLOAD_BASE defines the base directory for uploads (default: /share/dms-data) | ||||
| // - Directory structure: UPLOAD_BASE/module/org_id/project_id/YYYY-MM | ||||
| // - Filename format: timestamp__originalname (with unsafe characters replaced by '_') | ||||
| // - Response: { ok: 1, module, ref_id, filename, path, size, mime } | ||||
| // - Assumes existence of necessary database tables and columns | ||||
| // - Assumes existence of necessary middleware and utility functions | ||||
| // - Assumes Express.js app is set up to use this router for /api/uploads path | ||||
| // - Assumes existence of necessary environment variables | ||||
| // - Assumes existence of necessary directories and permissions for file storage | ||||
| // - Assumes multer is installed and configured | ||||
| // - Assumes fs and path modules are available for file system operations | ||||
| // - Assumes sql module is set up for database interactions | ||||
| // - Assumes PERM constants are defined in config/permissions.js | ||||
| // - Assumes requirePerm middleware is defined in middleware/requirePerm.js | ||||
| // - Assumes Express.js app is set up to use this router for /api/uploads path | ||||
| // - Assumes multer is installed and configured | ||||
| // - Assumes fs and path modules are available for file system operations | ||||
| // - Assumes sql module is set up for database interactions | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import multer from "multer"; | ||||
| import fs from "node:fs"; | ||||
| import path from "node:path"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const UPLOAD_BASE = process.env.UPLOAD_BASE || '/share/dms-data'; | ||||
| function ensureDir(p) { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); } | ||||
|  | ||||
| const UPLOAD_BASE = process.env.UPLOAD_BASE || "/share/dms-data"; | ||||
| function ensureDir(p) { | ||||
|   if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); | ||||
| } | ||||
|  | ||||
| const storage = multer.diskStorage({ | ||||
| destination: async (req, file, cb) => { | ||||
| try { | ||||
| const { module, id } = req.params; | ||||
| const [[row]] = await sql.query(`SELECT org_id, project_id, created_at FROM ${module} WHERE id=?`, [Number(id)]); | ||||
| if (!row) return cb(new Error('Resource not found')); | ||||
| const dt = new Date(row.created_at || Date.now()); | ||||
| const ym = `${dt.getUTCFullYear()}-${String(dt.getUTCMonth()+1).padStart(2,'0')}`; | ||||
| const dir = path.join(UPLOAD_BASE, module, String(row.org_id), String(row.project_id), ym); | ||||
| ensureDir(dir); | ||||
| cb(null, dir); | ||||
| } catch (e) { cb(e); } | ||||
| }, | ||||
| filename: (req, file, cb) => { | ||||
| const ts = Date.now(); | ||||
| const safe = file.originalname.replace(/[\^\w.\-]+/g, '_'); | ||||
| cb(null, `${ts}__${safe}`); | ||||
| } | ||||
|   destination: async (req, file, cb) => { | ||||
|     try { | ||||
|       const { module, id } = req.params; | ||||
|       const [[row]] = await sql.query( | ||||
|         `SELECT org_id, project_id, created_at FROM ${module} WHERE id=?`, | ||||
|         [Number(id)] | ||||
|       ); | ||||
|       if (!row) return cb(new Error("Resource not found")); | ||||
|       const dt = new Date(row.created_at || Date.now()); | ||||
|       const ym = `${dt.getUTCFullYear()}-${String( | ||||
|         dt.getUTCMonth() + 1 | ||||
|       ).padStart(2, "0")}`; | ||||
|       const dir = path.join( | ||||
|         UPLOAD_BASE, | ||||
|         module, | ||||
|         String(row.org_id), | ||||
|         String(row.project_id), | ||||
|         ym | ||||
|       ); | ||||
|       ensureDir(dir); | ||||
|       cb(null, dir); | ||||
|     } catch (e) { | ||||
|       cb(e); | ||||
|     } | ||||
|   }, | ||||
|   filename: (req, file, cb) => { | ||||
|     const ts = Date.now(); | ||||
|     const safe = file.originalname.replace(/[\^\w.\-]+/g, "_"); | ||||
|     cb(null, `${ts}__${safe}`); | ||||
|   }, | ||||
| }); | ||||
| const upload = multer({ storage }); | ||||
|  | ||||
|  | ||||
| const PERM_UPLOAD = { | ||||
| correspondences: PERM.correspondence.upload, | ||||
| rfas: PERM.rfa.upload, | ||||
| drawings: PERM.drawing.upload, | ||||
| transmittals: PERM.transmittal?.upload, | ||||
|   correspondences: PERM.correspondence.upload, | ||||
|   rfas: PERM.rfa.upload, | ||||
|   drawings: PERM.drawing.upload, | ||||
|   transmittals: PERM.transmittal?.upload, | ||||
| }; | ||||
|  | ||||
|  | ||||
| async function getProjectIdByModule(req){ | ||||
| const { module, id } = req.params; | ||||
| const [[row]] = await sql.query(`SELECT project_id FROM ${module} WHERE id=?`, [Number(id)]); | ||||
| return row?.project_id ?? null; | ||||
| async function getProjectIdByModule(req) { | ||||
|   const { module, id } = req.params; | ||||
|   const [[row]] = await sql.query( | ||||
|     `SELECT project_id FROM ${module} WHERE id=?`, | ||||
|     [Number(id)] | ||||
|   ); | ||||
|   return row?.project_id ?? null; | ||||
| } | ||||
|  | ||||
|  | ||||
| r.post('/:module/:id/file', | ||||
| (req, res, next) => { | ||||
| const perm = PERM_UPLOAD[req.params.module]; | ||||
| if (!perm) return res.status(400).json({ error: 'Unsupported module' }); | ||||
| return requirePerm(perm, { scope: 'project', getProjectId: getProjectIdByModule })(req, res, next); | ||||
| }, | ||||
| upload.single('file'), | ||||
| async (req, res) => { | ||||
| const { module, id } = req.params; | ||||
| const file = req.file; | ||||
| res.json({ ok: 1, module, ref_id: Number(id), filename: file.filename, path: file.path, size: file.size, mime: file.mimetype }); | ||||
| } | ||||
| r.post( | ||||
|   "/:module/:id/file", | ||||
|   (req, res, next) => { | ||||
|     const perm = PERM_UPLOAD[req.params.module]; | ||||
|     if (!perm) return res.status(400).json({ error: "Unsupported module" }); | ||||
|     return requirePerm(perm, { | ||||
|       scope: "project", | ||||
|       getProjectId: getProjectIdByModule, | ||||
|     })(req, res, next); | ||||
|   }, | ||||
|   upload.single("file"), | ||||
|   async (req, res) => { | ||||
|     const { module, id } = req.params; | ||||
|     const file = req.file; | ||||
|     res.json({ | ||||
|       ok: 1, | ||||
|       module, | ||||
|       ref_id: Number(id), | ||||
|       filename: file.filename, | ||||
|       path: file.path, | ||||
|       size: file.size, | ||||
|       mime: file.mimetype, | ||||
|     }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| export default r; | ||||
| export default r; | ||||
|   | ||||
| @@ -1,32 +1,51 @@ | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/users.js | ||||
| // 03.2 11) เพิ่ม routes/users.js (ใหม่) | ||||
| // - ใช้ร่วมกับ requirePerm() | ||||
| // - สำหรับดูข้อมูลผู้ใช้ตัวเอง และรายชื่อผู้ใช้ (สำหรับ SUPER_ADMIN หรือ ADMIN เท่านั้น) | ||||
| // Users routes | ||||
| // - GET /me to get current user info and roles | ||||
| // - GET /api/users to list users (for SUPER_ADMIN or ADMIN only) | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses req.principal loaded by loadPrincipal middleware | ||||
| //   (make sure to use loadPrincipalMw() in app.js or the parent router) | ||||
| //   (e.g. app.use('/api', requireAuth(), enrichPermissions(), loadPrincipalMw(), apiRouter);) | ||||
| // - req.principal has { userId, roleIds, roleCodes, permissions } | ||||
| //   (see utils/rbac.js for details) | ||||
| // - Uses Sequelize ORM for DB access | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
|  | ||||
| // ME | ||||
| r.get('/me', async (req, res) => { | ||||
|   const [[u]] = await sql.query('SELECT user_id, username, email, first_name, last_name FROM users WHERE user_id=?', | ||||
|     [req.principal.userId]); | ||||
|   if (!u) return res.status(404).json({ error: 'User not found' }); | ||||
| r.get("/me", async (req, res) => { | ||||
|   const [[u]] = await sql.query( | ||||
|     "SELECT user_id, username, email, first_name, last_name FROM users WHERE user_id=?", | ||||
|     [req.principal.userId] | ||||
|   ); | ||||
|   if (!u) return res.status(404).json({ error: "User not found" }); | ||||
|  | ||||
|   // roles in plain | ||||
|   const [roles] = await sql.query(` | ||||
|   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=?`, [req.principal.userId]); | ||||
|     WHERE ur.user_id=?`, | ||||
|     [req.principal.userId] | ||||
|   ); | ||||
|  | ||||
|   res.json({ ...u, roles, role_codes: [...req.principal.roleCodes] }); | ||||
| }); | ||||
|  | ||||
| // (optional) USERS LIST – ให้เฉพาะ SUPER_ADMIN หรือ ADMIN (ใน org ตัวเอง) | ||||
| r.get('/', | ||||
|   requirePerm('user.read', { scope: 'global' }), | ||||
|   async (req, res) => { | ||||
|     const [rows] = await sql.query('SELECT user_id, username, email FROM users LIMIT 200'); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
| r.get("/", requirePerm("user.read", { scope: "global" }), async (req, res) => { | ||||
|   const [rows] = await sql.query( | ||||
|     "SELECT user_id, username, email FROM users LIMIT 200" | ||||
|   ); | ||||
|   res.json(rows); | ||||
| }); | ||||
|  | ||||
| export default r; | ||||
|   | ||||
| @@ -1,28 +1,51 @@ | ||||
| import { Router } from 'express'; | ||||
| import { requireAuth } from '../middleware/auth.js'; | ||||
| import { requireRole } from '../middleware/rbac.js'; | ||||
| import { User } from '../db/sequelize.js'; | ||||
| import { hashPassword } from '../utils/passwords.js'; | ||||
| import { sequelize } from '../db/sequelize.js'; | ||||
| import UPRModel from '../db/models/UserProjectRole.js'; | ||||
| import ProjectModel from '../db/models/Project.js'; | ||||
| // FILE: src/routes/users_extras.js | ||||
| // Users extra routes | ||||
| // - PATCH /users/:id/password to change user password (self or admin) | ||||
| // - GET /users/search for user search (admin only) | ||||
| // - GET /users/me/projects to list user's projects and roles | ||||
| // - Requires authentication and appropriate permissions/roles | ||||
| //   via requireAuth and requireRole middleware | ||||
| // - Uses Sequelize ORM for DB access | ||||
| // - Passwords are hashed using bcrypt | ||||
| // - UserProjectRole and Project models are used for project-role listing | ||||
| // - Assumes User model is defined in Sequelize setup | ||||
| // - Assumes hashPassword utility function is defined for password hashing | ||||
| // - Assumes requireAuth and requireRole middleware are defined for auth | ||||
| // - Assumes sequelize instance is set up and connected to DB | ||||
| // - Assumes UserProjectRole and Project Sequelize models are defined | ||||
| // - Assumes User Sequelize model is defined | ||||
| // - Assumes hashPassword function is defined in utils/passwords.js | ||||
| // - Assumes requireAuth middleware is defined in middleware/auth.js | ||||
| // - Assumes requireRole middleware is defined in middleware/rbac.js | ||||
| // - Assumes sequelize instance is imported from db/sequelize.js | ||||
| // - Assumes UserProjectRole and Project models are imported from db/models/UserProjectRole.js and db/models/Project.js respectively | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import { requireAuth } from "../middleware/auth.js"; | ||||
| import { requireRole } from "../middleware/rbac.js"; | ||||
| import { User } from "../db/sequelize.js"; | ||||
| import { hashPassword } from "../utils/passwords.js"; | ||||
| import { sequelize } from "../db/sequelize.js"; | ||||
| import UPRModel from "../db/models/UserProjectRole.js"; | ||||
| import ProjectModel from "../db/models/Project.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const UPR = UPRModel(sequelize); | ||||
| const Project = ProjectModel(sequelize); | ||||
|  | ||||
| // self or admin change password | ||||
| r.patch('/users/:id/password', requireAuth, async (req, res) => { | ||||
| r.patch("/users/:id/password", requireAuth, async (req, res) => { | ||||
|   const targetId = Number(req.params.id); | ||||
|   const isSelf = req.user?.user_id === targetId; | ||||
|   const isAdmin = (req.user?.roles || []).includes('Admin'); | ||||
|   if (!isSelf && !isAdmin) return res.status(403).json({ error: 'Forbidden' }); | ||||
|   const isAdmin = (req.user?.roles || []).includes("Admin"); | ||||
|   if (!isSelf && !isAdmin) return res.status(403).json({ error: "Forbidden" }); | ||||
|  | ||||
|   const { new_password } = req.body || {}; | ||||
|   if (!new_password) return res.status(400).json({ error: 'new_password required' }); | ||||
|   if (!new_password) | ||||
|     return res.status(400).json({ error: "new_password required" }); | ||||
|  | ||||
|   const row = await User.findByPk(targetId); | ||||
|   if (!row) return res.status(404).json({ error: 'Not found' }); | ||||
|   if (!row) return res.status(404).json({ error: "Not found" }); | ||||
|  | ||||
|   row.password_hash = await hashPassword(new_password); | ||||
|   await row.save(); | ||||
| @@ -30,25 +53,40 @@ r.patch('/users/:id/password', requireAuth, async (req, res) => { | ||||
| }); | ||||
|  | ||||
| // user search (autocomplete) | ||||
| r.get('/users/search', requireAuth, requireRole('Admin'), async (req, res) => { | ||||
|   const q = String(req.query.q || '').toLowerCase(); | ||||
|   const where = q ? { | ||||
|     username: sequelize.where(sequelize.fn('LOWER', sequelize.col('username')), 'LIKE', `%${q}%`), | ||||
|   } : {}; | ||||
|   const rows = await User.findAll({ where, limit: 20, order:[['username','ASC']], attributes:['user_id','username','first_name','last_name','email'] }); | ||||
| r.get("/users/search", requireAuth, requireRole("Admin"), async (req, res) => { | ||||
|   const q = String(req.query.q || "").toLowerCase(); | ||||
|   const where = q | ||||
|     ? { | ||||
|         username: sequelize.where( | ||||
|           sequelize.fn("LOWER", sequelize.col("username")), | ||||
|           "LIKE", | ||||
|           `%${q}%` | ||||
|         ), | ||||
|       } | ||||
|     : {}; | ||||
|   const rows = await User.findAll({ | ||||
|     where, | ||||
|     limit: 20, | ||||
|     order: [["username", "ASC"]], | ||||
|     attributes: ["user_id", "username", "first_name", "last_name", "email"], | ||||
|   }); | ||||
|   res.json(rows); | ||||
| }); | ||||
|  | ||||
| // my projects/roles | ||||
| r.get('/users/me/projects', requireAuth, async (req, res) => { | ||||
| r.get("/users/me/projects", requireAuth, async (req, res) => { | ||||
|   const user_id = req.user?.user_id; | ||||
|   if (!user_id) return res.status(401).json({ error: 'Unauthorized' }); | ||||
|   if (!user_id) return res.status(401).json({ error: "Unauthorized" }); | ||||
|   const rows = await UPR.findAll({ where: { user_id } }); | ||||
|   // Optionally join project names | ||||
|   const projectIds = [...new Set(rows.map(r => r.project_id))]; | ||||
|   const projectIds = [...new Set(rows.map((r) => r.project_id))]; | ||||
|   const projects = await Project.findAll({ where: { project_id: projectIds } }); | ||||
|   const map = new Map(projects.map(p => [p.project_id, p.project_name])); | ||||
|   const result = rows.map(r => ({ project_id: r.project_id, role_name: r.role_name, project_name: map.get(r.project_id) || null })); | ||||
|   const map = new Map(projects.map((p) => [p.project_id, p.project_name])); | ||||
|   const result = rows.map((r) => ({ | ||||
|     project_id: r.project_id, | ||||
|     role_name: r.role_name, | ||||
|     project_name: map.get(r.project_id) || null, | ||||
|   })); | ||||
|   res.json(result); | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -1,86 +1,160 @@ | ||||
| // src/routes/view.js | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import { buildScopeWhere, ownerResolvers } from '../utils/scope.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/view.js | ||||
| // Saved Views routes | ||||
| // - CRUD operations for saved views | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Supports filtering and pagination on list endpoint | ||||
| // - Uses ownerResolvers utility to determine org ownership for permission checks | ||||
| // - Permissions required are defined in config/permissions.js | ||||
| //   - savedview.read | ||||
| //   - savedview.create | ||||
| //   - savedview.update | ||||
| //   - savedview.delete | ||||
| // - Scope can be 'global' (list), 'org' (get/create/update/delete) | ||||
| // - List endpoint supports filtering by project_id, org_id, shared flag, and search query (q) | ||||
| // - Pagination via limit and offset query parameters | ||||
| // - Results ordered by id DESC | ||||
| // - Error handling for not found and no fields to update scenarios | ||||
| // - Uses async/await for asynchronous operations | ||||
| // - SQL queries use parameterized queries to prevent SQL injection | ||||
| // - Responses are in JSON format | ||||
| // - Middleware functions are used for permission checks | ||||
| // - Owner resolvers are used to fetch org_id for specific view ids | ||||
| // - Code is modular and organized for maintainability | ||||
| // - Comments are provided for clarity/documentation | ||||
| // - Follows best practices for Express.js route handling | ||||
| // - Uses ES6+ features for cleaner code | ||||
| // - Assumes existence of saved_views table with appropriate columns | ||||
| // - Assumes existence of users table for owner | ||||
| // - Assumes existence of config/permissions.js with defined permission codes | ||||
| // - Assumes existence of utils/scope.js with buildScopeWhere and ownerResolvers functions | ||||
| // - Assumes existence of middleware/requirePerm.js for permission checks | ||||
| // - Assumes existence of db/index.js for database connection/querying | ||||
| // - Assumes Express.js app is set up to use this router for /api/saved_views path | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import { buildScopeWhere, ownerResolvers } from "../utils/scope.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const OWN = ownerResolvers(sql, 'saved_views', 'id'); | ||||
| const OWN = ownerResolvers(sql, "saved_views", "id"); | ||||
|  | ||||
| // LIST: GET /api/view?project_id=&org_id=&shared=1 | ||||
| r.get('/', | ||||
|   requirePerm(PERM.savedview.read, { scope: 'global' }), | ||||
| r.get( | ||||
|   "/", | ||||
|   requirePerm(PERM.savedview.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const { project_id, org_id, shared, q, limit = 50, offset = 0 } = req.query; | ||||
|  | ||||
|     const base = buildScopeWhere(req.principal, { | ||||
|       tableAlias: 'v', | ||||
|       orgColumn: 'v.org_id', | ||||
|       projectColumn: 'v.project_id', | ||||
|       tableAlias: "v", | ||||
|       orgColumn: "v.org_id", | ||||
|       projectColumn: "v.project_id", | ||||
|       permCode: PERM.savedview.read, | ||||
|       preferProject: true, | ||||
|     }); | ||||
|  | ||||
|     const extra = []; | ||||
|     const params = { ...base.params, limit: Number(limit), offset: Number(offset), my: req.principal.userId }; | ||||
|     if (project_id) { extra.push('v.project_id = :project_id'); params.project_id = Number(project_id); } | ||||
|     if (org_id)     { extra.push('v.org_id = :org_id');         params.org_id = Number(org_id); } | ||||
|     if (shared === '1') extra.push('v.is_shared = 1'); | ||||
|     if (q)          { extra.push('(v.name LIKE :q)'); params.q = `%${q}%`; } | ||||
|     const params = { | ||||
|       ...base.params, | ||||
|       limit: Number(limit), | ||||
|       offset: Number(offset), | ||||
|       my: req.principal.userId, | ||||
|     }; | ||||
|     if (project_id) { | ||||
|       extra.push("v.project_id = :project_id"); | ||||
|       params.project_id = Number(project_id); | ||||
|     } | ||||
|     if (org_id) { | ||||
|       extra.push("v.org_id = :org_id"); | ||||
|       params.org_id = Number(org_id); | ||||
|     } | ||||
|     if (shared === "1") extra.push("v.is_shared = 1"); | ||||
|     if (q) { | ||||
|       extra.push("(v.name LIKE :q)"); | ||||
|       params.q = `%${q}%`; | ||||
|     } | ||||
|  | ||||
|     // ให้ผู้ใช้เห็นของตัวเองเสมอ + ของที่อยู่ใน scope | ||||
|     const where = `(${base.where}) AND (v.is_shared=1 OR v.owner_user_id=:my${extra.length ? ' OR ' + extra.join(' AND ') : ''})`; | ||||
|     const where = `(${base.where}) AND (v.is_shared=1 OR v.owner_user_id=:my${ | ||||
|       extra.length ? " OR " + extra.join(" AND ") : "" | ||||
|     })`; | ||||
|  | ||||
|     const [rows] = await sql.query( | ||||
|       `SELECT v.* FROM saved_views v | ||||
|        WHERE ${where} | ||||
|        ORDER BY v.id DESC | ||||
|        LIMIT :limit OFFSET :offset`, params | ||||
|        LIMIT :limit OFFSET :offset`, | ||||
|       params | ||||
|     ); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // GET by id | ||||
| r.get('/:id', | ||||
|   requirePerm(PERM.savedview.read, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.get( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.savedview.read, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   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' }); | ||||
|     const [[row]] = await sql.query("SELECT * FROM saved_views WHERE id=?", [ | ||||
|       id, | ||||
|     ]); | ||||
|     if (!row) return res.status(404).json({ error: "Not found" }); | ||||
|     res.json(row); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // CREATE | ||||
| r.post('/', | ||||
|   requirePerm(PERM.savedview.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }), | ||||
| r.post( | ||||
|   "/", | ||||
|   requirePerm(PERM.savedview.create, { | ||||
|     scope: "org", | ||||
|     getOrgId: async (req) => req.body?.org_id ?? null, | ||||
|   }), | ||||
|   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, project_id, name, JSON.stringify(payload_json ?? {}), Number(is_shared) ? 1 : 0, req.principal.userId] | ||||
|       [ | ||||
|         org_id, | ||||
|         project_id, | ||||
|         name, | ||||
|         JSON.stringify(payload_json ?? {}), | ||||
|         Number(is_shared) ? 1 : 0, | ||||
|         req.principal.userId, | ||||
|       ] | ||||
|     ); | ||||
|     res.json({ id: rs.insertId }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // UPDATE (เฉพาะใน scope และถ้าเป็นของตนเอง หรือเป็นแอดมินตามนโยบาย) | ||||
| r.put('/:id', | ||||
|   requirePerm(PERM.savedview.update, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.put( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.savedview.update, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     const { name, payload_json, is_shared } = req.body; | ||||
|  | ||||
|     // ตรวจ owner ถ้าต้องการบังคับเป็นของตัวเอง (option) | ||||
|     const [[sv]] = await sql.query('SELECT owner_user_id FROM saved_views WHERE id=?', [id]); | ||||
|     if (!sv) return res.status(404).json({ error: 'Not found' }); | ||||
|     const [[sv]] = await sql.query( | ||||
|       "SELECT owner_user_id FROM saved_views WHERE id=?", | ||||
|       [id] | ||||
|     ); | ||||
|     if (!sv) return res.status(404).json({ error: "Not found" }); | ||||
|     // ถ้าจะจำกัดเฉพาะเจ้าของ: if (sv.owner_user_id !== req.principal.userId && !req.principal.isSuperAdmin) return res.status(403).json({ error: 'NOT_OWNER' }); | ||||
|  | ||||
|     await sql.query( | ||||
|       'UPDATE saved_views SET name=?, payload_json=?, is_shared=? WHERE id=?', | ||||
|       "UPDATE saved_views SET name=?, payload_json=?, is_shared=? WHERE id=?", | ||||
|       [name, JSON.stringify(payload_json ?? {}), Number(is_shared) ? 1 : 0, id] | ||||
|     ); | ||||
|     res.json({ ok: 1 }); | ||||
| @@ -88,11 +162,15 @@ r.put('/:id', | ||||
| ); | ||||
|  | ||||
| // DELETE | ||||
| r.delete('/:id', | ||||
|   requirePerm(PERM.savedview.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }), | ||||
| r.delete( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.savedview.delete, { | ||||
|     scope: "org", | ||||
|     getOrgId: OWN.getOrgIdById, | ||||
|   }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     await sql.query('DELETE FROM saved_views WHERE id=?', [id]); | ||||
|     await sql.query("DELETE FROM saved_views WHERE id=?", [id]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|   | ||||
| @@ -1,28 +1,36 @@ | ||||
| // src/routes/views.js (ESM) | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/views.js | ||||
| // Views routes | ||||
| // - GET /api/views to list all views | ||||
| // - GET /api/views/:view_name to get view definition | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
| const DB_NAME = process.env.DB_NAME || 'dms_db'; | ||||
| const DB_NAME = process.env.DB_NAME || "dms_db"; | ||||
|  | ||||
| // LIST views | ||||
| r.get('/', | ||||
|   requirePerm(PERM.viewdef.read, { scope: 'global' }), | ||||
| r.get( | ||||
|   "/", | ||||
|   requirePerm(PERM.viewdef.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const [rows] = await sql.query( | ||||
|       `SELECT TABLE_SCHEMA AS db, TABLE_NAME AS view_name | ||||
|        FROM information_schema.VIEWS | ||||
|        WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME`, [DB_NAME] | ||||
|        WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME`, | ||||
|       [DB_NAME] | ||||
|     ); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // GET view definition | ||||
| r.get('/:view_name', | ||||
|   requirePerm(PERM.viewdef.read, { scope: 'global' }), | ||||
| r.get( | ||||
|   "/:view_name", | ||||
|   requirePerm(PERM.viewdef.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const viewName = req.params.view_name; | ||||
|     const [[row]] = await sql.query( | ||||
| @@ -31,7 +39,7 @@ r.get('/:view_name', | ||||
|        WHERE TABLE_SCHEMA=? AND TABLE_NAME=?`, | ||||
|       [DB_NAME, viewName] | ||||
|     ); | ||||
|     if (!row) return res.status(404).json({ error: 'Not found' }); | ||||
|     if (!row) return res.status(404).json({ error: "Not found" }); | ||||
|     res.json({ view: viewName, definition: row.definition }); | ||||
|   } | ||||
| ); | ||||
|   | ||||
| @@ -1,50 +1,70 @@ | ||||
| import { Router } from 'express'; | ||||
| import sql from '../db/index.js'; | ||||
| import { requirePerm } from '../middleware/requirePerm.js'; | ||||
| import PERM from '../config/permissions.js'; | ||||
| // FILE: src/routes/volumes.js | ||||
| // Volumes routes | ||||
| // - CRUD operations for volumes | ||||
| // - Requires appropriate permissions via requirePerm middleware | ||||
| // - Uses global scope for all permissions | ||||
| // - volume:read, volume:create, volume:update, volume:delete | ||||
| // - Volume fields: volume_id (PK), volume_code, volume_name | ||||
| // - volume_code is unique | ||||
| // - Basic validation: volume_code and volume_name required for create | ||||
|  | ||||
| import { Router } from "express"; | ||||
| import sql from "../db/index.js"; | ||||
| import { requirePerm } from "../middleware/requirePerm.js"; | ||||
| import PERM from "../config/permissions.js"; | ||||
|  | ||||
| const r = Router(); | ||||
|  | ||||
|  | ||||
| r.get('/', | ||||
| requirePerm(PERM.volume.read, { scope: 'global' }), | ||||
| async (req, res) => { | ||||
| const [rows] = await sql.query('SELECT * FROM volumes ORDER BY volume_id DESC'); | ||||
| res.json(rows); | ||||
| } | ||||
| // LIST: GET /api/volumes | ||||
| r.get( | ||||
|   "/", | ||||
|   requirePerm(PERM.volume.read, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const [rows] = await sql.query( | ||||
|       "SELECT * FROM volumes ORDER BY volume_id DESC" | ||||
|     ); | ||||
|     res.json(rows); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| r.post('/', | ||||
| requirePerm(PERM.volume.create, { scope: 'global' }), | ||||
| async (req, res) => { | ||||
| const { volume_code, volume_name } = req.body; | ||||
| const [rs] = await sql.query('INSERT INTO volumes (volume_code, volume_name) VALUES (?,?)', [volume_code, volume_name]); | ||||
| res.json({ volume_id: rs.insertId }); | ||||
| } | ||||
| // CREATE | ||||
| r.post( | ||||
|   "/", | ||||
|   requirePerm(PERM.volume.create, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const { volume_code, volume_name } = req.body; | ||||
|     const [rs] = await sql.query( | ||||
|       "INSERT INTO volumes (volume_code, volume_name) VALUES (?,?)", | ||||
|       [volume_code, volume_name] | ||||
|     ); | ||||
|     res.json({ volume_id: rs.insertId }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| r.put('/:id', | ||||
| requirePerm(PERM.volume.update, { scope: 'global' }), | ||||
| async (req, res) => { | ||||
| const id = Number(req.params.id); | ||||
| const { volume_name } = req.body; | ||||
| await sql.query('UPDATE volumes SET volume_name=? WHERE volume_id=?', [volume_name, id]); | ||||
| res.json({ ok: 1 }); | ||||
| } | ||||
| // UPDATE | ||||
| r.put( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.volume.update, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     const { volume_name } = req.body; | ||||
|     await sql.query("UPDATE volumes SET volume_name=? WHERE volume_id=?", [ | ||||
|       volume_name, | ||||
|       id, | ||||
|     ]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| r.delete('/:id', | ||||
| requirePerm(PERM.volume.delete, { scope: 'global' }), | ||||
| async (req, res) => { | ||||
| const id = Number(req.params.id); | ||||
| await sql.query('DELETE FROM volumes WHERE volume_id=?', [id]); | ||||
| res.json({ ok: 1 }); | ||||
| } | ||||
| // DELETE | ||||
| r.delete( | ||||
|   "/:id", | ||||
|   requirePerm(PERM.volume.delete, { scope: "global" }), | ||||
|   async (req, res) => { | ||||
|     const id = Number(req.params.id); | ||||
|     await sql.query("DELETE FROM volumes WHERE volume_id=?", [id]); | ||||
|     res.json({ ok: 1 }); | ||||
|   } | ||||
| ); | ||||
|  | ||||
|  | ||||
| export default r; | ||||
| export default r; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 admin
					admin