Update frontend login page.jsx และ backend

This commit is contained in:
admin
2025-09-29 13:25:09 +07:00
parent aca3667a9d
commit 7dd5ce8015
52 changed files with 2903 additions and 1289 deletions

View File

@@ -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";

View File

@@ -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 แทน

View File

@@ -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";

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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 });
}
);

View File

@@ -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 });
});

View File

@@ -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;

View File

@@ -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 = [];

View File

@@ -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;

View File

@@ -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);

View File

@@ -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 });
}
);

View File

@@ -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" });
}
});

View File

@@ -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);
}
);

View File

@@ -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);
}
);

View File

@@ -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 });
}
);

View File

@@ -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 });

View File

@@ -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);
}
}
);

View File

@@ -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" });
}
}
);

View File

@@ -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;

View File

@@ -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';

View File

@@ -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" });
}
}
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
});

View File

@@ -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 });
}
);

View File

@@ -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 });
}
);

View File

@@ -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;