05.1 ปรบปรง backend ทงหมด และ frontend/login
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
.devcontainer/
|
||||
@Recently-Snapshot/
|
||||
Documents/
|
||||
mariadb/data/
|
||||
# =====================================================
|
||||
# IDE/Editor settings
|
||||
# =====================================================
|
||||
|
||||
159
backend/README2.md
Normal file
159
backend/README2.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# DMS Backend – คู่มือ Auth & RBAC/ABAC
|
||||
|
||||
## ภาพรวมระบบ
|
||||
Backend ใช้ **Bearer Token** สำหรับการยืนยันตัวตน และตรวจสอบสิทธิ์ด้วย **RBAC (Role-Based Access Control)** ร่วมกับ **ABAC (Attribute-Based Access Control)**
|
||||
|
||||
โครงหลักคือ:
|
||||
1. **authJwt()** → ตรวจสอบ JWT ใน header `Authorization: Bearer ...`
|
||||
2. **loadPrincipalMw()** → โหลดข้อมูลผู้ใช้ + บทบาท + สิทธิ์ + ขอบเขตโปรเจ็ค/องค์กร
|
||||
3. **requirePerm()** → ตรวจสอบ `perm_code` จากตาราง `permissions` และบังคับ ABAC (ORG/PROJECT scope)
|
||||
|
||||
---
|
||||
|
||||
## การยืนยันตัวตน (Authentication)
|
||||
|
||||
### Frontend ส่งอย่างไร
|
||||
```http
|
||||
GET /api/projects
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
- **ไม่มีการใช้ cookie** (Bearer-only)
|
||||
- ถ้า token หมดอายุ ให้ใช้ `refresh_token` ไปขอใหม่ที่ `/api/auth/refresh`
|
||||
|
||||
### Middleware `authJwt()`
|
||||
- อ่าน `Authorization: Bearer ...`
|
||||
- ตรวจสอบด้วย `JWT_SECRET`
|
||||
- เติม `req.auth = { user_id, username }`
|
||||
|
||||
---
|
||||
|
||||
## การโหลด Principal
|
||||
|
||||
### Middleware `loadPrincipalMw()`
|
||||
- ใช้ `user_id` ไป query DB:
|
||||
- users, roles, permissions, project_ids, org_ids
|
||||
- สร้าง `req.principal`:
|
||||
```js
|
||||
{
|
||||
user_id, username, email, first_name, last_name, org_id,
|
||||
roles: [{ role_id, role_code, role_name }],
|
||||
permissions: Set<perm_code>,
|
||||
project_ids: [..],
|
||||
org_ids: [..],
|
||||
is_superadmin: true/false,
|
||||
|
||||
// helper functions
|
||||
can(code),
|
||||
canAny(codes[]),
|
||||
canAll(codes[]),
|
||||
inProject(pid),
|
||||
inOrg(oid)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## การตรวจสอบสิทธิ์ (RBAC + ABAC)
|
||||
|
||||
### Middleware `requirePerm(permCode, { projectParam?, orgParam? })`
|
||||
1. ตรวจว่า user มี `permCode` หรือเป็น superadmin
|
||||
2. อ่าน `scope_level` จากตาราง `permissions`
|
||||
- `GLOBAL` → มีสิทธิ์ก็พอ
|
||||
- `ORG` → ต้องมีสิทธิ์ + อยู่ใน org scope
|
||||
- `PROJECT` → ต้องมีสิทธิ์ + อยู่ใน project scope
|
||||
3. อ่าน `project_id` / `org_id` จาก request (`params`, `query`, `body`)
|
||||
4. ถ้าไม่ผ่าน → คืน `403 FORBIDDEN`
|
||||
|
||||
### Error response ตัวอย่าง
|
||||
```json
|
||||
{ "error": "FORBIDDEN", "need": "projects.manage" }
|
||||
{ "error": "FORBIDDEN_PROJECT", "project_id": 12 }
|
||||
{ "error": "FORBIDDEN_ORG", "org_id": 5 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## รูปแบบที่แนะนำ
|
||||
|
||||
### List (PROJECT scope)
|
||||
```js
|
||||
r.get("/", requirePerm("documents.view", { projectParam: "project_id" }), async (req, res) => {
|
||||
const P = req.principal;
|
||||
const { project_id } = req.query;
|
||||
const cond = [], params = [];
|
||||
|
||||
if (!P.is_superadmin) {
|
||||
if (project_id) {
|
||||
if (!P.inProject(+project_id)) return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
cond.push("project_id=?"); params.push(+project_id);
|
||||
} else if (P.project_ids?.length) {
|
||||
cond.push(`project_id IN (${P.project_ids.map(()=>"?").join(",")})`);
|
||||
params.push(...P.project_ids);
|
||||
}
|
||||
}
|
||||
|
||||
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
|
||||
const [rows] = await sql.query(`SELECT * FROM documents ${where} ORDER BY created_at DESC LIMIT 50`, params);
|
||||
res.json(rows);
|
||||
});
|
||||
```
|
||||
|
||||
### Item (PROJECT scope)
|
||||
```js
|
||||
r.delete("/:id", requirePerm("drawings.delete"), async (req, res) => {
|
||||
const id = +req.params.id;
|
||||
const [[row]] = await sql.query("SELECT project_id FROM drawings WHERE id=?", [id]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
if (!req.principal.is_superadmin && !req.principal.inProject(row.project_id)) {
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
}
|
||||
await sql.query("DELETE FROM drawings WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## การแมปสิทธิ์ (perm_code)
|
||||
|
||||
| หมวด | สิทธิ์ (perm_code) | scope |
|
||||
|--------------|---------------------------------------------|------------|
|
||||
| Organizations| `organizations.view`, `organizations.manage`| GLOBAL |
|
||||
| Projects | `projects.view`, `projects.manage` | ORG |
|
||||
| Drawings | `drawings.view`, `drawings.upload`, `drawings.delete` | PROJECT |
|
||||
| Documents | `documents.view`, `documents.manage` | PROJECT |
|
||||
| RFAs | `rfas.view`, `rfas.create`, `rfas.respond`, `rfas.delete` | PROJECT |
|
||||
| Correspondences | `corr.view`, `corr.manage` | PROJECT |
|
||||
| Transmittals | `transmittals.manage` | PROJECT |
|
||||
| Reports | `reports.view` | GLOBAL |
|
||||
| Settings | `settings.manage` | GLOBAL |
|
||||
| Admin | `admin.access` | ORG |
|
||||
|
||||
---
|
||||
|
||||
## Checklist สำหรับเพิ่ม Endpoint ใหม่
|
||||
|
||||
1. เลือก `perm_code` ที่ตรงกับ seed
|
||||
2. ใส่ `requirePerm("<perm>", { projectParam?: "...", orgParam?: "..." })`
|
||||
3. ถ้าเป็น GET/PUT/DELETE record เดี่ยว → ตรวจสอบซ้ำด้วย `inProject`/`inOrg`
|
||||
4. ใช้ `callProc("sp_name", [...])` ถ้า endpoint เรียก Stored Procedure
|
||||
5. ฝั่ง FE ต้องส่ง `Authorization: Bearer ...` และ parameter `project_id`/`org_id` ที่จำเป็น
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls ที่พบบ่อย
|
||||
- ลืมส่ง `project_id` ในคำขอ → 403
|
||||
- อ้าง perm_code ผิด (เช่น `document.view` แทน `documents.view`)
|
||||
- ไม่กรอง project/org scope ใน query → ข้อมูลรั่ว
|
||||
- ลืมเช็ค item-level ABAC → ข้ามขอบเขตได้
|
||||
- ปน cookie-auth เข้ามา → backend จะไม่รองรับแล้ว
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
- ทุกคำขอ → Bearer Token
|
||||
- `authJwt()` → ใส่ `req.auth`
|
||||
- `loadPrincipalMw()` → ใส่ `req.principal` (roles, perms, scope)
|
||||
- `requirePerm()` → บังคับ RBAC + ABAC อัตโนมัติ
|
||||
- เพิ่ม endpoint ใหม่ → ใช้ checklist ข้างบน
|
||||
@@ -1,39 +1,45 @@
|
||||
// FILE: src/config.js
|
||||
// Purpose: Centralized configuration for the backend application
|
||||
// - Loads settings from environment variables with sensible defaults
|
||||
// - Exports a config object for use throughout the application
|
||||
// =============================================================
|
||||
// Load environment variables from .env file if present
|
||||
// (uncomment the next line if using dotenv)
|
||||
// import dotenv from 'dotenv'; dotenv.config();
|
||||
// (Make sure to install dotenv package if using this line)
|
||||
// =============================================================
|
||||
// FILE: backend/src/config.js
|
||||
// Centralized configuration (ESM)
|
||||
|
||||
const toInt = (v, d) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : d;
|
||||
};
|
||||
const parseAllowlist = (s) =>
|
||||
String(s || "")
|
||||
.split(",")
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
export const config = {
|
||||
PORT: Number(process.env.BACKEND_PORT || 3001),
|
||||
PORT: toInt(process.env.PORT ?? process.env.BACKEND_PORT, 3001),
|
||||
|
||||
DB: {
|
||||
HOST: process.env.DB_HOST || "mariadb",
|
||||
PORT: Number(process.env.DB_PORT || 3306),
|
||||
PORT: toInt(process.env.DB_PORT, 3306),
|
||||
USER: process.env.DB_USER || "center",
|
||||
PASS: process.env.DB_PASSWORD || "Center#2025",
|
||||
NAME: process.env.DB_NAME || "dms",
|
||||
},
|
||||
|
||||
JWT: {
|
||||
SECRET:
|
||||
process.env.JWT_SECRET ||
|
||||
"8b0df02e4aee9f9f79a4f2d8ba77b0b82c1ee3446b68cb0bae94ab54d60f8d9e",
|
||||
SECRET: process.env.JWT_SECRET || "dev-secret",
|
||||
EXPIRES_IN: process.env.JWT_EXPIRES_IN || "8h",
|
||||
REFRESH_SECRET:
|
||||
process.env.JWT_REFRESH_SECRET ||
|
||||
"31r2Wp59E8JukXjYiMopzoHDxJBLVefjR9YOOHjk62R5PbFVXzbx2Wjyc9weVDgK",
|
||||
REFRESH_SECRET: process.env.JWT_REFRESH_SECRET || "dev-refresh",
|
||||
REFRESH_EXPIRES_IN: process.env.JWT_REFRESH_EXPIRES_IN || "7d",
|
||||
},
|
||||
|
||||
SECURITY: {
|
||||
RATE_LIMIT_WINDOW_MS: Number(process.env.RATE_LIMIT_WINDOW_MS || 60_000),
|
||||
RATE_LIMIT_MAX: Number(process.env.RATE_LIMIT_MAX || 100),
|
||||
RATE_LIMIT_WINDOW_MS: toInt(process.env.RATE_LIMIT_WINDOW_MS, 60_000),
|
||||
RATE_LIMIT_MAX: toInt(process.env.RATE_LIMIT_MAX, 100),
|
||||
},
|
||||
CORS_ORIGINS: (process.env.CORS_ALLOWLIST || "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
|
||||
CORS_ORIGINS: parseAllowlist(
|
||||
process.env.CORS_ALLOWLIST ||
|
||||
// เผื่อ dev ทั่วไป
|
||||
"http://localhost:3000,http://127.0.0.1:3000"
|
||||
),
|
||||
};
|
||||
|
||||
// เผื่อไฟล์ไหน import แบบ default
|
||||
export default config;
|
||||
|
||||
@@ -1,156 +1,60 @@
|
||||
// FILE: src/config/permissions.js
|
||||
// Purpose: Map permission_code to your seed naming convention.
|
||||
// - Choose one PROFILE (by SEED_PROFILE env) or edit inline to match exactly
|
||||
// what's in 06_dms_v0_5_0_data_v5_1_seed_users.sql
|
||||
// =============================================================
|
||||
// Two built-in profiles:
|
||||
// - V5_DOT (default): permission codes with dot notation (e.g. rfa.create)
|
||||
// - V5_SNAKE: permission codes with snake_case (e.g. rfa_create)
|
||||
// =============================================================
|
||||
// You can also create your own profile by editing below or
|
||||
// setting SEED_PROFILE env variable to your custom profile name
|
||||
// and adding your custom profile object here.
|
||||
// =============================================================
|
||||
// Note: Changing permission codes after users/roles/permissions have been created
|
||||
// will require updating the database records accordingly.
|
||||
// =============================================================
|
||||
// Example: to use V5_SNAKE profile, set environment variable:
|
||||
// SEED_PROFILE=V5_SNAKE
|
||||
// =============================================================
|
||||
// Example: to use custom profile, set environment variable:
|
||||
// SEED_PROFILE=MY_CUSTOM_PROFILE
|
||||
// and add your custom profile object below.
|
||||
// =============================================================
|
||||
// Reference enum สำหรับฝั่งโค้ด/ฟรอนต์เท่านั้น
|
||||
// แหล่งจริงของ scope อ่านจาก DB: permissions.scope_level (ผ่าน requirePerm())
|
||||
|
||||
const V5_DOT = {
|
||||
organization: { read: "organization.read" },
|
||||
project: {
|
||||
read: "project.read",
|
||||
create: "project.create",
|
||||
update: "project.update",
|
||||
delete: "project.delete",
|
||||
const PERM = {
|
||||
organizations: {
|
||||
view: "organizations.view",
|
||||
manage: "organizations.manage",
|
||||
},
|
||||
correspondence: {
|
||||
read: "correspondence.read",
|
||||
create: "correspondence.create",
|
||||
update: "correspondence.update",
|
||||
delete: "correspondence.delete",
|
||||
upload: "correspondence.upload",
|
||||
projects: {
|
||||
view: "projects.view",
|
||||
manage: "projects.manage",
|
||||
partiesManage: "project_parties.manage",
|
||||
},
|
||||
rfa: {
|
||||
read: "rfa.read",
|
||||
create: "rfa.create",
|
||||
update: "rfa.update",
|
||||
delete: "rfa.delete",
|
||||
upload: "rfa.upload",
|
||||
drawings: {
|
||||
view: "drawings.view",
|
||||
upload: "drawings.upload",
|
||||
delete: "drawings.delete",
|
||||
},
|
||||
drawing: {
|
||||
read: "drawing.read",
|
||||
create: "drawing.create",
|
||||
update: "drawing.update",
|
||||
delete: "drawing.delete",
|
||||
upload: "drawing.upload",
|
||||
documents: {
|
||||
view: "documents.view",
|
||||
manage: "documents.manage",
|
||||
},
|
||||
transmittal: {
|
||||
read: "transmittal.read",
|
||||
create: "transmittal.create",
|
||||
update: "transmittal.update",
|
||||
delete: "transmittal.delete",
|
||||
upload: "transmittal.upload",
|
||||
materials: {
|
||||
view: "materials.view",
|
||||
manage: "materials.manage",
|
||||
},
|
||||
contract: {
|
||||
read: "contract.read",
|
||||
create: "contract.create",
|
||||
update: "contract.update",
|
||||
delete: "contract.delete",
|
||||
ms: {
|
||||
view: "ms.view",
|
||||
manage: "ms.manage",
|
||||
},
|
||||
contract_dwg: {
|
||||
read: "contract_dwg.read",
|
||||
create: "contract_dwg.create",
|
||||
update: "contract_dwg.update",
|
||||
delete: "contract_dwg.delete",
|
||||
rfas: {
|
||||
view: "rfas.view",
|
||||
create: "rfas.create",
|
||||
respond: "rfas.respond",
|
||||
delete: "rfas.delete",
|
||||
},
|
||||
category: {
|
||||
read: "category.read",
|
||||
create: "category.create",
|
||||
update: "category.update",
|
||||
delete: "category.delete",
|
||||
correspondences: {
|
||||
view: "corr.view",
|
||||
manage: "corr.manage",
|
||||
},
|
||||
volume: {
|
||||
read: "volume.read",
|
||||
create: "volume.create",
|
||||
update: "volume.update",
|
||||
delete: "volume.delete",
|
||||
transmittals: {
|
||||
manage: "transmittals.manage",
|
||||
},
|
||||
circulations: {
|
||||
manage: "cirs.manage",
|
||||
},
|
||||
admin: {
|
||||
access: "admin.access",
|
||||
},
|
||||
reports: {
|
||||
view: "reports.view",
|
||||
},
|
||||
settings: {
|
||||
manage: "settings.manage",
|
||||
},
|
||||
permission: { read: "permission.read" },
|
||||
user: { read: "user.read" },
|
||||
};
|
||||
|
||||
const V5_SNAKE = {
|
||||
organization: { read: "organization_read" },
|
||||
project: {
|
||||
read: "project_read",
|
||||
create: "project_create",
|
||||
update: "project_update",
|
||||
delete: "project_delete",
|
||||
},
|
||||
correspondence: {
|
||||
read: "correspondence_read",
|
||||
create: "correspondence_create",
|
||||
update: "correspondence_update",
|
||||
delete: "correspondence_delete",
|
||||
upload: "correspondence_upload",
|
||||
},
|
||||
rfa: {
|
||||
read: "rfa_read",
|
||||
create: "rfa_create",
|
||||
update: "rfa_update",
|
||||
delete: "rfa_delete",
|
||||
upload: "rfa_upload",
|
||||
},
|
||||
drawing: {
|
||||
read: "drawing_read",
|
||||
create: "drawing_create",
|
||||
update: "drawing_update",
|
||||
delete: "drawing_delete",
|
||||
upload: "drawing_upload",
|
||||
},
|
||||
transmittal: {
|
||||
read: "transmittal_read",
|
||||
create: "transmittal_create",
|
||||
update: "transmittal_update",
|
||||
delete: "transmittal_delete",
|
||||
upload: "transmittal_upload",
|
||||
},
|
||||
contract: {
|
||||
read: "contract_read",
|
||||
create: "contract_create",
|
||||
update: "contract_update",
|
||||
delete: "contract_delete",
|
||||
},
|
||||
contract_dwg: {
|
||||
read: "contract_dwg_read",
|
||||
create: "contract_dwg_create",
|
||||
update: "contract_dwg_update",
|
||||
delete: "contract_dwg_delete",
|
||||
},
|
||||
category: {
|
||||
read: "category_read",
|
||||
create: "category_create",
|
||||
update: "category_update",
|
||||
delete: "category_delete",
|
||||
},
|
||||
volume: {
|
||||
read: "volume_read",
|
||||
create: "volume_create",
|
||||
update: "volume_update",
|
||||
delete: "volume_delete",
|
||||
},
|
||||
permission: { read: "permission_read" },
|
||||
user: { read: "user_read" },
|
||||
};
|
||||
|
||||
const PROFILE = (process.env.SEED_PROFILE || "V5_DOT").toUpperCase();
|
||||
|
||||
export const PERM = PROFILE === "V5_SNAKE" ? V5_SNAKE : V5_DOT;
|
||||
export { PERM };
|
||||
export default PERM;
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
// FILE: src/db/index.js (ESM)
|
||||
// Database connection and query utility
|
||||
// - Uses mysql2/promise for connection pooling and async/await
|
||||
// - Exports a query function for executing SQL with parameters
|
||||
// - Connection settings are read from environment variables with defaults
|
||||
// - Uses named placeholders for query parameters
|
||||
// - Dates are handled as strings in UTC timezone to avoid timezone issues
|
||||
// - Connection pool is configured to handle multiple concurrent requests
|
||||
// =============================================================
|
||||
// Load environment variables from .env file if present
|
||||
// (uncomment the next line if using dotenv)
|
||||
// import dotenv from 'dotenv'; dotenv.config();
|
||||
// (Make sure to install dotenv package if using this line)
|
||||
|
||||
// FILE: backend/src/db/index.js (ESM)
|
||||
import mysql from "mysql2/promise";
|
||||
|
||||
const {
|
||||
@@ -30,21 +17,23 @@ const pool = mysql.createPool({
|
||||
password: DB_PASSWORD,
|
||||
database: DB_NAME,
|
||||
connectionLimit: Number(DB_CONN_LIMIT),
|
||||
waitForConnections: true, // Recommended for handling connection spikes
|
||||
waitForConnections: true,
|
||||
namedPlaceholders: true,
|
||||
dateStrings: true, // Keep dates as strings
|
||||
timezone: "Z", // Store and retrieve dates in UTC
|
||||
dateStrings: true, // คงวันที่เป็น string
|
||||
timezone: "Z", // ใช้ UTC
|
||||
});
|
||||
|
||||
/**
|
||||
* Executes a SQL query with parameters.
|
||||
* @param {string} sql The SQL query string.
|
||||
* @param {object} [params={}] The parameters to bind to the query.
|
||||
* @returns {Promise<any[]>} A promise that resolves to an array of rows.
|
||||
* เรียก Stored Procedure แบบง่าย
|
||||
* @param {string} procName ชื่อโปรซีเยอร์ เช่น "sp_rfa_create_with_items"
|
||||
* @param {Array<any>} params ลำดับพารามิเตอร์
|
||||
* @returns {Promise<any>} rows จาก CALL
|
||||
*/
|
||||
export async function query(sql, params = {}) {
|
||||
const [rows] = await pool.execute(sql, params);
|
||||
export async function callProc(procName, params = []) {
|
||||
const placeholders = params.map(() => "?").join(",");
|
||||
const sql = `CALL ${procName}(${placeholders})`;
|
||||
const [rows] = await pool.query(sql, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
export default pool;
|
||||
export default pool; // ใช้ sql.query(...) ได้ตามที่ routes เรียกอยู่
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// FILE: src/db/sequelize.js
|
||||
// Sequelize initialization and model definitions
|
||||
// - Configured via config.js
|
||||
// - Defines User, Role, Permission, UserRole, RolePermission models
|
||||
// - Sets up associations between models
|
||||
// - Exports sequelize instance and models for use in other modules
|
||||
|
||||
// FILE: backend/src/db/sequelize.js
|
||||
// “lazy-load” ตาม env ปลอดภัยกว่า และยังคง dbReady() ให้เรียกทดสอบได้
|
||||
// ใช้ได้เมื่อจำเป็น (เช่น งาน admin tool เฉพาะกิจ)
|
||||
// ตั้ง ENABLE_SEQUELIZE=1 เพื่อเปิดใช้ Model loader; ไม่งั้นจะเป็นโหมดเบา ๆ
|
||||
import { Sequelize } from "sequelize";
|
||||
import { config } from "../config.js";
|
||||
|
||||
@@ -18,49 +15,57 @@ export const sequelize = new Sequelize(
|
||||
dialect: "mariadb",
|
||||
logging: false,
|
||||
dialectOptions: { timezone: "Z" },
|
||||
define: {
|
||||
freezeTableName: true,
|
||||
underscored: false,
|
||||
timestamps: false,
|
||||
},
|
||||
define: { freezeTableName: true, underscored: false, timestamps: false },
|
||||
pool: { max: 10, min: 0, idle: 10000 },
|
||||
}
|
||||
);
|
||||
|
||||
import UserModel from "./models/User.js";
|
||||
import RoleModel from "./models/Role.js";
|
||||
import PermissionModel from "./models/Permission.js";
|
||||
import UserRoleModel from "./models/UserRole.js";
|
||||
import RolePermissionModel from "./models/RolePermission.js";
|
||||
export let User = null;
|
||||
export let Role = null;
|
||||
export let Permission = null;
|
||||
export let UserRole = null;
|
||||
export let RolePermission = null;
|
||||
|
||||
export const User = UserModel(sequelize);
|
||||
export const Role = RoleModel(sequelize);
|
||||
export const Permission = PermissionModel(sequelize);
|
||||
export const UserRole = UserRoleModel(sequelize);
|
||||
export const RolePermission = RolePermissionModel(sequelize);
|
||||
if (process.env.ENABLE_SEQUELIZE === "1") {
|
||||
// โหลดโมเดลแบบ on-demand เพื่อลดความเสี่ยง runtime หากไฟล์โมเดลไม่มี
|
||||
const mdlUser = await import("./models/User.js").catch(() => null);
|
||||
const mdlRole = await import("./models/Role.js").catch(() => null);
|
||||
const mdlPerm = await import("./models/Permission.js").catch(() => null);
|
||||
const mdlUR = await import("./models/UserRole.js").catch(() => null);
|
||||
const mdlRP = await import("./models/RolePermission.js").catch(() => null);
|
||||
|
||||
User.belongsToMany(Role, {
|
||||
through: UserRole,
|
||||
foreignKey: "user_id",
|
||||
otherKey: "role_id",
|
||||
});
|
||||
Role.belongsToMany(User, {
|
||||
through: UserRole,
|
||||
foreignKey: "role_id",
|
||||
otherKey: "user_id",
|
||||
});
|
||||
if (mdlUser?.default) User = mdlUser.default(sequelize);
|
||||
if (mdlRole?.default) Role = mdlRole.default(sequelize);
|
||||
if (mdlPerm?.default) Permission = mdlPerm.default(sequelize);
|
||||
if (mdlUR?.default) UserRole = mdlUR.default(sequelize);
|
||||
if (mdlRP?.default) RolePermission = mdlRP.default(sequelize);
|
||||
|
||||
Role.belongsToMany(Permission, {
|
||||
through: RolePermission,
|
||||
foreignKey: "role_id",
|
||||
otherKey: "permission_id",
|
||||
});
|
||||
Permission.belongsToMany(Role, {
|
||||
through: RolePermission,
|
||||
foreignKey: "permission_id",
|
||||
otherKey: "role_id",
|
||||
});
|
||||
if (User && Role && Permission && UserRole && RolePermission) {
|
||||
User.belongsToMany(Role, {
|
||||
through: UserRole,
|
||||
foreignKey: "user_id",
|
||||
otherKey: "role_id",
|
||||
});
|
||||
Role.belongsToMany(User, {
|
||||
through: UserRole,
|
||||
foreignKey: "role_id",
|
||||
otherKey: "user_id",
|
||||
});
|
||||
|
||||
Role.belongsToMany(Permission, {
|
||||
through: RolePermission,
|
||||
foreignKey: "role_id",
|
||||
otherKey: "permission_id",
|
||||
});
|
||||
Permission.belongsToMany(Role, {
|
||||
through: RolePermission,
|
||||
foreignKey: "permission_id",
|
||||
otherKey: "role_id",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function dbReady() {
|
||||
// โหมดเบา ๆ: แค่ทดสอบเชื่อมต่อ
|
||||
await sequelize.authenticate();
|
||||
}
|
||||
|
||||
@@ -1,72 +1,6 @@
|
||||
// FILE: src/index.js (ESM)
|
||||
// Main entry point for the backend API server
|
||||
// - Sets up Express app with middleware, routes, error handling
|
||||
// - Connects to database
|
||||
// - Starts server and handles graceful shutdown
|
||||
// ==========================
|
||||
// Context:
|
||||
// - Node.js >= 18 (ESM)
|
||||
// - Express.js 4/5
|
||||
// - MySQL database (using mysql2/promise)
|
||||
// ==========================
|
||||
// Features:
|
||||
// - CORS with dynamic origin checking
|
||||
// - Cookie parsing
|
||||
// - JSON and URL-encoded body parsing
|
||||
// - Access logging
|
||||
// - Health, livez, readyz, info endpoints
|
||||
// - JWT authentication middleware
|
||||
// - Principal loading middleware
|
||||
// - Modular route handlers for various resources
|
||||
// - 404 and error handling middleware
|
||||
// - Graceful shutdown on SIGTERM/SIGINT
|
||||
// ==========================
|
||||
// Assumptions:
|
||||
// - Environment variables for configuration (e.g., PORT, DB connection, FRONTEND_ORIGIN)
|
||||
// - Database connection module at ./db/index.js
|
||||
// - Middleware modules for auth, permissions, principal loading
|
||||
// - Route modules for different API resources
|
||||
// - Logs directory exists or can be created
|
||||
// - Code is written in JavaScript (ESM) and runs in Node.js environment
|
||||
// - Uses ES6+ features for cleaner and more maintainable code
|
||||
// ==========================
|
||||
// Notes:
|
||||
// - Adjust CORS origins as needed for your frontend applications
|
||||
// - Ensure proper error handling and logging as per your requirements
|
||||
// - Customize middleware and routes as per your application's needs
|
||||
// ==========================
|
||||
// Best Practices Followed:
|
||||
// - 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 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 middleware modules are correctly implemented and exported
|
||||
// - Assumes route modules are correctly implemented and exported
|
||||
// - Uses environment variables for configuration
|
||||
// - Uses middleware for modular functionality
|
||||
// - Uses async/await for asynchronous operations
|
||||
// - Uses try/catch for error handling in async functions (if needed)
|
||||
// - Uses parameterized queries to prevent SQL injection
|
||||
// - Uses HTTP status codes for responses (e.g., 404 for not found, 400 for bad request)
|
||||
// - Uses JSON responses for API endpoints
|
||||
// - Uses destructuring and default parameters for cleaner function signatures
|
||||
// - Uses best practices for Express.js route handling
|
||||
// - Uses modular code structure for maintainability
|
||||
// - Uses comments for documentation and clarity
|
||||
// - Uses ES6+ features for cleaner and more maintainable code
|
||||
// - Uses template literals for SQL query construction
|
||||
// - Uses array methods for filtering and joining conditions
|
||||
// - Uses utility functions for common tasks (e.g., building SQL WHERE clauses)
|
||||
// ==========================
|
||||
|
||||
// FILE: backend/src/index.js (ESM) ไฟล์ฉบับ “Bearer-only”
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import express from "express";
|
||||
import cookieParser from "cookie-parser";
|
||||
import cors from "cors";
|
||||
|
||||
import sql from "./db/index.js";
|
||||
@@ -91,93 +25,87 @@ import uploadsRoutes from "./routes/uploads.js";
|
||||
import usersRoutes from "./routes/users.js";
|
||||
import permissionsRoutes from "./routes/permissions.js";
|
||||
|
||||
/* ==========================
|
||||
* CONFIG
|
||||
* ========================== */
|
||||
const PORT = Number(process.env.PORT || 3001);
|
||||
const NODE_ENV = process.env.NODE_ENV || "production";
|
||||
const NODE_ENV = process.env.NODE_ENV || "development";
|
||||
|
||||
// Origin ของ Frontend (ตั้งผ่าน ENV ต่อ environment; dev ใช้ localhost)
|
||||
const FRONTEND_ORIGIN =
|
||||
process.env.FRONTEND_ORIGIN || "https://lcbp3.np-dms.work";
|
||||
|
||||
const ALLOW_ORIGINS = [
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
FRONTEND_ORIGIN,
|
||||
...(process.env.CORS_ALLOWLIST
|
||||
? process.env.CORS_ALLOWLIST.split(",")
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean)
|
||||
: []),
|
||||
].filter(Boolean);
|
||||
|
||||
// ที่เก็บ log ภายใน container ถูก bind ไปที่ /share/Container/dms/logs/backend
|
||||
const LOG_DIR = process.env.BACKEND_LOG_DIR || "/app/logs";
|
||||
|
||||
// สร้างโฟลเดอร์ log ถ้ายังไม่มี
|
||||
try {
|
||||
if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||
} catch (e) {
|
||||
console.warn("[WARN] Cannot ensure LOG_DIR:", LOG_DIR, e?.message);
|
||||
}
|
||||
|
||||
/* ==========================
|
||||
* APP INIT
|
||||
* ========================== */
|
||||
const app = express();
|
||||
|
||||
// ✅ อยู่หลัง NPM/Reverse proxy → ให้ trust proxy เพื่อให้ cookie secure / proto ทำงานถูก
|
||||
app.set("trust proxy", 1);
|
||||
|
||||
// CORS แบบกำหนด origin ตามรายการที่อนุญาต + อนุญาต credentials (จำเป็นสำหรับ cookie)
|
||||
// CORS: allow list
|
||||
app.use(
|
||||
cors({
|
||||
origin(origin, cb) {
|
||||
if (!origin) return cb(null, true); // server-to-server / curl
|
||||
return cb(null, ALLOW_ORIGINS.includes(origin));
|
||||
cb(null, ALLOW_ORIGINS.includes(origin));
|
||||
},
|
||||
credentials: true,
|
||||
credentials: false, // Bearer-only
|
||||
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
||||
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
|
||||
allowedHeaders: [
|
||||
"Content-Type",
|
||||
"Authorization",
|
||||
"X-Requested-With",
|
||||
"Accept",
|
||||
"Origin",
|
||||
"Referer",
|
||||
"User-Agent",
|
||||
"Cache-Control",
|
||||
"Pragma",
|
||||
],
|
||||
exposedHeaders: ["Content-Disposition", "Content-Length"],
|
||||
})
|
||||
);
|
||||
// preflight
|
||||
app.options(
|
||||
"*",
|
||||
cors({
|
||||
origin(origin, cb) {
|
||||
if (!origin) return cb(null, true);
|
||||
return cb(null, ALLOW_ORIGINS.includes(origin));
|
||||
cb(null, ALLOW_ORIGINS.includes(origin));
|
||||
},
|
||||
credentials: true,
|
||||
credentials: false,
|
||||
})
|
||||
);
|
||||
|
||||
app.use(cookieParser());
|
||||
|
||||
// Payload limits
|
||||
app.use(express.json({ limit: "10mb" }));
|
||||
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
||||
|
||||
// Access log (ขั้นต่ำ)
|
||||
// minimal access log
|
||||
app.use((req, _res, next) => {
|
||||
console.log(`[REQ] ${req.method} ${req.originalUrl}`);
|
||||
next();
|
||||
});
|
||||
|
||||
/* ==========================
|
||||
* HEALTH / READY / INFO
|
||||
* ========================== */
|
||||
app.get("/health", async (req, res) => {
|
||||
// health/info (เปิดทั้ง /health, /livez, /readyz, /info)
|
||||
app.get("/health", async (_req, res) => {
|
||||
try {
|
||||
const [[{ now }]] = await sql.query("SELECT NOW() AS now");
|
||||
return res.json({ status: "ok", db: "ok", now });
|
||||
res.json({ status: "ok", db: "ok", now });
|
||||
} catch (e) {
|
||||
return res
|
||||
.status(500)
|
||||
.json({ status: "degraded", db: "fail", error: e?.message });
|
||||
res.status(500).json({ status: "degraded", db: "fail", error: e?.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Kubernetes-style endpoints (ถ้าใช้)
|
||||
app.get("/livez", (req, res) => res.send("ok"));
|
||||
app.get("/readyz", async (req, res) => {
|
||||
app.get("/livez", (_req, res) => res.send("ok"));
|
||||
app.get("/readyz", async (_req, res) => {
|
||||
try {
|
||||
await sql.query("SELECT 1");
|
||||
res.send("ready");
|
||||
@@ -185,26 +113,20 @@ app.get("/readyz", async (req, res) => {
|
||||
res.status(500).send("not-ready");
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/info", (req, res) => {
|
||||
app.get("/info", (_req, res) =>
|
||||
res.json({
|
||||
name: "dms-backend",
|
||||
env: NODE_ENV,
|
||||
version: process.env.APP_VERSION || "0.5.0",
|
||||
commit: process.env.GIT_COMMIT || undefined,
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
/* ==========================
|
||||
* ROUTES
|
||||
* ========================== */
|
||||
// /api/health (ถอดจาก healthRouter)
|
||||
// ---------- Public (no auth) ----------
|
||||
app.use("/api", healthRouter);
|
||||
|
||||
// ✅ auth กลุ่มนี้ "ไม่ต้อง" ผ่าน authJwt
|
||||
app.use("/api/auth", authRoutes);
|
||||
|
||||
// จากนี้ไป ทุก /api/* ต้องผ่าน JWT + principal
|
||||
// ---------- Protected (Bearer + Principal) ----------
|
||||
app.use("/api", authJwt(), loadPrincipalMw());
|
||||
|
||||
app.use("/api/lookup", lookupRoutes);
|
||||
@@ -222,33 +144,22 @@ app.use("/api/uploads", uploadsRoutes);
|
||||
app.use("/api/users", usersRoutes);
|
||||
app.use("/api/permissions", permissionsRoutes);
|
||||
|
||||
/* ==========================
|
||||
* NOT FOUND & ERROR HANDLERS
|
||||
* ========================== */
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ error: "NOT_FOUND", path: req.originalUrl });
|
||||
});
|
||||
|
||||
// 404 / error
|
||||
app.use((req, res) =>
|
||||
res.status(404).json({ error: "NOT_FOUND", path: req.originalUrl })
|
||||
);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
app.use((err, req, res, _next) => {
|
||||
app.use((err, _req, res, _next) => {
|
||||
console.error("[UNHANDLED ERROR]", err);
|
||||
const status = err?.status || 500;
|
||||
res.status(status).json({
|
||||
error: "SERVER_ERROR",
|
||||
message: NODE_ENV === "production" ? undefined : err?.message,
|
||||
});
|
||||
res.status(err?.status || 500).json({ error: "SERVER_ERROR" });
|
||||
});
|
||||
|
||||
/* ==========================
|
||||
* START SERVER
|
||||
* ========================== */
|
||||
// START
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`Backend API listening on ${PORT} (env=${NODE_ENV})`);
|
||||
});
|
||||
|
||||
/* ==========================
|
||||
* GRACEFUL SHUTDOWN
|
||||
* ========================== */
|
||||
// Shutdown
|
||||
async function shutdown(signal) {
|
||||
try {
|
||||
console.log(`[SHUTDOWN] ${signal} received`);
|
||||
|
||||
@@ -10,19 +10,24 @@
|
||||
// - ไม่ได้เติม roles/permissions (ถ้าต้องการให้ใช้ auth.js แทนหรือร่วมกัน)
|
||||
|
||||
import jwt from "jsonwebtoken";
|
||||
const { JWT_SECRET = "dev-secret" } = process.env;
|
||||
|
||||
export function authJwt() {
|
||||
const { JWT_SECRET = "dev-secret" } = process.env;
|
||||
return (req, res, next) => {
|
||||
const h = req.headers.authorization || "";
|
||||
const token = h.startsWith("Bearer ") ? h.slice(7) : null;
|
||||
if (!token) return res.status(401).json({ error: "Unauthenticated" });
|
||||
// const token = h.startsWith("Bearer ") ? h.slice(7) : null;
|
||||
const m = /^Bearer\s+(.+)$/i.exec(h || "");
|
||||
//if (!token) return res.status(401).json({ error: "Unauthenticated" });
|
||||
if (!m) return res.status(401).json({ error: "Unauthenticated" });
|
||||
try {
|
||||
const payload = jwt.verify(token, JWT_SECRET);
|
||||
req.user = { user_id: payload.user_id, username: payload.username };
|
||||
//const payload = jwt.verify(token, JWT_SECRET);
|
||||
const payload = jwt.verify(m[1], JWT_SECRET, { issuer: "dms-backend" });
|
||||
// แนบข้อมูลขั้นต่ำให้ middleware ถัดไป
|
||||
req.auth = { user_id: payload.user_id, username: payload.username };
|
||||
//req.user = { user_id: payload.user_id, username: payload.username };
|
||||
next();
|
||||
} catch (e) {
|
||||
return res.status(401).json({ error: "Invalid token" });
|
||||
return res.status(401).json({ error: "Unauthenticated" });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,36 +1,21 @@
|
||||
// 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";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
|
||||
// แนะนำ: ensure DB connection once (กันเผลอเรียกก่อน DB พร้อม)
|
||||
await dbReady().catch((e) => {
|
||||
console.error("[admin] DB not ready:", e?.message);
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/admin/sysinfo
|
||||
* perm: admin.read (global)
|
||||
* perm: admin.access (ORG scope) – ใช้สิทธิ์กลุ่ม admin
|
||||
*/
|
||||
r.get(
|
||||
"/sysinfo",
|
||||
requirePerm(PERM.admin.read, { scope: "global" }),
|
||||
requirePerm("admin.access", { orgParam: "org_id" }),
|
||||
async (_req, res) => {
|
||||
try {
|
||||
// ping DB เบา ๆ
|
||||
await sequelize.query("SELECT 1");
|
||||
await sql.query("SELECT 1");
|
||||
res.json({
|
||||
now: new Date().toISOString(),
|
||||
node: process.version,
|
||||
@@ -53,16 +38,15 @@ r.get(
|
||||
|
||||
/**
|
||||
* POST /api/admin/maintenance/reindex
|
||||
* perm: admin.maintain (global)
|
||||
* หมายเหตุ: ปรับรายชื่อตารางตามโปรเจ็คจริงของคุณ
|
||||
* perm: settings.manage (GLOBAL) – งานดูแลระบบ
|
||||
*/
|
||||
r.post(
|
||||
"/maintenance/reindex",
|
||||
requirePerm(PERM.admin.maintain, { scope: "global" }),
|
||||
requirePerm("settings.manage"),
|
||||
async (_req, res) => {
|
||||
try {
|
||||
// ตัวอย่าง ใช้ RAW ก็ได้เมื่อเหมาะสม
|
||||
await sequelize.query("ANALYZE TABLE correspondences, rfas, drawings");
|
||||
// ปรับตามตารางจริงของคุณ
|
||||
await sql.query("ANALYZE TABLE correspondences, rfas, drawings");
|
||||
res.json({ ok: 1 });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: "MAINT_FAIL", message: e?.message });
|
||||
@@ -71,65 +55,39 @@ r.post(
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/admin/perm-matrix?format=md|json
|
||||
* perm: admin.read (global)
|
||||
* ดึง Role -> Permissions ด้วย association ของ Sequelize
|
||||
* GET /api/admin/perm-matrix?format=json
|
||||
* perm: admin.access (ORG)
|
||||
*/
|
||||
r.get(
|
||||
"/perm-matrix",
|
||||
requirePerm(PERM.admin.read, { scope: "global" }),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const format = String(req.query.format || "md").toLowerCase();
|
||||
|
||||
const roles = await Role.findAll({
|
||||
attributes: ["role_id", "role_code", "role_name"],
|
||||
include: [
|
||||
{
|
||||
model: Permission,
|
||||
attributes: ["perm_code"],
|
||||
through: { attributes: [] }, // ไม่ต้องข้อมูลตาราง join
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
order: [["role_code", "ASC"]],
|
||||
logging: false,
|
||||
});
|
||||
|
||||
if (format === "json") {
|
||||
const data = roles.map((r) => ({
|
||||
role_id: r.role_id,
|
||||
role_code: r.role_code,
|
||||
role_name: r.role_name,
|
||||
perm_codes: (r.Permissions || []).map((p) => p.perm_code).sort(),
|
||||
}));
|
||||
return res.json({ roles: data });
|
||||
}
|
||||
|
||||
// สร้าง Markdown table
|
||||
const lines = [];
|
||||
lines.push(`# Permission Matrix (Role → Permissions)`);
|
||||
lines.push(`_Generated at: ${new Date().toISOString()}_\n`);
|
||||
lines.push(`| # | Role Code | Role Name | Permissions |`);
|
||||
lines.push(`|---:|:---------|:----------|:------------|`);
|
||||
|
||||
roles.forEach((r, idx) => {
|
||||
const perms = (r.Permissions || [])
|
||||
.map((p) => p.perm_code)
|
||||
.sort()
|
||||
.join(", ");
|
||||
lines.push(
|
||||
`| ${idx + 1} | \`${r.role_code}\` | ${
|
||||
r.role_name || ""
|
||||
} | ${perms} |`
|
||||
);
|
||||
});
|
||||
|
||||
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
||||
return res.send(lines.join("\n"));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
requirePerm("admin.access", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const format = String(req.query.format || "json").toLowerCase();
|
||||
const [roles] = await sql.query(
|
||||
`SELECT r.role_id, r.role_code, r.role_name,
|
||||
GROUP_CONCAT(p.perm_code ORDER BY p.perm_code SEPARATOR ', ') AS perm_codes
|
||||
FROM roles r
|
||||
LEFT JOIN role_permissions rp ON rp.role_id = r.role_id
|
||||
LEFT JOIN permissions p ON p.permission_id = rp.permission_id
|
||||
GROUP BY r.role_id, r.role_code, r.role_name
|
||||
ORDER BY r.role_code`
|
||||
);
|
||||
if (format === "json") return res.json({ roles });
|
||||
// markdown แบบง่าย
|
||||
const lines = [
|
||||
`# Permission Matrix`,
|
||||
`_Generated at: ${new Date().toISOString()}_`,
|
||||
`| # | Role Code | Role Name | Permissions |`,
|
||||
`|---:|:---------|:----------|:------------|`,
|
||||
...roles.map(
|
||||
(r, i) =>
|
||||
`| ${i + 1} | \`${r.role_code}\` | ${r.role_name || ""} | ${
|
||||
r.perm_codes || ""
|
||||
} |`
|
||||
),
|
||||
];
|
||||
res.setHeader("Content-Type", "text/markdown; charset=utf-8");
|
||||
res.send(lines.join("\n"));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,47 +1,20 @@
|
||||
// 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)
|
||||
|
||||
// Deprecated for this project (เราใช้ Bearer + authJwt() แล้ว)
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
const JWT_ACCESS_SECRET = process.env.JWT_ACCESS_SECRET || "dev-access-secret";
|
||||
|
||||
/**
|
||||
* ตรวจสอบ access_token จาก httpOnly cookie
|
||||
* ใช้เป็น middleware กับเส้นทางที่ต้องการป้องกันฝั่ง API (ซ้ำกับ authJwt เดิมได้ แต่ตัวนี้อ่านคุกกี้ตรง ๆ)
|
||||
*/
|
||||
export function requireAuth(req, res, next) {
|
||||
const token = req.cookies?.access_token;
|
||||
if (!token) return res.status(401).json({ error: "Unauthenticated" });
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(token, JWT_ACCESS_SECRET, {
|
||||
issuer: "dms-backend",
|
||||
});
|
||||
req.user = {
|
||||
user_id: payload.user_id,
|
||||
username: payload.username,
|
||||
};
|
||||
req.user = { user_id: payload.user_id, username: payload.username };
|
||||
return next();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return res.status(401).json({ error: "INVALID_TOKEN" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* เฉพาะกรณีในอนาคต: ตรวจบทบาท/สิทธิ์ง่าย ๆ
|
||||
* ใช้หลัง requireAuth เช่น app.get('/api/admin/xxx', requireAuth, requireRole('Admin'), handler)
|
||||
*/
|
||||
export function requireRole(roleName) {
|
||||
return function (req, res, next) {
|
||||
// สมมติว่ามี req.principal.roles จาก middleware อื่น (เช่น loadPrincipalMw)
|
||||
const roles = req.principal?.roles || req.user?.roles || [];
|
||||
if (!Array.isArray(roles) || !roles.includes(roleName)) {
|
||||
return res.status(403).json({ error: "FORBIDDEN" });
|
||||
}
|
||||
next();
|
||||
};
|
||||
export function requireRole(_role) {
|
||||
return (_req, res, next) => res.status(403).json({ error: "FORBIDDEN" });
|
||||
}
|
||||
// หมายเหตุ: ในโปรเจกต์นี้ เราใช้ requirePerm จาก src/middleware/requirePerm.js แทน
|
||||
// เพราะมีความยืดหยุ่นกว่า (ตรวจสิทธิ์เป็นรายรายการ และมี scope ด้วย)
|
||||
|
||||
@@ -1,63 +1,39 @@
|
||||
// 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.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 });
|
||||
}
|
||||
);
|
||||
|
||||
// Categories
|
||||
r.get("/categories", requirePerm("organizations.view"), async (_req, res) => {
|
||||
const [rows] = await sql.query(
|
||||
"SELECT * FROM categories ORDER BY cat_id DESC"
|
||||
);
|
||||
res.json(rows);
|
||||
});
|
||||
r.post("/categories", requirePerm("settings.manage"), async (req, res) => {
|
||||
const { cat_code, cat_name } = req.body || {};
|
||||
if (!cat_code || !cat_name)
|
||||
return res.status(400).json({ error: "cat_code and cat_name required" });
|
||||
const [rs] = await sql.query(
|
||||
"INSERT INTO categories (cat_code, cat_name) VALUES (?,?)",
|
||||
[cat_code, cat_name]
|
||||
);
|
||||
res.json({ cat_id: rs.insertId });
|
||||
});
|
||||
r.put("/categories/:id", requirePerm("settings.manage"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { cat_name } = req.body || {};
|
||||
await sql.query("UPDATE categories SET cat_name=? WHERE cat_id=?", [
|
||||
cat_name,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
r.delete(
|
||||
"/categories/:id",
|
||||
requirePerm(PERM.category.delete, { scope: "global" }),
|
||||
requirePerm("settings.manage"),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query("DELETE FROM categories WHERE cat_id=?", [id]);
|
||||
@@ -65,22 +41,20 @@ r.delete(
|
||||
}
|
||||
);
|
||||
|
||||
// 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);
|
||||
// Subcategories
|
||||
r.get("/subcategories", requirePerm("organizations.view"), async (req, res) => {
|
||||
const { cat_id } = req.query;
|
||||
const params = [];
|
||||
let where = "";
|
||||
if (cat_id) {
|
||||
where = " WHERE cat_id=?";
|
||||
params.push(Number(cat_id));
|
||||
}
|
||||
);
|
||||
const [rows] = await sql.query(
|
||||
`SELECT * FROM subcategories${where} ORDER BY sub_cat_id DESC`,
|
||||
params
|
||||
);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,85 +1,73 @@
|
||||
// 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");
|
||||
|
||||
// LIST mappings
|
||||
// LIST (PROJECT scope enforce via params ifส่ง project_id, ไม่ส่งจะถูก filter ด้วย scope ภายใน)
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm(PERM.contract_dwg.read, { scope: "global" }),
|
||||
requirePerm("drawings.view", { projectParam: "project_id" }),
|
||||
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);
|
||||
const p = req.principal;
|
||||
const params = [];
|
||||
const cond = [];
|
||||
|
||||
// ABAC filter ฝั่ง server กันหลุดขอบเขต
|
||||
if (!p.is_superadmin) {
|
||||
if (project_id) {
|
||||
if (!p.inProject(Number(project_id)))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
cond.push("m.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
} else if (p.project_ids?.length) {
|
||||
cond.push(
|
||||
`m.project_id IN (${p.project_ids.map(() => "?").join(",")})`
|
||||
);
|
||||
params.push(...p.project_ids);
|
||||
}
|
||||
} else if (project_id) {
|
||||
cond.push("m.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
}
|
||||
|
||||
if (org_id) {
|
||||
extra.push("m.org_id = :org_id");
|
||||
params.org_id = Number(org_id);
|
||||
cond.push("m.org_id=?");
|
||||
params.push(Number(org_id));
|
||||
}
|
||||
if (condwg_no) {
|
||||
extra.push("m.condwg_no = :condwg_no");
|
||||
params.condwg_no = condwg_no;
|
||||
cond.push("m.condwg_no=?");
|
||||
params.push(condwg_no);
|
||||
}
|
||||
const where = [base.where, ...extra].filter(Boolean).join(" AND ");
|
||||
|
||||
const where = cond.length ? `WHERE ${cond.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
|
||||
`SELECT m.* FROM contract_dwg m ${where} ORDER BY m.id DESC LIMIT ? OFFSET ?`,
|
||||
[...params, Number(limit), Number(offset)]
|
||||
);
|
||||
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);
|
||||
}
|
||||
);
|
||||
// GET item (ตรวจ ABAC หลังอ่านเรคคอร์ด)
|
||||
r.get("/:id", requirePerm("drawings.view"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query("SELECT * FROM contract_dwg WHERE id=?", [
|
||||
id,
|
||||
]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
res.json(row);
|
||||
});
|
||||
|
||||
// CREATE mapping (1 drawing per contract or per rule)
|
||||
// CREATE
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm(PERM.contract_dwg.create, {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
requirePerm("drawings.upload", { projectParam: "project_id" }),
|
||||
async (req, res) => {
|
||||
const {
|
||||
org_id,
|
||||
@@ -91,20 +79,25 @@ r.post(
|
||||
sub_cat_id,
|
||||
sub_no,
|
||||
remark,
|
||||
} = req.body;
|
||||
} = req.body || {};
|
||||
if (!project_id || !condwg_no)
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "project_id and condwg_no required" });
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO contract_dwg (org_id, project_id, condwg_no, title, drawing_id, volume_id, sub_cat_id, sub_no, remark, created_by) VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
||||
`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,
|
||||
org_id || null,
|
||||
project_id,
|
||||
condwg_no,
|
||||
title,
|
||||
drawing_id,
|
||||
volume_id,
|
||||
sub_cat_id,
|
||||
sub_no,
|
||||
remark,
|
||||
req.principal.userId,
|
||||
title || null,
|
||||
drawing_id || null,
|
||||
volume_id || null,
|
||||
sub_cat_id || null,
|
||||
sub_no || null,
|
||||
remark || null,
|
||||
req.principal.user_id,
|
||||
]
|
||||
);
|
||||
res.json({ id: rs.insertId });
|
||||
@@ -112,36 +105,37 @@ r.post(
|
||||
);
|
||||
|
||||
// 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("drawings.upload"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query("SELECT * FROM contract_dwg WHERE id=?", [
|
||||
id,
|
||||
]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
|
||||
const { title, remark } = req.body || {};
|
||||
await sql.query("UPDATE contract_dwg SET title=?, remark=? WHERE id=?", [
|
||||
title ?? row.title,
|
||||
remark ?? row.remark,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
// DELETE
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm(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("drawings.delete"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query("SELECT * FROM contract_dwg WHERE id=?", [
|
||||
id,
|
||||
]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
await sql.query("DELETE FROM contract_dwg WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
// 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");
|
||||
|
||||
// LIST
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm(PERM.contract.read, { scope: "global" }),
|
||||
requirePerm("projects.view", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const {
|
||||
project_id,
|
||||
@@ -31,97 +18,118 @@ r.get(
|
||||
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);
|
||||
const p = req.principal;
|
||||
const params = [];
|
||||
const cond = [];
|
||||
if (!p.is_superadmin) {
|
||||
if (org_id) {
|
||||
if (!p.inOrg(Number(org_id)))
|
||||
return res.status(403).json({ error: "FORBIDDEN_ORG" });
|
||||
cond.push("c.org_id=?");
|
||||
params.push(Number(org_id));
|
||||
} else if (p.org_ids?.length) {
|
||||
cond.push(`c.org_id IN (${p.org_ids.map(() => "?").join(",")})`);
|
||||
params.push(...p.org_ids);
|
||||
}
|
||||
} else if (org_id) {
|
||||
cond.push("c.org_id=?");
|
||||
params.push(Number(org_id));
|
||||
}
|
||||
if (org_id) {
|
||||
extra.push("c.org_id = :org_id");
|
||||
params.org_id = Number(org_id);
|
||||
|
||||
if (project_id) {
|
||||
cond.push("c.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
}
|
||||
if (contract_no) {
|
||||
extra.push("c.contract_no = :contract_no");
|
||||
params.contract_no = contract_no;
|
||||
cond.push("c.contract_no=?");
|
||||
params.push(contract_no);
|
||||
}
|
||||
if (q) {
|
||||
extra.push("(c.contract_no LIKE :q OR c.title LIKE :q)");
|
||||
params.q = `%${q}%`;
|
||||
cond.push("(c.contract_no LIKE ? OR c.title LIKE ?)");
|
||||
params.push(`%${q}%`, `%${q}%`);
|
||||
}
|
||||
const where = [base.where, ...extra].filter(Boolean).join(" AND ");
|
||||
|
||||
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
|
||||
const [rows] = await sql.query(
|
||||
`SELECT c.* FROM contracts c WHERE ${where} ORDER BY c.id DESC LIMIT :limit OFFSET :offset`,
|
||||
params
|
||||
`SELECT c.* FROM contracts c ${where} ORDER BY c.id DESC LIMIT ? OFFSET ?`,
|
||||
[...params, Number(limit), Number(offset)]
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
// GET
|
||||
r.get(
|
||||
"/:id",
|
||||
requirePerm(PERM.contract.read, { scope: "org", getOrgId: OWN.getOrgIdById }),
|
||||
requirePerm("projects.view", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query("SELECT * FROM contracts WHERE id=?", [id]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inOrg(row.org_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_ORG" });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
|
||||
// CREATE
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm(PERM.contract.create, {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
requirePerm("projects.manage", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const { org_id, project_id, contract_no, title, status } = req.body;
|
||||
const { org_id, project_id, contract_no, title, status } = req.body || {};
|
||||
if (!org_id || !project_id || !contract_no)
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "org_id, project_id, contract_no required" });
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO contracts (org_id, project_id, contract_no, title, status, created_by) VALUES (?,?,?,?,?,?)`,
|
||||
[org_id, project_id, contract_no, title, status, req.principal.userId]
|
||||
[
|
||||
org_id,
|
||||
project_id,
|
||||
contract_no,
|
||||
title || null,
|
||||
status || null,
|
||||
req.principal.user_id,
|
||||
]
|
||||
);
|
||||
res.json({ id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
// UPDATE
|
||||
r.put(
|
||||
"/:id",
|
||||
requirePerm(PERM.contract.update, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
requirePerm("projects.manage", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { title, status } = req.body;
|
||||
const [[row]] = await sql.query("SELECT * FROM contracts WHERE id=?", [id]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inOrg(row.org_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_ORG" });
|
||||
const { title, status } = req.body || {};
|
||||
await sql.query("UPDATE contracts SET title=?, status=? WHERE id=?", [
|
||||
title,
|
||||
status,
|
||||
title ?? row.title,
|
||||
status ?? row.status,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
// DELETE
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm(PERM.contract.delete, {
|
||||
scope: "org",
|
||||
getOrgId: OWN.getOrgIdById,
|
||||
}),
|
||||
requirePerm("projects.manage", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query("SELECT * FROM contracts WHERE id=?", [id]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inOrg(row.org_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_ORG" });
|
||||
await sql.query("DELETE FROM contracts WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
|
||||
@@ -1,124 +1,124 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/correspondences.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, "correspondences", "id");
|
||||
|
||||
// LIST
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm(PERM.correspondence.read, { scope: "global" }),
|
||||
requirePerm("corr.view", { projectParam: "project_id" }),
|
||||
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);
|
||||
const p = req.principal;
|
||||
const params = [];
|
||||
const cond = [];
|
||||
|
||||
if (!p.is_superadmin) {
|
||||
if (project_id) {
|
||||
if (!p.inProject(Number(project_id)))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
cond.push("c.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
} else if (p.project_ids?.length) {
|
||||
cond.push(
|
||||
`c.project_id IN (${p.project_ids.map(() => "?").join(",")})`
|
||||
);
|
||||
params.push(...p.project_ids);
|
||||
}
|
||||
} else if (project_id) {
|
||||
cond.push("c.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
}
|
||||
|
||||
if (org_id) {
|
||||
extra.push("c.org_id = :org_id");
|
||||
params.org_id = Number(org_id);
|
||||
cond.push("c.org_id=?");
|
||||
params.push(Number(org_id));
|
||||
}
|
||||
if (q) {
|
||||
extra.push("(c.corr_no LIKE :q OR c.subject LIKE :q)");
|
||||
params.q = `%${q}%`;
|
||||
cond.push("(c.corr_no LIKE ? OR c.subject LIKE ?)");
|
||||
params.push(`%${q}%`, `%${q}%`);
|
||||
}
|
||||
const where = [base.where, ...extra].join(" AND ");
|
||||
|
||||
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
|
||||
const [rows] = await sql.query(
|
||||
`SELECT c.* FROM correspondences c WHERE ${where} ORDER BY c.id DESC LIMIT :limit OFFSET :offset`,
|
||||
params
|
||||
`SELECT c.* FROM correspondences c ${where} ORDER BY c.id DESC LIMIT ? OFFSET ?`,
|
||||
[...params, Number(limit), Number(offset)]
|
||||
);
|
||||
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);
|
||||
}
|
||||
);
|
||||
// GET
|
||||
r.get("/:id", requirePerm("corr.view"), 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" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
res.json(row);
|
||||
});
|
||||
|
||||
// CREATE
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm(PERM.correspondence.create, {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
requirePerm("corr.manage", { projectParam: "project_id" }),
|
||||
async (req, res) => {
|
||||
const { org_id, project_id, corr_no, subject, status } = req.body;
|
||||
const { org_id, project_id, corr_no, subject, status } = req.body || {};
|
||||
if (!project_id || !corr_no)
|
||||
return res.status(400).json({ error: "project_id and corr_no required" });
|
||||
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]
|
||||
`INSERT INTO correspondences (org_id, project_id, corr_no, subject, status, created_by)
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
[
|
||||
org_id || null,
|
||||
project_id,
|
||||
corr_no,
|
||||
subject || null,
|
||||
status || null,
|
||||
req.principal.user_id,
|
||||
]
|
||||
);
|
||||
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 });
|
||||
}
|
||||
);
|
||||
// UPDATE
|
||||
r.put("/:id", requirePerm("corr.manage"), 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" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
|
||||
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 });
|
||||
}
|
||||
);
|
||||
const { subject, status } = req.body || {};
|
||||
await sql.query("UPDATE correspondences SET subject=?, status=? WHERE id=?", [
|
||||
subject ?? row.subject,
|
||||
status ?? row.status,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
// DELETE
|
||||
r.delete("/:id", requirePerm("corr.manage"), 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" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
await sql.query("DELETE FROM correspondences WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,67 +1,149 @@
|
||||
// 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';
|
||||
import { requireRole } from '../middleware/rbac.js';
|
||||
import { requirePerm } from '../middleware/permGuard.js';
|
||||
import { sequelize } from '../db/sequelize.js';
|
||||
import DocumentModel from '../db/models/Document.js';
|
||||
// FILE: backend/src/routes/documents.js
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
|
||||
const r = Router();
|
||||
const Doc = DocumentModel(sequelize);
|
||||
|
||||
r.get('/documents', requireAuth, async (req, res) => {
|
||||
const { q, project_id, status, category, page=1, page_size=20 } = req.query;
|
||||
const limit = Math.min(Number(page_size)||20, 100);
|
||||
const offset = (Math.max(Number(page)||1,1)-1) * limit;
|
||||
// LIST
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm("documents.view", { projectParam: "project_id" }),
|
||||
async (req, res) => {
|
||||
const {
|
||||
q,
|
||||
project_id,
|
||||
status,
|
||||
category,
|
||||
page = 1,
|
||||
page_size = 20,
|
||||
} = req.query;
|
||||
const limit = Math.min(Number(page_size) || 20, 100);
|
||||
const offset = (Math.max(Number(page) || 1, 1) - 1) * limit;
|
||||
|
||||
const where = {};
|
||||
if (project_id) where.project_id = project_id;
|
||||
if (status) where.status = status;
|
||||
if (category) where.category = category;
|
||||
if (q) where.title = sequelize.where(sequelize.fn('LOWER', sequelize.col('title')), 'LIKE', `%${String(q).toLowerCase()}%`);
|
||||
const p = req.principal;
|
||||
const params = [];
|
||||
const cond = [];
|
||||
|
||||
const { rows, count } = await Doc.findAndCountAll({ where, limit, offset, order:[['created_at','DESC']] });
|
||||
res.json({ items: rows, total: count, page: Number(page), page_size: limit });
|
||||
});
|
||||
if (!p.is_superadmin) {
|
||||
if (project_id) {
|
||||
if (!p.inProject(Number(project_id)))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
cond.push("d.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
} else if (p.project_ids?.length) {
|
||||
cond.push(
|
||||
`d.project_id IN (${p.project_ids.map(() => "?").join(",")})`
|
||||
);
|
||||
params.push(...p.project_ids);
|
||||
}
|
||||
} else if (project_id) {
|
||||
cond.push("d.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
}
|
||||
|
||||
r.get('/documents/:id', requireAuth, async (req, res) => {
|
||||
const row = await Doc.findByPk(Number(req.params.id));
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
if (status) {
|
||||
cond.push("d.status=?");
|
||||
params.push(status);
|
||||
}
|
||||
if (category) {
|
||||
cond.push("d.category=?");
|
||||
params.push(category);
|
||||
}
|
||||
if (q) {
|
||||
cond.push("(LOWER(d.title) LIKE ? OR d.doc_no LIKE ?)");
|
||||
params.push(`%${String(q).toLowerCase()}%`, `%${q}%`);
|
||||
}
|
||||
|
||||
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
|
||||
const [[{ cnt }]] = await sql.query(
|
||||
`SELECT COUNT(*) AS cnt FROM documents d ${where}`,
|
||||
params
|
||||
);
|
||||
const [rows] = await sql.query(
|
||||
`SELECT d.* FROM documents d ${where} ORDER BY d.created_at DESC LIMIT ? OFFSET ?`,
|
||||
[...params, limit, offset]
|
||||
);
|
||||
res.json({ items: rows, total: cnt, page: Number(page), page_size: limit });
|
||||
}
|
||||
);
|
||||
|
||||
// GET
|
||||
r.get("/:id", requirePerm("documents.view"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT * FROM documents WHERE document_id=?",
|
||||
[id]
|
||||
);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
res.json(row);
|
||||
});
|
||||
|
||||
r.post('/documents', requireAuth, enrichPermissions(), requireProjectMembershipFromBody(), enrichPermissions(), requirePerm('document:create'), async (req, res) => {
|
||||
const { project_id, doc_no, title, category, status } = req.body || {};
|
||||
if (!project_id || !doc_no) return res.status(400).json({ error: 'project_id and doc_no required' });
|
||||
const created = await Doc.create({ project_id, doc_no, title, category, status, created_by: req.user?.user_id });
|
||||
res.status(201).json({ document_id: created.document_id });
|
||||
});
|
||||
// CREATE
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm("documents.manage", { projectParam: "project_id" }),
|
||||
async (req, res) => {
|
||||
const { project_id, doc_no, title, category, status } = req.body || {};
|
||||
if (!project_id || !doc_no)
|
||||
return res.status(400).json({ error: "project_id and doc_no required" });
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO documents (project_id, doc_no, title, category, status, created_by)
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
[
|
||||
project_id,
|
||||
doc_no,
|
||||
title || null,
|
||||
category || null,
|
||||
status || null,
|
||||
req.principal.user_id,
|
||||
]
|
||||
);
|
||||
res.status(201).json({ document_id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
// UPDATE
|
||||
r.patch("/:id", requirePerm("documents.manage"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT * FROM documents WHERE document_id=?",
|
||||
[id]
|
||||
);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
|
||||
r.patch('/documents/:id', requireAuth, enrichPermissions(), requireProjectMembershipByRecord({ modelLoader: (s)=> (await import('../db/models/Document.js')).default(s) }), enrichPermissions(), requirePerm('document:update'), async (req, res) => {
|
||||
const row = await Doc.findByPk(Number(req.params.id));
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
const { title, category, status } = req.body || {};
|
||||
if (title !== undefined) row.title = title;
|
||||
if (category !== undefined) row.category = category;
|
||||
if (status !== undefined) row.status = status;
|
||||
row.updated_by = req.user?.user_id;
|
||||
await row.save();
|
||||
await sql.query(
|
||||
"UPDATE documents SET title=?, category=?, status=?, updated_by=? WHERE document_id=?",
|
||||
[
|
||||
title ?? row.title,
|
||||
category ?? row.category,
|
||||
status ?? row.status,
|
||||
req.principal.user_id,
|
||||
id,
|
||||
]
|
||||
);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
r.delete('/documents/:id', requireAuth, enrichPermissions(), requireProjectMembershipByRecord({ modelLoader: (s)=> (await import('../db/models/Document.js')).default(s) }), enrichPermissions(), requirePerm('document:delete'), async (req, res) => {
|
||||
const row = await Doc.findByPk(Number(req.params.id));
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
await row.destroy();
|
||||
// DELETE
|
||||
r.delete("/:id", requirePerm("documents.manage"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT * FROM documents WHERE document_id=?",
|
||||
[id]
|
||||
);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
await sql.query("DELETE FROM documents WHERE document_id=?", [id]);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,124 +1,120 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/drawings.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, "drawings", "id");
|
||||
|
||||
// LIST
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm("drawing.read", { scope: "global" }),
|
||||
requirePerm("drawings.view", { projectParam: "project_id" }),
|
||||
async (req, res) => {
|
||||
const { project_id, org_id, code, q, limit = 50, offset = 0 } = req.query;
|
||||
const p = req.principal;
|
||||
const params = [];
|
||||
const cond = [];
|
||||
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
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 (!p.is_superadmin) {
|
||||
if (project_id) {
|
||||
if (!p.inProject(Number(project_id)))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
cond.push("d.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
} else if (p.project_ids?.length) {
|
||||
cond.push(
|
||||
`d.project_id IN (${p.project_ids.map(() => "?").join(",")})`
|
||||
);
|
||||
params.push(...p.project_ids);
|
||||
}
|
||||
} else if (project_id) {
|
||||
cond.push("d.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
}
|
||||
|
||||
if (org_id) {
|
||||
extra.push("d.org_id = :org_id");
|
||||
params.org_id = Number(org_id);
|
||||
cond.push("d.org_id=?");
|
||||
params.push(Number(org_id));
|
||||
}
|
||||
if (code) {
|
||||
extra.push("d.dwg_code = :code");
|
||||
params.code = code;
|
||||
cond.push("d.dwg_code=?");
|
||||
params.push(code);
|
||||
}
|
||||
if (q) {
|
||||
extra.push("(d.dwg_no LIKE :q OR d.title LIKE :q)");
|
||||
params.q = `%${q}%`;
|
||||
cond.push("(d.dwg_no LIKE ? OR d.title LIKE ?)");
|
||||
params.push(`%${q}%`, `%${q}%`);
|
||||
}
|
||||
|
||||
const where = [base.where, ...extra].filter(Boolean).join(" AND ");
|
||||
|
||||
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
|
||||
const [rows] = await sql.query(
|
||||
`SELECT d.* FROM drawings d WHERE ${where}
|
||||
ORDER BY d.id DESC LIMIT :limit OFFSET :offset`,
|
||||
params
|
||||
`SELECT d.* FROM drawings d ${where} ORDER BY d.id DESC LIMIT ? OFFSET ?`,
|
||||
[...params, Number(limit), Number(offset)]
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
// GET
|
||||
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" });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
r.get("/:id", requirePerm("drawings.view"), 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 p = req.principal;
|
||||
if (!p.is_superadmin && !p.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
res.json(row);
|
||||
});
|
||||
|
||||
// CREATE
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm("drawing.create", {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
requirePerm("drawings.upload", { projectParam: "project_id" }),
|
||||
async (req, res) => {
|
||||
const { org_id, project_id, dwg_no, dwg_code, title } = req.body;
|
||||
const { org_id, project_id, dwg_no, dwg_code, title } = req.body || {};
|
||||
if (!project_id || !dwg_no)
|
||||
return res.status(400).json({ error: "project_id and dwg_no required" });
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO drawings (org_id, project_id, dwg_no, dwg_code, title, created_by)
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
[org_id, project_id, dwg_no, dwg_code, title, req.principal.userId]
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
[
|
||||
org_id || null,
|
||||
project_id,
|
||||
dwg_no,
|
||||
dwg_code || null,
|
||||
title || null,
|
||||
req.principal.user_id,
|
||||
]
|
||||
);
|
||||
res.json({ id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
// UPDATE
|
||||
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]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
// UPDATE (ใช้สิทธิ์ drawings.upload)
|
||||
r.put("/:id", requirePerm("drawings.upload"), 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 p = req.principal;
|
||||
if (!p.is_superadmin && !p.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
const { title } = req.body || {};
|
||||
await sql.query("UPDATE drawings SET title=? WHERE id=?", [
|
||||
title ?? row.title,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
// DELETE
|
||||
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]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
r.delete("/:id", requirePerm("drawings.delete"), 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 p = req.principal;
|
||||
if (!p.is_superadmin && !p.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
await sql.query("DELETE FROM drawings WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,60 +1,47 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/files_extras.js
|
||||
// NOTE: generic file actions – ผูกสิทธิ์ตามโมดูลปลายทาง และบังคับ ABAC จาก project_id ของเรคคอร์ด
|
||||
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";
|
||||
import fs from "node:fs";
|
||||
import sql from "../db/index.js";
|
||||
|
||||
const r = Router();
|
||||
const Files = FileModel(sequelize);
|
||||
|
||||
async function projectForFile(rec) {
|
||||
const mod = rec.module;
|
||||
const refId = rec.ref_id;
|
||||
switch (mod) {
|
||||
// โปรเจ็คของไฟล์อิงโมดูล/ตารางอ้างอิง
|
||||
switch (rec.module) {
|
||||
case "rfa": {
|
||||
const M = (await import("../db/models/RFA.js")).default(sequelize);
|
||||
const row = await M.findByPk(refId);
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT project_id FROM rfas WHERE id=?",
|
||||
[rec.ref_id]
|
||||
);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
case "correspondence": {
|
||||
const M = (await import("../db/models/Correspondence.js")).default(
|
||||
sequelize
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT project_id FROM correspondences WHERE id=?",
|
||||
[rec.ref_id]
|
||||
);
|
||||
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);
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT project_id FROM drawings WHERE id=?",
|
||||
[rec.ref_id]
|
||||
);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
case "document": {
|
||||
const M = (await import("../db/models/Document.js")).default(sequelize);
|
||||
const row = await M.findByPk(refId);
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT project_id FROM documents WHERE document_id=?",
|
||||
[rec.ref_id]
|
||||
);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
case "transmittal": {
|
||||
const M = (await import("../db/models/Transmittal.js")).default(
|
||||
sequelize
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT project_id FROM transmittals WHERE id=?",
|
||||
[rec.ref_id]
|
||||
);
|
||||
const row = await M.findByPk(refId);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
default:
|
||||
@@ -62,90 +49,106 @@ async function projectForFile(rec) {
|
||||
}
|
||||
}
|
||||
|
||||
// HEAD meta only
|
||||
r.head("/files/:file_id", requireAuth, async (req, res) => {
|
||||
const rec = await Files.findByPk(Number(req.params.file_id));
|
||||
function permForFile(rec, action /* 'read'|'update'|'delete' */) {
|
||||
// map เป็น permission ของโมดูลจริง
|
||||
const m = rec.module;
|
||||
if (m === "document")
|
||||
return action === "read" ? "documents.view" : "documents.manage";
|
||||
if (m === "drawing")
|
||||
return action === "read"
|
||||
? "drawings.view"
|
||||
: action === "delete"
|
||||
? "drawings.delete"
|
||||
: "drawings.upload";
|
||||
if (m === "correspondence")
|
||||
return action === "read" ? "corr.view" : "corr.manage";
|
||||
if (m === "rfa") return action === "read" ? "rfas.view" : "rfas.respond";
|
||||
if (m === "transmittal") return "transmittals.manage";
|
||||
return "documents.manage"; // fallback
|
||||
}
|
||||
|
||||
// HEAD meta
|
||||
r.head("/files/:file_id", async (req, res) => {
|
||||
const id = Number(req.params.file_id);
|
||||
const [[rec]] = await sql.query("SELECT * FROM files WHERE file_id=?", [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.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" });
|
||||
}
|
||||
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" });
|
||||
}
|
||||
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) => {
|
||||
const rec = await Files.findByPk(Number(req.params.file_id));
|
||||
// DELETE
|
||||
r.delete("/files/:file_id", async (req, res) => {
|
||||
const id = Number(req.params.file_id);
|
||||
const [[rec]] = await sql.query("SELECT * FROM files WHERE file_id=?", [id]);
|
||||
if (!rec) return res.status(404).json({ error: "Not found" });
|
||||
|
||||
const p = req.principal;
|
||||
if (!p) return res.status(401).json({ error: "Unauthenticated" });
|
||||
|
||||
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" });
|
||||
if (!p.is_superadmin) {
|
||||
if (!pid || !p.inProject(pid))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
const need = permForFile(rec, "delete");
|
||||
if (!p.can?.(need))
|
||||
return res.status(403).json({ error: "FORBIDDEN", need });
|
||||
}
|
||||
|
||||
try {
|
||||
if (rec.disk_path) fs.unlinkSync(rec.disk_path);
|
||||
} catch {}
|
||||
await sql.query("DELETE FROM files WHERE file_id=?", [id]);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// RENAME (meta only)
|
||||
r.post("/files/:file_id/rename", async (req, res) => {
|
||||
const id = Number(req.params.file_id);
|
||||
const [[rec]] = await sql.query("SELECT * FROM files WHERE file_id=?", [id]);
|
||||
if (!rec) return res.status(404).json({ error: "Not found" });
|
||||
|
||||
const p = req.principal;
|
||||
if (!p) return res.status(401).json({ error: "Unauthenticated" });
|
||||
|
||||
const pid = await projectForFile(rec);
|
||||
if (!p.is_superadmin) {
|
||||
if (!pid || !p.inProject(pid))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
const need = permForFile(rec, "update");
|
||||
if (!p.can?.(need))
|
||||
return res.status(403).json({ error: "FORBIDDEN", need });
|
||||
}
|
||||
|
||||
const { orig_name } = req.body || {};
|
||||
if (!orig_name) return res.status(400).json({ error: "orig_name required" });
|
||||
await sql.query("UPDATE files SET orig_name=? WHERE file_id=?", [
|
||||
orig_name,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// refresh signed download URL – ปกติใช้ signed URL service ภายนอก; ที่นี่คืน URL ภายในเป็นตัวอย่าง
|
||||
r.post("/files/:file_id/refresh-url", async (req, res) => {
|
||||
const id = Number(req.params.file_id);
|
||||
const [[rec]] = await sql.query("SELECT * FROM files WHERE file_id=?", [id]);
|
||||
if (!rec) return res.status(404).json({ error: "Not found" });
|
||||
|
||||
const p = req.principal;
|
||||
if (!p) return res.status(401).json({ error: "Unauthenticated" });
|
||||
|
||||
const pid = await projectForFile(rec);
|
||||
if (!p.is_superadmin) {
|
||||
if (!pid || !p.inProject(pid))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
const need = permForFile(rec, "read");
|
||||
if (!p.can?.(need))
|
||||
return res.status(403).json({ error: "FORBIDDEN", need });
|
||||
}
|
||||
|
||||
const expSec = Number(process.env.FILE_URL_EXPIRES || 600);
|
||||
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}`;
|
||||
const download_url = `/api/files/${rec.file_id}?token=internal-placeholder&exp=${expSec}`;
|
||||
res.json({ download_url, expires_in: expSec });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
// FILE: src/routes/health.js
|
||||
// Health check route
|
||||
// - GET /health to check server and database status
|
||||
// - Requires appropriate permissions via requirePerm middleware
|
||||
|
||||
// FILE: backend/src/routes/health.js
|
||||
import { Router } from "express";
|
||||
import { sequelize } from "../db/sequelize.js";
|
||||
import sql from "../db/index.js";
|
||||
|
||||
const r = Router();
|
||||
|
||||
// /api/health — ไม่ต้องใช้สิทธิ์
|
||||
r.get("/health", async (_req, res) => {
|
||||
try {
|
||||
await sequelize.query("SELECT 1 AS ok");
|
||||
res.status(200).json({ ok: true, db: "up" });
|
||||
const [[{ now }]] = await sql.query("SELECT NOW() AS now");
|
||||
res.status(200).json({ ok: true, db: "up", now });
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, db: "down", error: String(e) });
|
||||
res
|
||||
.status(500)
|
||||
.json({ ok: false, db: "down", error: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
Volume in drive S is Container
|
||||
Volume Serial Number is 1F5F-1DEB
|
||||
|
||||
Directory of S:\backend\src\routes
|
||||
|
||||
11 Sep 25 16:34 <DIR> .
|
||||
11 Sep 25 16:33 <DIR> ..
|
||||
10 Sep 25 16:42 1,713 auth.js
|
||||
11 Sep 25 16:20 1,097 users.js
|
||||
11 Sep 25 16:20 2,624 correspondences.js
|
||||
08 Sep 25 08:58 1,306 rfa.js
|
||||
11 Sep 25 16:20 2,671 transmittals.js
|
||||
08 Sep 25 08:58 2,448 technicaldocs.js
|
||||
08 Sep 25 08:58 2,410 contractdwg.js
|
||||
08 Sep 25 08:46 2,234 admin.js
|
||||
10 Sep 25 16:42 359 health.js
|
||||
10 Sep 25 17:35 501 auth_extras.js
|
||||
10 Sep 25 18:18 2,980 documents.js
|
||||
11 Sep 25 16:20 2,848 drawings.js
|
||||
10 Sep 25 18:18 4,514 files_extras.js
|
||||
10 Sep 25 17:35 1,222 lookups.js
|
||||
10 Sep 25 18:18 4,285 maps.js
|
||||
10 Sep 25 17:35 630 module_files.js
|
||||
11 Sep 25 16:23 5,182 mvp.js
|
||||
10 Sep 25 17:35 729 ops.js
|
||||
11 Sep 25 16:20 1,197 organizations.js
|
||||
11 Sep 25 16:20 2,266 projects.js
|
||||
10 Sep 25 17:42 3,604 rbac_admin.js
|
||||
11 Sep 25 16:20 2,826 rfas.js
|
||||
10 Sep 25 17:53 2,376 subcategories.js
|
||||
11 Sep 25 16:20 2,090 uploads.js
|
||||
10 Sep 25 17:35 2,320 users_extras.js
|
||||
10 Sep 25 18:18 4,766 views.js
|
||||
11 Sep 25 16:20 1,199 volumes.js
|
||||
11 Sep 25 16:20 479 permissions.js
|
||||
11 Sep 25 16:20 2,751 contracts.js
|
||||
11 Sep 25 16:20 2,630 contract_dwg.js
|
||||
11 Sep 25 16:20 1,782 categories.js
|
||||
11 Sep 25 16:34 148 list.txt
|
||||
32 File(s) 70,187 bytes
|
||||
2 Dir(s) 3,725,382,303,744 bytes free
|
||||
@@ -1,158 +1,125 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/lookup.js
|
||||
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();
|
||||
|
||||
/**
|
||||
* ช่วยอ่าน query pick (คั่นด้วย comma)
|
||||
*/
|
||||
function parsePick(qs) {
|
||||
if (!qs) return null;
|
||||
return String(qs)
|
||||
.split(",")
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
// GET /api/lookup?pick=org,project,category,subcategory,volume,permission
|
||||
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",
|
||||
]
|
||||
);
|
||||
r.get("/", async (req, res) => {
|
||||
const picks = new Set(
|
||||
String(
|
||||
req.query.pick || "org,project,category,subcategory,volume,permission"
|
||||
)
|
||||
.split(",")
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const result = {};
|
||||
const out = {};
|
||||
|
||||
// 1) Organizations (scoped list) — require organization.read
|
||||
if (pick.has("org")) {
|
||||
// มีสิทธิ์ถึงจะดึง
|
||||
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",
|
||||
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
|
||||
);
|
||||
result.organizations = rows;
|
||||
} else {
|
||||
result.organizations = [];
|
||||
}
|
||||
// Organizations — GLOBAL (อ่านได้ด้วย organizations.view)
|
||||
if (picks.has("org")) {
|
||||
try {
|
||||
// มี perm ไหม? (GLOBAL)
|
||||
const ok =
|
||||
req.principal?.is_superadmin ||
|
||||
req.principal?.permissions?.has?.("organizations.view");
|
||||
out.organizations = ok
|
||||
? (
|
||||
await sql.query(
|
||||
"SELECT org_id, org_name FROM organizations ORDER BY org_name"
|
||||
)
|
||||
)[0]
|
||||
: [];
|
||||
} catch {
|
||||
out.organizations = [];
|
||||
}
|
||||
|
||||
// 2) Projects (scoped list) — require 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",
|
||||
permCode: PERM.project.read,
|
||||
preferProject: true,
|
||||
});
|
||||
const [rows] = await sql.query(
|
||||
`SELECT p.project_id, p.org_id, p.project_code, p.project_name
|
||||
FROM projects p WHERE ${where} ORDER BY p.project_name`,
|
||||
params
|
||||
);
|
||||
result.projects = rows;
|
||||
} else {
|
||||
result.projects = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Categories (global master) — require 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"
|
||||
);
|
||||
result.categories = rows;
|
||||
} else {
|
||||
result.categories = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Subcategories (global master) — require 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"
|
||||
);
|
||||
result.subcategories = rows;
|
||||
} else {
|
||||
result.subcategories = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 5) Volumes (global master) — require 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"
|
||||
);
|
||||
result.volumes = rows;
|
||||
} else {
|
||||
result.volumes = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 6) Permissions (global master) — require 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"
|
||||
);
|
||||
result.permissions = rows;
|
||||
} else {
|
||||
result.permissions = [];
|
||||
}
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
}
|
||||
);
|
||||
|
||||
// Projects — ORG scope (projects.view)
|
||||
if (picks.has("project")) {
|
||||
try {
|
||||
const ok =
|
||||
req.principal?.is_superadmin ||
|
||||
req.principal?.permissions?.has?.("projects.view");
|
||||
if (!ok) out.projects = [];
|
||||
else {
|
||||
// จำกัดตาม org scope
|
||||
const p = req.principal;
|
||||
let rows = [];
|
||||
if (p.is_superadmin) {
|
||||
[rows] = await sql.query(
|
||||
"SELECT project_id, org_id, project_code, project_name FROM projects ORDER BY project_name"
|
||||
);
|
||||
} else if (p.org_ids?.length) {
|
||||
const inSql = p.org_ids.map(() => "?").join(",");
|
||||
[rows] = await sql.query(
|
||||
`SELECT project_id, org_id, project_code, project_name
|
||||
FROM projects WHERE org_id IN (${inSql})
|
||||
ORDER BY project_name`,
|
||||
p.org_ids
|
||||
);
|
||||
} else {
|
||||
rows = [];
|
||||
}
|
||||
out.projects = rows;
|
||||
}
|
||||
} catch {
|
||||
out.projects = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Categories/Subcategories/Volumes — GLOBAL master (settings.manage ไม่จำเป็นสำหรับการอ่าน lookup)
|
||||
if (picks.has("category")) {
|
||||
try {
|
||||
out.categories = (
|
||||
await sql.query(
|
||||
"SELECT cat_id, cat_code, cat_name FROM categories ORDER BY cat_name"
|
||||
)
|
||||
)[0];
|
||||
} catch {
|
||||
out.categories = [];
|
||||
}
|
||||
}
|
||||
if (picks.has("subcategory")) {
|
||||
try {
|
||||
out.subcategories = (
|
||||
await sql.query(
|
||||
"SELECT sub_cat_id, cat_id, sub_cat_name FROM subcategories ORDER BY sub_cat_name"
|
||||
)
|
||||
)[0];
|
||||
} catch {
|
||||
out.subcategories = [];
|
||||
}
|
||||
}
|
||||
if (picks.has("volume")) {
|
||||
try {
|
||||
out.volumes = (
|
||||
await sql.query(
|
||||
"SELECT volume_id, volume_code, volume_name FROM volumes ORDER BY volume_code"
|
||||
)
|
||||
)[0];
|
||||
} catch {
|
||||
out.volumes = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Permissions — GLOBAL (settings.manage เท่านั้นที่ควรเห็นทั้งหมด)
|
||||
if (picks.has("permission")) {
|
||||
const ok =
|
||||
req.principal?.is_superadmin ||
|
||||
req.principal?.permissions?.has?.("settings.manage");
|
||||
out.permissions = ok
|
||||
? (
|
||||
await sql.query(
|
||||
"SELECT permission_id, perm_code AS permission_code, description FROM permissions ORDER BY perm_code"
|
||||
)
|
||||
)[0]
|
||||
: [];
|
||||
}
|
||||
|
||||
res.json(out);
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,149 +1,162 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/maps.js
|
||||
// Map ความสัมพันธ์ระหว่าง RFA<->Drawing และ Correspondence<->Document
|
||||
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";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
|
||||
const r = Router();
|
||||
const RFA = RfaModel(sequelize);
|
||||
const Drawing = DrawingModel(sequelize);
|
||||
const RfaDraw = RfaDrawMapModel(sequelize);
|
||||
const Corr = CorrModel(sequelize);
|
||||
const Doc = DocModel(sequelize);
|
||||
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 (isAdmin) return true;
|
||||
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;
|
||||
}
|
||||
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 (isAdmin) return true;
|
||||
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;
|
||||
}
|
||||
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) },
|
||||
});
|
||||
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 });
|
||||
}
|
||||
);
|
||||
|
||||
// Correspondence <-> Document
|
||||
// ========= RFA <-> Drawing =========
|
||||
// LIST
|
||||
r.get(
|
||||
"/maps/correspondence/:corr_id/documents",
|
||||
requireAuth,
|
||||
"/maps/rfa/:rfa_id/drawings",
|
||||
requirePerm("rfas.view", { projectParam: "project_id" }), // ABAC enforced เมื่อส่ง query project_id; ถ้าไม่ส่งเราจะตรวจจากเรคคอร์ด
|
||||
async (req, res) => {
|
||||
const rows = await CorrDoc.findAll({
|
||||
where: { correspondence_id: Number(req.params.corr_id) },
|
||||
});
|
||||
const rfa_id = Number(req.params.rfa_id);
|
||||
const [[rfa]] = await sql.query("SELECT project_id FROM rfas WHERE id=?", [
|
||||
rfa_id,
|
||||
]);
|
||||
if (!rfa) return res.status(404).json({ error: "RFA not found" });
|
||||
if (
|
||||
!req.principal.is_superadmin &&
|
||||
!req.principal.inProject(rfa.project_id)
|
||||
) {
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
}
|
||||
const [rows] = await sql.query(
|
||||
`SELECT m.* FROM rfa_drawing_map m WHERE m.rfa_id=? ORDER BY m.id DESC`,
|
||||
[rfa_id]
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
// ADD
|
||||
r.post(
|
||||
"/maps/correspondence/:corr_id/documents/:doc_id",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("correspondence:update"),
|
||||
"/maps/rfa/:rfa_id/drawings/:drawing_id",
|
||||
requirePerm("rfas.respond", { projectParam: "project_id" }),
|
||||
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 });
|
||||
const rfa_id = Number(req.params.rfa_id);
|
||||
const drawing_id = Number(req.params.drawing_id);
|
||||
|
||||
const [[rfa]] = await sql.query("SELECT project_id FROM rfas WHERE id=?", [
|
||||
rfa_id,
|
||||
]);
|
||||
if (!rfa) return res.status(404).json({ error: "RFA not found" });
|
||||
if (
|
||||
!req.principal.is_superadmin &&
|
||||
!req.principal.inProject(rfa.project_id)
|
||||
) {
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
}
|
||||
await sql.query(
|
||||
"INSERT IGNORE INTO rfa_drawing_map (rfa_id, drawing_id, created_by) VALUES (?,?,?)",
|
||||
[rfa_id, drawing_id, req.principal.user_id]
|
||||
);
|
||||
res.status(201).json({ ok: true });
|
||||
}
|
||||
);
|
||||
|
||||
// REMOVE
|
||||
r.delete(
|
||||
"/maps/rfa/:rfa_id/drawings/:drawing_id",
|
||||
requirePerm("rfas.respond"),
|
||||
async (req, res) => {
|
||||
const rfa_id = Number(req.params.rfa_id);
|
||||
const drawing_id = Number(req.params.drawing_id);
|
||||
const [[rfa]] = await sql.query("SELECT project_id FROM rfas WHERE id=?", [
|
||||
rfa_id,
|
||||
]);
|
||||
if (!rfa) return res.status(404).json({ error: "RFA not found" });
|
||||
if (
|
||||
!req.principal.is_superadmin &&
|
||||
!req.principal.inProject(rfa.project_id)
|
||||
) {
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
}
|
||||
const [rs] = await sql.query(
|
||||
"DELETE FROM rfa_drawing_map WHERE rfa_id=? AND drawing_id=?",
|
||||
[rfa_id, drawing_id]
|
||||
);
|
||||
res.json({ ok: rs.affectedRows > 0 });
|
||||
}
|
||||
);
|
||||
|
||||
// ========= Correspondence <-> Document =========
|
||||
r.get(
|
||||
"/maps/correspondence/:corr_id/documents",
|
||||
requirePerm("corr.view", { projectParam: "project_id" }),
|
||||
async (req, res) => {
|
||||
const corr_id = Number(req.params.corr_id);
|
||||
const [[corr]] = await sql.query(
|
||||
"SELECT project_id FROM correspondences WHERE id=?",
|
||||
[corr_id]
|
||||
);
|
||||
if (!corr)
|
||||
return res.status(404).json({ error: "Correspondence not found" });
|
||||
if (
|
||||
!req.principal.is_superadmin &&
|
||||
!req.principal.inProject(corr.project_id)
|
||||
) {
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
}
|
||||
const [rows] = await sql.query(
|
||||
`SELECT m.* FROM corr_document_map m WHERE m.correspondence_id=? ORDER BY m.id DESC`,
|
||||
[corr_id]
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
r.post(
|
||||
"/maps/correspondence/:corr_id/documents/:doc_id",
|
||||
requirePerm("corr.manage", { projectParam: "project_id" }),
|
||||
async (req, res) => {
|
||||
const corr_id = Number(req.params.corr_id);
|
||||
const doc_id = Number(req.params.doc_id);
|
||||
const [[corr]] = await sql.query(
|
||||
"SELECT project_id FROM correspondences WHERE id=?",
|
||||
[corr_id]
|
||||
);
|
||||
if (!corr)
|
||||
return res.status(404).json({ error: "Correspondence not found" });
|
||||
if (
|
||||
!req.principal.is_superadmin &&
|
||||
!req.principal.inProject(corr.project_id)
|
||||
) {
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
}
|
||||
await sql.query(
|
||||
"INSERT IGNORE INTO corr_document_map (correspondence_id, document_id, created_by) VALUES (?,?,?)",
|
||||
[corr_id, doc_id, req.principal.user_id]
|
||||
);
|
||||
res.status(201).json({ ok: true });
|
||||
}
|
||||
);
|
||||
|
||||
r.delete(
|
||||
"/maps/correspondence/:corr_id/documents/:doc_id",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("correspondence:update"),
|
||||
requirePerm("corr.manage"),
|
||||
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 });
|
||||
const corr_id = Number(req.params.corr_id);
|
||||
const doc_id = Number(req.params.doc_id);
|
||||
const [[corr]] = await sql.query(
|
||||
"SELECT project_id FROM correspondences WHERE id=?",
|
||||
[corr_id]
|
||||
);
|
||||
if (!corr)
|
||||
return res.status(404).json({ error: "Correspondence not found" });
|
||||
if (
|
||||
!req.principal.is_superadmin &&
|
||||
!req.principal.inProject(corr.project_id)
|
||||
) {
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
}
|
||||
const [rs] = await sql.query(
|
||||
"DELETE FROM corr_document_map WHERE correspondence_id=? AND document_id=?",
|
||||
[corr_id, doc_id]
|
||||
);
|
||||
res.json({ ok: rs.affectedRows > 0 });
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,25 +1,69 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/module_files.js
|
||||
import { Router } from "express";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
import { sequelize } from "../db/sequelize.js";
|
||||
import FileModel from "../db/models/FileObject.js";
|
||||
import sql from "../db/index.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"]],
|
||||
});
|
||||
// อ่านไฟล์ของแต่ละโมดูล โดยเช็ค ABAC + permission จาก principal
|
||||
function readPermFor(mod) {
|
||||
switch (mod) {
|
||||
case "rfa":
|
||||
return "rfas.view";
|
||||
case "correspondence":
|
||||
return "corr.view";
|
||||
case "drawing":
|
||||
return "drawings.view";
|
||||
case "document":
|
||||
return "documents.view";
|
||||
case "transmittal":
|
||||
return "transmittals.manage"; // โมดูลนี้ seed เป็น manage
|
||||
default:
|
||||
return "documents.view";
|
||||
}
|
||||
}
|
||||
async function projectOf(mod, id) {
|
||||
switch (mod) {
|
||||
case "rfa": {
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT project_id FROM rfas WHERE id=?",
|
||||
[id]
|
||||
);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
case "correspondence": {
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT project_id FROM correspondences WHERE id=?",
|
||||
[id]
|
||||
);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
case "drawing": {
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT project_id FROM drawings WHERE id=?",
|
||||
[id]
|
||||
);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
case "document": {
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT project_id FROM documents WHERE document_id=?",
|
||||
[id]
|
||||
);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
case "transmittal": {
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT project_id FROM transmittals WHERE id=?",
|
||||
[id]
|
||||
);
|
||||
return row?.project_id || null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// /:module(s)/:id/files
|
||||
for (const mod of [
|
||||
"rfa",
|
||||
"correspondence",
|
||||
@@ -27,9 +71,26 @@ for (const mod of [
|
||||
"document",
|
||||
"transmittal",
|
||||
]) {
|
||||
r.get(`/${mod}s/:id/files`, requireAuth, async (req, res) => {
|
||||
const items = await listBy(mod, Number(req.params.id));
|
||||
res.json(items);
|
||||
r.get(`/${mod}s/:id/files`, async (req, res) => {
|
||||
const ref_id = Number(req.params.id);
|
||||
const p = req.principal;
|
||||
if (!p) return res.status(401).json({ error: "Unauthenticated" });
|
||||
|
||||
const need = readPermFor(mod);
|
||||
if (!(p.is_superadmin || p.permissions?.has?.(need))) {
|
||||
return res.status(403).json({ error: "FORBIDDEN", need });
|
||||
}
|
||||
|
||||
const pid = await projectOf(mod, ref_id);
|
||||
if (!p.is_superadmin && (!pid || !p.inProject(pid))) {
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
}
|
||||
|
||||
const [rows] = await sql.query(
|
||||
`SELECT f.* FROM files f WHERE f.module=? AND f.ref_id=? ORDER BY f.file_id DESC`,
|
||||
[mod, ref_id]
|
||||
);
|
||||
res.json(rows);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/mvp.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, "entity_maps", "id");
|
||||
|
||||
// LIST
|
||||
// LIST — projects.view (ORG scope)
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm(PERM.map.read, { scope: "global" }),
|
||||
requirePerm("projects.view", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const {
|
||||
project_id,
|
||||
@@ -29,60 +19,55 @@ r.get(
|
||||
limit = 100,
|
||||
offset = 0,
|
||||
} = req.query;
|
||||
const p = req.principal;
|
||||
const params = [];
|
||||
const cond = [];
|
||||
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
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 (!p.is_superadmin) {
|
||||
if (org_id) {
|
||||
if (!p.inOrg(Number(org_id)))
|
||||
return res.status(403).json({ error: "FORBIDDEN_ORG" });
|
||||
cond.push("m.org_id=?");
|
||||
params.push(Number(org_id));
|
||||
} else if (p.org_ids?.length) {
|
||||
cond.push(`m.org_id IN (${p.org_ids.map(() => "?").join(",")})`);
|
||||
params.push(...p.org_ids);
|
||||
}
|
||||
} else if (org_id) {
|
||||
cond.push("m.org_id=?");
|
||||
params.push(Number(org_id));
|
||||
}
|
||||
if (org_id) {
|
||||
extra.push("m.org_id = :org_id");
|
||||
params.org_id = Number(org_id);
|
||||
|
||||
if (project_id) {
|
||||
cond.push("m.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
}
|
||||
if (module) {
|
||||
extra.push("m.module = :module");
|
||||
params.module = module;
|
||||
cond.push("m.module=?");
|
||||
params.push(module);
|
||||
}
|
||||
if (src_type) {
|
||||
extra.push("m.src_type = :src_type");
|
||||
params.src_type = src_type;
|
||||
cond.push("m.src_type=?");
|
||||
params.push(src_type);
|
||||
}
|
||||
if (dst_type) {
|
||||
extra.push("m.dst_type = :dst_type");
|
||||
params.dst_type = dst_type;
|
||||
cond.push("m.dst_type=?");
|
||||
params.push(dst_type);
|
||||
}
|
||||
|
||||
const where = [base.where, ...extra].filter(Boolean).join(" AND ");
|
||||
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
|
||||
const [rows] = await sql.query(
|
||||
`SELECT m.* FROM entity_maps m
|
||||
WHERE ${where}
|
||||
ORDER BY m.id DESC LIMIT :limit OFFSET :offset`,
|
||||
params
|
||||
`SELECT m.* FROM entity_maps m ${where} ORDER BY m.id DESC LIMIT ? OFFSET ?`,
|
||||
[...params, Number(limit), Number(offset)]
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
// CREATE
|
||||
// CREATE — projects.manage (ORG scope)
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm(PERM.map.create, {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
requirePerm("projects.manage", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const {
|
||||
org_id,
|
||||
@@ -93,30 +78,34 @@ r.post(
|
||||
dst_type,
|
||||
dst_id,
|
||||
remark,
|
||||
} = req.body;
|
||||
} = req.body || {};
|
||||
if (!org_id || !project_id || !module)
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "org_id, project_id, module required" });
|
||||
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,
|
||||
Number(org_id),
|
||||
Number(project_id),
|
||||
module,
|
||||
src_type,
|
||||
Number(src_id),
|
||||
dst_type,
|
||||
Number(dst_id),
|
||||
remark ?? null,
|
||||
req.principal.userId,
|
||||
src_type || null,
|
||||
src_id ? Number(src_id) : null,
|
||||
dst_type || null,
|
||||
dst_id ? Number(dst_id) : null,
|
||||
remark || null,
|
||||
req.principal.user_id,
|
||||
]
|
||||
);
|
||||
res.json({ id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
// DELETE (by id)
|
||||
// DELETE — projects.manage (ORG scope)
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm(PERM.map.delete, { scope: "org", getOrgId: OWN.getOrgIdById }),
|
||||
requirePerm("projects.manage", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query("DELETE FROM entity_maps WHERE id=?", [id]);
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/ops.js
|
||||
import { Router } from "express";
|
||||
import { sequelize } from "../db/sequelize.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import sql from "../db/index.js";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const r = Router();
|
||||
|
||||
r.get("/ready", async (_req, res) => {
|
||||
try {
|
||||
await sequelize.query("SELECT 1");
|
||||
return res.json({ ready: true });
|
||||
await sql.query("SELECT 1");
|
||||
res.json({ ready: true });
|
||||
} catch {
|
||||
return res.status(500).json({ ready: false });
|
||||
res.status(500).json({ ready: false });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,59 +1,52 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/organizations.js
|
||||
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" }),
|
||||
async (req, res) => {
|
||||
const { where, params } = buildScopeWhere(req.principal, {
|
||||
tableAlias: "o",
|
||||
orgColumn: "o.org_id",
|
||||
projectColumn: "NULL",
|
||||
permCode: "organization.read",
|
||||
});
|
||||
// LIST
|
||||
r.get("/", requirePerm("organizations.view"), async (_req, res) => {
|
||||
const [rows] = await sql.query(
|
||||
"SELECT * FROM organizations ORDER BY org_name"
|
||||
);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
const [rows] = await sql.query(
|
||||
`SELECT o.* FROM organizations o WHERE ${where}`,
|
||||
params
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
// GET
|
||||
r.get("/:id", requirePerm("organizations.view"), 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" });
|
||||
res.json(row);
|
||||
});
|
||||
|
||||
// GET by 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" });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
// CREATE / UPDATE / DELETE — settings.manage
|
||||
r.post("/", requirePerm("settings.manage"), async (req, res) => {
|
||||
const { org_name } = req.body || {};
|
||||
if (!org_name) return res.status(400).json({ error: "org_name required" });
|
||||
const [rs] = await sql.query(
|
||||
"INSERT INTO organizations (org_name) VALUES (?)",
|
||||
[org_name]
|
||||
);
|
||||
res.status(201).json({ org_id: rs.insertId });
|
||||
});
|
||||
r.put("/:id", requirePerm("settings.manage"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { org_name } = req.body || {};
|
||||
await sql.query("UPDATE organizations SET org_name=? WHERE org_id=?", [
|
||||
org_name,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
r.delete("/:id", requirePerm("settings.manage"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query("DELETE FROM organizations WHERE org_id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
// CREATE/UPDATE/DELETE ตามสิทธิ์ของคุณ (optional)
|
||||
export default r;
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/permissions.js
|
||||
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" }),
|
||||
async (req, res) => {
|
||||
const [rows] = await sql.query(
|
||||
"SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code"
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
// GLOBAL: settings.manage จึงเห็นได้ทั้งหมด
|
||||
r.get("/", requirePerm("settings.manage"), async (_req, res) => {
|
||||
const [rows] = await sql.query(
|
||||
"SELECT permission_id, perm_code AS permission_code, scope_level, description FROM permissions ORDER BY perm_code"
|
||||
);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,50 +1,49 @@
|
||||
// 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
|
||||
// FILE: backend/src/routes/projects.js
|
||||
|
||||
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 ของผู้ใช้
|
||||
// LIST — ORG scope
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm("project.read", { scope: "global" }),
|
||||
requirePerm("projects.view", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const { where, params } = buildScopeWhere(req.principal, {
|
||||
tableAlias: "p",
|
||||
orgColumn: "p.org_id",
|
||||
projectColumn: "p.project_id",
|
||||
permCode: "project.read",
|
||||
preferProject: true,
|
||||
});
|
||||
const p = req.principal;
|
||||
const { org_id } = req.query;
|
||||
const params = [];
|
||||
const cond = [];
|
||||
|
||||
if (!p.is_superadmin) {
|
||||
if (org_id) {
|
||||
if (!p.inOrg(Number(org_id)))
|
||||
return res.status(403).json({ error: "FORBIDDEN_ORG" });
|
||||
cond.push("p.org_id=?");
|
||||
params.push(Number(org_id));
|
||||
} else if (p.org_ids?.length) {
|
||||
cond.push(`p.org_id IN (${p.org_ids.map(() => "?").join(",")})`);
|
||||
params.push(...p.org_ids);
|
||||
}
|
||||
} else if (org_id) {
|
||||
cond.push("p.org_id=?");
|
||||
params.push(Number(org_id));
|
||||
}
|
||||
|
||||
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
|
||||
const [rows] = await sql.query(
|
||||
`SELECT p.* FROM projects p WHERE ${where}`,
|
||||
`SELECT p.* FROM projects p ${where} ORDER BY p.project_name`,
|
||||
params
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
// GET
|
||||
// GET — PROJECT scope
|
||||
r.get(
|
||||
"/:id",
|
||||
requirePerm("project.read", {
|
||||
scope: "project",
|
||||
getProjectId: async (req) => Number(req.params.id),
|
||||
}),
|
||||
requirePerm("projects.view", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query(
|
||||
@@ -52,54 +51,71 @@ r.get(
|
||||
[id]
|
||||
);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inOrg(row.org_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_ORG" });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
|
||||
// CREATE
|
||||
// CREATE — ORG scope
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm("project.create", {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
requirePerm("projects.manage", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const { org_id, project_code, project_name } = req.body;
|
||||
const { org_id, project_code, project_name } = req.body || {};
|
||||
if (!org_id || !project_code || !project_name) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "org_id, project_code, project_name required" });
|
||||
}
|
||||
const [rs] = await sql.query(
|
||||
"INSERT INTO projects (org_id, project_code, project_name) VALUES (?,?,?)",
|
||||
[org_id, project_code, project_name]
|
||||
"INSERT INTO projects (org_id, project_code, project_name, created_by) VALUES (?,?,?,?)",
|
||||
[Number(org_id), project_code, project_name, req.principal.user_id]
|
||||
);
|
||||
res.json({ project_id: rs.insertId });
|
||||
res.status(201).json({ project_id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
// UPDATE
|
||||
// UPDATE — ORG scope
|
||||
r.put(
|
||||
"/:id",
|
||||
requirePerm("project.update", {
|
||||
scope: "project",
|
||||
getProjectId: async (req) => Number(req.params.id),
|
||||
}),
|
||||
requirePerm("projects.manage", { orgParam: "org_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,
|
||||
]);
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT * FROM projects WHERE project_id=?",
|
||||
[id]
|
||||
);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const p = req.principal;
|
||||
if (!p.is_superadmin && !p.inOrg(row.org_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_ORG" });
|
||||
|
||||
const { project_name } = req.body || {};
|
||||
await sql.query(
|
||||
"UPDATE projects SET project_name=?, updated_by=? WHERE project_id=?",
|
||||
[project_name ?? row.project_name, req.principal.user_id, id]
|
||||
);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
// DELETE
|
||||
// DELETE — ORG scope
|
||||
r.delete(
|
||||
"/:id",
|
||||
requirePerm("project.delete", {
|
||||
scope: "project",
|
||||
getProjectId: async (req) => Number(req.params.id),
|
||||
}),
|
||||
requirePerm("projects.manage", { orgParam: "org_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 p = req.principal;
|
||||
if (!p.is_superadmin && !p.inOrg(row.org_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_ORG" });
|
||||
|
||||
await sql.query("DELETE FROM projects WHERE project_id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
|
||||
@@ -1,62 +1,47 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/rbac_admin.js
|
||||
// RBAC admin — ใช้ settings.manage ทั้งหมด
|
||||
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" }),
|
||||
async (req, res) => {
|
||||
const [rows] = await sql.query(
|
||||
"SELECT role_id, role_code, role_name, description FROM roles ORDER BY role_code"
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
// ROLES
|
||||
r.get("/roles", requirePerm("settings.manage"), async (_req, res) => {
|
||||
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" }),
|
||||
async (req, res) => {
|
||||
const [rows] = await sql.query(
|
||||
"SELECT permission_id, permission_code, description FROM permissions ORDER BY permission_code"
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
// PERMISSIONS
|
||||
r.get("/permissions", requirePerm("settings.manage"), async (_req, res) => {
|
||||
const [rows] = await sql.query(
|
||||
"SELECT permission_id, perm_code AS permission_code, scope_level, description FROM permissions ORDER BY perm_code"
|
||||
);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
/** LIST: role→permissions */
|
||||
// role -> permissions
|
||||
r.get(
|
||||
"/roles/:role_id/permissions",
|
||||
requirePerm(PERM.rbac_admin.read, { scope: "global" }),
|
||||
requirePerm("settings.manage"),
|
||||
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`,
|
||||
`SELECT p.permission_id, p.perm_code AS 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.perm_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" }),
|
||||
requirePerm("settings.manage"),
|
||||
async (req, res) => {
|
||||
const role_id = Number(req.params.role_id);
|
||||
const { permission_id } = req.body || {};
|
||||
@@ -70,7 +55,7 @@ r.post(
|
||||
|
||||
r.delete(
|
||||
"/roles/:role_id/permissions/:permission_id",
|
||||
requirePerm(PERM.rbac_admin.grant_perm, { scope: "global" }),
|
||||
requirePerm("settings.manage"),
|
||||
async (req, res) => {
|
||||
const role_id = Number(req.params.role_id);
|
||||
const permission_id = Number(req.params.permission_id);
|
||||
@@ -82,26 +67,25 @@ r.delete(
|
||||
}
|
||||
);
|
||||
|
||||
/** LIST: user→roles(+scope) */
|
||||
// user -> roles (global/org/project scope columns มีในตาราง user_roles ตามสคีมา)
|
||||
r.get(
|
||||
"/users/:user_id/roles",
|
||||
requirePerm(PERM.rbac_admin.read, { scope: "global" }),
|
||||
requirePerm("settings.manage"),
|
||||
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`,
|
||||
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" }),
|
||||
requirePerm("settings.manage"),
|
||||
async (req, res) => {
|
||||
const user_id = Number(req.params.user_id);
|
||||
const { role_id, org_id = null, project_id = null } = req.body || {};
|
||||
@@ -120,18 +104,20 @@ r.post(
|
||||
|
||||
r.delete(
|
||||
"/users/:user_id/roles",
|
||||
requirePerm(PERM.rbac_admin.assign_role, { scope: "global" }),
|
||||
requirePerm("settings.manage"),
|
||||
async (req, res) => {
|
||||
const user_id = Number(req.params.user_id);
|
||||
const { role_id, org_id = null, project_id = null } = req.body || {};
|
||||
// สร้างเงื่อนไขแบบ dynamic สำหรับ NULL-safe compare
|
||||
const whereOrg = org_id === null ? "ur.org_id IS NULL" : "ur.org_id = ?";
|
||||
const wherePrj =
|
||||
project_id === null ? "ur.project_id IS NULL" : "ur.project_id = ?";
|
||||
const params = [user_id, Number(role_id)];
|
||||
if (org_id !== null) params.push(Number(org_id));
|
||||
if (project_id !== null) params.push(Number(project_id));
|
||||
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]
|
||||
`DELETE FROM user_roles ur WHERE ur.user_id=? AND ur.role_id=? AND ${whereOrg} AND ${wherePrj}`,
|
||||
params
|
||||
);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/rfa.js
|
||||
// RFA: create + update-status ผ่าน stored procedures
|
||||
import { Router } from "express";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
import { requirePermission } from "../middleware/perm.js";
|
||||
import { callProc } from "../db/index.js";
|
||||
import sql, { callProc } from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
|
||||
const router = Router();
|
||||
const r = Router();
|
||||
|
||||
router.post(
|
||||
// CREATE (PROJECT scope) -> rfas.create
|
||||
r.post(
|
||||
"/create",
|
||||
requireAuth,
|
||||
requirePermission(["RFA_CREATE"], { projectRequired: true }),
|
||||
requirePerm("rfas.create", { projectParam: "project_id" }),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
@@ -31,20 +23,26 @@ router.post(
|
||||
pdf_path = null,
|
||||
item_doc_ids = [],
|
||||
} = req.body || {};
|
||||
const json = JSON.stringify(item_doc_ids.map(Number));
|
||||
|
||||
if (!project_id || !title) {
|
||||
return res.status(400).json({ error: "project_id and title required" });
|
||||
}
|
||||
|
||||
const json = JSON.stringify((item_doc_ids || []).map(Number));
|
||||
await callProc("sp_rfa_create_with_items", [
|
||||
req.user.user_id,
|
||||
req.principal.user_id,
|
||||
project_id,
|
||||
cor_status_id,
|
||||
cor_no,
|
||||
cor_status_id ?? null,
|
||||
cor_no ?? null,
|
||||
title,
|
||||
originator_id,
|
||||
recipient_id,
|
||||
originator_id ?? null,
|
||||
recipient_id ?? null,
|
||||
keywords,
|
||||
pdf_path,
|
||||
json,
|
||||
null,
|
||||
]);
|
||||
|
||||
res.status(201).json({ ok: true });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
@@ -52,15 +50,33 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
// UPDATE STATUS (PROJECT scope) -> rfas.respond
|
||||
r.post(
|
||||
"/update-status",
|
||||
requireAuth,
|
||||
requirePermission(["RFA_STATUS_UPDATE"], { projectRequired: true }),
|
||||
requirePerm("rfas.respond"),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const { rfa_corr_id, status_id, set_issue = 0 } = req.body || {};
|
||||
if (!rfa_corr_id || !status_id) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "rfa_corr_id and status_id required" });
|
||||
}
|
||||
// enforce ABAC: find project_id of the RFA
|
||||
const [[ref]] = await sql.query(
|
||||
"SELECT project_id FROM rfas WHERE id=? LIMIT 1",
|
||||
[Number(rfa_corr_id)]
|
||||
);
|
||||
if (!ref) return res.status(404).json({ error: "RFA not found" });
|
||||
if (
|
||||
!req.principal.is_superadmin &&
|
||||
!req.principal.inProject(ref.project_id)
|
||||
) {
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
}
|
||||
|
||||
await callProc("sp_rfa_update_status", [
|
||||
req.user.user_id,
|
||||
req.principal.user_id,
|
||||
rfa_corr_id,
|
||||
status_id,
|
||||
set_issue ? 1 : 0,
|
||||
@@ -72,4 +88,4 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
export default r;
|
||||
|
||||
@@ -1,270 +1,167 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/rfas.js
|
||||
// RFAs list/get/create/update/delete — มาตรฐาน Bearer + requirePerm
|
||||
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");
|
||||
|
||||
/* ----------------------------- 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"],
|
||||
]);
|
||||
|
||||
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";
|
||||
const [c, d] = String(sort).split(":");
|
||||
const col = ALLOWED_SORT.get(c) || "updated_at";
|
||||
const dir = (d || "desc").toLowerCase() === "asc" ? "ASC" : "DESC";
|
||||
return `\`${col}\` ${dir}`;
|
||||
}
|
||||
|
||||
function parsePaging({ page = 1, pageSize = 20 }) {
|
||||
function paging({ page = 1, pageSize = 20 }) {
|
||||
const p = Math.max(1, Number(page) || 1);
|
||||
const ps = Math.min(200, Math.max(1, Number(pageSize) || 20));
|
||||
return { limit: ps, offset: (p - 1) * ps, page: p, pageSize: ps };
|
||||
}
|
||||
|
||||
// ตัวกรองเพิ่มเติม (จาก rfas-1.js) + ผสมกับเงื่อนไข scope เดิม
|
||||
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')"
|
||||
);
|
||||
}
|
||||
return { where: parts.join(" AND "), params };
|
||||
}
|
||||
|
||||
/* -------------------------------- LIST --------------------------------
|
||||
GET /rfas
|
||||
- คง 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;
|
||||
// LIST (PROJECT scope enforced: filter ด้วย principal)
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm("rfas.view", { projectParam: "project_id" }),
|
||||
async (req, res) => {
|
||||
const { q, status, overdue, sort, page, pageSize, project_id } = req.query;
|
||||
const orderBy = parseSort(sort);
|
||||
const {
|
||||
limit,
|
||||
offset,
|
||||
page: p,
|
||||
pageSize: ps,
|
||||
} = parsePaging({ page, pageSize });
|
||||
const { limit, offset, page: p, pageSize: ps } = paging({ 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,
|
||||
});
|
||||
const P = req.principal;
|
||||
const cond = [];
|
||||
const params = [];
|
||||
|
||||
// extra filters
|
||||
const extra = buildExtraFilters({ q, status, overdue, project_id, org_id });
|
||||
if (!P.is_superadmin) {
|
||||
if (project_id) {
|
||||
const pid = Number(project_id);
|
||||
if (!P.inProject(pid))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
cond.push("r.project_id=?");
|
||||
params.push(pid);
|
||||
} else if (P.project_ids?.length) {
|
||||
cond.push(
|
||||
`r.project_id IN (${P.project_ids.map(() => "?").join(",")})`
|
||||
);
|
||||
params.push(...P.project_ids);
|
||||
}
|
||||
} else if (project_id) {
|
||||
cond.push("r.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
}
|
||||
|
||||
// รวม where
|
||||
const where =
|
||||
[base.where, extra.where].filter(Boolean).join(" AND ") || "1=1";
|
||||
const params = { ...base.params, ...extra.params, limit, offset };
|
||||
if (status) {
|
||||
cond.push("r.status=?");
|
||||
params.push(status);
|
||||
}
|
||||
if (q) {
|
||||
cond.push("(r.rfa_no LIKE ? OR r.title LIKE ? OR r.code LIKE ?)");
|
||||
params.push(`%${q}%`, `%${q}%`, `%${q}%`);
|
||||
}
|
||||
if (String(overdue) === "1") {
|
||||
cond.push(
|
||||
"r.due_date IS NOT NULL AND r.due_date < CURDATE() AND r.status NOT IN ('Closed','Approved')"
|
||||
);
|
||||
}
|
||||
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
|
||||
|
||||
// total
|
||||
const [[{ cnt: total }]] = await sql.query(
|
||||
`SELECT COUNT(*) AS cnt FROM rfas r WHERE ${where}`,
|
||||
const [[{ cnt }]] = await sql.query(
|
||||
`SELECT COUNT(*) AS cnt FROM rfas r ${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
|
||||
FROM rfas r
|
||||
WHERE ${where}
|
||||
ORDER BY ${orderBy}
|
||||
LIMIT :limit OFFSET :offset`,
|
||||
params
|
||||
`SELECT r.id, r.code, r.rfa_no, r.title, r.status, r.discipline, r.due_date, r.updated_at, r.project_id
|
||||
FROM rfas r ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`,
|
||||
[...params, limit, offset]
|
||||
);
|
||||
|
||||
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(cnt || 0), page: p, pageSize: ps });
|
||||
}
|
||||
);
|
||||
|
||||
// GET ONE
|
||||
r.get("/:id", requirePerm("rfas.view"), async (req, res) => {
|
||||
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 P = req.principal;
|
||||
if (!P.is_superadmin && !P.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
res.json(row);
|
||||
});
|
||||
|
||||
/* ------------------------------- GET ONE ------------------------------
|
||||
// ยึดรูปแบบตรวจสิทธิ์จาก rfas.js
|
||||
------------------------------------------------------------------------*/
|
||||
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" });
|
||||
res.json(row);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || "rfas/detail failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/* ------------------------------- CREATE -------------------------------
|
||||
// ยึดรูปแบบฟิลด์ของ rfas.js (org_id, project_id, rfa_no, title, status)
|
||||
// เพิ่ม validation เบื้องต้น (title required)
|
||||
------------------------------------------------------------------------*/
|
||||
// CREATE
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm("rfa.create", {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
requirePerm("rfas.create", { projectParam: "project_id" }),
|
||||
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" });
|
||||
|
||||
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())`,
|
||||
[org_id, project_id, rfa_no, title, st, req.principal.userId]
|
||||
);
|
||||
res.status(201).json({ id: rs.insertId });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || "rfas/create failed" });
|
||||
}
|
||||
const { org_id, project_id, rfa_no, title, status } = req.body || {};
|
||||
if (!project_id || !title)
|
||||
return res.status(400).json({ error: "project_id and title required" });
|
||||
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())`,
|
||||
[
|
||||
org_id ?? null,
|
||||
project_id,
|
||||
rfa_no ?? null,
|
||||
title,
|
||||
status ?? "draft",
|
||||
req.principal.user_id,
|
||||
]
|
||||
);
|
||||
res.status(201).json({ id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
/* ------------------------------- UPDATE -------------------------------
|
||||
// PUT: รูปแบบเดิมของ rfas.js (อัปเดต title, status)
|
||||
// PATCH: แบบยืดหยุ่น (บางฟิลด์) ผสานแนวทางจาก rfas-1.js
|
||||
------------------------------------------------------------------------*/
|
||||
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]
|
||||
);
|
||||
res.json({ ok: 1, id });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || "rfas/update failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
// UPDATE (respond/edit)
|
||||
r.patch("/:id", requirePerm("rfas.respond"), async (req, res) => {
|
||||
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 P = req.principal;
|
||||
if (!P.is_superadmin && !P.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
|
||||
// PATCH แบบ partial fields
|
||||
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 patch = {};
|
||||
for (const k of allowed) if (k in req.body) patch[k] = req.body[k];
|
||||
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)
|
||||
return res.status(400).json({ error: "no fields to update" });
|
||||
|
||||
if (Object.keys(patch).length === 0) {
|
||||
return res.status(400).json({ error: "no fields to update" });
|
||||
}
|
||||
const sets = Object.keys(patch).map((k) => `\`${k}\`=?`);
|
||||
await sql.query(
|
||||
`UPDATE rfas SET ${sets.join(", ")}, updated_at=NOW() WHERE id=?`,
|
||||
[...Object.values(patch), id]
|
||||
);
|
||||
res.json({ ok: 1, id });
|
||||
});
|
||||
|
||||
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 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`,
|
||||
patch
|
||||
);
|
||||
res.json({ ok: 1, id });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || "rfas/patch failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/* ------------------------------- DELETE ------------------------------- */
|
||||
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]);
|
||||
res.json({ ok: 1, id });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || "rfas/delete failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
// DELETE
|
||||
r.delete("/:id", requirePerm("rfas.delete"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query("SELECT project_id FROM rfas WHERE id=?", [
|
||||
id,
|
||||
]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const P = req.principal;
|
||||
if (!P.is_superadmin && !P.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
await sql.query("DELETE FROM rfas WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,91 +1,93 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/subcategories.js
|
||||
// Master data: subcategories — GLOBAL read/write (ตาม categories.js)
|
||||
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";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.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;
|
||||
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"]],
|
||||
});
|
||||
res.json({ items: rows, total: count, page: Number(page), page_size: limit });
|
||||
});
|
||||
|
||||
r.post(
|
||||
// LIST (GLOBAL read)
|
||||
r.get(
|
||||
"/sub_categories",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("lookup:edit"),
|
||||
requirePerm("organizations.view"),
|
||||
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,
|
||||
const { q, cat_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 cond = [];
|
||||
const params = [];
|
||||
if (cat_id) {
|
||||
cond.push("cat_id=?");
|
||||
params.push(Number(cat_id));
|
||||
}
|
||||
if (q) {
|
||||
cond.push("LOWER(sub_cat_name) LIKE ?");
|
||||
params.push(`%${String(q).toLowerCase()}%`);
|
||||
}
|
||||
|
||||
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
|
||||
const [[{ cnt }]] = await sql.query(
|
||||
`SELECT COUNT(*) AS cnt FROM subcategories ${where}`,
|
||||
params
|
||||
);
|
||||
const [rows] = await sql.query(
|
||||
`SELECT * FROM subcategories ${where} ORDER BY sub_cat_name ASC LIMIT ? OFFSET ?`,
|
||||
[...params, limit, offset]
|
||||
);
|
||||
res.json({
|
||||
items: rows,
|
||||
total: Number(cnt || 0),
|
||||
page: Number(page) || 1,
|
||||
page_size: limit,
|
||||
});
|
||||
res.status(201).json({ sub_cat_id: created.sub_cat_id });
|
||||
}
|
||||
);
|
||||
|
||||
// CREATE (GLOBAL write)
|
||||
r.post("/sub_categories", requirePerm("settings.manage"), async (req, res) => {
|
||||
const { cat_id, sub_cat_name, code } = req.body || {};
|
||||
if (!cat_id || !sub_cat_name)
|
||||
return res.status(400).json({ error: "cat_id and sub_cat_name required" });
|
||||
const [rs] = await sql.query(
|
||||
"INSERT INTO subcategories (cat_id, sub_cat_name, code) VALUES (?,?,?)",
|
||||
[Number(cat_id), sub_cat_name, code ?? null]
|
||||
);
|
||||
res.status(201).json({ sub_cat_id: rs.insertId });
|
||||
});
|
||||
|
||||
// UPDATE
|
||||
r.patch(
|
||||
"/sub_categories/:id",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("lookup:edit"),
|
||||
requirePerm("settings.manage"),
|
||||
async (req, res) => {
|
||||
const row = await SubCat.findByPk(Number(req.params.id));
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query(
|
||||
"SELECT * FROM subcategories WHERE sub_cat_id=?",
|
||||
[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();
|
||||
const { sub_cat_name, cat_id, code } = req.body || {};
|
||||
await sql.query(
|
||||
"UPDATE subcategories SET sub_cat_name=?, cat_id=?, code=? WHERE sub_cat_id=?",
|
||||
[
|
||||
sub_cat_name ?? row.sub_cat_name,
|
||||
cat_id ?? row.cat_id,
|
||||
code ?? row.code,
|
||||
id,
|
||||
]
|
||||
);
|
||||
res.json({ ok: true });
|
||||
}
|
||||
);
|
||||
|
||||
// DELETE
|
||||
r.delete(
|
||||
"/sub_categories/:id",
|
||||
requireAuth,
|
||||
enrichPermissions(),
|
||||
requirePerm("lookup:edit"),
|
||||
requirePerm("settings.manage"),
|
||||
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();
|
||||
const id = Number(req.params.id);
|
||||
await sql.query("DELETE FROM subcategories WHERE sub_cat_id=?", [id]);
|
||||
res.json({ ok: true });
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,222 +1,124 @@
|
||||
// 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';
|
||||
import { buildScopeWhere, ownerResolvers } from '../utils/scope.js';
|
||||
import PERM from '../config/permissions.js';
|
||||
// FILE: backend/src/routes/technicaldocs.js
|
||||
// แมปเป็นเอกสารประเภทหนึ่ง → ใช้สิทธิ์ documents.view/manage (PROJECT)
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
|
||||
const r = Router();
|
||||
const OWN = ownerResolvers(sql, 'technicaldocs', 'id');
|
||||
|
||||
// LIST (filter + scope)
|
||||
r.get('/',
|
||||
requirePerm(PERM.technicaldoc.read, { scope: 'global' }),
|
||||
async (req, res) => {
|
||||
const { project_id, org_id, status, q, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
tableAlias: 't',
|
||||
orgColumn: 't.org_id',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');
|
||||
|
||||
// LIST
|
||||
r.get('/',
|
||||
requirePerm(PERM.transmittal.read, { scope: 'global' }),
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm("documents.view", { projectParam: "project_id" }),
|
||||
async (req, res) => {
|
||||
const { project_id, org_id, tr_no, q, limit = 50, offset = 0 } = req.query;
|
||||
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
tableAlias: 't',
|
||||
orgColumn: 't.org_id',
|
||||
projectColumn: 't.project_id',
|
||||
permCode: PERM.transmittal.read,
|
||||
preferProject: true,
|
||||
});
|
||||
|
||||
const extra = [];
|
||||
const params = { ...base.params, limit: Number(limit), offset: Number(offset) };
|
||||
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) { extra.push('(t.tr_no LIKE :q OR t.subject LIKE :q)'); params.q = `%${q}%`; }
|
||||
|
||||
const where = [base.where, ...extra].filter(Boolean).join(' AND ');
|
||||
|
||||
const [rows] = await sql.query(
|
||||
`SELECT t.* FROM transmittals t
|
||||
WHERE ${where}
|
||||
ORDER BY t.id DESC
|
||||
LIMIT :limit OFFSET :offset`,
|
||||
params
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
// GET
|
||||
r.get('/:id',
|
||||
requirePerm(PERM.transmittal.read, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
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' });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
|
||||
// CREATE
|
||||
r.post('/',
|
||||
requirePerm(PERM.transmittal.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }),
|
||||
async (req, res) => {
|
||||
const { org_id, project_id, tr_no, subject, status } = req.body;
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO transmittals (org_id, project_id, tr_no, subject, status, created_by)
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
[org_id, project_id, tr_no, subject, status, req.principal.userId]
|
||||
);
|
||||
res.json({ id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
// UPDATE (รองรับ PATCH)
|
||||
r.patch('/:id',
|
||||
requirePerm(PERM.transmittal.update, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const fields = [];
|
||||
const { project_id, status, q, limit = 50, offset = 0 } = req.query;
|
||||
const P = req.principal;
|
||||
const cond = [];
|
||||
const params = [];
|
||||
|
||||
// อนุญาตแก้ฟิลด์หลัก
|
||||
const allow = ['tr_no','subject','status'];
|
||||
for (const k of allow) {
|
||||
if (k in req.body) {
|
||||
fields.push(`${k} = ?`);
|
||||
params.push(req.body[k]);
|
||||
if (!P.is_superadmin) {
|
||||
if (project_id) {
|
||||
const pid = Number(project_id);
|
||||
if (!P.inProject(pid))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
cond.push("t.project_id=?");
|
||||
params.push(pid);
|
||||
} else if (P.project_ids?.length) {
|
||||
cond.push(
|
||||
`t.project_id IN (${P.project_ids.map(() => "?").join(",")})`
|
||||
);
|
||||
params.push(...P.project_ids);
|
||||
}
|
||||
} else if (project_id) {
|
||||
cond.push("t.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
}
|
||||
if (!fields.length) return res.status(400).json({ error: 'NO_FIELDS' });
|
||||
|
||||
params.push(id);
|
||||
await sql.query(`UPDATE transmittals SET ${fields.join(', ')} WHERE id=?`, params);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
if (status) {
|
||||
cond.push("t.status=?");
|
||||
params.push(status);
|
||||
}
|
||||
if (q) {
|
||||
cond.push("(t.doc_no LIKE ? OR t.title LIKE ?)");
|
||||
params.push(`%${q}%`, `%${q}%`);
|
||||
}
|
||||
|
||||
// DELETE
|
||||
r.delete('/:id',
|
||||
requirePerm(PERM.transmittal.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('DELETE FROM transmittals WHERE id=?', [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
export default r;
|
||||
|
||||
projectColumn: 't.project_id',
|
||||
permCode: PERM.technicaldoc.read,
|
||||
preferProject: true,
|
||||
});
|
||||
|
||||
const extra = [];
|
||||
const params = { ...base.params, limit: Number(limit), offset: Number(offset) };
|
||||
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 (status) { extra.push('t.status = :status'); params.status = status; }
|
||||
if (q) { extra.push('(t.doc_no LIKE :q OR t.title LIKE :q)'); params.q = `%${q}%`; }
|
||||
|
||||
const where = [base.where, ...extra].filter(Boolean).join(' AND ');
|
||||
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
|
||||
const [rows] = await sql.query(
|
||||
`SELECT t.* FROM technicaldocs t WHERE ${where}
|
||||
ORDER BY t.id DESC LIMIT :limit OFFSET :offset`, params
|
||||
`SELECT t.* FROM technicaldocs t ${where} ORDER BY t.id DESC LIMIT ? OFFSET ?`,
|
||||
[...params, Number(limit), Number(offset)]
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
// GET
|
||||
r.get('/:id',
|
||||
requirePerm(PERM.technicaldoc.read, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query('SELECT * FROM technicaldocs WHERE id=?', [id]);
|
||||
if (!row) return res.status(404).json({ error: 'Not found' });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
r.get("/:id", requirePerm("documents.view"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query("SELECT * FROM technicaldocs WHERE id=?", [
|
||||
id,
|
||||
]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const P = req.principal;
|
||||
if (!P.is_superadmin && !P.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
res.json(row);
|
||||
});
|
||||
|
||||
// CREATE
|
||||
r.post('/',
|
||||
requirePerm(PERM.technicaldoc.create, { scope: 'org', getOrgId: async req => req.body?.org_id ?? null }),
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm("documents.manage", { projectParam: "project_id" }),
|
||||
async (req, res) => {
|
||||
const { org_id, project_id, doc_no, title, status } = req.body;
|
||||
const { org_id, project_id, doc_no, title, status } = req.body || {};
|
||||
if (!project_id || !doc_no)
|
||||
return res.status(400).json({ error: "project_id and doc_no required" });
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO technicaldocs (org_id, project_id, doc_no, title, status, created_by)
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
[org_id, project_id, doc_no, title, status, req.principal.userId]
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
[
|
||||
org_id ?? null,
|
||||
project_id,
|
||||
doc_no,
|
||||
title ?? null,
|
||||
status ?? null,
|
||||
req.principal.user_id,
|
||||
]
|
||||
);
|
||||
res.json({ id: rs.insertId });
|
||||
res.status(201).json({ id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
// UPDATE
|
||||
r.put('/:id',
|
||||
requirePerm(PERM.technicaldoc.update, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { title, status } = req.body;
|
||||
await sql.query('UPDATE technicaldocs SET title=?, status=? WHERE id=?', [title, status, id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
r.put("/:id", requirePerm("documents.manage"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query("SELECT * FROM technicaldocs WHERE id=?", [
|
||||
id,
|
||||
]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const P = req.principal;
|
||||
if (!P.is_superadmin && !P.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
const { title, status } = req.body || {};
|
||||
await sql.query("UPDATE technicaldocs SET title=?, status=? WHERE id=?", [
|
||||
title ?? row.title,
|
||||
status ?? row.status,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
// DELETE
|
||||
r.delete('/:id',
|
||||
requirePerm(PERM.technicaldoc.delete, { scope: 'org', getOrgId: OWN.getOrgIdById }),
|
||||
async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query('DELETE FROM technicaldocs WHERE id=?', [id]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
r.delete("/:id", requirePerm("documents.manage"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query("SELECT * FROM technicaldocs WHERE id=?", [
|
||||
id,
|
||||
]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
const P = req.principal;
|
||||
if (!P.is_superadmin && !P.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
await sql.query("DELETE FROM technicaldocs WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,281 +1,131 @@
|
||||
// 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)
|
||||
|
||||
// FILE: backend/src/routes/transmittals.js
|
||||
// ทั้งโมดูลใช้สิทธิ์เดียว: transmittals.manage (PROJECT)
|
||||
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");
|
||||
|
||||
/* ----------------------------- Utilities ----------------------------- */
|
||||
// จำกัดฟิลด์ที่อนุญาตให้ sort เพื่อลดความเสี่ยง SQLi
|
||||
const ALLOWED_SORT = new Map([
|
||||
["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";
|
||||
return `\`${col}\` ${dir}`;
|
||||
}
|
||||
function parsePaging({ page = 1, pageSize = 20 }) {
|
||||
const p = Math.max(1, Number(page) || 1);
|
||||
const ps = Math.min(200, Math.max(1, Number(pageSize) || 20));
|
||||
return { limit: ps, offset: (p - 1) * ps, page: p, pageSize: ps };
|
||||
}
|
||||
|
||||
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 (q) {
|
||||
// ใช้ฟิลด์พื้นฐานที่ transmittals.js มีแน่นอน (tr_no, subject)
|
||||
extra.push("(t.tr_no LIKE :q OR t.subject LIKE :q)");
|
||||
params.q = `%${q}%`;
|
||||
}
|
||||
return { where: extra.join(" AND "), params };
|
||||
}
|
||||
|
||||
/* -------------------------------- LIST --------------------------------
|
||||
GET /transmittals
|
||||
- คง RBAC/Scope เดิม (global + project/org scope ผ่าน buildScopeWhere)
|
||||
- เพิ่ม sort/page/pageSize/q ตามสไตล์ transmittals-1.js และตอบ meta
|
||||
------------------------------------------------------------------------*/
|
||||
// LIST
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm(PERM.transmittal.read, { scope: "global" }),
|
||||
requirePerm("transmittals.manage", { projectParam: "project_id" }),
|
||||
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 { project_id, tr_no, q, limit = 50, offset = 0 } = req.query;
|
||||
const P = req.principal;
|
||||
const cond = [];
|
||||
const params = [];
|
||||
|
||||
const base = buildScopeWhere(req.principal, {
|
||||
tableAlias: "t",
|
||||
orgColumn: "t.org_id",
|
||||
projectColumn: "t.project_id",
|
||||
permCode: PERM.transmittal.read,
|
||||
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 params = { ...base.params, ...extra.params, limit, offset };
|
||||
|
||||
// total
|
||||
const [[{ cnt: total }]] = await sql.query(
|
||||
`SELECT COUNT(*) AS cnt FROM transmittals t WHERE ${where}`,
|
||||
params
|
||||
);
|
||||
|
||||
// rows
|
||||
const [rows] = await sql.query(
|
||||
`SELECT t.*
|
||||
FROM transmittals t
|
||||
WHERE ${where}
|
||||
ORDER BY ${orderBy}
|
||||
LIMIT :limit OFFSET :offset`,
|
||||
params
|
||||
);
|
||||
|
||||
res.json({
|
||||
data: rows,
|
||||
total: Number(total || 0),
|
||||
page: p,
|
||||
pageSize: ps,
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || "transmittals/list failed" });
|
||||
if (!P.is_superadmin) {
|
||||
if (project_id) {
|
||||
const pid = Number(project_id);
|
||||
if (!P.inProject(pid))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
cond.push("t.project_id=?");
|
||||
params.push(pid);
|
||||
} else if (P.project_ids?.length) {
|
||||
cond.push(
|
||||
`t.project_id IN (${P.project_ids.map(() => "?").join(",")})`
|
||||
);
|
||||
params.push(...P.project_ids);
|
||||
}
|
||||
} else if (project_id) {
|
||||
cond.push("t.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
}
|
||||
|
||||
if (tr_no) {
|
||||
cond.push("t.tr_no=?");
|
||||
params.push(tr_no);
|
||||
}
|
||||
if (q) {
|
||||
cond.push("(t.tr_no LIKE ? OR t.subject LIKE ?)");
|
||||
params.push(`%${q}%`, `%${q}%`);
|
||||
}
|
||||
|
||||
const where = cond.length ? `WHERE ${cond.join(" AND ")}` : "";
|
||||
const [rows] = await sql.query(
|
||||
`SELECT t.* FROM transmittals t ${where} ORDER BY t.id DESC LIMIT ? OFFSET ?`,
|
||||
[...params, Number(limit), Number(offset)]
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
/* ------------------------------- GET ONE ------------------------------ */
|
||||
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" });
|
||||
res.json(row);
|
||||
} catch (e) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: e.message || "transmittals/detail failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
// GET
|
||||
r.get("/:id", requirePerm("transmittals.manage"), async (req, res) => {
|
||||
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 P = req.principal;
|
||||
if (!P.is_superadmin && !P.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
res.json(row);
|
||||
});
|
||||
|
||||
/* -------------------------------- CREATE ------------------------------ */
|
||||
// CREATE
|
||||
r.post(
|
||||
"/",
|
||||
requirePerm(PERM.transmittal.create, {
|
||||
scope: "org",
|
||||
getOrgId: async (req) => req.body?.org_id ?? null,
|
||||
}),
|
||||
requirePerm("transmittals.manage", { projectParam: "project_id" }),
|
||||
async (req, res) => {
|
||||
try {
|
||||
// ยึดสคีมาหลักจาก transmittals.js
|
||||
const { org_id, project_id, tr_no, subject, status } = req.body;
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO transmittals (org_id, project_id, tr_no, subject, status, created_by)
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
[org_id, project_id, tr_no, subject, status, req.principal.userId]
|
||||
);
|
||||
res.status(201).json({ id: rs.insertId });
|
||||
} catch (e) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: e.message || "transmittals/create failed" });
|
||||
}
|
||||
const { org_id, project_id, tr_no, subject, status } = req.body || {};
|
||||
if (!project_id || !tr_no)
|
||||
return res.status(400).json({ error: "project_id and tr_no required" });
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO transmittals (org_id, project_id, tr_no, subject, status, created_by)
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
[
|
||||
org_id ?? null,
|
||||
project_id,
|
||||
tr_no,
|
||||
subject ?? null,
|
||||
status ?? null,
|
||||
req.principal.user_id,
|
||||
]
|
||||
);
|
||||
res.status(201).json({ id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
/* -------------------------------- UPDATE ------------------------------ */
|
||||
// PUT: รูปแบบเดิม (อัปเดต subject, status)
|
||||
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]
|
||||
);
|
||||
res.json({ ok: 1 });
|
||||
} catch (e) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: e.message || "transmittals/update failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
// UPDATE
|
||||
r.patch("/:id", requirePerm("transmittals.manage"), async (req, res) => {
|
||||
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 P = req.principal;
|
||||
if (!P.is_superadmin && !P.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
|
||||
// PATCH: อัปเดตบางฟิลด์ (ไม่บังคับคอลัมน์ใหม่ใน DB — จะอัปเดตก็ต่อเมื่อ body ส่งมา)
|
||||
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"];
|
||||
const patch = {};
|
||||
for (const k of allowed) if (k in req.body) patch[k] = req.body[k];
|
||||
if (!Object.keys(patch).length)
|
||||
return res.status(400).json({ error: "no fields to update" });
|
||||
|
||||
// ถ้าฐานข้อมูลของคุณมีคอลัมน์เพิ่ม เช่น to_party, sent_date, description
|
||||
// และต้องการอัปเดตด้วย ให้เพิ่มชื่อคอลัมน์เข้าไปใน allowed ได้
|
||||
// const allowed = ['tr_no','subject','status','to_party','sent_date','description'];
|
||||
const sets = Object.keys(patch).map((k) => `\`${k}\`=?`);
|
||||
await sql.query(`UPDATE transmittals SET ${sets.join(", ")} WHERE id=?`, [
|
||||
...Object.values(patch),
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
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" });
|
||||
}
|
||||
|
||||
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 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`,
|
||||
patch
|
||||
);
|
||||
res.json({ ok: 1, id });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message || "transmittals/patch failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/* -------------------------------- DELETE ------------------------------ */
|
||||
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]);
|
||||
res.json({ ok: 1 });
|
||||
} catch (e) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: e.message || "transmittals/delete failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
// DELETE
|
||||
r.delete("/:id", requirePerm("transmittals.manage"), async (req, res) => {
|
||||
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 P = req.principal;
|
||||
if (!P.is_superadmin && !P.inProject(row.project_id))
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
await sql.query("DELETE FROM transmittals WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,69 +1,46 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/uploads.js
|
||||
// อัปโหลดไฟล์ผูกกับโมดูล (PROJECT scope): documents/drawings/correspondences/rfas/transmittals
|
||||
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) {
|
||||
const ensureDir = (p) => {
|
||||
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchRef(module, id) {
|
||||
const tbl = String(module);
|
||||
const idCol = "id";
|
||||
const [[row]] = await sql.query(
|
||||
`SELECT org_id, project_id, created_at FROM ${tbl} WHERE ${idCol}=?`,
|
||||
[Number(id)]
|
||||
);
|
||||
return row || null;
|
||||
}
|
||||
function sanitize(name) {
|
||||
// แทนที่อักขระไม่ปลอดภัย
|
||||
return String(name).replace(/[^A-Za-z0-9._-]+/g, "_");
|
||||
}
|
||||
const storage = multer.diskStorage({
|
||||
destination: async (req, file, cb) => {
|
||||
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 ref = await fetchRef(module, id);
|
||||
if (!ref) return cb(new Error("Resource not found"));
|
||||
const dt = new Date(ref.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),
|
||||
String(ref.org_id || "0"),
|
||||
String(ref.project_id || "0"),
|
||||
ym
|
||||
);
|
||||
ensureDir(dir);
|
||||
@@ -72,23 +49,21 @@ const storage = multer.diskStorage({
|
||||
cb(e);
|
||||
}
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const ts = Date.now();
|
||||
const safe = file.originalname.replace(/[\^\w.\-]+/g, "_");
|
||||
cb(null, `${ts}__${safe}`);
|
||||
},
|
||||
filename: (_req, file, cb) =>
|
||||
cb(null, `${Date.now()}__${sanitize(file.originalname)}`),
|
||||
});
|
||||
const upload = multer({ storage });
|
||||
|
||||
const PERM_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;
|
||||
// map module -> permission
|
||||
function uploadPerm(module) {
|
||||
if (module === "documents") return "documents.manage";
|
||||
if (module === "drawings") return "drawings.upload";
|
||||
if (module === "correspondences") return "corr.manage";
|
||||
if (module === "rfas") return "rfas.respond";
|
||||
if (module === "transmittals") return "transmittals.manage";
|
||||
return null;
|
||||
}
|
||||
async function refProjectId(module, id) {
|
||||
const [[row]] = await sql.query(
|
||||
`SELECT project_id FROM ${module} WHERE id=?`,
|
||||
[Number(id)]
|
||||
@@ -99,12 +74,20 @@ async function getProjectIdByModule(req) {
|
||||
r.post(
|
||||
"/:module/:id/file",
|
||||
(req, res, next) => {
|
||||
const perm = PERM_UPLOAD[req.params.module];
|
||||
const perm = uploadPerm(req.params.module);
|
||||
if (!perm) return res.status(400).json({ error: "Unsupported module" });
|
||||
return requirePerm(perm, {
|
||||
scope: "project",
|
||||
getProjectId: getProjectIdByModule,
|
||||
})(req, res, next);
|
||||
return requirePerm(perm, { projectParam: undefined })(req, res, next);
|
||||
},
|
||||
async (req, res, next) => {
|
||||
// ABAC: ตรวจ project scope ของ record
|
||||
const pid = await refProjectId(req.params.module, req.params.id);
|
||||
if (
|
||||
!req.principal.is_superadmin &&
|
||||
(!pid || !req.principal.inProject(pid))
|
||||
) {
|
||||
return res.status(403).json({ error: "FORBIDDEN_PROJECT" });
|
||||
}
|
||||
next();
|
||||
},
|
||||
upload.single("file"),
|
||||
async (req, res) => {
|
||||
|
||||
@@ -1,51 +1,55 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/users.js
|
||||
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
|
||||
// ME (ทุกคน)
|
||||
r.get("/me", async (req, res) => {
|
||||
const p = req.principal;
|
||||
const [[u]] = await sql.query(
|
||||
"SELECT user_id, username, email, first_name, last_name FROM users WHERE user_id=?",
|
||||
[req.principal.userId]
|
||||
`SELECT user_id, username, email, first_name, last_name, org_id FROM users WHERE user_id=?`,
|
||||
[p.user_id]
|
||||
);
|
||||
if (!u) return res.status(404).json({ error: "User not found" });
|
||||
|
||||
// roles in plain
|
||||
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]
|
||||
`SELECT r.role_code, r.role_name, ur.org_id, ur.project_id
|
||||
FROM user_roles ur JOIN roles r ON r.role_id = ur.role_id
|
||||
WHERE ur.user_id=?`,
|
||||
[p.user_id]
|
||||
);
|
||||
|
||||
res.json({ ...u, roles, role_codes: [...req.principal.roleCodes] });
|
||||
res.json({
|
||||
...u,
|
||||
roles,
|
||||
role_codes: roles.map((r) => r.role_code),
|
||||
permissions: [...(p.permissions || [])],
|
||||
project_ids: p.project_ids,
|
||||
org_ids: p.org_ids,
|
||||
is_superadmin: p.is_superadmin,
|
||||
});
|
||||
});
|
||||
|
||||
// (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);
|
||||
});
|
||||
// USERS LIST (ORG scope) — admin.access
|
||||
r.get(
|
||||
"/",
|
||||
requirePerm("admin.access", { orgParam: "org_id" }),
|
||||
async (req, res) => {
|
||||
const P = req.principal;
|
||||
let rows = [];
|
||||
if (P.is_superadmin) {
|
||||
[rows] = await sql.query(
|
||||
"SELECT user_id, username, email, org_id FROM users ORDER BY user_id DESC LIMIT 500"
|
||||
);
|
||||
} else if (P.org_ids?.length) {
|
||||
const inSql = P.org_ids.map(() => "?").join(",");
|
||||
[rows] = await sql.query(
|
||||
`SELECT user_id, username, email, org_id FROM users WHERE org_id IN (${inSql}) ORDER BY user_id DESC LIMIT 500`,
|
||||
P.org_ids
|
||||
);
|
||||
}
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,93 +1,39 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/users_extras.js
|
||||
// NOTE: ของเดิมใช้ cookie + Sequelize -> ปรับให้อยู่หลัง Bearer stack และจำกัดความสามารถ
|
||||
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";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.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) => {
|
||||
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" });
|
||||
/**
|
||||
* PATCH /users/:id/password
|
||||
* เฉพาะผู้มี settings.manage (GLOBAL) — (คำเตือน: ต้องมีระบบ hash/rotate ที่ service auth)
|
||||
*/
|
||||
r.patch(
|
||||
"/users/:id/password",
|
||||
requirePerm("settings.manage"),
|
||||
async (_req, res) => {
|
||||
// ในโปรเจคนี้การเปลี่ยนรหัสผ่านควรวิ่งที่ auth service/procedure โดยเฉพาะ
|
||||
return res
|
||||
.status(501)
|
||||
.json({ error: "Not implemented here. Use auth service." });
|
||||
}
|
||||
);
|
||||
|
||||
const { new_password } = req.body || {};
|
||||
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" });
|
||||
|
||||
row.password_hash = await hashPassword(new_password);
|
||||
await row.save();
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// 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"],
|
||||
});
|
||||
/**
|
||||
* GET /users/me/projects — สรุปโปรเจ็ค/บทบาทของผู้ใช้
|
||||
*/
|
||||
r.get("/users/me/projects", async (req, res) => {
|
||||
const uid = req.principal.user_id;
|
||||
const [rows] = await sql.query(
|
||||
`SELECT upr.project_id, r.role_code, r.role_name
|
||||
FROM user_project_roles upr
|
||||
JOIN roles r ON r.role_id = upr.role_id
|
||||
WHERE upr.user_id=? ORDER BY upr.project_id`,
|
||||
[uid]
|
||||
);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// my projects/roles
|
||||
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" });
|
||||
const rows = await UPR.findAll({ where: { user_id } });
|
||||
// Optionally join project names
|
||||
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,
|
||||
}));
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,178 +1,100 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/view.js
|
||||
// Saved Views: อ่านด้วย reports.view (GLOBAL); เขียนด้วย settings.manage
|
||||
import { Router } from "express";
|
||||
import sql from "../db/index.js";
|
||||
import { requirePerm } from "../middleware/requirePerm.js";
|
||||
import { buildScopeWhere, ownerResolvers } from "../utils/scope.js";
|
||||
import PERM from "../config/permissions.js";
|
||||
|
||||
const r = Router();
|
||||
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" }),
|
||||
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",
|
||||
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}%`;
|
||||
}
|
||||
|
||||
// ให้ผู้ใช้เห็นของตัวเองเสมอ + ของที่อยู่ใน scope
|
||||
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
|
||||
);
|
||||
res.json(rows);
|
||||
// LIST (ทุกคนที่มี reports.view)
|
||||
r.get("/", requirePerm("reports.view"), async (req, res) => {
|
||||
const { project_id, shared = "1", q, limit = 50, offset = 0 } = req.query;
|
||||
const p = req.principal;
|
||||
const cond = [];
|
||||
const params = [];
|
||||
// ให้เห็นของตัวเองเสมอ + shared
|
||||
cond.push("(v.is_shared=1 OR v.owner_user_id=?)");
|
||||
params.push(p.user_id);
|
||||
if (project_id) {
|
||||
cond.push("v.project_id=?");
|
||||
params.push(Number(project_id));
|
||||
}
|
||||
if (q) {
|
||||
cond.push("v.name LIKE ?");
|
||||
params.push(`%${q}%`);
|
||||
}
|
||||
if (shared === "0") {
|
||||
cond.push("v.is_shared=0");
|
||||
}
|
||||
);
|
||||
|
||||
// GET by id
|
||||
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=?", [
|
||||
const where = `WHERE ${cond.join(" AND ")}`;
|
||||
const [rows] = await sql.query(
|
||||
`SELECT v.* FROM saved_views v ${where} ORDER BY v.id DESC LIMIT ? OFFSET ?`,
|
||||
[...params, Number(limit), Number(offset)]
|
||||
);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// GET
|
||||
r.get("/:id", requirePerm("reports.view"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const [[row]] = await sql.query("SELECT * FROM saved_views WHERE id=?", [id]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
if (
|
||||
!(
|
||||
row.is_shared ||
|
||||
row.owner_user_id === req.principal.user_id ||
|
||||
req.principal.is_superadmin
|
||||
)
|
||||
) {
|
||||
return res.status(403).json({ error: "FORBIDDEN" });
|
||||
}
|
||||
res.json(row);
|
||||
});
|
||||
|
||||
// CREATE / UPDATE / DELETE (ต้องมี settings.manage)
|
||||
r.post("/", requirePerm("settings.manage"), async (req, res) => {
|
||||
const {
|
||||
org_id,
|
||||
project_id,
|
||||
name,
|
||||
payload_json,
|
||||
is_shared = 0,
|
||||
} = req.body || {};
|
||||
const [rs] = await sql.query(
|
||||
`INSERT INTO saved_views (org_id, project_id, name, payload_json, is_shared, owner_user_id)
|
||||
VALUES (?,?,?,?,?,?)`,
|
||||
[
|
||||
org_id ?? null,
|
||||
project_id ?? null,
|
||||
name ?? "",
|
||||
JSON.stringify(payload_json ?? {}),
|
||||
Number(is_shared) ? 1 : 0,
|
||||
req.principal.user_id,
|
||||
]
|
||||
);
|
||||
res.status(201).json({ id: rs.insertId });
|
||||
});
|
||||
|
||||
r.put("/:id", requirePerm("settings.manage"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { name, payload_json, is_shared } = req.body || {};
|
||||
await sql.query(
|
||||
"UPDATE saved_views SET name=?, payload_json=?, is_shared=? WHERE id=?",
|
||||
[
|
||||
name ?? null,
|
||||
JSON.stringify(payload_json ?? {}),
|
||||
Number(is_shared) ? 1 : 0,
|
||||
id,
|
||||
]);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
res.json(row);
|
||||
}
|
||||
);
|
||||
]
|
||||
);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
// CREATE
|
||||
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,
|
||||
]
|
||||
);
|
||||
res.json({ id: rs.insertId });
|
||||
}
|
||||
);
|
||||
|
||||
// UPDATE (เฉพาะใน scope และถ้าเป็นของตนเอง หรือเป็นแอดมินตามนโยบาย)
|
||||
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" });
|
||||
// ถ้าจะจำกัดเฉพาะเจ้าของ: 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=?",
|
||||
[name, JSON.stringify(payload_json ?? {}), Number(is_shared) ? 1 : 0, id]
|
||||
);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
|
||||
// DELETE
|
||||
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]);
|
||||
res.json({ ok: 1 });
|
||||
}
|
||||
);
|
||||
r.delete("/:id", requirePerm("settings.manage"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query("DELETE FROM saved_views WHERE id=?", [id]);
|
||||
res.json({ ok: 1 });
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,47 +1,32 @@
|
||||
// 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
|
||||
|
||||
// FILE: backend/src/routes/views.js
|
||||
// จำกัดเฉพาะแอดมินระบบ: settings.manage
|
||||
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";
|
||||
|
||||
// LIST views
|
||||
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
|
||||
r.get("/", requirePerm("settings.manage"), 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]
|
||||
);
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
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" }),
|
||||
async (req, res) => {
|
||||
const viewName = req.params.view_name;
|
||||
const [[row]] = await sql.query(
|
||||
`SELECT VIEW_DEFINITION AS definition
|
||||
r.get("/:view_name", requirePerm("settings.manage"), async (req, res) => {
|
||||
const viewName = req.params.view_name;
|
||||
const [[row]] = await sql.query(
|
||||
`SELECT VIEW_DEFINITION AS definition
|
||||
FROM information_schema.VIEWS
|
||||
WHERE TABLE_SCHEMA=? AND TABLE_NAME=?`,
|
||||
[DB_NAME, viewName]
|
||||
);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
res.json({ view: viewName, definition: row.definition });
|
||||
}
|
||||
);
|
||||
WHERE TABLE_SCHEMA=? AND TABLE_NAME=?`,
|
||||
[DB_NAME, viewName]
|
||||
);
|
||||
if (!row) return res.status(404).json({ error: "Not found" });
|
||||
res.json({ view: viewName, definition: row.definition });
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,70 +1,55 @@
|
||||
// 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
|
||||
// FILE: backend/src/routes/volumes.js
|
||||
// Master data: volumes
|
||||
// - Read: organizations.view (GLOBAL)
|
||||
// - Write: settings.manage (GLOBAL)
|
||||
|
||||
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: 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);
|
||||
}
|
||||
);
|
||||
// LIST
|
||||
r.get("/", requirePerm("organizations.view"), async (_req, res) => {
|
||||
const [rows] = await sql.query(
|
||||
"SELECT volume_id, volume_code, volume_name FROM volumes ORDER BY volume_code ASC"
|
||||
);
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
// 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.post("/", requirePerm("settings.manage"), async (req, res) => {
|
||||
const { volume_code, volume_name } = req.body || {};
|
||||
if (!volume_code || !volume_name) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "volume_code and volume_name required" });
|
||||
}
|
||||
);
|
||||
const [rs] = await sql.query(
|
||||
"INSERT INTO volumes (volume_code, volume_name) VALUES (?, ?)",
|
||||
[volume_code, volume_name]
|
||||
);
|
||||
res.status(201).json({ volume_id: rs.insertId });
|
||||
});
|
||||
|
||||
// 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.put("/:id", requirePerm("settings.manage"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
const { volume_name } = req.body || {};
|
||||
if (!volume_name)
|
||||
return res.status(400).json({ error: "volume_name required" });
|
||||
await sql.query("UPDATE volumes SET volume_name=? WHERE volume_id=?", [
|
||||
volume_name,
|
||||
id,
|
||||
]);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// 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 });
|
||||
}
|
||||
);
|
||||
r.delete("/:id", requirePerm("settings.manage"), async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
await sql.query("DELETE FROM volumes WHERE volume_id=?", [id]);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
export default r;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// FILE: src/utils/passwords.js
|
||||
// FILE: backend/src/utils/passwords.js
|
||||
// Password hashing and verification utilities
|
||||
// - Uses bcrypt for secure password hashing
|
||||
// - Provides hashPassword(plain) and verifyPassword(plain, hash) functions
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// FILE: src/utils/rbac.js
|
||||
// FILE: backend/src/utils/rbac.js
|
||||
// 03.2 2) เพิ่มตัวช่วย RBAC (ใหม่)
|
||||
// Role-Based Access Control (RBAC) utilities
|
||||
// - loadPrincipal(userId) to load user's roles, permissions, orgs, projects
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// FILE: src/utils/scope.js
|
||||
// FILE: backend/src/utils/scope.js
|
||||
// 03.2 5) เพิ่ม utils/scope.js (ใหม่)
|
||||
// - ใช้ร่วมกับ requirePerm() และ loadPrincipal()
|
||||
// - สำหรับสร้าง SQL WHERE clause ในการ list entities ตาม scope ของผู้ใช้
|
||||
|
||||
@@ -36,6 +36,7 @@ DROP TABLE IF EXISTS memorandum_details;
|
||||
DROP TABLE IF EXISTS minutes_of_meeting_details;
|
||||
DROP TABLE IF EXISTS rfi_details;
|
||||
DROP TABLE IF EXISTS rfa_items;
|
||||
DROP TABLE IF EXISTS rfas;
|
||||
DROP TABLE IF EXISTS technicaldocs;
|
||||
DROP TABLE IF EXISTS approve_codes;
|
||||
DROP TABLE IF EXISTS document_status_codes;
|
||||
@@ -422,6 +423,27 @@ CREATE TABLE rfa_items (
|
||||
CREATE INDEX idx_rfaitems_rfa ON rfa_items(rfa_corr_id);
|
||||
CREATE INDEX idx_rfaitems_techdoc ON rfa_items(technical_doc_id);
|
||||
|
||||
CREATE TABLE rfas (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
discipline VARCHAR(64) NULL,
|
||||
due_date DATE NULL,
|
||||
description TEXT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'draft',
|
||||
owner_id INT UNSIGNED NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_rfas_code (code),
|
||||
KEY idx_rfas_status_updated (status, updated_at),
|
||||
KEY idx_rfas_due_date (due_date),
|
||||
KEY idx_rfas_updated_at (updated_at),
|
||||
CONSTRAINT fk_rfas_owner
|
||||
FOREIGN KEY (owner_id) REFERENCES users(id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- =========================================================
|
||||
-- CONTRACT DRAWINGS (normalized cat 1:M subcat; dwg → sub_cat)
|
||||
-- =========================================================
|
||||
|
||||
Reference in New Issue
Block a user