Apply .gitignore cleanup
This commit is contained in:
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
0
backend/ed25519
Executable file → Normal file
0
backend/ed25519.pub
Executable file → Normal file
0
backend/ed25519.pub
Executable file → Normal file
128
backend/fix-bearer-index.patch.diff
Executable file → Normal file
128
backend/fix-bearer-index.patch.diff
Executable file → Normal 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
5494
backend/package-lock.json
generated
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
76
backend/package_v0_6_0.json
Executable file → Normal 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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 เรียกอยู่
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user