feat(wordaddin): serve manifest with automatic origin rewrite
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m7s
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 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m7s
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
manifest ของ add-in เดิมเป็น static ใน public/ ฝัง URL dev (localhost:3000) ทั้งไฟล์ — deploy จริงต้อง rewrite มือเองทุกครั้ง ก่อนนำไป sideload ใน Word ตอนนี้ serve ผ่าน route แทน: app/wordaddin/manifest.xml/route.ts อ่าน template (public/wordaddin/manifest.template.xml — ย้ายจาก manifest.xml ด้วย git mv) แล้วเขียนทับ origin ให้ตรงโฮสต์ที่ขอ ไฟล์ — dev/UAT/production ดาวน์โหลด manifest.xml ขอ environment ตัวเองแล้ว sideload ได้ทันที - ถอด requestOrigin จาก N10 route เป็น utils/request-origin.ts ใช้ร่วม + เพิ่ม heuristic: โฮสต์ไม่ใช่ localhost + proto http = บังคับ https (Office รับ https เท่านั้น และ hop ภายในหลัง IIS เป็น http เสมอ — Next server ก็ฉีด x-forwarded-proto เอง จึง ชี้ขาดจากโฮสต์ปลายทาง ไม่เดาว่า header มาจาก proxy หรือถูกฉีด) deep link ของ N10 ได้พฤติกรรมเดียวกันเป็นผลพวง - ตรวจ nft trace แล้ว template ถูกดึงเข้า standalone ด้วย (คู่ กับ COPY public จาก commit ก่อน = ชัวร์สองชั้น) ยืนยัน: tsc + vitest 215 tests ผ่าน · ทดสอบบน dev server ด้วย curl 3 โหมด — direct ได้ http://localhost:3000 (Word ยกเว้น localhost), ผ่าน proxy ส่ง header ครบได้ https://<โฮสต์>, ผ่าน proxy ไม่ส่ง proto (กรณี IIS) ได้ https://<โฮสต์> จากการ bump · next build compile route ผ่าน (ขั้น standalone symlink พังเฉพาะเครื่อง local Windows สิทธิ์ symlink — ไม่เกี่ยวกับโค้ด Docker build ผ่าน) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
808233191d
commit
4bcd01a95f
6 changed files with 87 additions and 21 deletions
18
app/api/note/external/documents/route.ts
vendored
18
app/api/note/external/documents/route.ts
vendored
|
|
@ -4,6 +4,7 @@ import { db } from "@/db";
|
|||
import { files } from "@/db/schema";
|
||||
import { externalApiError, requireExternalApiKey } from "@/lib/external-api-auth";
|
||||
import { loadDocumentBundles, parseQueryBody } from "@/lib/external-api";
|
||||
import { requestOrigin } from "@/utils/request-origin";
|
||||
|
||||
// N10 (PLAN-API.md D2) — POST /api/note/external/documents
|
||||
// (เดิมชื่อ /api/external/query ถึงรอบสาม — เปลี่ยนชื่อตาม resource ที่คืน 2026-09-25)
|
||||
|
|
@ -19,21 +20,8 @@ import { loadDocumentBundles, parseQueryBody } from "@/lib/external-api";
|
|||
|
||||
const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
|
||||
|
||||
/** origin เต็มของ request นี้ (ใช้ประกอบ url เต็มใน response — คำขอผู้ใช้
|
||||
* 2026-09-25): เชื่อ header ของ reverse proxy ก่อน (x-forwarded-proto /
|
||||
* x-forwarded-host — proxy หลายชั้นส่งเป็น comma list ใช้ค่าแรก) แล้วค่อย
|
||||
* fallback จาก host/protocol ของ request ตรง ๆ */
|
||||
function requestOrigin(req: Request): string {
|
||||
const url = new URL(req.url);
|
||||
const proto =
|
||||
req.headers.get("x-forwarded-proto")?.split(",")[0]?.trim() ??
|
||||
url.protocol.replace(":", "");
|
||||
const host =
|
||||
req.headers.get("x-forwarded-host")?.split(",")[0]?.trim() ??
|
||||
req.headers.get("host") ??
|
||||
url.host;
|
||||
return `${proto}://${host}`;
|
||||
}
|
||||
// origin เต็มของ request สำหรับ url เต็มใน response — ถอดเป็น helper ร่วม
|
||||
// (utils/request-origin.ts) ให้ route manifest ของ wordaddin ใช้เหมือนกัน (2026-10-16)
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const unauthorized = requireExternalApiKey(req);
|
||||
|
|
|
|||
33
app/wordaddin/manifest.xml/route.ts
Normal file
33
app/wordaddin/manifest.xml/route.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { requestOrigin } from "@/utils/request-origin";
|
||||
|
||||
// GET /wordaddin/manifest.xml — manifest ของ Word add-in สำหรับ sideload
|
||||
//
|
||||
// URL ใน manifest เป็น absolute https ตามข้อบังคับของ Office แต่ต้องต่างกันตาม
|
||||
// environment — เดิมใช้ไฟล์ static ใน public/ ค่า dev (localhost:3000) แล้วให้ผู้
|
||||
// deploy rewrite มือเองทุกครั้ง (docs/WORDADDIN-DEV.md) ตอนนี้ serve ผ่าน route
|
||||
// แทน: อ่าน template (public/wordaddin/manifest.template.xml — ค่า dev) แล้ว
|
||||
// เขียนทับ origin ให้ตรงโฮสต์ที่ขอไฟล์ (dev/UAT/production ใช้ template
|
||||
// เดียวกัน — โหลดจาก environment ไหน manifest ก็ชี้ environment นั้น)
|
||||
//
|
||||
// อ่านจาก public/ เพราะเป็นที่เดียวที่การันตีว่ามีใน standalone ของ Docker
|
||||
// (runner stage COPY public — ไฟล์ app/ ต้นฉบับไม่ถูกก๊อปเข้า image)
|
||||
|
||||
/** origin dev ที่ฝังใน template — สตริงเดียวที่ถูกแทนที่ทั้งไฟล์ (6 จุด) */
|
||||
const DEV_ORIGIN = "https://localhost:3000";
|
||||
|
||||
const TEMPLATE = path.join(process.cwd(), "public", "wordaddin", "manifest.template.xml");
|
||||
|
||||
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": "application/xml; charset=utf-8",
|
||||
// origin ขึ้นกับ header ของผู้ขอ — ห้ามแคกลามผู้ใช้เกินครั้งเดียวต่อ download
|
||||
"cache-control": "no-store",
|
||||
},
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue