clear environment variable, update download-docx(sample) to check latest version before query database.
This commit is contained in:
parent
bc61f0fd2e
commit
45ff0001e6
7 changed files with 103 additions and 51 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<NextResponse<unknown>>
|
||||
{
|
||||
// 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 }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>));
|
||||
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');
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
<a href="/docedit/api/template/download-docx?tid=T12_X&fid=T12_FD155_3s-long&uid=john&uname=John%20Doe">T12_FD155_3s-long_john_xxx.docx</a><br/>
|
||||
|
||||
<h2 className="font-semibold text-blue-600 mb-2">Template & Data</h2>
|
||||
Template: <a href="../tmpl/T_X.docx">T_X.docx</a>,
|
||||
Template: <a href="/docedit/tmpl/T_X.docx">T_X.docx</a>,
|
||||
<a href="/docedit/tmpl/T12_X.docx">T12_X.docx</a><br/>
|
||||
Data: <a href="/docedit/tmpl/TF_no_warning_P2.json">TF_no_warning_P2.json</a>,
|
||||
<a href="/docedit/tmpl/T_no_warning.json">T_no_warning.json</a>,
|
||||
|
|
@ -23,6 +23,5 @@
|
|||
<a href="/docedit/tmpl/T12_b2_IN.json">T12_b2_IN.json</a>,
|
||||
<a href="/docedit/tmpl/T12_FD155_3s-long.json">T12_FD155_3s-long.json</a>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue