260211:1703 First Deploy (Not Complete)
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
admin
2026-02-11 17:03:38 +07:00
parent d9df4e66b4
commit 912f379016
11 changed files with 98 additions and 91 deletions

View File

@@ -43,8 +43,8 @@ COPY backend/ ./backend/
# Build NestJS → backend/dist
RUN cd backend && pnpm run build
# Prune dev dependencies
RUN pnpm prune --prod --filter backend...
# Deploy with production deps only (pnpm workspace isolation)
RUN pnpm --filter backend deploy --prod --legacy /app/backend-prod
# =========================
# Stage 3: Production Runtime
@@ -65,8 +65,8 @@ RUN addgroup -g 1001 -S nestjs && \
# Copy production artifacts only
COPY --from=build --chown=nestjs:nestjs /app/backend/dist ./dist
COPY --from=build --chown=nestjs:nestjs /app/backend/node_modules ./node_modules
COPY --from=build --chown=nestjs:nestjs /app/backend/package.json ./
COPY --from=build --chown=nestjs:nestjs /app/backend-prod/node_modules ./node_modules
COPY --from=build --chown=nestjs:nestjs /app/backend-prod/package.json ./
# Create uploads directory (Two-Phase Storage)
RUN mkdir -p /app/uploads/temp /app/uploads/permanent && \

View File

@@ -56,6 +56,7 @@
"cache-manager-redis-yet": "^5.1.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"express": "^5.1.0",
"fs-extra": "^11.3.2",
"helmet": "^8.1.0",
"ioredis": "^5.8.2",

View File

@@ -18,21 +18,26 @@ import { ForbiddenException } from '@nestjs/common'; // ✅ Import เพิ่
@Injectable()
export class FileStorageService {
private readonly logger = new Logger(FileStorageService.name);
private readonly uploadRoot: string;
private readonly tempDir: string;
private readonly permanentDir: string;
constructor(
@InjectRepository(Attachment)
private attachmentRepository: Repository<Attachment>,
private configService: ConfigService,
private configService: ConfigService
) {
// ใช้ Path จริงถ้าอยู่บน Server (Production) หรือใช้ ./uploads ถ้าอยู่ Local
this.uploadRoot =
this.configService.get('NODE_ENV') === 'production'
? '/share/dms-data'
: path.join(process.cwd(), 'uploads');
// ใช้ env vars จาก docker-compose สำหรับ Production
// ถ้าไม่ได้กำหนดจะ fallback เป็น ./uploads/temp และ ./uploads/permanent
this.tempDir =
this.configService.get<string>('UPLOAD_TEMP_DIR') ||
path.join(process.cwd(), 'uploads', 'temp');
this.permanentDir =
this.configService.get<string>('UPLOAD_PERMANENT_DIR') ||
path.join(process.cwd(), 'uploads', 'permanent');
// สร้างโฟลเดอร์ temp รอไว้เลยถ้ายังไม่มี
fs.ensureDirSync(path.join(this.uploadRoot, 'temp'));
// สร้างโฟลเดอร์ temp และ permanent รอไว้เลยถ้ายังไม่มี
fs.ensureDirSync(this.tempDir);
fs.ensureDirSync(this.permanentDir);
}
/**
@@ -42,7 +47,7 @@ export class FileStorageService {
const tempId = uuidv4();
const fileExt = path.extname(file.originalname);
const storedFilename = `${uuidv4()}${fileExt}`;
const tempPath = path.join(this.uploadRoot, 'temp', storedFilename);
const tempPath = path.join(this.tempDir, storedFilename);
// 1. คำนวณ Checksum (SHA-256) เพื่อความปลอดภัยและความถูกต้องของไฟล์
const checksum = this.calculateChecksum(file.buffer);
@@ -89,7 +94,7 @@ export class FileStorageService {
// แจ้งเตือนแต่อาจจะไม่ throw ถ้าต้องการให้ process ต่อไปได้บางส่วน (ขึ้นอยู่กับ business logic)
// แต่เพื่อความปลอดภัยควรแจ้งว่าไฟล์ไม่ครบ
this.logger.warn(
`Expected ${tempIds.length} files to commit, but found ${attachments.length}`,
`Expected ${tempIds.length} files to commit, but found ${attachments.length}`
);
throw new NotFoundException('Some files not found or already committed');
}
@@ -100,7 +105,7 @@ export class FileStorageService {
const month = (today.getMonth() + 1).toString().padStart(2, '0');
// โฟลเดอร์ถาวรแยกตาม ปี/เดือน
const permanentDir = path.join(this.uploadRoot, 'permanent', year, month);
const permanentDir = path.join(this.permanentDir, year, month);
await fs.ensureDir(permanentDir);
for (const att of attachments) {
@@ -122,16 +127,16 @@ export class FileStorageService {
} else {
this.logger.error(`File missing during commit: ${oldPath}`);
throw new NotFoundException(
`File not found on disk: ${att.originalFilename}`,
`File not found on disk: ${att.originalFilename}`
);
}
} catch (error) {
this.logger.error(
`Failed to move file from ${oldPath} to ${newPath}`,
error,
error
);
throw new BadRequestException(
`Failed to commit file: ${att.originalFilename}`,
`Failed to commit file: ${att.originalFilename}`
);
}
}
@@ -144,7 +149,7 @@ export class FileStorageService {
* ดึงไฟล์มาเป็น Stream เพื่อส่งกลับไปให้ Controller
*/
async download(
id: number,
id: number
): Promise<{ stream: fs.ReadStream; attachment: Attachment }> {
// 1. ค้นหาข้อมูลไฟล์จาก DB
const attachment = await this.attachmentRepository.findOne({
@@ -191,7 +196,7 @@ export class FileStorageService {
// (ในอนาคตอาจเพิ่มเงื่อนไข OR User เป็น Admin/Document Control)
if (attachment.uploadedByUserId !== userId) {
this.logger.warn(
`User ${userId} tried to delete file ${id} owned by ${attachment.uploadedByUserId}`,
`User ${userId} tried to delete file ${id} owned by ${attachment.uploadedByUserId}`
);
throw new ForbiddenException('You are not allowed to delete this file');
}
@@ -202,13 +207,13 @@ export class FileStorageService {
await fs.remove(attachment.filePath);
} else {
this.logger.warn(
`File not found on disk during deletion: ${attachment.filePath}`,
`File not found on disk during deletion: ${attachment.filePath}`
);
}
} catch (error) {
this.logger.error(
`Failed to delete file from disk: ${attachment.filePath}`,
error,
error
);
throw new BadRequestException('Failed to delete file from storage');
}

View File

@@ -1,7 +1,4 @@
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { config } from 'dotenv';
config();
export const databaseConfig: TypeOrmModuleOptions = {
type: 'mysql',

View File

@@ -48,8 +48,10 @@ async function bootstrap() {
app.use(json({ limit: '50mb' }));
app.use(urlencoded({ extended: true, limit: '50mb' }));
// 🌐 4. Global Prefix
app.setGlobalPrefix('api');
// 🌐 4. Global Prefix (ยกเว้น /health, /metrics สำหรับ monitoring)
app.setGlobalPrefix('api', {
exclude: ['health', 'metrics'],
});
// ⚙️ 5. Global Pipes & Interceptors & Filters
app.useGlobalPipes(

View File

@@ -1,4 +1,11 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts", "documentation"]
"exclude": [
"node_modules",
"test",
"dist",
"scripts",
"**/*spec.ts",
"documentation"
]
}

BIN
lcbp3-backend.tar Normal file

Binary file not shown.

BIN
lcbp3-frontend.tar Normal file

Binary file not shown.

54
pnpm-lock.yaml generated
View File

@@ -109,6 +109,9 @@ importers:
class-validator:
specifier: ^0.14.2
version: 0.14.3
express:
specifier: ^5.1.0
version: 5.1.0
fs-extra:
specifier: ^11.3.2
version: 11.3.2
@@ -4708,6 +4711,10 @@ packages:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
cors@2.8.6:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'}
cosmiconfig@8.3.6:
resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
engines: {node: '>=14'}
@@ -7312,10 +7319,6 @@ packages:
engines: {node: '>=10'}
hasBin: true
send@1.2.0:
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
engines: {node: '>= 18'}
send@1.2.1:
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
engines: {node: '>= 18'}
@@ -9790,7 +9793,7 @@ snapshots:
chokidar: 3.6.0
colors: 1.4.0
connect: 3.7.0
cors: 2.8.5
cors: 2.8.6
event-stream: 4.0.1
faye-websocket: 0.11.4
http-auth: 4.1.9
@@ -13476,6 +13479,11 @@ snapshots:
object-assign: 4.1.1
vary: 1.1.2
cors@2.8.6:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
cosmiconfig@8.3.6(typescript@5.9.3):
dependencies:
import-fresh: 3.3.1
@@ -13949,8 +13957,8 @@ snapshots:
'@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.5(eslint@8.57.1)
eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1)
@@ -13973,7 +13981,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -13984,22 +13992,22 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.48.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -14010,7 +14018,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -14295,7 +14303,7 @@ snapshots:
qs: 6.14.0
range-parser: 1.2.1
router: 2.2.0
send: 1.2.0
send: 1.2.1
serve-static: 2.2.0
statuses: 2.0.2
type-is: 2.0.1
@@ -16492,22 +16500,6 @@ snapshots:
semver@7.7.3: {}
send@1.2.0:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
fresh: 2.0.0
http-errors: 2.0.1
mime-types: 3.0.2
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.2
transitivePeerDependencies:
- supports-color
send@1.2.1:
dependencies:
debug: 4.4.3
@@ -16547,7 +16539,7 @@ snapshots:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
send: 1.2.0
send: 1.2.1
transitivePeerDependencies:
- supports-color

View File

@@ -22,25 +22,25 @@
## 1. Build Docker Images
### Option A: Build บน Dev Machine แล้ว Transfer
### Option A: Build บน Dev Machine (Windows) แล้ว Transfer
```bash
```powershell
# อยู่ที่ workspace root (nap-dms.lcbp3/)
# Build Backend
docker build -f backend/Dockerfile -t lcbp3-backend:latest .
# Build Frontend (NEXT_PUBLIC_API_URL bake เข้าไปตอน build)
docker build -f frontend/Dockerfile \
--build-arg NEXT_PUBLIC_API_URL=https://backend.np-dms.work/api \
docker build -f frontend/Dockerfile `
--build-arg NEXT_PUBLIC_API_URL=https://backend.np-dms.work/api `
-t lcbp3-frontend:latest .
# Export เป็น .tar เพื่อ Transfer
docker save lcbp3-backend:latest | gzip > lcbp3-backend.tar.gz
docker save lcbp3-frontend:latest | gzip > lcbp3-frontend.tar.gz
docker save lcbp3-backend:latest -o lcbp3-backend.tar
docker save lcbp3-frontend:latest -o lcbp3-frontend.tar
# Transfer ไปยัง QNAP (ผ่าน SCP หรือ Shared Folder)
scp lcbp3-*.tar.gz admin@192.168.10.8:/share/np-dms/app/
# Transfer ไปยัง QNAP (ผ่าน SMB Shared Folder)
# Copy lcbp3-backend.tar และ lcbp3-frontend.tar ไปที่ \\192.168.10.8\np-dms\app\
```
### Option B: Build บน QNAP โดยตรง (SSH)
@@ -69,8 +69,8 @@ docker build -f frontend/Dockerfile \
ssh admin@192.168.10.8
# Load images
docker load < /share/np-dms/app/lcbp3-backend.tar.gz
docker load < /share/np-dms/app/lcbp3-frontend.tar.gz
docker load < /share/np-dms/app/lcbp3-backend.tar
docker load < /share/np-dms/app/lcbp3-frontend.tar
# ตรวจสอบ
docker images | grep lcbp3
@@ -82,15 +82,15 @@ docker images | grep lcbp3
```bash
# สร้าง directories สำหรับ volumes
mkdir -p /share/dms-data/uploads/temp
mkdir -p /share/dms-data/uploads/permanent
mkdir -p /share/dms-data/logs/backend
mkdir -p /share/np-dms/data/uploads/temp
mkdir -p /share/np-dms/data/uploads/permanent
mkdir -p /share/np-dms/data/logs/backend
mkdir -p /share/np-dms/app
# กำหนดสิทธิ์ให้ non-root user ใน container (UID 1001)
chown -R 1001:1001 /share/dms-data/uploads
chown -R 1001:1001 /share/dms-data/logs/backend
chmod -R 750 /share/dms-data/uploads
chown -R 1001:1001 /share/np-dms/data/uploads
chown -R 1001:1001 /share/np-dms/data/logs/backend
chmod -R 750 /share/np-dms/data/uploads
```
---
@@ -160,20 +160,22 @@ docker logs -f frontend
เมื่อต้องการ deploy version ใหม่:
```bash
# 1. Build images ใหม่ (บน Dev Machine)
```powershell
# 1. Build images ใหม่ (บน Dev Machine - PowerShell)
docker build -f backend/Dockerfile -t lcbp3-backend:latest .
docker build -f frontend/Dockerfile -t lcbp3-frontend:latest .
docker build -f frontend/Dockerfile `
--build-arg NEXT_PUBLIC_API_URL=https://backend.np-dms.work/api `
-t lcbp3-frontend:latest .
# 2. Export & Transfer
docker save lcbp3-backend:latest | gzip > lcbp3-backend.tar.gz
docker save lcbp3-frontend:latest | gzip > lcbp3-frontend.tar.gz
scp lcbp3-*.tar.gz admin@192.168.10.8:/share/np-dms/app/
docker save lcbp3-backend:latest -o lcbp3-backend.tar
docker save lcbp3-frontend:latest -o lcbp3-frontend.tar
# Copy ไปที่ \\192.168.10.8\np-dms\app\ ผ่าน SMB Shared Folder
# 3. Load บน QNAP
# 3. Load บน QNAP (SSH)
ssh admin@192.168.10.8
docker load < /share/np-dms/app/lcbp3-backend.tar.gz
docker load < /share/np-dms/app/lcbp3-frontend.tar.gz
docker load < /share/np-dms/app/lcbp3-backend.tar
docker load < /share/np-dms/app/lcbp3-frontend.tar
# 4. Restart ใน Container Station
# Applications → lcbp3-app → Restart

View File

@@ -48,8 +48,8 @@ services:
# --- Database ---
DB_HOST: 'mariadb'
DB_PORT: '3306'
DB_NAME: 'lcbp3'
DB_USER: 'center'
DB_DATABASE: 'lcbp3'
DB_USERNAME: 'center'
DB_PASSWORD: 'Center#2025'
# --- Redis ---
REDIS_HOST: 'cache'
@@ -60,7 +60,8 @@ services:
ELASTICSEARCH_PORT: '9200'
# --- JWT ---
JWT_SECRET: 'eebc122aa65adde8c76c6a0847d9649b2b67a06db1504693e6c912e51499b76e'
JWT_EXPIRES_IN: '8h'
JWT_EXPIRATION: '8h'
JWT_REFRESH_SECRET: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2'
# --- Numbering ---
NUMBERING_LOCK_TIMEOUT: '5000'
NUMBERING_RESERVATION_TTL: '300'
@@ -72,9 +73,9 @@ services:
- lcbp3
volumes:
# Two-Phase Storage: จัดเก็บไฟล์นอก container
- '/share/dms-data/uploads/temp:/app/uploads/temp'
- '/share/dms-data/uploads/permanent:/app/uploads/permanent'
- '/share/dms-data/logs/backend:/app/logs'
- '/share/np-dms/data/uploads/temp:/app/uploads/temp'
- '/share/np-dms/data/uploads/permanent:/app/uploads/permanent'
- '/share/np-dms/data/logs/backend:/app/logs'
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
interval: 30s