Add generate API, Prototype for developer, Clean data
Some checks failed
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 5s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Failing after 1m11s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Has been skipped
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Failing after 0s

This commit is contained in:
oom 2026-10-27 01:47:08 +07:00
parent 6ce071ee14
commit a3d14fd58d
26 changed files with 911 additions and 1267 deletions

View file

@ -1,73 +1,49 @@
import { NextResponse } from "next/server";
import { DocumentTemplate, templateContentControls, setCustomDocProps, getDocProps, templateDynamicContents } from "@/lib/lib-tmpl";
import { generateHmacSha256, verifyHmacSha256, signDataEd25519, verifyDataEd25519, sortObjectKeys } from '@/lib/lib-utils';
import { DocumentTemplate, templateContentControls, setCustomDocProps, templateDynamicContents } from "@/lib/lib-tmpl";
import { generateHmacSha256, signEd25519, sortObjectKeys } from '@/lib/lib-utils';
export const runtime = "nodejs";
const DOCX_CONTENT_TYPE =
"application/vnd.openxmlformats-officedocument.wordprocessingml.document";
// cap เดียวกับ N4 (app/actions/files.ts) — route นี้เปิดกว้างให้ภายนอกยิงได้
// ต้องเช็คขนาดก่อนอ่านเข้าหน่วยความจำ กัน DoS (bodySizeLimit ครอบเฉพาะ Server Actions)
const MAX_UPLOAD_BYTES = 100 * 1024 * 1024; // 100MB
const TICKET_SEC = Number(process.env.TICKET_SEC||'300')
export async function POST(request: Request) {
try {
const formData = await request.formData();
const template = formData.get("template");
const dataFile = formData.get("data");
const dataJson = formData.get("dataJson");
const TEST_SECRET = (typeof formData.get("secret") === "string") ?
String(formData.get("secret")) : "super_secret";
const TEST_PRIVATE_KEY_HEX = (typeof formData.get("private_key") === "string") ?
String(formData.get("private_key")) : "8f2077b57b39399b6a7828f35da03a63ade8e3a36a6bf437b091a8bdb3d18ee1";
const TEST_SECRET = String(formData.get("secret")||"")
const TEST_PRIVATE_KEY_HEX = String(formData.get("private_key")||"")
console.log(`secret:${TEST_SECRET}`)
console.log(`private:${TEST_PRIVATE_KEY_HEX}`)
// -----------------------------
// ตรวจสอบ Template
// -----------------------------
if (!(template instanceof File)) {
return NextResponse.json(
{
success: false,
error: 'Missing DOCX file in field "template".',
},
{ status: 400 },
);
}
if (
template.type !== DOCX_CONTENT_TYPE &&
!template.name.toLowerCase().endsWith(".docx")
if (template.type !== DOCX_CONTENT_TYPE &&
!template.name.toLowerCase().endsWith(".docx") &&
template.size > MAX_UPLOAD_BYTES
) {
return NextResponse.json(
{
success: false,
error: 'Field "template" must be a .docx file.',
error: 'Field "template" must be a .docx file and not exceeds the 100MB size limit.',
},
{ status: 400 },
);
}
if (template.size > MAX_UPLOAD_BYTES) {
return NextResponse.json(
{
success: false,
error: 'Field "template" exceeds the 100MB size limit.',
},
{ status: 400 },
);
}
// -----------------------------
// อ่าน JSON
// -----------------------------
let templateData: DocumentTemplate;
// JSON ผิด syntax เป็นความผิดของฝั่งผู้เรียก — คืน 400 ไม่ใช่ 500
// (catch กลางท้ายฟังก์ชันสงวนไว้ให้ error ของระบบเท่านั้น)
try {
@ -78,13 +54,11 @@ export async function POST(request: Request) {
if (dataFile.size > MAX_UPLOAD_BYTES) {
return NextResponse.json(
{
success: false,
error: 'Field "data" exceeds the 100MB size limit.',
},
{ status: 400 },
);
}
// กรณี upload data.json
const text = await dataFile.text();
@ -92,7 +66,6 @@ export async function POST(request: Request) {
} else {
return NextResponse.json(
{
success: false,
error:
'Missing JSON data. Send either "data" file or "dataJson" field.',
},
@ -102,14 +75,12 @@ export async function POST(request: Request) {
} catch (error) {
return NextResponse.json(
{
success: false,
error: `Invalid JSON data (${error instanceof Error ? error.message : "parse error"
}).`,
},
{ status: 400 },
);
}
// JSON ต้องเป็น object
if (
templateData === null ||
@ -118,7 +89,6 @@ export async function POST(request: Request) {
) {
return NextResponse.json(
{
success: false,
error: "JSON data must be an object.",
},
{ status: 400 },
@ -132,34 +102,64 @@ export async function POST(request: Request) {
let templateBuffer: Buffer = Buffer.from(
await template.arrayBuffer(),
);
const output = await applyTemplate(templateBuffer,templateData,TEST_SECRET,TEST_PRIVATE_KEY_HEX);
// -----------------------------
// ส่ง DOCX กลับ
// -----------------------------
if(templateData?.info?.ticket_data)
return new NextResponse(output as BodyInit, {
status: 200,
headers: {
"Content-Type": DOCX_CONTENT_TYPE,
"Content-Disposition":
'attachment; filename="generated-document.docx"',
"Content-Length": String(output.length),
},
});
} catch (error) {
console.error(error);
return NextResponse.json(
{
error:
error instanceof Error
? error.message
: "Internal Server Error",
},
{ status: 500 },
);
}
}
export async function applyTemplate(templateBuffer:Buffer,templateData:DocumentTemplate,
secret:string,private_key:string):Promise<Buffer<ArrayBufferLike>>
{
//Calculate hash
const originalTicket_data = templateData?.info?.ticket_data //expect valid json string
// console.log("templateData",templateData?.info?.ticket_data)
if (originalTicket_data && typeof originalTicket_data === 'string') {
try {
const ttlSeconds: number = 300
const originalTicketObj = JSON.parse(originalTicket_data)//sort and add expire to ticket data
if (originalTicketObj && typeof originalTicketObj === 'object') {
console.log("Found valid ticket data: hash it")
// คำนวณค่า expire (ปัจจุบัน + จำนวนวินาทีที่ต้องการให้หมดอายุ)
// หาร 1000 เพราะต้องการหน่วยวินาที (sec) ไม่ใช่มิลลิวินาที (ms)
const expireTimestamp = Math.floor(Date.now() / 1000) + ttlSeconds;
const expireTimestamp = Math.floor(Date.now() / 1000) + TICKET_SEC;
const fullObj = { ...originalTicketObj, exp: expireTimestamp };
const sortedObj = sortObjectKeys(fullObj);
// แปลงกลับเป็น JSON string เพื่อนำไปใช้ส่งและทำ Hash
const ticket_data = JSON.stringify(sortedObj);
// console.log("check secret:",TEST_SECRET,TEST_PRIVATE_KEY_HEX,ticket_data)
const HmacSha256 = generateHmacSha256(ticket_data, TEST_SECRET)
const HmacSha256 = generateHmacSha256(ticket_data, secret)
const Ed25519 = signDataEd25519(ticket_data, TEST_PRIVATE_KEY_HEX)
const Ed25519 = signEd25519(ticket_data, private_key)
templateBuffer = setCustomDocProps(templateBuffer, { ticket_data, HmacSha256, Ed25519 })
}
} catch (error) {
} catch (error) { //just skip
console.error("Set custom property fail",error);
}
}
@ -173,36 +173,6 @@ export async function POST(request: Request) {
console.log("Apply Content Control")
templateBuffer = templateContentControls(templateBuffer, templateData.cc);
}
return templateBuffer
const output = templateBuffer;
// -----------------------------
// ส่ง DOCX กลับ
// -----------------------------
return new NextResponse(output as BodyInit, {
status: 200,
headers: {
"Content-Type": DOCX_CONTENT_TYPE,
"Content-Disposition":
'attachment; filename="generated-document.docx"',
"Content-Length": String(output.length),
},
});
} catch (error) {
console.error(error);
return NextResponse.json(
{
success: false,
error:
error instanceof Error
? error.message
: "Internal Server Error",
},
{ status: 500 },
);
}
}