Merge branch 'forgejo-dev' into working
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m12s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 4s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m12s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 4s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s
Resolve CLAUDE.md: คำอธิบาย lib/document-versioning.ts เอาของ forgejo-dev (field `fields` ถอดออก 2026-10-30) ส่วน lib/docver-token.ts เอาของ working (purpose login / verifyDocverLoginToken — โค้ดฝั่ง forgejo-dev มีครบแล้ว แค่คำอธิบายล้าหลัง) Verify: npx tsc --noEmit ผ่าน · vitest 23 files / 338 tests ผ่าน Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
commit
a288e5643c
31 changed files with 1445 additions and 278 deletions
120
app/api/documents/latest/route.ts
Normal file
120
app/api/documents/latest/route.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// PLAN-DOCVER.md §1 — GET /api/documents/latest?type=docx|pdf&ticket_data=<json>
|
||||
// คืนไฟล์ของ version ล่าสุดให้ backend e-Court (server-to-server):
|
||||
// - type=pdf → ไบต์ pdf ตรง ๆ (version ล่าสุดไม่มี pdf = 404)
|
||||
// - type=docx → stamp ticket ใหม่ (exp + HmacSha256 + Ed25519 เมื่อมี private key)
|
||||
// ลงสำเนาในหน่วยความจำก่อนคืน — ไม่เขียนกลับ MinIO (non-destructive)
|
||||
// - auth: header x-api-key เทียบ env DOCVER_API_KEY หรือ EXTERNAL_API_KEY
|
||||
// (ต่างจาก /token ที่รับ DOCVER_API_KEY อย่างเดียว) · docId = fid ใน ticket_data
|
||||
// - secret ของ HMAC = addinTicketSecret() ให้ตรงฝั่ง verify ของ add-in /
|
||||
// POST /api/documents/token ทาง ticket · TTL = env TICKET_SEC (default 300)
|
||||
// carve-out invariant #4: route handler ของกลุ่ม docver — ไม่ publish SSE
|
||||
// ไม่ revalidatePath (ไม่มีหน้า note ใดอ่านตารางของกลุ่มนี้)
|
||||
import { addinTicketSecret } from "@/lib/addin-ticket";
|
||||
import {
|
||||
contentDispositionValue,
|
||||
docverError,
|
||||
DOCX_CONTENT_TYPE,
|
||||
isDocxBytes,
|
||||
isPdfBytes,
|
||||
parseLatestTypeParam,
|
||||
PDF_CONTENT_TYPE,
|
||||
} from "@/lib/document-versioning";
|
||||
import { requireDocumentApiKey } from "@/lib/docver-token";
|
||||
import {
|
||||
findLatestVersionForDoc,
|
||||
getVersionObjectBytes,
|
||||
} from "@/lib/docver-store";
|
||||
import { buildStampedTicketData, stampTicketProps } from "@/lib/lib-tmpl";
|
||||
import { parseTicketDataParam } from "@/utils/docver-ticket";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const GENERIC_500 = "เกิดข้อผิดพลาดของระบบ โปรดลองใหม่อีกครั้ง";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const auth = requireDocumentApiKey(request);
|
||||
if (auth !== null) return auth;
|
||||
|
||||
let searchParams: URLSearchParams;
|
||||
try {
|
||||
searchParams = new URL(request.url).searchParams;
|
||||
} catch {
|
||||
return docverError("URL ของ request ไม่ถูกต้อง", 400);
|
||||
}
|
||||
|
||||
const type = parseLatestTypeParam(searchParams.get("type"));
|
||||
if (!type.ok) return docverError(type.error, 400);
|
||||
|
||||
const ticket = parseTicketDataParam(searchParams.get("ticket_data"));
|
||||
if (!ticket.ok) return docverError(ticket.error, 400);
|
||||
|
||||
// เติม exp + เรียง key + เช็คความยาวก่อนแตะ MinIO — Word ตัดทอนค่า custom
|
||||
// property ที่ 255 ตัวอักษร คืนไฟล์ที่โดนตัด = HMAC เสียเงียบ ๆ ปฏิเสธไปเลยดีกว่า
|
||||
let ticketData: string | null = null;
|
||||
if (type.type === "docx") {
|
||||
const ttl = Number(process.env.TICKET_SEC);
|
||||
const ttlSeconds = Number.isFinite(ttl) && ttl > 0 ? ttl : 300;
|
||||
ticketData = buildStampedTicketData(ticket.ticket, ttlSeconds);
|
||||
if (ticketData.length > 255) {
|
||||
return docverError(
|
||||
"ticket_data ยาวเกิน 255 ตัวอักษร (ข้อจำกัดของ Word custom property)",
|
||||
400,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let row;
|
||||
try {
|
||||
row = await findLatestVersionForDoc(ticket.ticket.fid);
|
||||
} catch (err) {
|
||||
console.error("docver: ค้น version ล่าสุดล้ม", err);
|
||||
return docverError(GENERIC_500, 500);
|
||||
}
|
||||
if (!row) {
|
||||
return docverError("ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยอัปโหลด version)", 404);
|
||||
}
|
||||
if (type.type === "pdf" && row.pdfObjectKey === null) {
|
||||
return docverError("version นี้ไม่มีสำเนา PDF", 404);
|
||||
}
|
||||
|
||||
let bytes: Buffer;
|
||||
try {
|
||||
bytes = await getVersionObjectBytes(row.id, type.type);
|
||||
} catch (err) {
|
||||
console.error("docver: ดึง object จาก MinIO ล้ม", err);
|
||||
return docverError(GENERIC_500, 500);
|
||||
}
|
||||
// object เขียนผ่านกลุ่มนี้เองตอน upload (ผ่าน magic bytes แล้ว) — ไม่ตรงตอนนี้
|
||||
// = ความผิดปกติของพื้นที่เก็บ ไม่ใช่ความผิดของผู้เรียก จึงเป็น 500
|
||||
if (type.type === "docx" ? !isDocxBytes(bytes) : !isPdfBytes(bytes)) {
|
||||
console.error(`docver: object ของ version ${row.id} magic bytes ไม่ตรงชนิดไฟล์`);
|
||||
return docverError(GENERIC_500, 500);
|
||||
}
|
||||
|
||||
if (type.type === "docx") {
|
||||
// ticketData ถูกตั้งค่าแล้วเสมอเมื่อ type=docx (ด้านบน) — ห้ามกลืน error:
|
||||
// ไฟล์ไม่มี stamp = add-in เปิดไม่ได้ ต้องตอบ 500 ไม่ใช่คืนไฟล์เสียเงียบ ๆ
|
||||
try {
|
||||
bytes = stampTicketProps(bytes, ticketData!, {
|
||||
secret: addinTicketSecret(),
|
||||
privateKeyHex: process.env.PRIVATE_KEY_HEX?.trim() || null,
|
||||
}).buffer;
|
||||
} catch (err) {
|
||||
console.error("docver: stamp ticket ล้ม", err);
|
||||
return docverError(GENERIC_500, 500);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(
|
||||
// Uint8Array ใหม่ (ก็อปปี้) ตามที่ BodyInit รับ — pattern เดียวกับ route pdf/export
|
||||
new Uint8Array(bytes),
|
||||
{
|
||||
headers: {
|
||||
"content-type": type.type === "docx" ? DOCX_CONTENT_TYPE : PDF_CONTENT_TYPE,
|
||||
"content-disposition": contentDispositionValue(row.fileName, type.type),
|
||||
"cache-control": "no-store, max-age=0",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -6,7 +6,8 @@
|
|||
// ไม่รับจาก URL/form อีก (lib/docver-token.ts)
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
|
||||
import { toDocumentVersionDTO } from "@/db/schema";
|
||||
import { toDocumentVersionDTO, type DocumentVersionFields } from "@/db/schema";
|
||||
import { addinTicketSecret } from "@/lib/addin-ticket";
|
||||
import {
|
||||
docverError,
|
||||
DOCVER_MAX_UPLOAD_BYTES,
|
||||
|
|
@ -16,7 +17,9 @@ import {
|
|||
parseVersionUploadForm,
|
||||
sanitizeFilePartName,
|
||||
} from "@/lib/document-versioning";
|
||||
import { extractContentControls } from "@/lib/lib-tmpl";
|
||||
import { requireDocverBearer } from "@/lib/docver-token";
|
||||
import { extractDocumentSnapshot } from "@/lib/lib-tmpl";
|
||||
import {
|
||||
findDocumentByDocId,
|
||||
insertVersionRecord,
|
||||
|
|
@ -31,6 +34,7 @@ export const dynamic = "force-dynamic";
|
|||
|
||||
/** POST — อัปโหลด version ใหม่ (เอกสารสร้างแบบ implicit ถ้าเป็น version แรก)
|
||||
* ลำดับ: Bearer token → validate text field ก่อนแตะไฟล์ → cap ขนาดก่อน buffer → magic bytes →
|
||||
* แกะ fields จากไฟล์ (lib-tmpl เหมือน /api/template/extract) →
|
||||
* putObject MinIO → insert DB (tx) — DB ล้ม = เก็บกวาด object (pattern N4)
|
||||
* response = UploadVersionResult เป๊ะ โดย versionNumber เป็นค่า authoritative
|
||||
* จาก server (client เดา latest+1 แล้วแก้เองเมื่ออัปโหลดชนกัน) */
|
||||
|
|
@ -92,6 +96,21 @@ export async function POST(request: Request) {
|
|||
return docverError("เนื้อไฟล์ .pdf ไม่ถูกต้อง", 400);
|
||||
}
|
||||
|
||||
// fields snapshot แกะจากไฟล์เองฝั่ง server — **เก็บทั้งก้อนเหมือน return ของ
|
||||
// /api/template/extract** ({cc, bookmark, properties, verify} ผ่าน extractDocumentSnapshot
|
||||
// ของ lib/lib-tmpl.ts ตัวเดียวกับ route นั้น — 2026-10-30 ถอด form field "fields"
|
||||
// ที่ add-in อ่านผ่าน Office.js ออกจาก contract): มองเห็น CC ทุกชนิดเสมอไม่ขึ้นกับ
|
||||
// เวอร์ชัน WordApi ของ host · verify.HmacSha256 ตรวจด้วย secret ของ add-in ticket
|
||||
// ฝั่ง server (env chain เดียวกับ lib/addin-ticket.ts) · ZIP ไม่มี word/document.xml,
|
||||
// parse ไม่ได้ หรือ ticket_data ไม่ใช่ JSON = 400
|
||||
let fields: DocumentVersionFields;
|
||||
try {
|
||||
fields = extractDocumentSnapshot(docxBytes, { secret: addinTicketSecret() });
|
||||
} catch (err) {
|
||||
console.error("docver: แกะข้อมูลจากไฟล์ไม่สำเร็จ", err);
|
||||
return docverError("เนื้อไฟล์ .docx ไม่ถูกต้อง (แกะข้อมูลไม่สำเร็จ)", 400);
|
||||
}
|
||||
|
||||
const contentHash = createHash("sha256").update(docxBytes).digest("hex");
|
||||
const versionId = randomUUID();
|
||||
// ชื่อแสดงผลตอนดาวน์โหลด: field "fileName" ของผู้เรียก (add-in ส่งมาเอง) →
|
||||
|
|
@ -119,7 +138,7 @@ export async function POST(request: Request) {
|
|||
clientVersionId: parsed.form.clientVersionId,
|
||||
contentHash,
|
||||
message: parsed.form.message,
|
||||
fields: parsed.form.fields,
|
||||
fields,
|
||||
templateId: auth.claims.template,
|
||||
fileName: docxDisplayName,
|
||||
docxObjectKey: keys.docx,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import { extractContentControls, extractBookmark, templateContentControls, setCustomDocProps, getDocProps,templateDynamicContents } from "@/lib/lib-tmpl";
|
||||
import { verifyEd25519, verifyHmacSha256 } from "@/lib/lib-utils";
|
||||
import { extractDocumentSnapshot } from "@/lib/lib-tmpl";
|
||||
// export const runtime = "nodejs";
|
||||
|
||||
const DOCX_CONTENT_TYPE =
|
||||
|
|
@ -40,32 +39,13 @@ export async function POST(request: Request) {
|
|||
// File → Buffer
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
// ใช้ lib-tmpl.ts เดิม
|
||||
// const data = extractContentControls(buffer);
|
||||
|
||||
const cc = extractContentControls(buffer);
|
||||
const bookmark = extractBookmark(buffer)
|
||||
const properties = getDocProps(buffer)
|
||||
let verify: Record<string, unknown> ={}
|
||||
const ticket_data = properties?.custom?.ticket_data
|
||||
const HmacSha256 = String(properties?.custom?.HmacSha256)
|
||||
const Ed25519 = String(properties?.custom?.Ed25519)
|
||||
|
||||
if(ticket_data && typeof ticket_data === 'string'){
|
||||
const ticketDataObj= JSON.parse(ticket_data)
|
||||
const unix_timestamp = Number(ticketDataObj.exp)*1000
|
||||
if(unix_timestamp){
|
||||
verify['exp'] = new Date(unix_timestamp).toLocaleString("th-TH", { timeZone: "Asia/Bangkok" })
|
||||
}
|
||||
if(TEST_SECRET && HmacSha256){
|
||||
verify['HmacSha256'] = verifyHmacSha256(ticket_data,TEST_SECRET,HmacSha256)
|
||||
}
|
||||
if(TEST_PUBLIC_KEY_HEX&& Ed25519){
|
||||
verify["Ed25519"]=verifyEd25519(ticket_data,Ed25519,TEST_PUBLIC_KEY_HEX)
|
||||
}
|
||||
}
|
||||
|
||||
let returnData : Record<string, unknown> = {cc,bookmark,properties,verify}
|
||||
// ชุดแกะข้อมูล `{cc, bookmark, properties, verify}` จาก lib-tmpl.ts — ตัวเดียวกับที่
|
||||
// ใช้เก็บเป็น fields ของ version ตอน POST /api/documents/versions (2026-10-30) ·
|
||||
// secret/public_key เป็นของผู้เรียกเพื่อ "ทดสอบ verify" เฉพาะฝั่งนี้
|
||||
const returnData = extractDocumentSnapshot(buffer, {
|
||||
secret: TEST_SECRET || undefined,
|
||||
publicKeyHex: TEST_PUBLIC_KEY_HEX || undefined,
|
||||
});
|
||||
return NextResponse.json(returnData);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default function HomePage() {
|
|||
const handleTmplSelect = (template: string, data: string) => {
|
||||
//console.log('Location Selected:', { template, data });
|
||||
};
|
||||
const handleInput = (e: ChangeEvent<HTMLInputElement>)=>{
|
||||
const handleInput = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
// console.log(e.target)
|
||||
return true;
|
||||
}
|
||||
|
|
@ -24,8 +24,8 @@ export default function HomePage() {
|
|||
<div className="p-8 max-w-md mx-auto space-y-8 bg-gray-50 min-h-screen">
|
||||
<h1 className="text-xl font-bold text-center">ตัวอย่างการใช้ สร้างเอกสารจาก Template</h1>
|
||||
<div>
|
||||
ตัวอย่าง Frontend ส่งข้อมูลผ่าน <form>(POST) หรือ <a>(GET)
|
||||
เพื่อรองรับ Vanilla JavaScript และ Classic Framework ใช้การเปลี่ยนหน้าเพื่อดาว์นโหลดไฟล์
|
||||
ตัวอย่าง Frontend ส่งข้อมูลผ่าน <form>(POST) หรือ <a>(GET)
|
||||
เพื่อรองรับ Vanilla JavaScript และ Classic Framework ใช้การเปลี่ยนหน้าเพื่อดาว์นโหลดไฟล์
|
||||
ตัวอย่างหน้าแบบ <Link href='/tmpl/dl.html'>Plain HTML</Link>
|
||||
</div>
|
||||
<div className="p-4 border rounded-xl bg-white shadow-sm">
|
||||
|
|
@ -35,10 +35,10 @@ export default function HomePage() {
|
|||
label1="Template(tid):" label2="ข้อมูล(fid):" onSelectChange={handleTmplSelect} />
|
||||
<div className="mb-2"></div>
|
||||
<div>
|
||||
uid:<input type="text" name="uid" className="p-2 mb-2 border border-gray-300 rounded-md" defaultValue="john"/>
|
||||
uid:<input type="text" name="uid" className="p-2 mb-2 border border-gray-300 rounded-md" defaultValue="john" />
|
||||
</div>
|
||||
<div>
|
||||
uname:<input type="text" name="uname" className="p-2 mb-2 border border-gray-300 rounded-md" defaultValue="John Doe"/>
|
||||
uname:<input type="text" name="uname" className="p-2 mb-2 border border-gray-300 rounded-md" defaultValue="John Doe" />
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
|
|
@ -57,45 +57,56 @@ export default function HomePage() {
|
|||
</div>
|
||||
<div className="p-4 border rounded-xl bg-white shadow-sm">
|
||||
<h2 className="font-semibold text-blue-600 mb-2">A Link Get(new)</h2>
|
||||
<Link href="/api/template/download-docx?tid=T_X&fid=T_no_warning&uid=john&uname=John%20Doe">T_no_warning_john_xxx.docx</Link><br/>
|
||||
<Link href="/api/template/download-docx?tid=T_X&fid=TF_no_warning_P2&uid=john&uname=John%20Doe">TF_no_warning_P2_john_xxx.docx</Link><br/>
|
||||
<Link href="/api/template/download-docx?tid=T_X&fid=TS_P2_IN&uid=john&uname=John%20Doe">TS_P2_IN_john_xxx.docx</Link><br/>
|
||||
<Link href="/api/template/download-docx?tid=T_X&fid=T_no_warning&uid=john&uname=John%20Doe">T_no_warning_john_xxx.docx</Link><br />
|
||||
<Link href="/api/template/download-docx?tid=T_X&fid=TF_no_warning_P2&uid=john&uname=John%20Doe">TF_no_warning_P2_john_xxx.docx</Link><br />
|
||||
<Link href="/api/template/download-docx?tid=T_X&fid=TS_P2_IN&uid=john&uname=John%20Doe">TS_P2_IN_john_xxx.docx</Link><br />
|
||||
|
||||
<Link href="/api/template/download-docx?tid=T12_X&fid=T12_b2_IN&uid=john&uname=John%20Doe">T12_b2_IN_john_xxx.docx</Link><br/>
|
||||
<Link href="/api/template/download-docx?tid=T12_X&fid=T12_FD155_3s-long&uid=john&uname=John%20Doe">T12_FD155_3s-long_john_xxx.docx</Link><br/>
|
||||
<Link href="/api/template/download-docx?tid=T12_X&fid=T12_b2_IN&uid=john&uname=John%20Doe">T12_b2_IN_john_xxx.docx</Link><br />
|
||||
<Link href="/api/template/download-docx?tid=T12_X&fid=T12_FD155_3s-long&uid=john&uname=John%20Doe">T12_FD155_3s-long_john_xxx.docx</Link><br />
|
||||
</div>
|
||||
<div className="p-4 border rounded-xl bg-white shadow-sm">
|
||||
<h2 className="font-semibold text-blue-600 mb-2">Template & Data</h2>
|
||||
Template: <Link href="/tmpl/T_X.docx">T_X.docx</Link>,
|
||||
<Link href="/tmpl/T12_X.docx">T12_X.docx</Link><br/>
|
||||
<Link href="/tmpl/T12_X.docx">T12_X.docx</Link><br />
|
||||
Data: <Link href="tmpl/TF_no_warning_P2.json">TF_no_warning_P2.json</Link>,
|
||||
<Link href="/tmpl/T_no_warning.json">T_no_warning.json</Link>,
|
||||
<Link href="/tmpl/TS_P2_IN.json">TS_P2_IN.json</Link><br/>
|
||||
<Link href="/tmpl/TS_P2_IN.json">TS_P2_IN.json</Link><br />
|
||||
<Link href="/tmpl/T12_b2_IN.json">T12_b2_IN.json</Link>,
|
||||
<Link href="/tmpl/T12_FD155_3s-long.json">T12_FD155_3s-long.json</Link>
|
||||
</div>
|
||||
<div className="p-4 border rounded-xl bg-white shadow-sm">
|
||||
<h2 className="font-semibold text-blue-600 mb-2">Flow</h2>
|
||||
<div className="text-xl font-bold text-center">
|
||||
หน้านี้(เลือกไฟล์)<br/>
|
||||
↓ <br/>
|
||||
download-docx(ตรวจสิทธิ์,ส่งเทมเพลต ID,ส่งข้อมูล)<br/>
|
||||
↓ <br/>
|
||||
generate(เปิดเทมเพลต,สร้าง docx)<br/>
|
||||
↓ <br/>
|
||||
ดาว์นโหลด docx เปิด MS Word อัตโนมัติ<br/>
|
||||
↓ <br/>
|
||||
Add-ins ตรวจสอบไฟล์, ผู้ใช้แก้ไข docx<br/>
|
||||
↓ <br/>
|
||||
Versioning(คุมเวอร์ชั่น)<br/>
|
||||
↓ <br/>
|
||||
ecourt(อัปเดตฐานข้อมูลและไฟล์)<br/>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="p-4 border rounded-xl bg-white shadow-sm">
|
||||
<h2 className="font-semibold text-blue-600 mb-2">Flow</h2>
|
||||
<div className="text-xl font-bold text-center">
|
||||
หน้านี้(เลือกไฟล์)<br />
|
||||
↓ <br />
|
||||
download-docx(ตรวจสิทธิ์,ส่งเทมเพลต ID,ส่งข้อมูล)<br />
|
||||
↓ <br />
|
||||
generate(เปิดเทมเพลต,สร้าง docx)<br />
|
||||
↓ <br />
|
||||
ดาว์นโหลด docx เปิด MS Word อัตโนมัติ<br />
|
||||
↓ <br />
|
||||
Add-ins ตรวจสอบไฟล์, ผู้ใช้แก้ไข docx<br />
|
||||
↓ <br />
|
||||
Versioning(คุมเวอร์ชั่น)<br />
|
||||
↓ <br />
|
||||
ecourt(อัปเดตฐานข้อมูลและไฟล์)<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-8 max-w-md mx-auto space-y-8 bg-gray-50 min-h-screen">
|
||||
<div className="p-10 max-w-md border rounded-xl bg-white shadow-sm">
|
||||
<h2 className="font-semibold text-blue-600 mb-2">Addin</h2>
|
||||
ติดตั้ง Word Add-ins ด้วยสคริปต์นี้ <br/>
|
||||
<Link href="/wordaddin/install-word-addins.ps1"> install-word-addins.ps1 </Link>
|
||||
<br/>
|
||||
ติดติดปัญหาบน Power Shell ให้เรียกสองบรรทัดล่างก่อน <br />
|
||||
<p className="whitespace-nowrap">Unblock-File .\install-word-addins.ps1</p>
|
||||
<p className="whitespace-nowrap">Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned -Force</p>
|
||||
<p className="whitespace-nowrap">.\install-word-addins.ps1 -Install</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
app/wordaddin/install-word-addins.ps1/route.ts
Normal file
17
app/wordaddin/install-word-addins.ps1/route.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { requestOrigin } from "@/utils/request-origin";
|
||||
// GET /wordaddin/install-word-addin.ps1 — สำหรับติดตั้ง Word add-in
|
||||
const DEV_ORIGIN = "https://localhost:3000";
|
||||
const TEMPLATE = path.join(process.cwd(),"app","wordaddin","install-word-addins.ps1", "template.ps1");
|
||||
export function GET(req: Request): Response {
|
||||
const template = fs.readFileSync(TEMPLATE, "utf8");
|
||||
const manifest = template.replaceAll(DEV_ORIGIN, requestOrigin(req));
|
||||
return new Response(manifest, {
|
||||
headers: {
|
||||
"content-type": "text/plain; charset=utf-8",
|
||||
"Content-Disposition": "attachment; filename=install-word-addins.ps1",
|
||||
"cache-control": "no-store",
|
||||
},
|
||||
});
|
||||
}
|
||||
98
app/wordaddin/install-word-addins.ps1/template.ps1
Normal file
98
app/wordaddin/install-word-addins.ps1/template.ps1
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# ติดตั้ง (sideload) / ถอด Word add-in "e-Court Documents Versioning" บน Windows
|
||||
# เวอร์ชัน PowerShell แบบส่งให้เครื่องทดสอบที่ไม่มี Node/repo — คู่หูของ
|
||||
# scripts/install-wordaddin.mjs (รันใน repo) และ scripts/install-wordaddin.sh (macOS)
|
||||
# กลไกเดียวกันทั้งสามไฟล์: โหลด manifest จาก server เก็บไว้
|
||||
# %USERPROFILE%\Documents\wordaddin-manifest\ แล้วจดทะเบียน registry
|
||||
# HKCU\Software\Microsoft\Office\16.0\Wef\Developer (กลไกเดียวกับ office-addin-dev-settings)
|
||||
#
|
||||
# ใช้: powershell -ExecutionPolicy Bypass -File install-wordaddin.ps1 [-Url <manifest-url>] [-Remove]
|
||||
# -Url default https://ecourt-note.chamomind.com/docedit/wordaddin/manifest.xml
|
||||
# -Remove ถอดการติดตั้ง
|
||||
#
|
||||
# (ไฟล์นี้ต้องมี BOM — Windows PowerShell 5.1 อ่าน UTF-8 ไม่มี BOM เป็น ANSI แล้วข้อความไทยพัง)
|
||||
# หลังลง/ถอดต้อง restart Word ทุกครั้ง · รายละเอียดเต็ม: docs/WORDADDIN-DEV.md
|
||||
|
||||
param(
|
||||
[string]$Url = "https://localhost:3000/docedit/wordaddin/manifest.xml",
|
||||
[switch]$Remove,
|
||||
[switch]$Install
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
# ให้ console พิมพ์ไทย/✓ ถูกต้อง และบังคับ TLS 1.2 (PS 5.1 บางเครื่อง default ยังเป็น TLS 1.0)
|
||||
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }
|
||||
[Net.ServicePointManager]::SecurityProtocol = `
|
||||
[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
$Prefix = "[install-addin]"
|
||||
$DevKeyCmd = "HKCU\Software\Microsoft\Office\16.0\Wef\Developer"
|
||||
$ManifestDir = Join-Path $env:USERPROFILE "Documents\wordaddin-manifest"
|
||||
$ManifestPath = Join-Path $ManifestDir "manifest.xml"
|
||||
|
||||
function Fail($message) {
|
||||
Write-Host "$Prefix ✗ $message" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
if ($Remove) {
|
||||
$Prefix = "[Uninstall-addin]"
|
||||
Write-Host "$Prefix platform: win32 · โหมดถอดการติดตั้ง"
|
||||
if (-not (Test-Path $ManifestPath)) {
|
||||
Fail "ไม่พบ manifest ที่เก็บไว้ ($ManifestPath) จึงไม่รู้ GUID ของ add-in — ลบเองได้ที่: reg delete `"$DevKeyCmd`" /v <GUID> /f"
|
||||
}
|
||||
$stored = Get-Content -Raw -Encoding UTF8 $ManifestPath
|
||||
if ($stored -notmatch "<Id>([0-9a-fA-F-]{36})</Id>") {
|
||||
Fail "อ่าน GUID จาก $ManifestPath ไม่ได้"
|
||||
}
|
||||
$guid = $Matches[1].ToLower()
|
||||
& reg.exe delete $DevKeyCmd /v $guid /f | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Fail "reg delete ไม่สำเร็จ (exit $LASTEXITCODE) — ลบเองได้ที่: reg delete `"$DevKeyCmd`" /v $guid /f"
|
||||
}
|
||||
Remove-Item $ManifestPath -ErrorAction SilentlyContinue
|
||||
Write-Host "$Prefix ✓ ถอดการติดตั้งแล้ว (registry $guid + ไฟล์ $ManifestPath)"
|
||||
Write-Host "$Prefix ✓ เสร็จ"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($Install) {
|
||||
Write-Host "$Prefix platform: win32"
|
||||
Write-Host "Install from $Url"
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 30
|
||||
} catch {
|
||||
Fail "โหลด manifest จาก $Url ไม่ได้ ($_)"
|
||||
}
|
||||
$xml = $response.Content
|
||||
if ($xml -notmatch "<OfficeApp") {
|
||||
Fail "response จาก $Url ไม่ใช่ manifest ของ Office add-in (ไม่มี <OfficeApp)"
|
||||
}
|
||||
if ($xml -notmatch "<Id>([0-9a-fA-F-]{36})</Id>") {
|
||||
Fail "หา <Id> (GUID) ใน manifest ไม่เจอจาก $Url"
|
||||
}
|
||||
$guid = $Matches[1].ToLower()
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $ManifestDir | Out-Null
|
||||
# เขียน UTF-8 ไม่มี BOM — ให้ manifest เหมือนต้นฉบับบน server เป๊ะ
|
||||
[System.IO.File]::WriteAllText($ManifestPath, $xml, (New-Object System.Text.UTF8Encoding($false)))
|
||||
Write-Host "$Prefix ✓ เก็บ manifest ไว้ที่ $ManifestPath"
|
||||
|
||||
& reg.exe add $DevKeyCmd /v $guid /t REG_SZ /d $ManifestPath /f | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Fail "เขียน registry ไม่สำเร็จ (exit $LASTEXITCODE)"
|
||||
}
|
||||
Write-Host "$Prefix ✓ จดทะเบียน add-in ใน registry ($DevKeyCmd → $guid)"
|
||||
Write-Host "$Prefix ✓ เสร็จ"
|
||||
# Print-NextSteps
|
||||
Write-Host ""
|
||||
Write-Host "ขั้นต่อไป:"
|
||||
Write-Host " 1. restart Word ให้ครบทุกหน้าต่าง (จำเป็นทุกครั้งหลังติดตั้ง/ถอนการติดตั้ง)"
|
||||
Write-Host ' 2. เมนู Home > Add-ins> กดแล้วเลือก "e-Court Documents Versioning"'
|
||||
Write-Host " 3. เปิดไฟล์ .docx ที่มี ticket ที่ยังไม่หมดอายุ เพื่อเริ่มทดสอบ"
|
||||
exit 0
|
||||
}
|
||||
Write-Host "วิธีการใช้:"
|
||||
Write-Host "ปิด Microsoft Word แล้วเรียกคำสั่งนี้"
|
||||
Write-Host " install-word-addins [-Install] [-Remove] [-Url http://domain/manifest.yaml]"
|
||||
Write-Host "ถ้าติดสิทธิ์ในการรันสคริปต์ให้เรียกคำสั่งนี้:"
|
||||
Loading…
Add table
Add a link
Reference in a new issue