diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..8f1d938b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.gitignore b/.gitignore index dbac1ada..61ecad49 100755 --- a/.gitignore +++ b/.gitignore @@ -1,31 +1,51 @@ # ยกเว้นโฟลเดอร์ - +.devcontainer/ @Recently-Snapshot/ Documents/ -# VS Code +# ===================================================== +# IDE/Editor settings +# ===================================================== .vscode/ -# Dependencies -/node_modules +.idea/ +# ===================================================== +# Node.js dependencies (เฉพาะ backend และ frontend) +# ===================================================== +/backend/node_modules/ +/frontend/node_modules/ +**/node_modules/ +# lockfiles +/backend/package-lock.json +/frontend/package-lock.json +**/package-lock.json +# ===================================================== +# Next.js build output +# ===================================================== +/frontend/.next/ +/frontend/out/ +/frontend/.vercel/ + # Build outputs /dist /build /.next/ /out/ /bin/ -# Environment variables (VERY IMPORTANT) -.env -.env.local -.env.*.local -.env.development.local -.env.test.local -.env.production.local -# Environment variables (VERY IMPORTANT) + +# ===================================================== +# Environment files +# ===================================================== .env .env.local .env.development.local .env.test.local .env.production.local -# Log files + +# ===================================================== +# Logs +# ===================================================== +/backend/logs/ +/frontend/logs/ +/logs/ *.log npm-debug.log* yarn-debug.log* @@ -33,7 +53,6 @@ yarn-error.log* pnpm-debug.log* *.tmp *.temp -Thumbs.db ehthumbs.db desktop.ini # =================================================================== @@ -43,13 +62,26 @@ desktop.ini *.dump *.bak -# =================================================================== -# Operating System & IDE specific files -# =================================================================== -# macOS +# ===================================================== +# OS-specific junk +# ===================================================== .DS_Store +Thumbs.db .AppleDouble -# Windows -Thumbs.db -desktop.ini \ No newline at end of file +# ===================================================== +# Docker-related +# ===================================================== +*.pid +*.seed +*.pid.lock +docker-compose.override.yml +docker-compose.override.*.yml + +# ===================================================== +# Cache / temp +# ===================================================== +/backend/.cache/ +/frontend/.cache/ +.tmp/ +.cache/ \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index dedf9b45..fff45215 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -17,6 +17,7 @@ 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 diff --git a/backend/Dockerfile.dev b/backend/Dockerfile.dev index 31460508..fff45215 100644 --- a/backend/Dockerfile.dev +++ b/backend/Dockerfile.dev @@ -1,22 +1,69 @@ -FROM node:20-alpine -# สำหรับอ่านค่า .env ที่วางไว้ระดับ compose (ไม่ copy เข้า image) -ENV NODE_ENV=development -ENV TZ=Asia/Bangkok +# 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 -# สร้าง user ไม่ใช่ root (ปลอดภัยขึ้น) -RUN addgroup -S dms && adduser -S dms -G dms +########## 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 -# runtime tools + build deps ชั่วคราว (สำหรับ bcrypt ฯลฯ) -RUN apk add --no-cache python3 make g++ curl -# ติดตั้ง nodemon ไว้ให้ -RUN npm i -g nodemon +########## 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 -COPY package.json package-lock.json* ./ -RUN (npm ci --omit=dev || npm install --omit=dev) +########## 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 -USER dms -CMD ["node", "src/index.js"] -# backend/Dockerfile (Node.js ESM) \ No newline at end of file +HEALTHCHECK --interval=30s --timeout=5s --retries=10 \ + CMD wget -qO- http://127.0.0.1:3001/health || exit 1 +CMD ["node","src/index.js"] \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 422dc9e6..a2e68759 100755 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "dms-backend", "version": "0.6.0", + "hasInstallScript": true, "dependencies": { "bcrypt": "5.1.1", "bcryptjs": "^2.4.3", @@ -2698,7 +2699,7 @@ "node": ">= 12.0.0" } }, - "node_modules/winston/node_modules/winston-transport": { + "node_modules/winston-transport": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", diff --git a/backend/package.json b/backend/package.json index cde74eee..452371e9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,6 +10,7 @@ "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)})\"", diff --git a/backend/src/index.js b/backend/src/index.js index cd3e3407..9df3503b 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -40,7 +40,13 @@ const PORT = Number(process.env.PORT || 3001); const NODE_ENV = process.env.NODE_ENV || 'production'; // Origin ของ Frontend (ถ้ามี Nginx ด้านหน้า ให้ใช้โดเมน/พอร์ตของ Frontend) -const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN || 'https://dcs.mycloudnas.com'; // ใส่เช่น 'https://dcs.mycloudnas.com' +// Origin ของ Frontend (ตั้งผ่าน ENV ในแต่ละสภาพแวดล้อม; dev ใช้ localhost) +const FRONTEND_ORIGIN = process.env.FRONTEND_ORIGIN || 'https://lcbp3.mycloudnas.com'; +const ALLOW_ORIGINS = [ + 'http://localhost:3000', + 'http://127.0.0.1:3000', + FRONTEND_ORIGIN, +].filter(Boolean); // ที่เก็บ log ภายใน container ถูก bind ไปที่ /share/Container/dms/logs/backend const LOG_DIR = process.env.BACKEND_LOG_DIR || '/app/logs'; @@ -57,23 +63,29 @@ try { * ========================== */ const app = express(); -// หลัง Nginx/Reverse Proxy ควรเปิด trust proxy เพื่ออ่าน X-Forwarded-* -app.set('trust proxy', 1); +// CORS แบบกำหนด origin ตามรายการที่อนุญาต + อนุญาต credentials +app.use(cors({ + origin(origin, cb) { + // อนุญาต server-to-server / curl ที่ไม่มี Origin + if (!origin) return cb(null, true); + return cb(null, ALLOW_ORIGINS.includes(origin)); + }, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'], + 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, +})); -app.use(cors({ origin: true, credentials: true })); -app.use(express.json()); app.use(cookieParser()); -// CORS แบบง่าย (ต้องการละเอียดกว่านี้ใช้ cors package ได้ แต่ที่นี่ทำ manual) -app.use((req, res, next) => { - res.setHeader('Access-Control-Allow-Origin', FRONTEND_ORIGIN); - res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition, Content-Length'); - if (req.method === 'OPTIONS') return res.sendStatus(204); - next(); -}); - // Payload limits app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); diff --git a/docker-backend-build.yml b/docker-backend-build.yml index 240e50eb..4be9dfe2 100755 --- a/docker-backend-build.yml +++ b/docker-backend-build.yml @@ -1,7 +1,8 @@ services: backend_dev_image: build: - context: /share/Container/dms/backend + ## context: /share/Container/dms/backend + context: ./backend dockerfile: Dockerfile target: dev image: dms-backend:dev @@ -9,9 +10,14 @@ services: backend_prod_image: build: - context: /share/Container/dms/backend + ## context: /share/Container/dms/backend + context: ./backend dockerfile: Dockerfile target: prod image: dms-backend:prod command: ["true"] -# docker compose -f docker-backend-build.yml build --no-cache \ No newline at end of file +# docker compose -f docker-backend-build.yml build --no-cache +# ***** สำหรับ build บน server เอา ## ออก ***** +# สำหรับ build บน local +# cd backend +# docker build -t dms-backend:dev --target dev . \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 63dc1c50..2d6751df 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -83,6 +83,7 @@ services: JWT_SECRET: "8b0df02e4aee9f9f79a4f2d8ba77b0b82c1ee3446b68cb0bae94ab54d60f8d9e" JWT_EXPIRES_IN: "12h" PASSWORD_SALT_ROUNDS: "10" + FRONTEND_ORIGIN: "https://lcbp3.mycloudnas.com" CORS_ORIGINS: "https://backend.np-dms.work,http://localhost:3000,http://127.0.0.1:3000" RATE_LIMIT_WINDOW_MS: "900000" RATE_LIMIT_MAX: "200" @@ -127,7 +128,7 @@ services: NEXT_PUBLIC_API_BASE: "/api" CHOKIDAR_USEPOLLING: "1" WATCHPACK_POLLING: "true" - NEXT_PUBLIC_API_BASE=https: "//lcbp3.np-dms.work" + NEXT_PUBLIC_API_BASE=https: "//lcbp3.np-dms.work/api" NEXT_TELEMETRY_DISABLED: "1" expose: - "3000" diff --git a/docker-frontend-build.yml b/docker-frontend-build.yml index da45a4ff..dae1b301 100755 --- a/docker-frontend-build.yml +++ b/docker-frontend-build.yml @@ -1,7 +1,8 @@ services: frontend_dev_image: build: - context: /share/Container/dms/frontend + # context: /share/Container/dms/frontend + context: ./frontend dockerfile: Dockerfile target: dev image: dms-frontend:dev @@ -9,13 +10,22 @@ services: frontend_prod_image: build: - context: /share/Container/dms/frontend + ## context: /share/Container/dms/frontend + context: ./frontend args: - NEXT_PUBLIC_API_BASE: https://lcbp3.np-dms.work + - NEXT_PUBLIC_API_BASE=https://lcbp3.np-dms.work + - NODE_ENV=production #added dockerfile: Dockerfile target: prod - environment: - - NEXT_PUBLIC_API_BASE=https://lcbp3.np-dms.work + ## environment: + ## - NEXT_PUBLIC_API_BASE=https://lcbp3.np-dms.work image: dms-frontend:prod command: ["true"] -# docker compose -f docker-frontend-build.yml build --no-cache \ No newline at end of file + environment: + - NEXT_PUBLIC_API_BASE=https://lcbp3.np-dms.work + - NODE_ENV=production +# docker compose -f docker-frontend-build.yml build --no-cache +# **** สำหรับ build บน server เอา ## ออก ***** +# สำหรับ build บน local +# cd frontend +# docker build -t dms-frontend:dev --target dev . \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 55789579..8e4bb051 100755 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -35,6 +35,7 @@ RUN npx shadcn add -y button badge card input tabs progress dropdown-menu toolti # ใช้ร่วมกับ compose: bind ทั้งโปรเจ็กต์ → /app # และใช้ named volume แยกสำหรับ /app/node_modules และ /app/.next FROM base AS dev +RUN apk add --no-cache git openssh-client ca-certificates # สำคัญ: สร้างโฟลเดอร์ที่ Next ใช้งาน (เวลายังไม่ bind อะไรเข้ามา) RUN install -d -o node -g node /app/public /app/app /app/.logs /app/.next /app/.next/cache # นำ node_modules จากชั้น deps มาไว้ (ลดเวลาตอน start ครั้งแรก) diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev new file mode 100644 index 00000000..ba646530 --- /dev/null +++ b/frontend/Dockerfile.dev @@ -0,0 +1,70 @@ +# syntax=docker/dockerfile:1.6 + +############ Base ############ +FROM node:20-alpine AS base +WORKDIR /app +RUN apk add --no-cache bash curl tzdata \ + && ln -snf /usr/share/zoneinfo/Asia/Bangkok /etc/localtime \ + && echo "Asia/Bangkok" > /etc/timezone +ARG NEXT_PUBLIC_API_BASE +ENV TZ=Asia/Bangkok \ + NEXT_TELEMETRY_DISABLED=1 \ + CHOKIDAR_USEPOLLING=true \ + WATCHPACK_POLLING=true \ + NEXT_PUBLIC_API_BASE=${NEXT_PUBLIC_API_BASE} \ + npm_config_yes=true +# (ค่าพวกนี้ซ้ำกับ compose ได้ ไม่เป็นปัญหา) + +############ Deps (install) ############ +FROM base AS deps +COPY package.json package-lock.json* ./ +# ถ้ามี lock ใช้ npm ci; ถ้าไม่มีก็ npm i +RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm i --no-audit --no-fund; fi + +# เพิ่ม shadcn/ui + tailwind deps +RUN npm install -D tailwindcss postcss autoprefixer shadcn@latest \ + && npm install class-variance-authority clsx framer-motion lucide-react tailwind-merge tailwindcss-animate + +# init tailwind config (กัน No Tailwind CSS configuration found) +RUN npx tailwindcss init -p + +# bake components ของ shadcn แบบ non-interactive +RUN npx shadcn add -y button badge card input tabs progress dropdown-menu tooltip switch + +############ Dev (hot-reload) ############ +# ใช้ร่วมกับ compose: bind ทั้งโปรเจ็กต์ → /app +# และใช้ named volume แยกสำหรับ /app/node_modules และ /app/.next +FROM base AS dev +RUN apk add --no-cache git openssh-client ca-certificates +# สำคัญ: สร้างโฟลเดอร์ที่ Next ใช้งาน (เวลายังไม่ bind อะไรเข้ามา) +RUN install -d -o node -g node /app/public /app/app /app/.logs /app/.next /app/.next/cache +# นำ node_modules จากชั้น deps มาไว้ (ลดเวลาตอน start ครั้งแรก) +COPY --from=deps /app/node_modules /app/node_modules + +# ไม่กำหนด USER ที่นี่ ปล่อยให้ compose คุม (ตอนนี้คุณใช้ user: "1000:1000") +ENV NODE_ENV=development +EXPOSE 3000 +CMD ["npm", "run", "dev"] + +############ Build (production) ############ +FROM deps AS builder +COPY . . +RUN npm run build + +############ Prod runtime (optimized) ############ +FROM node:20-alpine AS prod +WORKDIR /app +ENV NODE_ENV=production +RUN apk add --no-cache libc6-compat +# RUN apk add --no-cache libc6-compat \ +# && addgroup -g 1000 node && adduser -D -u 1000 -G node node +# คัดเฉพาะของจำเป็น +COPY --from=builder /app/package.json /app/package-lock.json* ./ +# ติดตั้งเฉพาะ prod deps +RUN if [ -f package-lock.json ]; then npm ci --omit=dev --no-audit --no-fund; else npm i --omit=dev --no-audit --no-fund; fi +COPY --from=builder --chown=node:node /app/.next ./.next +COPY --from=builder --chown=node:node /app/public ./public +USER node +EXPOSE 3000 +CMD ["npm", "start"] +# docker compose -f docker-frontend-build.yml build --no-cache \ No newline at end of file diff --git a/frontend/app/(auth)/login/page.jsx b/frontend/app/(auth)/login/page.jsx index eb59cccf..70669504 100755 --- a/frontend/app/(auth)/login/page.jsx +++ b/frontend/app/(auth)/login/page.jsx @@ -1,34 +1,74 @@ -"use client"; -import { useState } from "react"; -import { API_BASE } from "@/lib/api"; - -export default function LoginPage() { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [err, setErr] = useState(""); - - async function onSubmit(e){ - e.preventDefault(); - setErr(""); - const res = await fetch(`${API_BASE}/auth/login`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password }), - credentials: "include", - }); - if (!res.ok) { setErr("เข้าสู่ระบบไม่สำเร็จ"); return; } - location.href = "/dashboard"; - } - - return ( -
-
-
เข้าสู่ระบบ
- setEmail(e.target.value)} /> - setPassword(e.target.value)} /> - {err &&
{err}
} - -
-
- ); -} +"use client"; +import { useState } from "react"; +import { API_BASE } from "@/lib/api"; + +export default function LoginPage() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [err, setErr] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + async function onSubmit(e) { + e.preventDefault(); + setErr(""); + setIsLoading(true); + try { + const res = await fetch(`${API_BASE}/api/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + + const data = await res.json(); + + if (!res.ok) { + setErr(data.error || "เข้าสู่ระบบไม่สำเร็จ"); + return; + } + + if (data.token) { + localStorage.setItem("token", data.token); + localStorage.setItem("refresh_token", data.refresh_token); + location.href = "/dashboard"; + } else { + setErr("ไม่ได้รับ Token"); + } + } catch (error) { + console.error("Login failed:", error); + setErr("เกิดข้อผิดพลาดในการเชื่อมต่อ"); + } finally { + setIsLoading(false); + } + } + + return ( +
+
+
เข้าสู่ระบบ
+ setUsername(e.target.value)} + /> + setPassword(e.target.value)} + /> + {err &&
{err}
} + +
+
+ ); +}