feat: ...
This commit is contained in:
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -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
|
||||||
74
.gitignore
vendored
74
.gitignore
vendored
@@ -1,31 +1,51 @@
|
|||||||
# ยกเว้นโฟลเดอร์
|
# ยกเว้นโฟลเดอร์
|
||||||
|
.devcontainer/
|
||||||
@Recently-Snapshot/
|
@Recently-Snapshot/
|
||||||
Documents/
|
Documents/
|
||||||
# VS Code
|
# =====================================================
|
||||||
|
# IDE/Editor settings
|
||||||
|
# =====================================================
|
||||||
.vscode/
|
.vscode/
|
||||||
# Dependencies
|
.idea/
|
||||||
/node_modules
|
# =====================================================
|
||||||
|
# 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
|
# Build outputs
|
||||||
/dist
|
/dist
|
||||||
/build
|
/build
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
/bin/
|
/bin/
|
||||||
# Environment variables (VERY IMPORTANT)
|
|
||||||
.env
|
# =====================================================
|
||||||
.env.local
|
# Environment files
|
||||||
.env.*.local
|
# =====================================================
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
# Environment variables (VERY IMPORTANT)
|
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
# Log files
|
|
||||||
|
# =====================================================
|
||||||
|
# Logs
|
||||||
|
# =====================================================
|
||||||
|
/backend/logs/
|
||||||
|
/frontend/logs/
|
||||||
|
/logs/
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
@@ -33,7 +53,6 @@ yarn-error.log*
|
|||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
Thumbs.db
|
|
||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
desktop.ini
|
desktop.ini
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
@@ -43,13 +62,26 @@ desktop.ini
|
|||||||
*.dump
|
*.dump
|
||||||
*.bak
|
*.bak
|
||||||
|
|
||||||
# ===================================================================
|
# =====================================================
|
||||||
# Operating System & IDE specific files
|
# OS-specific junk
|
||||||
# ===================================================================
|
# =====================================================
|
||||||
# macOS
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
.AppleDouble
|
.AppleDouble
|
||||||
|
|
||||||
# Windows
|
# =====================================================
|
||||||
Thumbs.db
|
# Docker-related
|
||||||
desktop.ini
|
# =====================================================
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
docker-compose.override.yml
|
||||||
|
docker-compose.override.*.yml
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# Cache / temp
|
||||||
|
# =====================================================
|
||||||
|
/backend/.cache/
|
||||||
|
/frontend/.cache/
|
||||||
|
.tmp/
|
||||||
|
.cache/
|
||||||
@@ -17,6 +17,7 @@ RUN mkdir -p ${RUNTIME_HOME} && mv node_modules ${RUNTIME_HOME}/node_modules
|
|||||||
|
|
||||||
########## Deps สำหรับ Development (รวม devDeps) ##########
|
########## Deps สำหรับ Development (รวม devDeps) ##########
|
||||||
FROM base AS deps-dev
|
FROM base AS deps-dev
|
||||||
|
RUN apk add --no-cache git openssh-client ca-certificates
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci || npm install
|
RUN npm ci || npm install
|
||||||
|
|||||||
@@ -1,22 +1,69 @@
|
|||||||
FROM node:20-alpine
|
# syntax=docker/dockerfile:1.6
|
||||||
# สำหรับอ่านค่า .env ที่วางไว้ระดับ compose (ไม่ copy เข้า image)
|
|
||||||
ENV NODE_ENV=development
|
|
||||||
ENV TZ=Asia/Bangkok
|
|
||||||
|
|
||||||
|
########## Base (apk + common tools ติดตั้งตอน build) ##########
|
||||||
|
FROM node:20-alpine AS base
|
||||||
WORKDIR /app
|
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 (ปลอดภัยขึ้น)
|
########## Deps สำหรับ Production (no devDeps) ##########
|
||||||
RUN addgroup -S dms && adduser -S dms -G dms
|
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 ฯลฯ)
|
########## Deps สำหรับ Development (รวม devDeps) ##########
|
||||||
RUN apk add --no-cache python3 make g++ curl
|
FROM base AS deps-dev
|
||||||
# ติดตั้ง nodemon ไว้ให้
|
RUN apk add --no-cache git openssh-client ca-certificates
|
||||||
RUN npm i -g nodemon
|
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* ./
|
########## Runtime: Development ##########
|
||||||
RUN (npm ci --omit=dev || npm install --omit=dev)
|
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
|
EXPOSE 3001
|
||||||
USER dms
|
HEALTHCHECK --interval=30s --timeout=5s --retries=10 \
|
||||||
CMD ["node", "src/index.js"]
|
CMD wget -qO- http://127.0.0.1:3001/health || exit 1
|
||||||
# backend/Dockerfile (Node.js ESM)
|
CMD ["node","src/index.js"]
|
||||||
3
backend/package-lock.json
generated
3
backend/package-lock.json
generated
@@ -7,6 +7,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "dms-backend",
|
"name": "dms-backend",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
@@ -2698,7 +2699,7 @@
|
|||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/winston/node_modules/winston-transport": {
|
"node_modules/winston-transport": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
|
||||||
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
|
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon --watch src src/index.js",
|
"dev": "nodemon --watch src src/index.js",
|
||||||
|
"dev:desktop": "node --watch src/index.js",
|
||||||
"start": "node src/index.js",
|
"start": "node src/index.js",
|
||||||
"lint": "echo 'lint placeholder'",
|
"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)})\"",
|
"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)})\"",
|
||||||
|
|||||||
@@ -40,7 +40,13 @@ const PORT = Number(process.env.PORT || 3001);
|
|||||||
const NODE_ENV = process.env.NODE_ENV || 'production';
|
const NODE_ENV = process.env.NODE_ENV || 'production';
|
||||||
|
|
||||||
// Origin ของ Frontend (ถ้ามี Nginx ด้านหน้า ให้ใช้โดเมน/พอร์ตของ Frontend)
|
// 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
|
// ที่เก็บ log ภายใน container ถูก bind ไปที่ /share/Container/dms/logs/backend
|
||||||
const LOG_DIR = process.env.BACKEND_LOG_DIR || '/app/logs';
|
const LOG_DIR = process.env.BACKEND_LOG_DIR || '/app/logs';
|
||||||
@@ -57,23 +63,29 @@ try {
|
|||||||
* ========================== */
|
* ========================== */
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// หลัง Nginx/Reverse Proxy ควรเปิด trust proxy เพื่ออ่าน X-Forwarded-*
|
// CORS แบบกำหนด origin ตามรายการที่อนุญาต + อนุญาต credentials
|
||||||
app.set('trust proxy', 1);
|
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());
|
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
|
// Payload limits
|
||||||
app.use(express.json({ limit: '10mb' }));
|
app.use(express.json({ limit: '10mb' }));
|
||||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
backend_dev_image:
|
backend_dev_image:
|
||||||
build:
|
build:
|
||||||
context: /share/Container/dms/backend
|
## context: /share/Container/dms/backend
|
||||||
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
target: dev
|
target: dev
|
||||||
image: dms-backend:dev
|
image: dms-backend:dev
|
||||||
@@ -9,9 +10,14 @@ services:
|
|||||||
|
|
||||||
backend_prod_image:
|
backend_prod_image:
|
||||||
build:
|
build:
|
||||||
context: /share/Container/dms/backend
|
## context: /share/Container/dms/backend
|
||||||
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
target: prod
|
target: prod
|
||||||
image: dms-backend:prod
|
image: dms-backend:prod
|
||||||
command: ["true"]
|
command: ["true"]
|
||||||
# docker compose -f docker-backend-build.yml build --no-cache
|
# docker compose -f docker-backend-build.yml build --no-cache
|
||||||
|
# ***** สำหรับ build บน server เอา ## ออก *****
|
||||||
|
# สำหรับ build บน local
|
||||||
|
# cd backend
|
||||||
|
# docker build -t dms-backend:dev --target dev .
|
||||||
@@ -83,6 +83,7 @@ services:
|
|||||||
JWT_SECRET: "8b0df02e4aee9f9f79a4f2d8ba77b0b82c1ee3446b68cb0bae94ab54d60f8d9e"
|
JWT_SECRET: "8b0df02e4aee9f9f79a4f2d8ba77b0b82c1ee3446b68cb0bae94ab54d60f8d9e"
|
||||||
JWT_EXPIRES_IN: "12h"
|
JWT_EXPIRES_IN: "12h"
|
||||||
PASSWORD_SALT_ROUNDS: "10"
|
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"
|
CORS_ORIGINS: "https://backend.np-dms.work,http://localhost:3000,http://127.0.0.1:3000"
|
||||||
RATE_LIMIT_WINDOW_MS: "900000"
|
RATE_LIMIT_WINDOW_MS: "900000"
|
||||||
RATE_LIMIT_MAX: "200"
|
RATE_LIMIT_MAX: "200"
|
||||||
@@ -127,7 +128,7 @@ services:
|
|||||||
NEXT_PUBLIC_API_BASE: "/api"
|
NEXT_PUBLIC_API_BASE: "/api"
|
||||||
CHOKIDAR_USEPOLLING: "1"
|
CHOKIDAR_USEPOLLING: "1"
|
||||||
WATCHPACK_POLLING: "true"
|
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"
|
NEXT_TELEMETRY_DISABLED: "1"
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
frontend_dev_image:
|
frontend_dev_image:
|
||||||
build:
|
build:
|
||||||
context: /share/Container/dms/frontend
|
# context: /share/Container/dms/frontend
|
||||||
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
target: dev
|
target: dev
|
||||||
image: dms-frontend:dev
|
image: dms-frontend:dev
|
||||||
@@ -9,13 +10,22 @@ services:
|
|||||||
|
|
||||||
frontend_prod_image:
|
frontend_prod_image:
|
||||||
build:
|
build:
|
||||||
context: /share/Container/dms/frontend
|
## context: /share/Container/dms/frontend
|
||||||
|
context: ./frontend
|
||||||
args:
|
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
|
dockerfile: Dockerfile
|
||||||
target: prod
|
target: prod
|
||||||
environment:
|
## environment:
|
||||||
- NEXT_PUBLIC_API_BASE=https://lcbp3.np-dms.work
|
## - NEXT_PUBLIC_API_BASE=https://lcbp3.np-dms.work
|
||||||
image: dms-frontend:prod
|
image: dms-frontend:prod
|
||||||
command: ["true"]
|
command: ["true"]
|
||||||
# docker compose -f docker-frontend-build.yml build --no-cache
|
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 .
|
||||||
@@ -35,6 +35,7 @@ RUN npx shadcn add -y button badge card input tabs progress dropdown-menu toolti
|
|||||||
# ใช้ร่วมกับ compose: bind ทั้งโปรเจ็กต์ → /app
|
# ใช้ร่วมกับ compose: bind ทั้งโปรเจ็กต์ → /app
|
||||||
# และใช้ named volume แยกสำหรับ /app/node_modules และ /app/.next
|
# และใช้ named volume แยกสำหรับ /app/node_modules และ /app/.next
|
||||||
FROM base AS dev
|
FROM base AS dev
|
||||||
|
RUN apk add --no-cache git openssh-client ca-certificates
|
||||||
# สำคัญ: สร้างโฟลเดอร์ที่ Next ใช้งาน (เวลายังไม่ bind อะไรเข้ามา)
|
# สำคัญ: สร้างโฟลเดอร์ที่ Next ใช้งาน (เวลายังไม่ bind อะไรเข้ามา)
|
||||||
RUN install -d -o node -g node /app/public /app/app /app/.logs /app/.next /app/.next/cache
|
RUN install -d -o node -g node /app/public /app/app /app/.logs /app/.next /app/.next/cache
|
||||||
# นำ node_modules จากชั้น deps มาไว้ (ลดเวลาตอน start ครั้งแรก)
|
# นำ node_modules จากชั้น deps มาไว้ (ลดเวลาตอน start ครั้งแรก)
|
||||||
|
|||||||
70
frontend/Dockerfile.dev
Normal file
70
frontend/Dockerfile.dev
Normal file
@@ -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
|
||||||
@@ -1,34 +1,74 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { API_BASE } from "@/lib/api";
|
import { API_BASE } from "@/lib/api";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [email, setEmail] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
async function onSubmit(e){
|
|
||||||
e.preventDefault();
|
async function onSubmit(e) {
|
||||||
setErr("");
|
e.preventDefault();
|
||||||
const res = await fetch(`${API_BASE}/auth/login`, {
|
setErr("");
|
||||||
method: "POST",
|
setIsLoading(true);
|
||||||
headers: { "Content-Type": "application/json" },
|
try {
|
||||||
body: JSON.stringify({ email, password }),
|
const res = await fetch(`${API_BASE}/api/auth/login`, {
|
||||||
credentials: "include",
|
method: "POST",
|
||||||
});
|
headers: { "Content-Type": "application/json" },
|
||||||
if (!res.ok) { setErr("เข้าสู่ระบบไม่สำเร็จ"); return; }
|
body: JSON.stringify({ username, password }),
|
||||||
location.href = "/dashboard";
|
});
|
||||||
}
|
|
||||||
|
const data = await res.json();
|
||||||
return (
|
|
||||||
<div className="grid place-items-center min-h-screen">
|
if (!res.ok) {
|
||||||
<form onSubmit={onSubmit} className="bg-white/90 rounded-2xl p-6 w-full max-w-sm shadow">
|
setErr(data.error || "เข้าสู่ระบบไม่สำเร็จ");
|
||||||
<div className="text-lg font-semibold mb-4">เข้าสู่ระบบ</div>
|
return;
|
||||||
<input className="w-full border rounded-xl p-2 mb-2" placeholder="อีเมล" value={email} onChange={e=>setEmail(e.target.value)} />
|
}
|
||||||
<input type="password" className="w-full border rounded-xl p-2 mb-3" placeholder="รหัสผ่าน" value={password} onChange={e=>setPassword(e.target.value)} />
|
|
||||||
{err && <div className="text-red-600 text-sm mb-2">{err}</div>}
|
if (data.token) {
|
||||||
<button className="w-full rounded-xl p-2 text-white" style={{background:'#0D5C75'}}>เข้าสู่ระบบ</button>
|
localStorage.setItem("token", data.token);
|
||||||
</form>
|
localStorage.setItem("refresh_token", data.refresh_token);
|
||||||
</div>
|
location.href = "/dashboard";
|
||||||
);
|
} else {
|
||||||
}
|
setErr("ไม่ได้รับ Token");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Login failed:", error);
|
||||||
|
setErr("เกิดข้อผิดพลาดในการเชื่อมต่อ");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid min-h-screen place-items-center" style={{background: 'linear-gradient(to bottom right, #00c6ff, #0072ff)'}}>
|
||||||
|
<form onSubmit={onSubmit} className="w-full max-w-sm p-8 space-y-4 shadow-lg bg-white/20 backdrop-blur-md rounded-3xl">
|
||||||
|
<div className="text-2xl font-bold text-center text-white">เข้าสู่ระบบ</div>
|
||||||
|
<input
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full p-3 text-white placeholder-gray-200 border bg-white/30 border-white/30 rounded-xl focus:outline-none focus:ring-2 focus:ring-white/50 disabled:opacity-50"
|
||||||
|
placeholder="ชื่อผู้ใช้"
|
||||||
|
value={username}
|
||||||
|
onChange={e=>setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full p-3 text-white placeholder-gray-200 border bg-white/30 border-white/30 rounded-xl focus:outline-none focus:ring-2 focus:ring-white/50 disabled:opacity-50"
|
||||||
|
placeholder="รหัสผ่าน"
|
||||||
|
value={password}
|
||||||
|
onChange={e=>setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
{err && <div className="text-sm text-center text-yellow-300">{err}</div>}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full p-3 font-bold text-white transition-colors duration-300 bg-blue-500 rounded-xl hover:bg-blue-600 disabled:bg-blue-400 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isLoading ? 'กำลังเข้าสู่ระบบ...' : 'เข้าสู่ระบบ'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user