- Removed client-side HMAC verification; server now handles all HMAC checks. - Updated ticket processing logic to improve UX and security. - Adjusted API request handling to return discriminated results based on HTTP status. - Enhanced error messages for better clarity on authentication failures. - Added unit tests for the new token endpoint to ensure compliance with updated contract. - Cleaned up unused code and comments related to previous HMAC handling.
23 KiB
Word Add-in (wordaddin) — คู่มือ dev และการทดสอบ
Taskpane ของ Word add-in "e-Court Documents Versioning" พอร์ตมาเป็น route ของ Next.js ที่ /docedit/wordaddin (ต้นฉบับ webpack + Fluent UI: D:\word-addin-poc\addin) — spec พฤติกรรม UI ทั้งหมดอยู่ที่ docs/addin-ui-flow.md
ทางเข้า: เปิด /docedit เปล่า ๆ จะ redirect มาที่ taskpane ทันที (ไม่ผ่าน login — คำขอผู้ใช้ 2026-10-14) ยกเว้นลิงก์ e-Court /docedit/?fileUrl=&fileId=&typeId= ที่ยังส่งต่อให้ /note ตามสัญญา N8 · manifest ชี้ /docedit/wordaddin ตรง ๆ (ไม่ว่าผ่าน root เพื่อลด hop ใน webview)
สถานะช่วงนี้: ไม่มี login แล้ว (ยกเลิก Keycloak 2026-10-27 — auth เข้าทางเดียวคือ ticket ของ e-Court (ticket_data + HmacSha256 ในไฟล์): add-in ตรวจ HMAC เอง → ขอ token ที่ /api/documents/token → เก็บใน localStorage ตาม fid ดูหัว "ทดสอบ e-Court ticket" และแผน docs/PLAN-ADDIN-AUTH.md) และ upload/download เป็นของจริงแล้ว (ปลด mock 2026-10-27 — versioningService ยิงกลุ่ม /api/documents/* ผ่าน Bearer จาก ticket: Save & Version ขึ้น MinIO จริง ดาวน์โหลดเปิด presigned URL ในเบราว์เซอร์; เหลือ mock เฉพาะโหมด ?addindemo เพื่อดู UI ใน browser โดยไม่ต้องมี backend — ดูหัว "การรันกับ backend จริง")
ไฟล์หลัก
| ที่ | คือ |
|---|---|
app/wordaddin/page.tsx + components/wordaddin/WordAddinGate.tsx |
gate โหลด Office.js (self-host) → Office.onReady → mount TaskpaneApp |
app/wordaddin/manifest.xml/route.ts |
serve manifest พร้อม rewrite origin ตามโฮสต์ที่ขอ (อ่าน x-forwarded-proto/x-forwarded-host ก่อนแล้ว fallback host ตรง ผ่าน utils/request-origin.ts ที่แชร์กับ origin ของ deep link N10 — โฮสต์ที่ไม่ใช่ localhost บังคับ https เพราะ Office รับ https เท่านั้น) |
components/wordaddin/TaskpaneApp.tsx |
state machine checking/no-access/untracked/tracking + ทุก handler |
wordaddin/services/* |
ชั้น Office.js พอร์ตดิบจากต้นฉบับ + versioningService เป็น mock (authService + ticketService = ticket auth จริง ไม่มี login — PLAN-ADDIN-AUTH.md) |
public/wordaddin/manifest.template.xml |
template ของ manifest (ค่า default dev https://localhost:3000) — serve ผ่าน route ด้านบนเท่านั้น ไม่ได้ใช้ตรง ๆ |
public/wordaddin/office-js/ |
Office.js runtime — สร้างโดย postinstall scripts/fetch-office-js.mjs ดาวน์โหลดฉบับล่าสุดจาก CDN ของ Microsoft (gitignore — รัน npm install ทุกครั้งหลัง clone เครื่องต้องออกอินเทอร์เน็ตตอนติดตั้ง ส่วนตัวระบบ deploy ใช้ intranet ล้วน) |
wordaddin/config.ts |
env NEXT_PUBLIC_ADDIN_* (ตารางด้านล่าง) |
Env
| ตัวแปร | default | ใช้เมื่อไหร่ |
|---|---|---|
NEXT_PUBLIC_ADDIN_API_BASE_URL |
"" (ว่าง) |
ปลด mock ต่อ backend จริง (ต้องเป็น HTTPS) — ไม่เกี่ยวกับการขอ token (ยิง same-origin /docedit/api/documents/token เสมอ) |
NEXT_PUBLIC_ADDIN_MOCK_USERNAME |
"tester01" |
ชื่อ fallback ของโหมด ?addindemo (แสดง header + เป็น createdByUserId ของ mock) |
( — ถอดออก 2026-11-04 ตาม PLAN-SSO.md D13: add-in ไม่ตรวจ HMAC เองแล้ว ไม่มี secret ใน bundle ฝั่ง client)NEXT_PUBLIC_ADDIN_TICKET_SECRET
รัน dev ด้วย HTTPS (จำเป็นสำหรับ sideload ใน Word)
manifest ต้องชี้ https — 2 ทางเลือก:
- self-signed ของ Next (ง่ายสุด):
npm run dev:https→ https://localhost:3000 — Word อาจเตือน certificate ครั้งแรก (เปิด URL ใน browser กดผ่านก่อน 1 ครั้ง) - ใบรับรอง trusted (แนะนำ ไม่มี warning): รัน
npx office-addin-dev-certs installครั้งเดียว แล้ว copy%USERPROFILE%\.office-addin-dev-certs\localhost.crt+localhost.keyมาที่certs/(gitignore แล้ว) แล้วใช้npm run dev:https:cert
ติดตั้ง (sideload) ด้วยสคริปต์ — Windows + macOS
scripts/install-wordaddin.* (มี 3 รูปแบบ กลไกเดียวกัน) โหลด manifest จาก server แล้วจดทะเบียนให้ Word Desktop เห็น add-in ในคำสั่งเดียว:
# ใน repo (มี Node)
npm run addin:install
# ส่งให้เครื่องทดสอบที่ไม่มี Node/repo — ส่งไฟล์เดี่ยวได้เลย
./install-wordaddin.sh # macOS (bash + curl ของเครื่อง)
powershell -ExecutionPolicy Bypass -File install-wordaddin.ps1 # Windows
node install-wordaddin.mjs # ใช้ Node ก็ได้ทั้งสอง OS
- default โหลดจาก server ทดสอบ
https://ecourt-note.chamomind.com/docedit/wordaddin/manifest.xml— เครื่องทดสอบไม่ต้องรันอะไรเพิ่ม · จะชี้ dev ให้--url https://localhost:3000/docedit/wordaddin/manifest.xml(ต้องnpm run dev:httpsไว้ก่อน) - Windows: เก็บ manifest ไว้
%USERPROFILE%\Documents\wordaddin-manifest\แล้วเขียน registryHKCU\Software\Microsoft\Office\16.0\Wef\Developer(กลไกเดียวกับoffice-addin-dev-settings) — ไม่ต้องคลิก Trust Center - macOS: วางไฟล์ manifest เป็น
<GUID>.manifest.xmlใน~/Library/Containers/com.microsoft.Word/Data/Documents/wef/ - หลังรัน restart Word แล้วเปิดได้ที่ Insert > My Add-ins > SHARED FOLDER (Windows) / Insert > My Add-ins (macOS)
- ถอด:
--removeทุกไฟล์ (npm run addin:install -- --remove/./install-wordaddin.sh --remove/-Removeของ .ps1 — ลบ registry value / ไฟล์ใน wef · ถอดแบบ manual:reg delete "HKCU\Software\Microsoft\Office\16.0\Wef\Developer" /v <GUID> /fหรือลบไฟล์ในโฟลเดอร์ wef เอง) · ⚠️ ไฟล์ .ps1 ต้องมี BOM ติดตัวมา (PowerShell 5.1 อ่าน UTF-8 ไร้ BOM เป็น ANSI แล้วข้อความไทยพัง) — แก้ไฟล์แล้วอย่าลืมเซฟแบบ UTF-8 with BOM - รันซ้ำได้ (idempotent) — โหลด manifest ใหม่ทุกครั้งจึงได้ URL ของ environment ปัจจุบันเสมอ
Sideload แบบ manual (Windows)
เครื่อง dev ที่มี repo (แนะนำ — ไม่ต้องดาวน์โหลด/คัดลอกอะไรเลย):
- Word: File > Options > Trust Center > Trust Center Settings > Trusted Add-in Catalogs เพิ่มโฟลเดอร์
public/wordaddinของ repo (เช่นD:\dms\judes-note\public\wordaddin) > restart Word - Insert > My Add-ins > SHARED FOLDER > เปิด "e-Court Documents Versioning"
- ชื่อไฟล์ไม่สำคัญ — Word สแกน
*.xmlทุกไฟล์ในโฟลเดอร์ โดยmanifest.template.xmlคือ dev manifest อยู่แล้ว (URL localhost:3000) และเนื้อไฟล์อัปเดตตาม branch ที่ checkout อยู่เสมอ — ห้ามวาง xml manifest อื่นเพิ่มในโฟลเดอร์นี้ (GUID เดียวกันสองไฟล์ = Word สับสน)
เครื่องที่ไม่มี repo (production/เครื่องผู้ใช้):
- ดาวน์โหลด manifest จาก environment นั้น:
https://<โฮสต์>/docedit/wordaddin/manifest.xml - เอาไฟล์ไว้โฟลเดอร์ใดโฟลเดอร์หนึ่ง เช่น
%USERPROFILE%\Documents\wordaddin-manifest\ - ทำขั้น Trust Center ข้างบนด้วยโฟลเดอร์นั้น > restart Word > Insert > My Add-ins > SHARED FOLDER > เปิด "e-Court Documents Versioning"
Deploy จริง (intranet): ไม่ต้อง rewrite อะไรแล้ว — manifest serve ผ่าน app/wordaddin/manifest.xml/route.ts ซึ่งเขียนทับ origin ให้ตรงโฮสต์ที่ขอไฟล์อัตโนมัติ ดาวน์โหลด https://<โฮสต์>/docedit/wordaddin/manifest.xml แล้วใช้ sideload ได้ทันทีทุก environment (dev/UAT/production) · proxy ฝั่งหน้าควรส่ง X-Forwarded-Host/X-Forwarded-Proto ตามมาตรฐาน — ถ้าไม่ส่ง route ก็ยังใช้ Host ของ request และบังคับ https ให้โฮสต์ที่ไม่ใช่ localhost อยู่ดี
macOS (manual)
Word for Mac ไม่มี Trust Center แบบ Windows — สแกน add-in จากโฟลเดอร์ของตัวเอง:
- ดาวน์โหลด manifest จาก environment นั้น:
https://<โฮสต์>/docedit/wordaddin/manifest.xml - วางไฟล์ใน
~/Library/Containers/com.microsoft.Word/Data/Documents/wef/(สร้างโฟลเดอร์ได้ถ้ายังไม่มี) — ชื่อไฟล์เป็นอะไรก็ได้ ขอแค่ลงท้าย.xml(สคริปต์ด้านบนตั้งชื่อ<GUID>.manifest.xmlให้เอง) - restart Word แล้วเปิดได้ที่ Insert > My Add-ins
รุ่น Office ที่รองรับ: manifest กำหนด WordApi 1.3 เป็นค่าต่ำสุด (customXmlParts คือ API สูงสุดที่ใช้ประจำ) — Microsoft 365 / Office 2021+ ใช้ได้ครบทุกฟีเจอร์ · Office 2019 โหลดได้ แต่ปุ่ม "…และปิดเอกสาร" ถูกซ่อนอัตโนมัติ เพราะ document.close() ต้องการ WordApi 1.4/1.5 (Microsoft 365 เท่านั้น) — canCloseDocument() เช็ค isSetSupported ตอน runtime แล้วซ่อน พฤติกรรมเดียวกับ Word on the web · Office 2016 (WordApi 1.1) โหลดไม่ขึ้น (เดิมเคยตั้ง 1.5 ทำให้ Office 2019 โหลดไม่ขึ้นด้วย — แก้ 2026-10-16)
ทดสอบใน browser ธรรมดา (ไม่ต้องเปิด Word)
/docedit/wordaddin— เส้นทางจริง: Office.js โหลดได้แม้นอก host (host เป็น null) →detect()อ่าน custom properties ไม่ได้ (ไม่มีWord.run) → เห็นno-access— ตรวจ state machine ได้แค่นี้ใน browser ปกติ ส่วน phaseuntracked/trackingต้องใช้ demo ด้านล่าง (หรือ sideload ใน Word จริง)/docedit/wordaddin?addindemo=untracked/?addindemo=tracking— dev-only: ข้าม Office.js + ตั้ง session mock ทันที + ตั้ง phase ตามค่า (tracking มี history ตัวอย่าง 3 รายการ) — ทุกปุ่มกดได้ แต่ flow ที่แตะ Word จริง (Start Versioning / Save & Version / บันทึก) จะจบที่ error ตาม expected- หมายเหตุ: ใน browser ปกติ (ไม่ใช่ demo) banner "บันทึกอัตโนมัติไม่สำเร็จ" จะโผล่หลัง ~30 วิ เพราะ auto-save (FR-17) พยายาม save ทุก 30 วิแต่ไม่มี Word — ถือว่าถูกต้อง
ทดสอบ e-Court ticket (auth ทางเข้าเดียว)
ไฟล์ที่ e-Court stamp custom properties ticket_data (JSON exp/fid/tid/uid/uname) + HmacSha256 (HMAC-SHA256 ของ string ดิบ ด้วย secret ปัจจุบัน super_secret) ไว้ จะใช้ได้เลย — ไม่มี login ให้กด: detect() อ่านคู่นี้ตอนเปิด task pane แล้วเรียก authService.signInWithTicket() ตาม flow ของ docs/requirement/addin-workflow.md (แก้ 2026-11-04: pre-check ฟอร์แม็ต/exp ฝั่ง client → ยิง /docedit/api/documents/token ทุกครั้งที่เปิดเอกสาร ให้ server ตรวจลายเซ็นแล้วแยกตาม status 200/400/401/403 → เก็บ token ตาม fid · ต้อง online ตอนเปิดไฟล์เสมอ) · ไฟล์ที่ไม่มีคู่นี้ / ฟอร์แม็ตพัง / ลายเซ็นไม่ผ่าน (server 401) / ticket หมดอายุ = phase no-access แยกข้อความตามสาเหตุ
ทดสอบเองใน Word โดยไม่ต้องรอ e-Court — สร้างไฟล์ทดสอบด้วยสคริปต์ (คำนวณ HMAC ตรงกับตัว stamp จริงของกลุ่ม template):
# สร้าง <file>.ticket.docx (ไม่แตะไฟล์เดิม) exp +5 นาที เท่า TICKET_SEC ของตัว stamp
node scripts/make-ticket-docx.mjs path/to/file.docx --fid DEMO_FID_001 --uid oom --uname "สรวิชญ์" --tid T12
# ดู custom properties ที่มีในไฟล์
node scripts/make-ticket-docx.mjs path/to/file.ticket.docx --list
# ทดสอบ phase tracking ด้วย (stamp SavedByAddIn + DocumentTrackingId เพิ่ม)
node scripts/make-ticket-docx.mjs path/to/file.docx --tracked --in-place
แล้วเปิดไฟล์ใน Word ที่ sideload แล้ว → ต้องเข้าเฟส untracked (หรือ tracking เมื่อ --tracked) ทันที ชื่อผู้ใช้ = uname · ทดสอบ fail-case (แก้ 2026-11-04 ตาม contract ใหม่): แก้ ticket_data ผ่าน File → Info → Properties → Advanced Properties → Custom สัก 1 ตัวอักษร → no-access ข้อความลายเซ็นไม่ผ่าน (server 401) · สร้างด้วย --exp +60 รอหมดอายุแล้วเปิดใหม่ — มี session (JWT อายุ 7 วันยังไม่หมด) → ยังใช้ได้ (Network เห็น /token ตอบ 403 แต่ UI ไหลต่อด้วย JWT เดิม) · เคลียร์ localStorage wordaddin-auth ก่อนเปิด → no-access "ticket หมดอายุ" (ต้องขอไฟล์ใหม่จาก e-Court — เลิกตรวจ "เคยมี version" จาก DB ตาม requirement ขั้น 7 เดิมแล้ว) · หยุด dev server แล้วเปิด task pane (ticket ยังไม่หมด) → no-access "ขอ token ไม่สำเร็จ" (ยืนยันว่าต้อง online ตอนเปิดเอกสาร)
หมายเหตุ: การขอ token ยิง same-origin /docedit/api/documents/token จริง (ไม่ผูก NEXT_PUBLIC_ADDIN_API_BASE_URL) — dev server ต้องรันอยู่และ secret ฝั่ง server (ADDIN_TICKET_SECRET → SECRET → default super_secret) ตรงกับ --secret ของสคริปต์
การรันกับ backend จริง (ปลด mock แล้ว 2026-10-27)
- Auth: ไม่มี login — ยกเลิก Keycloak ถาวร 2026-10-27 · auth ของ add-in คือ ticket ของ e-Court ผ่าน
signInWithTicket(ticket_data + HmacSha256 ในไฟล์ — แผนเต็มdocs/PLAN-ADDIN-AUTH.mdแก้ 2026-11-04): add-in ไม่ตรวจ HMAC เอง (secret เป็นของ server อย่างเดียว — PLAN-SSO.md D13) — pre-check ฟอร์แม็ต/exp ฝั่ง client แล้วยิงขอ access_token ที่/api/documents/tokenทุกครั้งที่เปิดเอกสาร: 200 = ได้ token ใหม่ (อายุ default 7 วัน) · 401 = ลายเซ็นไม่ถูก · 403 = ลายเซ็นถูกแต่ ticket หมดอายุ (มี JWT เดิมใน session ที่ยังไม่หมด = ใช้ต่อ) · โดน 401 กลาง flow = ลอง renew จาก ticket 1 ครั้ง ยังไม่ผ่าน =AuthRequiredError→ หน้าno-access(ticket ต่ออายุไม่ได้ — ต้องได้ไฟล์ใหม่จาก e-Court) - Upload/download ของจริงเป็น default แล้ว:
wordaddin/services/versioningService.tsยิงกลุ่ม/api/documents/*ผ่านapiClient(Bearer จาก ticket) — ไม่ต้องตั้ง env อะไรเพิ่ม เพราะapiClient.toUrlใช้ same-originwithBasePath("/api/documents/…")เมื่อNEXT_PUBLIC_ADDIN_API_BASE_URLว่าง (API ของกลุ่ม docver อยู่แอปนี้เอง) · ตั้งค่า env นั้นเฉพาะวันที่ deploy add-in กับ API คนละ origin (ต้องรวม basePath เช่นhttps://host/doceditและเป็น HTTPS)- เอกสารถูกระบุด้วย fid ใน token — พารามิเตอร์
documentId(GUID ที่ stamp ในไฟล์) ของ uploadVersion/getVersions/getDownloadLink เหลือไว้ตาม signature เดิม ไม่ถูกส่งถึง server · templateId ก็เช่นกัน (มาจาก claims ของ token — ส่งใน form = 400)
- เอกสารถูกระบุด้วย fid ใน token — พารามิเตอร์
- สิ่งที่ต้องรันฝั่ง server ก่อนใช้จริง: MinIO (
docker compose up -d minio— bucketdocverapp สร้างเอง) + envDOCVER_API_KEY/DOCVER_TOKEN_SECRET/MINIO_*ตาม.env.exampleของกลุ่ม docver (docs/PLAN-DOCVER.md §6) · ทดสอบวงเต็ม: สร้างไฟล์ด้วยmake-ticket-docx.mjs→ เปิดใน Word → Save & Version → version ขึ้น MinIO จริง → กดดาวน์โหลด/PDF เปิด presigned URL ในเบราว์เซอร์ (โฮสต์ MinIO ต้องเข้าถึงได้จากเครื่องผู้ใช้) - mock เหลือเฉพาะโหมด
?addindemo(dev-only — ดู UI ใน browser ไม่ต้องมี backend: version เก็บในเครื่อง metadata ใน localStoragewordaddin-mock-versions, download-link คืน about:blank — เคลียร์ด้วยการลบ key นี้หรือ clear storage ของ origin) - ชุด unit test ของ services: ต้นฉบับ jest อยู่ที่
D:\word-addin-poc\addin\src\services\*.test.ts— ฝั่งนี้เขียนใหม่เป็น vitest เฉพาะส่วนที่เป็นโค้ดใหม่ (ticket auth + versioningService สองโหมด ที่tests/wordaddin/)
Docker
ไม่ต้องทำอะไรเพิ่ม — docker/web.Dockerfile รัน scripts/fetch-office-js.mjs ใน build อยู่แล้ว (build stage ต้องออกอินเทอร์เน็ตถึง appsforoffice.microsoft.com ได้) และ runner stage คัดลอก public/ เข้า image เอง (COPY /app/public ./public — Next 14.2 standalone ไม่รวม public/ ให้อัตโนมัติ ต้อง copy เองตามเอกสาร deploy ของ Next; เคยเข้าใจผิดว่า Next รวมให้ เพราะบางไฟล์หลุดเข้า image ผ่าน output file tracing ของ Next ที่ตาม fs.readFile ใน server code — แต่ public/wordaddin/office-js ไม่มีใครอ่านผ่าน fs จึงไม่ถูกดึงมา แล้ว taskpane พังใน production 2026-10-16)
ทำไมต้องเป็นฉบับ CDN ล่าสุด: แพ็กเกจ npm
@microsoft/office-jsเลิกอัปเดตที่ 1.1.110 (~2018) ก่อน WordApi 1.4/1.5 — runtime ตัวนั้นไม่มีDocument.close(ปุ่ม "บันทึกเวอร์ชันและปิดเอกสาร" พัง:context.document.close is not a function) และไม่มี enumWord.CloseBehavior/SaveBehaviorทับ ๆ กับ@types/office-jsฉบับใหม่ที่ใช้ compile