eCourt-note/app/api/template/download-docx/route.ts

155 lines
8.4 KiB
TypeScript
Raw Normal View History

// ตัวอย่างรองรับค่าจาก Frontend ที่เป็น Vanilla JavaScript และ Classic Framework
// โดย ผ่าน 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';
2026-11-03 14:48:37 +07:00
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 || '';
2026-11-03 14:48:37 +07:00
async function processRequest(fid: string, tid: string, uid: string, uname: string): Promise<NextResponse<unknown>> {
//กัน path traversal attack
const TID = path.basename(tid)
const FID = path.basename(fid)
2026-11-03 14:48:37 +07:00
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;
2026-11-03 14:48:37 +07:00
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 });
}
2026-11-03 14:48:37 +07:00
if (!FID || !TID || !uid || !uname) {
return NextResponse.json({ error: 'Missing fid, tid, uid or uname' }, { status: 400 });
}
try { //ตรวจสอบ docx เวอร์ชั่นล่าสุดก่อนถ้าไม่พบค่อยเอาจากฐานข้อมูลมาสร้าง raw Json
2026-11-03 14:48:37 +07:00
const params = new URLSearchParams({ type: 'docx', ticket_data });
const lastestDocxResponse = await fetch(`${DOCEDIT_BASE_URL}/api/documents/latest?${params.toString()}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'x-api-key': EXTERNAL_API_KEY,
}
});
2026-11-03 14:48:37 +07:00
// เช็ค Content-Type และ Status ก่อน
const contentType = lastestDocxResponse.headers.get('content-type') || '';
if (lastestDocxResponse.ok && !contentType.includes('text/html')) {
2026-11-03 14:48:37 +07:00
console.log('Fund latest docx from docedit, returning stream to browser');
const stream = lastestDocxResponse.body;
//ส่ง Binary Stream กลับไปยัง Browser พร้อม Header กำหนดประเภทไฟล์
const download_file = `${FID}_${uid}_latest`
return new NextResponse(stream as unknown as BodyInit, {
status: 200,
headers: {
'Content-Type':
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'Content-Disposition': `attachment; filename="${encodeURIComponent(download_file)}.docx"`,
'Cache-Control': 'no-store, max-age=0',
},
});
}
if (lastestDocxResponse.status !== 404) {//เกิดปัญหาที่ไม่ใช่ Not Found น่าจะเป็นปัญหาที่ gateway
const errorText = await lastestDocxResponse.text();
console.error('download-docx:Invalid latest docx Response:', errorText);
return NextResponse.json(
2026-11-03 14:48:37 +07:00
{ error: `Error from docedit latest: Expected docx stream but received ${lastestDocxResponse.status} ${lastestDocxResponse.statusText}` },
{ status: 502 }
);
}
} catch (error) {
console.error(error)
2026-11-03 14:48:37 +07:00
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
console.log(`Not found latest docx from docedit, fallback to generate from template and database: ${filePath}`);
try { // ไม่พบเวอร์ชั้นให้สร้าง docx จาก template และ json(ฐานข้อมูล)
// อ่านไฟล์ JSON จำลองการอ่านจากฐานข้อมูล
const rawJson = await fs.readFile(filePath, 'utf-8');
const jsonData = JSON.parse(rawJson);
2026-11-03 14:48:37 +07:00
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,
// ข้อมูลจำเป็นต้องมี jsonData.info.ticket_data ถ้าไม่มีให้สร้างขึ้นมาเอง
2026-11-03 14:48:37 +07:00
if (!Object.hasOwn(jsonData, 'info'))
jsonData['info'] = {}
jsonData.info['ticket_data'] = JSON.stringify(ticketObj)
// sign เองตรงนี้ให้อยู่ในรูปแบบ data.info={ticket_data,HmacSha256,Ed25519} หรือว่างไว้ให้ docedit สร้างให้
//ยิงไปหาระบบ Docedit เพื่อเอา Stream
const docxResponse = await fetch(`${DOCEDIT_BASE_URL}/api/template/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': EXTERNAL_API_KEY,
},
body: JSON.stringify(jsonData),
});
// เช็ค 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);
return NextResponse.json(
2026-11-03 14:48:37 +07:00
{ error: `Error from docedit generate: Expected docx stream but received ${contentType} (Status ${docxResponse.status})` },
{ status: 502 }
);
}
// 2. ดึง ReadableStream โดยตรงจาก Response ของระบบ
const stream = docxResponse.body;
// 3. ส่ง Binary Stream กลับไปยัง Browser พร้อม Header กำหนดประเภทไฟล์
const download_file = `${FID}_${uid}_xxx`
return new NextResponse(stream as unknown as BodyInit, {
status: 200,
headers: {
'Content-Type':
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'Content-Disposition': `attachment; filename="${encodeURIComponent(download_file)}.docx"`,
'Cache-Control': 'no-store, max-age=0',
},
});
2026-11-03 14:48:37 +07:00
} catch (error: any) {
if (error.code === 'ENOENT') {// ไฟล์ JSON ไม่พบ ถ้าใช้ฐานข้อมูลจริงให้ return 404 ตั้งแต่แต่แรก และเอา ENOENT ออก
return NextResponse.json(
2026-11-03 14:48:37 +07:00
{ error: `Data not found: ${filePath}` },
{ status: 404 }
);
}
console.error(error)
2026-11-03 14:48:37 +07:00
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
2026-11-03 14:48:37 +07:00
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const fid = searchParams.get('fid') || '';
const tid = searchParams.get('tid') || '';
const uid = searchParams.get('uid') || '';
const uname = searchParams.get('uname') || '';
2026-11-03 14:48:37 +07:00
return await processRequest(fid, tid, uid, uname)
}
export async function POST(request: NextRequest) {
const formData = await request.formData();
const fid = String(formData.get('fid'))
const tid = String(formData.get('tid'))
const uid = String(formData.get('uid'))
const uname = String(formData.get('uname'))
2026-11-03 14:48:37 +07:00
return await processRequest(fid, tid, uid, uname)
}