feat(api): add swagger ui and docx template api
เพิ่มกลุ่ม API docx template สำหรับ wordaddin (WIP): - POST /api/template/extract ดึงค่า Content Control ทั้งหมดจาก .docx - POST /api/template/render แทนค่า JSON ลง template คืน .docx ใหม่ - GET /api/template/test ตรวจว่า service มีชีวิต logic แชร์ที่ app/api/template/lib-tmpl.ts (pizzip+xpath) ไม่แตะ DB เพิ่มหน้า Swagger UI ที่ GET /api/swagger — spec inline จาก lib/openapi.ts ครอบ API ทุกกลุ่ม (template + N10), asset self-host ที่ public/note/swagger-ui/ จาก postinstall scripts/copy-swagger-ui.mjs (intranet ไม่ใช้ CDN — หาไฟล์ไม่เจอหลังอัปเกรด = exit 1 เหมือน patch-pdf-worker, Docker build รัน script นี้ด้วยผ่าน web.Dockerfile) middleware เปิดกว้างให้ api/template, api/swagger, note/swagger-ui จาก code review: จำกัดไฟล์แนบ 100MB ทั้ง extract/render ก่อนอ่าน เข้าหน่วยความจำกัน DoS, คืน 400 แทน 500 เมื่อ JSON ผิด format, sync คำอธิบาย BadRequest ใน openapi spec ให้ตรง behavior ใหม่ Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
8cc15ea75d
commit
073f72db7d
20 changed files with 1479 additions and 10 deletions
171
app/api/template/render/route.ts
Normal file
171
app/api/template/render/route.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
templateContentControls,
|
||||
type TemplateData,
|
||||
} from "../lib-tmpl";
|
||||
|
||||
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
|
||||
|
||||
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");
|
||||
|
||||
// -----------------------------
|
||||
// ตรวจสอบ 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")
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Field "template" must be a .docx file.',
|
||||
},
|
||||
{ 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 data: TemplateData;
|
||||
|
||||
// JSON ผิด syntax เป็นความผิดของฝั่งผู้เรียก — คืน 400 ไม่ใช่ 500
|
||||
// (catch กลางท้ายฟังก์ชันสงวนไว้ให้ error ของระบบเท่านั้น)
|
||||
try {
|
||||
if (typeof dataJson === "string" && dataJson.length > 0) {
|
||||
// กรณีส่ง JSON ผ่าน form field
|
||||
data = JSON.parse(dataJson) as TemplateData;
|
||||
} else if (dataFile instanceof File) {
|
||||
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();
|
||||
|
||||
data = JSON.parse(text) as TemplateData;
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
'Missing JSON data. Send either "data" file or "dataJson" field.',
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Invalid JSON data (${
|
||||
error instanceof Error ? error.message : "parse error"
|
||||
}).`,
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// JSON ต้องเป็น object
|
||||
if (
|
||||
data === null ||
|
||||
typeof data !== "object" ||
|
||||
Array.isArray(data)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "JSON data must be an object.",
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Template File → Buffer
|
||||
// -----------------------------
|
||||
|
||||
const templateBuffer = Buffer.from(
|
||||
await template.arrayBuffer(),
|
||||
);
|
||||
|
||||
// -----------------------------
|
||||
// ใช้ lib-tmpl.ts เดิม
|
||||
// -----------------------------
|
||||
|
||||
const output = templateContentControls(
|
||||
templateBuffer,
|
||||
data,
|
||||
);
|
||||
|
||||
// -----------------------------
|
||||
// ส่ง 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue