From 45ff0001e6bec30d663d1191ff798f2947aabd4b Mon Sep 17 00:00:00 2001 From: oom Date: Mon, 2 Nov 2026 10:00:12 +0700 Subject: [PATCH] clear environment variable, update download-docx(sample) to check latest version before query database. --- .env.example | 4 +- app/api/template/download-docx/route.ts | 85 +++++++++++++++++-------- app/api/template/generate/route.ts | 26 ++++---- lib/lib-tmpl.ts | 4 +- lib/lib-utils.ts | 21 +++++- lib/openapi.ts | 11 ++-- public/tmpl/dl.html | 3 +- 7 files changed, 103 insertions(+), 51 deletions(-) diff --git a/.env.example b/.env.example index 0aa4f54..ebc2217 100644 --- a/.env.example +++ b/.env.example @@ -10,8 +10,8 @@ DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/ecourt-note DOCEDIT_BASE_URL=http://192.168.2.51:3000/docedit # สำหรับตัวอย่างระบบนอกเรียกใช้ API ของ template EXTERNAL_API_KEY='secret-api-key-1234' -# ที่เก็บไฟล์เทมเพลต(docx) -TMPL_PATH='./public/tmpl' +# ที่เก็บไฟล์เทมเพลต(docx) ค่า default คือ /app/public/tmpl +#TMPL_PATH='./public/tmpl' # Sign/Hash ที่ใช้ฝังในเอกสาร(docx) SECRET="super_secret" diff --git a/app/api/template/download-docx/route.ts b/app/api/template/download-docx/route.ts index f7e8017..bc6148a 100644 --- a/app/api/template/download-docx/route.ts +++ b/app/api/template/download-docx/route.ts @@ -1,46 +1,79 @@ // ตัวอย่างรองรับค่าจาก Frontend ที่เป็น Vanilla JavaScript และ Classic Framework -// โดย ผ่าน Form หรือ การกดลิงค์(GET) เปลี่ยนหน้าเพื่อดาว์นโหลดไฟล์ +// โดย ผ่าน Form(POST) หรือ การกดลิงค์(GET) เปลี่ยนหน้าเพื่อดาว์นโหลดไฟล์ // จะมีข้อมูล info.ticket_data ที่สมบูรณ์แล้ว({fid,tid,uid,uname})แล้วเเรียก /api/template/generate เพื่อสร้างเอกสาร docx + +// import { headers } from "next/headers"; import { NextRequest, NextResponse } from 'next/server'; + import path from 'path'; import fs from 'fs/promises'; - -const DOCEDIT_BASE_URL = process.env.DOCEDIT_BASE_URL??""; -const EXTERNAL_API_KEY = process.env.EXTERNAL_API_KEY??""; -const TMPL_PATH = process.env.TMPL_PATH??""; -if (!DOCEDIT_BASE_URL||!EXTERNAL_API_KEY||!TMPL_PATH) { - console.error('Missing variable DOCEDIT_BASE_URL, EXTERNAL_API_KEY or TMPL_PATH') -} - -// console.log("download-docx env",DOCEDIT_BASE_URL,EXTERNAL_API_KEY,TMPL_PATH) +const EXTERNAL_API_KEY = process.env.EXTERNAL_API_KEY||''; +const TMPL_PATH = process.env.TMPL_PATH || "./public/tmpl"; +const DOCEDIT_BASE_URL = process.env.DOCEDIT_BASE_URL || ''; async function processRequest(fid: string,tid:string,uid:string,uname:string):Promise> { - // console.log("processRequest get:",fid,tid,uid,uname) - // console.log(process.cwd()) - const filePath = path.join(TMPL_PATH,`${fid}.json`); - try { - if (!fid||!tid||!uid||!uname) { - return NextResponse.json({ error: 'Missing fid, tid, uid or uname' }, { status: 400 }); - } + //กัน path traversal attack + const TID = path.basename(tid) + const FID = path.basename(fid) + const ticketObj = {fid:FID,tid:TID,uid,uname} + const ticket_data = JSON.stringify({fid:FID,tid:TID,uid,uname}) + const filePath = path.join(TMPL_PATH,`${FID}.json`); + // สำหรับการ demo ในแอปเดียวเท่านั้น สามารถคำนวนหา base path ได้ด้วยวิธีนี้ + // const host = headers().get("host"); // e.g., localhost:3000 + // const proto = headers().get("x-forwarded-proto") || "http"; + // const baseUrl = process.env.NEXT_PUBLIC_BASE_PATH || "" + // const fullUrl = `${proto}://${host}${baseUrl}`; + // const DOCEDIT_BASE_URL = process.env.DOCEDIT_BASE_URL || fullUrl; + if(!DOCEDIT_BASE_URL || !EXTERNAL_API_KEY) { + return NextResponse.json({ error: 'Server must set Environment variable DOCEDIT_BASE_URL or EXTERNAL_API_KEY' }, { status: 500 }); + } + if (!FID||!TID||!uid||!uname) { + return NextResponse.json({ error: 'Missing fid, tid, uid or uname' }, { status: 400 }); + } - // อ่านไฟล์ JSON จำลองการอ่านจากฐานข้อมูล + + try { //ตรวจสอบ docx เวอร์ชั่นล่าสุดก่อนถ้าไม่พบค่อยเอาจากฐานข้อมูลมาสร้าง raw Json + const params = new URLSearchParams({type:'docx',ticket_data}); + const lastestDocxResponse = await fetch(`${DOCEDIT_BASE_URL}/api/document/latest?${params.toString()}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': EXTERNAL_API_KEY, + } + }); + + if(!lastestDocxResponse.ok && lastestDocxResponse.status !== 404){//เกิดปัญหาที่ไม่ใช่ Not Found + const errorText = await lastestDocxResponse.text(); + console.error('download-docx:Invalid latest docx Response:', errorText); + return NextResponse.json( + {error:`Error from docedit latest: Expected docx stream but received ${lastestDocxResponse.status} ${lastestDocxResponse.statusText}`}, + { status: 502 } + ); + } + } catch (error) { + console.error(error) + return NextResponse.json({error:'Internal Server Error'}, { status: 500 }); + } + console.log('No version found, generating docx from template and JSON data...'); + + try { // ไม่พบเวอร์ชั้นให้สร้าง docx จาก template และ json(ฐานข้อมูล) + // อ่านไฟล์ JSON จำลองการอ่านจากฐานข้อมูล const rawJson = await fs.readFile(filePath, 'utf-8'); const jsonData = JSON.parse(rawJson); if (!jsonData || typeof jsonData !== 'object' ) { return NextResponse.json({error:'Invalid data format'}, { status: 400 }); } + /////// !! ตรวจสอบสิทธิ์ผู้ใช้ uid ว่ามีสิทธิ์เข้าถึงไฟล์ fid หรือไม่เพื่อกันการการยิง API ตรงๆ ไม่ผ่าน Frontend !!// + /////// ไม่มี session return 401, ถ้าเจอแต่ไม่มีสิทธิ์ให้ return 403 , อ่านจากฐานข้อมูล ถ้าไม่พบข้อมูลให้ return 404, - //!! ตรวจสอบสิทธิ์ผู้ใช้ uid ว่ามีสิทธิ์เข้าถึงไฟล์ fid หรือไม่เพื่อกันการการยิง API ตรงๆ ไม่ผ่าน Frontend!! - - // ข้อมูลจำเป็นต้องมี prop นี้เสมอถ้าไม่มีให้สร้าง jsonData.info.ticket_data + // ข้อมูลจำเป็นต้องมี jsonData.info.ticket_data ถ้าไม่มีให้สร้างขึ้นมาเอง if(!Object.hasOwn(jsonData, 'info')) jsonData['info']={} - jsonData.info['ticket_data'] = JSON.stringify({fid,tid,uid,uname}) + jsonData.info['ticket_data'] = JSON.stringify(ticketObj) // sign เองตรงนี้ให้อยู่ในรูปแบบ data.info={ticket_data,HmacSha256,Ed25519} หรือว่างไว้ให้ docedit สร้างให้ //ยิงไปหาระบบ Docedit เพื่อเอา Stream - // console.log(`fetch ${DOCEDIT_BASE_URL}/api/template/generate`) const docxResponse = await fetch(`${DOCEDIT_BASE_URL}/api/template/generate`, { method: 'POST', headers: { @@ -52,7 +85,6 @@ async function processRequest(fid: string,tid:string,uid:string,uname:string):Pr // เช็ค Content-Type และ Status ก่อน const contentType = docxResponse.headers.get('content-type') || ''; - if (!docxResponse.ok || contentType.includes('text/html')) { const errorText = await docxResponse.text(); console.error('download-docx:Invalid docx Response:', errorText); @@ -65,9 +97,8 @@ async function processRequest(fid: string,tid:string,uid:string,uname:string):Pr // 2. ดึง ReadableStream โดยตรงจาก Response ของระบบ const stream = docxResponse.body; - // 3. ส่ง Binary Stream กลับไปยัง Browser พร้อม Header กำหนดประเภทไฟล์ - const download_file = `${fid}_${uid}_xxx` + const download_file = `${FID}_${uid}_xxx` return new NextResponse(stream as unknown as BodyInit, { status: 200, headers: { @@ -78,7 +109,7 @@ async function processRequest(fid: string,tid:string,uid:string,uname:string):Pr }, }); } catch (error:any) { - if ( error.code === 'ENOENT') { + if ( error.code === 'ENOENT') {// ไฟล์ JSON ไม่พบ ถ้าใช้ฐานข้อมูลจริงให้ return 404 แทน return NextResponse.json( { error: `Data not found: ${filePath}`}, { status: 404 } diff --git a/app/api/template/generate/route.ts b/app/api/template/generate/route.ts index 7b0f506..8f9d3c0 100644 --- a/app/api/template/generate/route.ts +++ b/app/api/template/generate/route.ts @@ -6,25 +6,25 @@ import { NextRequest, NextResponse } from 'next/server'; import {readFile} from 'fs/promises'; import path from 'path'; import {DocumentTemplate,applyTemplate} from "@/lib/lib-tmpl"; + const EXTERNAL_API_KEY = process.env.EXTERNAL_API_KEY || 'secret-api-key-1234'; -const TMPL_PATH = process.env.TMPL_PATH || '' const PRIVATE_KEY_HEX = process.env.PRIVATE_KEY_HEX ||'' -const SECRET = process.env.SECRET || '' -const TICKET_SEC = Number(process.env.TICKET_SEC||'300') - -if (!EXTERNAL_API_KEY || !TMPL_PATH || !PRIVATE_KEY_HEX || !SECRET) - console.log("api/template/generate: require EXTERNAL_API_KEY, TMPL_PATH and PRIVATE_KEY_HEX") - -// console.log("generate env:",EXTERNAL_API_KEY,TMPL_PATH,PRIVATE_KEY_HEX,SECRET) - +const SECRET = process.env.SECRET || 'super_secret' +const TMPL_PATH = process.env.TMPL_PATH || "./public/tmpl"; +const TICKET_SEC = Number(process.env.TICKET_SEC||'120') export async function POST(request: NextRequest) { + if (!EXTERNAL_API_KEY || !PRIVATE_KEY_HEX || !SECRET){ + return NextResponse.json( + { error: 'Server Error: require EXTERNAL_API_KEY PRIVATE_KEY_HEX and SECRET' }, + { status: 500 } + ); + } // 1. ตรวจสอบ API Key จาก HTTP Header (x-api-key) - const apiKey = request.headers.get('x-api-key'); - + const apiKey = request.headers.get('x-api-key')||''; if (!apiKey || apiKey !== EXTERNAL_API_KEY) { return NextResponse.json( - { error: 'Unauthorized: Invalid API Key' }, + { error: 'Unauthorized: Invalid API Key ' }, { status: 401 } ); } @@ -61,7 +61,7 @@ export async function POST(request: NextRequest) { //เปิดไฟล์ template - const tmplPath = path.join(TMPL_PATH, `${tid}.docx`); + const tmplPath = path.join(TMPL_PATH, `${path.basename(tid)}.docx`); let templateBuffer: Buffer = await readFile(tmplPath) const output = await applyTemplate(templateBuffer, templateData, SECRET, PRIVATE_KEY_HEX, TICKET_SEC); diff --git a/lib/lib-tmpl.ts b/lib/lib-tmpl.ts index bb293d0..f40f3c0 100755 --- a/lib/lib-tmpl.ts +++ b/lib/lib-tmpl.ts @@ -1301,12 +1301,12 @@ export async function applyTemplate(templateBuffer:Buffer,templateData:DocumentT // Apply Dynamic Content if (templateData?.dc && typeof templateData?.dc === "object") { - console.log("Apply Dynamic Content") + // console.log("Apply Dynamic Content") templateBuffer = Buffer.from(await templateDynamicContents(templateBuffer, templateData.dc)) } // Apply Content Control if (templateData?.cc && typeof templateData?.cc === "object") { - console.log("Apply Content Control") + // console.log("Apply Content Control") templateBuffer = templateContentControls(templateBuffer, templateData.cc); } return templateBuffer diff --git a/lib/lib-utils.ts b/lib/lib-utils.ts index 312d3cf..8b92fda 100644 --- a/lib/lib-utils.ts +++ b/lib/lib-utils.ts @@ -3,8 +3,17 @@ import { createHmac, createPrivateKey, createPublicKey, timingSafeEqual, verify, /** * คำนวณ HMAC-SHA256 จาก string หรือ JSON Object */ + +/** + * ฟังก์ชันสำหรับลงนามดิจิทัล (Sign/hash) ข้อมูลด้วย HMAC-SHA256 secret + * @param data ข้อความดิบที่ต้องการเซ็น (string หรือ object) + * @param secretKey คีย์ลับในรูปแบบ string + * @returns ลายเซ็น (Signature/hash) ในรูปแบบ Hex String (ความยาว 64 ตัวอักษร) + */ + + export function generateHmacSha256(data: string | object, secretKey: string): string { - const payload = typeof data === 'string' ? data : JSON.stringify(data); + const payload = typeof data === 'string' ? data:JSON.stringify(sortObjectKeys(data as Record)); return createHmac('sha256', secretKey) .update(payload, 'utf8') .digest('hex'); @@ -13,6 +22,16 @@ export function generateHmacSha256(data: string | object, secretKey: string): st /** * ตรวจสอบความถูกต้องของ HMAC (ใช้ timingSafeEqual เพื่อป้องกัน Timing Attacks) */ + +/** + * ฟังก์ชันสำหรับตรวจสอบลายเซ็น (Verify) ด้วย Ed25519 Public Key (Hex) + * @param data ข้อความดิบที่ใช้ในการเซ็น (string หรือ Buffer) + * @param secretKey secret ที่นำมาใช้ตรวจสอบ + * @param expectedHmac ลายเซ็นในรูปแบบ Hex String (ความยาว 64 ตัวอักษร) + * @returns ผลลัพธ์ความถูกต้อง (true = ผ่าน, false = ไม่ผ่าน) + */ + + export function verifyHmacSha256(data: string | object, secretKey: string, expectedHmac: string): boolean { const calculatedHmac = generateHmacSha256(data, secretKey); const a = Buffer.from(calculatedHmac, 'hex'); diff --git a/lib/openapi.ts b/lib/openapi.ts index 7700d71..edd282f 100644 --- a/lib/openapi.ts +++ b/lib/openapi.ts @@ -362,7 +362,9 @@ export const openApiSpec = { tags: ["template"], summary: "ดาวน์โหลดเอกสาร DOCX ผ่าน Query Parameters (GET) ใช้เพื่อเป็นตัวอย่างจะถูกเอาออกในภายหลัง", description: - "รับค่า fid, tid, uid, uname จาก Query Parameter อ่านข้อมูล JSON ประจำไฟล์ fid จาก TMPL_PATH แล้วส่งไปประมวลผลต่อที่ /api/template/generate เพื่อสร้างและคืนค่าเป็นไฟล์ DOCX stream ใช้เพื่อเป็นตัวอย่างสำหรับนักพัฒนา ต้องนำออกก่อน production", + "รับค่า fid, tid, uid, uname จาก Query Parameter "+ + "ตรวจสอบรุ่นล่าสุดใน /api/document/latest ถ้าไม่พบ ต้องหาจากฐานข้อมูลแล้วส่งไปประมวลผลต่อที่ /api/template/generate "+ + "เพื่อสร้างและคืนค่าเป็นไฟล์ DOCX stream ใช้เพื่อเป็นตัวอย่างสำหรับนักพัฒนาเท่านั้น ไม่มีการตรวจสอบ user session ต้องนำออกก่อน production", parameters: [ { name: "fid", @@ -461,9 +463,10 @@ export const openApiSpec = { tags: ["template"], summary: "ดาวน์โหลดเอกสาร DOCX ผ่าน HTML Form / Multipart Form Data (POST) เป็นตัวอย่างเท่านั้น จะถูกเอาออกในภายหลัง", description: - "รับค่า fid, tid, uid, uname จาก Multipart Form Data หรือ Form-urlencoded อ่านข้อมูล JSON และประมวลผลสร้างไฟล์ DOCX stream คืนกลับไป "+ - "จะเรียก generate ในเบื้องหลัง ใช้เพื่อเป็นตัวอย่างสำหรับนักพัฒนา ต้องนำออกก่อน production", - requestBody: { + "รับค่า fid, tid, uid, uname จาก Multipart Form Data หรือ Form-urlencoded "+ + "ตรวจสอบรุ่นล่าสุดใน /api/document/latest ถ้าไม่พบ ต้องหาจากฐานข้อมูลแล้วส่งไปประมวลผลต่อที่ /api/template/generate "+ + "เพื่อสร้างและคืนค่าเป็นไฟล์ DOCX stream ใช้เพื่อเป็นตัวอย่างสำหรับนักพัฒนาเท่านั้น ไม่มีการตรวจสอบ user session ต้องนำออกก่อน production", + requestBody: { required: true, content: { "multipart/form-data": { diff --git a/public/tmpl/dl.html b/public/tmpl/dl.html index 6114ce9..dc09ba0 100644 --- a/public/tmpl/dl.html +++ b/public/tmpl/dl.html @@ -15,7 +15,7 @@ T12_FD155_3s-long_john_xxx.docx

Template & Data

- Template: T_X.docx,   + Template: T_X.docx,   T12_X.docx
Data: TF_no_warning_P2.json,   T_no_warning.json,   @@ -23,6 +23,5 @@ T12_b2_IN.json,   T12_FD155_3s-long.json - \ No newline at end of file