Apply .gitignore cleanup

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

View File

@@ -1,9 +1,9 @@
.git
node_modules
logs
*.log
Dockerfile*
README*.md
coverage
tmp
.git
node_modules
logs
*.log
Dockerfile*
README*.md
coverage
tmp
dist

View File

@@ -1,69 +1,69 @@
# syntax=docker/dockerfile:1.6
########## Base (apk + common tools ติดตั้งตอน build) ##########
FROM node:20-alpine AS base
WORKDIR /app
RUN apk add --no-cache bash curl tzdata python3 make g++ \
&& ln -snf /usr/share/zoneinfo/Asia/Bangkok /etc/localtime \
&& echo "Asia/Bangkok" > /etc/timezone
ENV TZ=Asia/Bangkok APP_HOME=/app RUNTIME_HOME=/opt/runtime
########## Deps สำหรับ Production (no devDeps) ##########
FROM base AS deps-prod
WORKDIR /work
COPY package*.json ./
RUN npm ci --omit=dev || npm install --omit=dev
RUN mkdir -p ${RUNTIME_HOME} && mv node_modules ${RUNTIME_HOME}/node_modules
########## Deps สำหรับ Development (รวม devDeps) ##########
FROM base AS deps-dev
RUN apk add --no-cache git openssh-client ca-certificates
WORKDIR /work
COPY package*.json ./
RUN npm ci || npm install
RUN mkdir -p ${RUNTIME_HOME} && mv node_modules ${RUNTIME_HOME}/node_modules
########## Runtime: Development ##########
FROM base AS dev
WORKDIR /app
# ทำงานเป็น root ชั่วคราวเพื่อจัดสิทธิ์/ลิงก์ แล้วค่อยเปลี่ยนเป็น node
# 1) คัดลอก deps dev
COPY --from=deps-dev /opt/runtime/node_modules /opt/runtime/node_modules
# 2) สร้าง symlink /app/node_modules → /opt/runtime/node_modules (กันปัญหา NODE_PATH/permission)
RUN ln -sfn /opt/runtime/node_modules /app/node_modules \
&& chown -R node:node /app
# 3) ใส่สคริปต์ start-dev แล้วค่อยสลับ USER
COPY --chown=node:node ./start-dev.sh /app/start-dev.sh
RUN chmod +x /app/start-dev.sh
USER node
# ให้หา nodemon ได้จาก node_modules/.bin ที่ bake มาแล้ว
# ENV NODE_ENV=development PATH="/opt/runtime/node_modules/.bin:${PATH}"
# ให้หา nodemon ได้ และระบุพอร์ตดีฟอลต์
ENV NODE_ENV=development \
PORT=3001 \
PATH="/opt/runtime/node_modules/.bin:${PATH}"
EXPOSE 3001 9229
HEALTHCHECK --interval=15s --timeout=5s --retries=10 \
CMD wget -qO- http://127.0.0.1:3001/health || exit 1
# HEALTHCHECK --interval=15s --timeout=5s --retries=10 CMD curl -fsS http://127.0.0.1:7001/health || exit 1
CMD ["/app/start-dev.sh"]
########## Runtime: Production ##########
FROM base AS prod
WORKDIR /app
ENV NODE_ENV=production
# ใส่ deps สำหรับ prod
COPY --from=deps-prod /opt/runtime/node_modules /opt/runtime/node_modules
# สร้าง symlink เช่นกัน เพื่อให้ Node resolve deps ได้จาก /app เหมือน dev
RUN ln -sfn /opt/runtime/node_modules /app/node_modules
# ใส่ซอร์ส (prod ไม่ bind โค้ด)
COPY . .
USER node
EXPOSE 3001
HEALTHCHECK --interval=30s --timeout=5s --retries=10 \
CMD wget -qO- http://127.0.0.1:3001/health || exit 1
# syntax=docker/dockerfile:1.6
########## Base (apk + common tools ติดตั้งตอน build) ##########
FROM node:20-alpine AS base
WORKDIR /app
RUN apk add --no-cache bash curl tzdata python3 make g++ \
&& ln -snf /usr/share/zoneinfo/Asia/Bangkok /etc/localtime \
&& echo "Asia/Bangkok" > /etc/timezone
ENV TZ=Asia/Bangkok APP_HOME=/app RUNTIME_HOME=/opt/runtime
########## Deps สำหรับ Production (no devDeps) ##########
FROM base AS deps-prod
WORKDIR /work
COPY package*.json ./
RUN npm ci --omit=dev || npm install --omit=dev
RUN mkdir -p ${RUNTIME_HOME} && mv node_modules ${RUNTIME_HOME}/node_modules
########## Deps สำหรับ Development (รวม devDeps) ##########
FROM base AS deps-dev
RUN apk add --no-cache git openssh-client ca-certificates
WORKDIR /work
COPY package*.json ./
RUN npm ci || npm install
RUN mkdir -p ${RUNTIME_HOME} && mv node_modules ${RUNTIME_HOME}/node_modules
########## Runtime: Development ##########
FROM base AS dev
WORKDIR /app
# ทำงานเป็น root ชั่วคราวเพื่อจัดสิทธิ์/ลิงก์ แล้วค่อยเปลี่ยนเป็น node
# 1) คัดลอก deps dev
COPY --from=deps-dev /opt/runtime/node_modules /opt/runtime/node_modules
# 2) สร้าง symlink /app/node_modules → /opt/runtime/node_modules (กันปัญหา NODE_PATH/permission)
RUN ln -sfn /opt/runtime/node_modules /app/node_modules \
&& chown -R node:node /app
# 3) ใส่สคริปต์ start-dev แล้วค่อยสลับ USER
COPY --chown=node:node ./start-dev.sh /app/start-dev.sh
RUN chmod +x /app/start-dev.sh
USER node
# ให้หา nodemon ได้จาก node_modules/.bin ที่ bake มาแล้ว
# ENV NODE_ENV=development PATH="/opt/runtime/node_modules/.bin:${PATH}"
# ให้หา nodemon ได้ และระบุพอร์ตดีฟอลต์
ENV NODE_ENV=development \
PORT=3001 \
PATH="/opt/runtime/node_modules/.bin:${PATH}"
EXPOSE 3001 9229
HEALTHCHECK --interval=15s --timeout=5s --retries=10 \
CMD wget -qO- http://127.0.0.1:3001/health || exit 1
# HEALTHCHECK --interval=15s --timeout=5s --retries=10 CMD curl -fsS http://127.0.0.1:7001/health || exit 1
CMD ["/app/start-dev.sh"]
########## Runtime: Production ##########
FROM base AS prod
WORKDIR /app
ENV NODE_ENV=production
# ใส่ deps สำหรับ prod
COPY --from=deps-prod /opt/runtime/node_modules /opt/runtime/node_modules
# สร้าง symlink เช่นกัน เพื่อให้ Node resolve deps ได้จาก /app เหมือน dev
RUN ln -sfn /opt/runtime/node_modules /app/node_modules
# ใส่ซอร์ส (prod ไม่ bind โค้ด)
COPY . .
USER node
EXPOSE 3001
HEALTHCHECK --interval=30s --timeout=5s --retries=10 \
CMD wget -qO- http://127.0.0.1:3001/health || exit 1
CMD ["node","src/index.js"]

View File

@@ -1,60 +1,60 @@
# STAGE 1: build - สร้าง stage พื้นฐานสำหรับติดตั้ง dependencies ทั้งหมด
# เราจะใช้ stage นี้เป็น cache ร่วมกันระหว่าง development และ production เพื่อความรวดเร็ว
FROM node:20-alpine AS build
# USER node
WORKDIR /app
# สร้าง user ไม่ใช่ root (ปลอดภัยขึ้น)
# RUN addgroup -S dms && adduser -S dms -G dms
# runtime tools + build deps ชั่วคราว (สำหรับ bcrypt ฯลฯ)
#RUN apk add --no-cache curl \
# && apk add --no-cache --virtual build-deps python3 make g++
RUN apk add --no-cache --virtual build-deps python3 make g++
# COPY --chown=node:node package*.json package-lock.json* ./
# COPY package*.json package-lock.json* ./
# COPY package.json ./
# RUN (npm ci --omit=dev || npm install --omit=dev)
# ติดตั้ง deps แบบ clean + ติดตั้ง dev tooling ที่จำเป็น
# RUN npm ci --include=dev || npm install --include=dev && \
# npx --yes nodemon --version > /dev/null 2>&1 || npm i -D nodemon
# RUN npm ci
RUN npm install
# คัดลอกโค้ด + ตั้ง owner/สิทธิ์
# COPY --chown=app:app src ./src
# ไม่ COPY src เข้ามา — เราจะใช้ bind mount แทน
# เพื่อ hot-reload จากโค้ดบน QNAP ได้ทันที
# ลบ build deps ลดขนาดอิมเมจ
# RUN apk del --no-network build-deps
# STAGE 2: development - สำหรับการพัฒนาใน local โดยเฉพาะ
# stage นี้จะใช้ dependencies ทั้งหมดจาก 'base'
FROM base AS development
# (ต้องมี script "dev" ใน package.json, เช่น "dev": "nodemon src/index.js")
CMD ["npm", "run", "dev"]
# ---------- Runtime stage ----------
FROM node:20-alpine AS production
WORKDIR /app
# สร้าง user และ group ที่ไม่ใช่ root สำหรับรันแอปพลิเคชัน
RUN addgroup -S dms && adduser -S dms -G dms
# COPY --from=build /app /app
ENV NODE_ENV=production
# คัดลอกไฟล์ package.json และ node_modules จาก stage 'base'
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
# ลบ devDependencies ที่ไม่จำเป็นสำหรับ production ออก
RUN npm prune --production
# เปลี่ยนไปใช้ user ที่ไม่ใช่ root
USER dms
EXPOSE 3001
CMD ["npm","start"]
# backend/Dockerfile (Node.js ESM)
# STAGE 1: build - สร้าง stage พื้นฐานสำหรับติดตั้ง dependencies ทั้งหมด
# เราจะใช้ stage นี้เป็น cache ร่วมกันระหว่าง development และ production เพื่อความรวดเร็ว
FROM node:20-alpine AS build
# USER node
WORKDIR /app
# สร้าง user ไม่ใช่ root (ปลอดภัยขึ้น)
# RUN addgroup -S dms && adduser -S dms -G dms
# runtime tools + build deps ชั่วคราว (สำหรับ bcrypt ฯลฯ)
#RUN apk add --no-cache curl \
# && apk add --no-cache --virtual build-deps python3 make g++
RUN apk add --no-cache --virtual build-deps python3 make g++
# COPY --chown=node:node package*.json package-lock.json* ./
# COPY package*.json package-lock.json* ./
# COPY package.json ./
# RUN (npm ci --omit=dev || npm install --omit=dev)
# ติดตั้ง deps แบบ clean + ติดตั้ง dev tooling ที่จำเป็น
# RUN npm ci --include=dev || npm install --include=dev && \
# npx --yes nodemon --version > /dev/null 2>&1 || npm i -D nodemon
# RUN npm ci
RUN npm install
# คัดลอกโค้ด + ตั้ง owner/สิทธิ์
# COPY --chown=app:app src ./src
# ไม่ COPY src เข้ามา — เราจะใช้ bind mount แทน
# เพื่อ hot-reload จากโค้ดบน QNAP ได้ทันที
# ลบ build deps ลดขนาดอิมเมจ
# RUN apk del --no-network build-deps
# STAGE 2: development - สำหรับการพัฒนาใน local โดยเฉพาะ
# stage นี้จะใช้ dependencies ทั้งหมดจาก 'base'
FROM base AS development
# (ต้องมี script "dev" ใน package.json, เช่น "dev": "nodemon src/index.js")
CMD ["npm", "run", "dev"]
# ---------- Runtime stage ----------
FROM node:20-alpine AS production
WORKDIR /app
# สร้าง user และ group ที่ไม่ใช่ root สำหรับรันแอปพลิเคชัน
RUN addgroup -S dms && adduser -S dms -G dms
# COPY --from=build /app /app
ENV NODE_ENV=production
# คัดลอกไฟล์ package.json และ node_modules จาก stage 'base'
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
# ลบ devDependencies ที่ไม่จำเป็นสำหรับ production ออก
RUN npm prune --production
# เปลี่ยนไปใช้ user ที่ไม่ใช่ root
USER dms
EXPOSE 3001
CMD ["npm","start"]
# backend/Dockerfile (Node.js ESM)

View File

@@ -1,34 +1,34 @@
FROM node:20-alpine
# สำหรับอ่านค่า .env ที่วางไว้ระดับ compose (ไม่ copy เข้า image)
ENV NODE_ENV=production
ENV TZ=Asia/Bangkok
WORKDIR /app
# สร้าง user ไม่ใช่ root (ปลอดภัยขึ้น)
RUN addgroup -S app && adduser -S app -G app
# runtime tools + build deps ชั่วคราว (สำหรับ bcrypt ฯลฯ)
RUN apk add --no-cache curl \
&& apk add --no-cache --virtual build-deps python3 make g++
# ติดตั้ง deps ของ npm (เช่น bcrypt ต้องมี python3/make/g++)
# ใช้ virtual package ชื่อ build-deps (ไม่ต้องมีจุด)
COPY package.json package-lock.json* ./
RUN (npm ci --omit=dev || npm install --omit=dev)
# คัดลอกโค้ด + ตั้ง owner/สิทธิ์
COPY --chown=app:app src ./src
# COPY src ./src
# COPY app ./app
# เตรียม logs + สิทธิ์อ่านไฟล์ใน /app
RUN mkdir -p /app/logs \
&& chown -R app:app /app/logs \
&& chmod -R a+rX /app
# ลบ build deps ลดขนาดอิมเมจ
RUN apk del --no-network build-deps
EXPOSE 3001
USER app
CMD ["node", "src/index.js"]
FROM node:20-alpine
# สำหรับอ่านค่า .env ที่วางไว้ระดับ compose (ไม่ copy เข้า image)
ENV NODE_ENV=production
ENV TZ=Asia/Bangkok
WORKDIR /app
# สร้าง user ไม่ใช่ root (ปลอดภัยขึ้น)
RUN addgroup -S app && adduser -S app -G app
# runtime tools + build deps ชั่วคราว (สำหรับ bcrypt ฯลฯ)
RUN apk add --no-cache curl \
&& apk add --no-cache --virtual build-deps python3 make g++
# ติดตั้ง deps ของ npm (เช่น bcrypt ต้องมี python3/make/g++)
# ใช้ virtual package ชื่อ build-deps (ไม่ต้องมีจุด)
COPY package.json package-lock.json* ./
RUN (npm ci --omit=dev || npm install --omit=dev)
# คัดลอกโค้ด + ตั้ง owner/สิทธิ์
COPY --chown=app:app src ./src
# COPY src ./src
# COPY app ./app
# เตรียม logs + สิทธิ์อ่านไฟล์ใน /app
RUN mkdir -p /app/logs \
&& chown -R app:app /app/logs \
&& chmod -R a+rX /app
# ลบ build deps ลดขนาดอิมเมจ
RUN apk del --no-network build-deps
EXPOSE 3001
USER app
CMD ["node", "src/index.js"]
# backend/Dockerfile (Node.js ESM)

View File

@@ -1,69 +1,69 @@
# syntax=docker/dockerfile:1.6
########## Base (apk + common tools ติดตั้งตอน build) ##########
FROM node:20-alpine AS base
WORKDIR /app
RUN apk add --no-cache bash curl tzdata python3 make g++ \
&& ln -snf /usr/share/zoneinfo/Asia/Bangkok /etc/localtime \
&& echo "Asia/Bangkok" > /etc/timezone
ENV TZ=Asia/Bangkok APP_HOME=/app RUNTIME_HOME=/opt/runtime
########## Deps สำหรับ Production (no devDeps) ##########
FROM base AS deps-prod
WORKDIR /work
COPY package*.json ./
RUN npm ci --omit=dev || npm install --omit=dev
RUN mkdir -p ${RUNTIME_HOME} && mv node_modules ${RUNTIME_HOME}/node_modules
########## Deps สำหรับ Development (รวม devDeps) ##########
FROM base AS deps-dev
RUN apk add --no-cache git openssh-client ca-certificates
WORKDIR /work
COPY package*.json ./
RUN npm ci || npm install
RUN mkdir -p ${RUNTIME_HOME} && mv node_modules ${RUNTIME_HOME}/node_modules
########## Runtime: Development ##########
FROM base AS dev
WORKDIR /app
# ทำงานเป็น root ชั่วคราวเพื่อจัดสิทธิ์/ลิงก์ แล้วค่อยเปลี่ยนเป็น node
# 1) คัดลอก deps dev
COPY --from=deps-dev /opt/runtime/node_modules /opt/runtime/node_modules
# 2) สร้าง symlink /app/node_modules → /opt/runtime/node_modules (กันปัญหา NODE_PATH/permission)
RUN ln -sfn /opt/runtime/node_modules /app/node_modules \
&& chown -R node:node /app
# 3) ใส่สคริปต์ start-dev แล้วค่อยสลับ USER
COPY --chown=node:node ./start-dev.sh /app/start-dev.sh
RUN chmod +x /app/start-dev.sh
USER node
# ให้หา nodemon ได้จาก node_modules/.bin ที่ bake มาแล้ว
# ENV NODE_ENV=development PATH="/opt/runtime/node_modules/.bin:${PATH}"
# ให้หา nodemon ได้ และระบุพอร์ตดีฟอลต์
ENV NODE_ENV=development \
PORT=3001 \
PATH="/opt/runtime/node_modules/.bin:${PATH}"
EXPOSE 3001 9229
HEALTHCHECK --interval=15s --timeout=5s --retries=10 \
CMD wget -qO- http://127.0.0.1:3001/health || exit 1
# HEALTHCHECK --interval=15s --timeout=5s --retries=10 CMD curl -fsS http://127.0.0.1:7001/health || exit 1
CMD ["/app/start-dev.sh"]
########## Runtime: Production ##########
FROM base AS prod
WORKDIR /app
ENV NODE_ENV=production
# ใส่ deps สำหรับ prod
COPY --from=deps-prod /opt/runtime/node_modules /opt/runtime/node_modules
# สร้าง symlink เช่นกัน เพื่อให้ Node resolve deps ได้จาก /app เหมือน dev
RUN ln -sfn /opt/runtime/node_modules /app/node_modules
# ใส่ซอร์ส (prod ไม่ bind โค้ด)
COPY . .
USER node
EXPOSE 3001
HEALTHCHECK --interval=30s --timeout=5s --retries=10 \
CMD wget -qO- http://127.0.0.1:3001/health || exit 1
# syntax=docker/dockerfile:1.6
########## Base (apk + common tools ติดตั้งตอน build) ##########
FROM node:20-alpine AS base
WORKDIR /app
RUN apk add --no-cache bash curl tzdata python3 make g++ \
&& ln -snf /usr/share/zoneinfo/Asia/Bangkok /etc/localtime \
&& echo "Asia/Bangkok" > /etc/timezone
ENV TZ=Asia/Bangkok APP_HOME=/app RUNTIME_HOME=/opt/runtime
########## Deps สำหรับ Production (no devDeps) ##########
FROM base AS deps-prod
WORKDIR /work
COPY package*.json ./
RUN npm ci --omit=dev || npm install --omit=dev
RUN mkdir -p ${RUNTIME_HOME} && mv node_modules ${RUNTIME_HOME}/node_modules
########## Deps สำหรับ Development (รวม devDeps) ##########
FROM base AS deps-dev
RUN apk add --no-cache git openssh-client ca-certificates
WORKDIR /work
COPY package*.json ./
RUN npm ci || npm install
RUN mkdir -p ${RUNTIME_HOME} && mv node_modules ${RUNTIME_HOME}/node_modules
########## Runtime: Development ##########
FROM base AS dev
WORKDIR /app
# ทำงานเป็น root ชั่วคราวเพื่อจัดสิทธิ์/ลิงก์ แล้วค่อยเปลี่ยนเป็น node
# 1) คัดลอก deps dev
COPY --from=deps-dev /opt/runtime/node_modules /opt/runtime/node_modules
# 2) สร้าง symlink /app/node_modules → /opt/runtime/node_modules (กันปัญหา NODE_PATH/permission)
RUN ln -sfn /opt/runtime/node_modules /app/node_modules \
&& chown -R node:node /app
# 3) ใส่สคริปต์ start-dev แล้วค่อยสลับ USER
COPY --chown=node:node ./start-dev.sh /app/start-dev.sh
RUN chmod +x /app/start-dev.sh
USER node
# ให้หา nodemon ได้จาก node_modules/.bin ที่ bake มาแล้ว
# ENV NODE_ENV=development PATH="/opt/runtime/node_modules/.bin:${PATH}"
# ให้หา nodemon ได้ และระบุพอร์ตดีฟอลต์
ENV NODE_ENV=development \
PORT=3001 \
PATH="/opt/runtime/node_modules/.bin:${PATH}"
EXPOSE 3001 9229
HEALTHCHECK --interval=15s --timeout=5s --retries=10 \
CMD wget -qO- http://127.0.0.1:3001/health || exit 1
# HEALTHCHECK --interval=15s --timeout=5s --retries=10 CMD curl -fsS http://127.0.0.1:7001/health || exit 1
CMD ["/app/start-dev.sh"]
########## Runtime: Production ##########
FROM base AS prod
WORKDIR /app
ENV NODE_ENV=production
# ใส่ deps สำหรับ prod
COPY --from=deps-prod /opt/runtime/node_modules /opt/runtime/node_modules
# สร้าง symlink เช่นกัน เพื่อให้ Node resolve deps ได้จาก /app เหมือน dev
RUN ln -sfn /opt/runtime/node_modules /app/node_modules
# ใส่ซอร์ส (prod ไม่ bind โค้ด)
COPY . .
USER node
EXPOSE 3001
HEALTHCHECK --interval=30s --timeout=5s --retries=10 \
CMD wget -qO- http://127.0.0.1:3001/health || exit 1
CMD ["node","src/index.js"]

0
backend/ed25519 Executable file → Normal file
View File

0
backend/ed25519.pub Executable file → Normal file
View File

128
backend/fix-bearer-index.patch.diff Executable file → Normal file
View File

@@ -1,64 +1,64 @@
diff --git a/src/index.js b/src/index.js
--- a/src/index.js
+++ b/src/index.js
@@ -1,9 +1,8 @@
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";
import healthRouter from "./routes/health.js";
import { authJwt } from "./middleware/authJwt.js";
@@ -64,7 +63,7 @@
// ✅ อยู่หลัง NPM/Reverse proxy → ให้ trust proxy เพื่อให้ cookie secure / proto ทำงานถูก
app.set("trust proxy", 1);
-// CORS แบบกำหนด origin ตามรายการที่อนุญาต + อนุญาต credentials (จำเป็นสำหรับ cookie)
+// ✅ CORS สำหรับ Bearer token: ไม่ต้องใช้ credentials (ไม่มีคุกกี้)
app.use(
cors({
origin(origin, cb) {
if (!origin) return cb(null, true); // server-to-server / curl
return cb(null, ALLOW_ORIGINS.includes(origin));
},
- credentials: true,
+ credentials: false,
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));
},
- credentials: true,
+ credentials: false,
})
);
-app.use(cookieParser());
+// ❌ ไม่ต้อง parse cookie แล้ว (เราไม่ใช้คุกกี้สำหรับ auth)
+// app.use(cookieParser());
// Payload limits
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
diff --git a/src/index.js b/src/index.js
--- a/src/index.js
+++ b/src/index.js
@@ -1,9 +1,8 @@
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";
import healthRouter from "./routes/health.js";
import { authJwt } from "./middleware/authJwt.js";
@@ -64,7 +63,7 @@
// ✅ อยู่หลัง NPM/Reverse proxy → ให้ trust proxy เพื่อให้ cookie secure / proto ทำงานถูก
app.set("trust proxy", 1);
-// CORS แบบกำหนด origin ตามรายการที่อนุญาต + อนุญาต credentials (จำเป็นสำหรับ cookie)
+// ✅ CORS สำหรับ Bearer token: ไม่ต้องใช้ credentials (ไม่มีคุกกี้)
app.use(
cors({
origin(origin, cb) {
if (!origin) return cb(null, true); // server-to-server / curl
return cb(null, ALLOW_ORIGINS.includes(origin));
},
- credentials: true,
+ credentials: false,
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));
},
- credentials: true,
+ credentials: false,
})
);
-app.use(cookieParser());
+// ❌ ไม่ต้อง parse cookie แล้ว (เราไม่ใช้คุกกี้สำหรับ auth)
+// app.use(cookieParser());
// Payload limits
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));

5494
backend/package-lock.json generated Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +1,38 @@
{
"name": "dms-backend",
"version": "0.6.0",
"private": true,
"type": "module",
"main": "src/index.js",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"dev": "nodemon --watch src src/index.js",
"dev:desktop": "node --watch src/index.js",
"start": "node src/index.js",
"lint": "echo 'lint placeholder'",
"health": "node -e \"fetch('http://localhost:'+ (process.env.BACKEND_PORT||3001) +'/health').then(r=>r.text()).then(console.log).catch(e=>{console.error(e);process.exit(1)})\"",
"postinstall": "node -e \"console.log('Installed dms-backend %s','0.6.0')\""
},
"dependencies": {
"bcrypt": "5.1.1",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.7",
"cors": "2.8.5",
"dotenv": "16.4.5",
"express": "^4.21.2",
"express-rate-limit": "7.4.0",
"helmet": "7.1.0",
"jsonwebtoken": "9.0.2",
"mariadb": "3.3.1",
"morgan": "^1.10.1",
"multer": "^2.0.2",
"mysql2": "^3.11.0",
"sequelize": "6.37.3",
"winston": "^3.13.0"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}
{
"name": "dms-backend",
"version": "0.6.0",
"private": true,
"type": "module",
"main": "src/index.js",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"dev": "nodemon --watch src src/index.js",
"dev:desktop": "node --watch src/index.js",
"start": "node src/index.js",
"lint": "echo 'lint placeholder'",
"health": "node -e \"fetch('http://localhost:'+ (process.env.BACKEND_PORT||3001) +'/health').then(r=>r.text()).then(console.log).catch(e=>{console.error(e);process.exit(1)})\"",
"postinstall": "node -e \"console.log('Installed dms-backend %s','0.6.0')\""
},
"dependencies": {
"bcrypt": "5.1.1",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.7",
"cors": "2.8.5",
"dotenv": "16.4.5",
"express": "^4.21.2",
"express-rate-limit": "7.4.0",
"helmet": "7.1.0",
"jsonwebtoken": "9.0.2",
"mariadb": "3.3.1",
"morgan": "^1.10.1",
"multer": "^2.0.2",
"mysql2": "^3.11.0",
"sequelize": "6.37.3",
"winston": "^3.13.0"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}

View File

@@ -1,25 +1,25 @@
{
"name": "dms-backend",
"version": "0.5.0",
"private": true,
"type": "module",
"scripts": {
"dev": "node --env-file=../.env src/index.js",
"start": "node src/index.js",
"health": "node -e \"fetch('http://localhost:'+ (process.env.BACKEND_PORT||3001) +'/health').then(r=>r.text()).then(console.log).catch(e=>{console.error(e);process.exit(1)})\""
},
"dependencies": {
"bcrypt": "5.1.1",
"cors": "2.8.5",
"dotenv": "16.4.5",
"express": "4.19.2",
"express-rate-limit": "7.4.0",
"helmet": "7.1.0",
"jsonwebtoken": "9.0.2",
"mariadb": "3.3.1",
"morgan": "1.10.0",
"sequelize": "6.37.3"
}
}
{
"name": "dms-backend",
"version": "0.5.0",
"private": true,
"type": "module",
"scripts": {
"dev": "node --env-file=../.env src/index.js",
"start": "node src/index.js",
"health": "node -e \"fetch('http://localhost:'+ (process.env.BACKEND_PORT||3001) +'/health').then(r=>r.text()).then(console.log).catch(e=>{console.error(e);process.exit(1)})\""
},
"dependencies": {
"bcrypt": "5.1.1",
"cors": "2.8.5",
"dotenv": "16.4.5",
"express": "4.19.2",
"express-rate-limit": "7.4.0",
"helmet": "7.1.0",
"jsonwebtoken": "9.0.2",
"mariadb": "3.3.1",
"morgan": "1.10.0",
"sequelize": "6.37.3"
}
}

76
backend/package_v0_6_0.json Executable file → Normal file
View File

@@ -1,38 +1,38 @@
{
"name": "dms-backend",
"version": "0.6.0",
"private": true,
"type": "module",
"main": "src/index.js",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"dev": "nodemon --watch src src/index.js",
"start": "node src/index.js",
"lint": "echo 'lint placeholder'",
"health": "node -e \"fetch('http://localhost:'+ (process.env.BACKEND_PORT||3001) +'/health').then(r=>r.text()).then(console.log).catch(e=>{console.error(e);process.exit(1)})\"",
"postinstall": "node -e \"console.log('Installed dms-backend %s','0.6.0')\""
},
"dependencies": {
"bcrypt": "5.1.1",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.7",
"cors": "2.8.5",
"dotenv": "16.4.5",
"express": "^4.21.2",
"express-rate-limit": "7.4.0",
"helmet": "7.1.0",
"jsonwebtoken": "9.0.2",
"mariadb": "3.3.1",
"morgan": "^1.10.1",
"multer": "^2.0.2",
"mysql2": "^3.11.0",
"sequelize": "6.37.3",
"winston": "^3.13.0"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}
{
"name": "dms-backend",
"version": "0.6.0",
"private": true,
"type": "module",
"main": "src/index.js",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"dev": "nodemon --watch src src/index.js",
"start": "node src/index.js",
"lint": "echo 'lint placeholder'",
"health": "node -e \"fetch('http://localhost:'+ (process.env.BACKEND_PORT||3001) +'/health').then(r=>r.text()).then(console.log).catch(e=>{console.error(e);process.exit(1)})\"",
"postinstall": "node -e \"console.log('Installed dms-backend %s','0.6.0')\""
},
"dependencies": {
"bcrypt": "5.1.1",
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.7",
"cors": "2.8.5",
"dotenv": "16.4.5",
"express": "^4.21.2",
"express-rate-limit": "7.4.0",
"helmet": "7.1.0",
"jsonwebtoken": "9.0.2",
"mariadb": "3.3.1",
"morgan": "^1.10.1",
"multer": "^2.0.2",
"mysql2": "^3.11.0",
"sequelize": "6.37.3",
"winston": "^3.13.0"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,23 @@
// FILE: src/middleware/loadPrincipal.js
// 03.2 4) เพิ่ม middleware loadPrincipal (ใหม่)
// นำ middleware นี้ไปใส่ หลัง verify JWT เสมอ เช่น app.use('/api', authJwt(), loadPrincipalMw(), routes)
// Load principal (roles + permissions) middleware
// - Uses rbac.js utility to load principal info
// - Attaches to req.principal
// - Requires req.user.user_id to be populated (e.g. via auth.js or authJwt.js)
import { loadPrincipal } from "../utils/rbac.js";
export function loadPrincipalMw() {
return async (req, res, next) => {
try {
if (!req.user?.user_id)
return res.status(401).json({ error: "Unauthenticated" });
req.principal = await loadPrincipal(req.user.user_id);
next();
} catch (err) {
console.error("loadPrincipal error", err);
res.status(500).json({ error: "Failed to load principal" });
}
};
}
// FILE: src/middleware/loadPrincipal.js
// 03.2 4) เพิ่ม middleware loadPrincipal (ใหม่)
// นำ middleware นี้ไปใส่ หลัง verify JWT เสมอ เช่น app.use('/api', authJwt(), loadPrincipalMw(), routes)
// Load principal (roles + permissions) middleware
// - Uses rbac.js utility to load principal info
// - Attaches to req.principal
// - Requires req.user.user_id to be populated (e.g. via auth.js or authJwt.js)
import { loadPrincipal } from "../utils/rbac.js";
export function loadPrincipalMw() {
return async (req, res, next) => {
try {
if (!req.user?.user_id)
return res.status(401).json({ error: "Unauthenticated" });
req.principal = await loadPrincipal(req.user.user_id);
next();
} catch (err) {
console.error("loadPrincipal error", err);
res.status(500).json({ error: "Failed to load principal" });
}
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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