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
|
|
@ -81,6 +81,7 @@ app/note/login/page.tsx ← หน้า login (มี session แ
|
||||||
app/note/annotate/[id]/page.tsx ← โหลด file + initial data → PDFViewer
|
app/note/annotate/[id]/page.tsx ← โหลด file + initial data → PDFViewer
|
||||||
app/note/annotate/[id]/summary/page.tsx ← หน้าสรุป Annotation+Bookmark ของไฟล์เดียว (server component — query ทั้งหมดฝั่ง server แล้วส่ง `ExplorerItem[]` เรียง หน้า → หน้าเดียวกัน bookmark ก่อน annotation (bookmark ต่อหน้าเรียงตาม yOffset N5 เหมือนเดิม, annotation ตาม createdAt) ให้ `SummaryExplorer` กรองใน memory; design "แฟ้มสำนวน" 2026-09-17 แถบหัวน้ำเงิน + การ์ดรายหน้าขอบ folio + รายการตามหน้าจุดไข่ปลา → **redesign 2026-09-24 ตามคำขอผู้ใช้เป็น explorer 2 คอลัมน์แบบหน้ากลุ่ม N9 โดยมิติแรกของตัวกรองเป็น "หน้า" แทน "ไฟล์"** — แถบหัวน้ำเงิน + action bar (กลับหน้าอ่าน — ปุ่มส่งออก CSV F16/ส่งออก Bookmark JSON N7 ถอด 2026-09-25 แทนด้วย N10 API) + empty state คงเดิม, ไฟล์ว่าง = ไม่ mount explorer เลย; generateMetadata ตั้งชื่อแท็บ browser ตามคดี; ตอนพิมพ์ header เป็นกรอบเส้นน้ำเงิน + แผงตัวกรอง/แถบ action ซ่อนด้วย print:hidden — กฎ @media print อยู่ใน globals.css) — อย่าถอด class `summary-page` ของ <main> (กฎ coarse 44px ใน globals.css ผูกอยู่กับ selector นี้ ครอบทั้ง `a` และ `button`)
|
app/note/annotate/[id]/summary/page.tsx ← หน้าสรุป Annotation+Bookmark ของไฟล์เดียว (server component — query ทั้งหมดฝั่ง server แล้วส่ง `ExplorerItem[]` เรียง หน้า → หน้าเดียวกัน bookmark ก่อน annotation (bookmark ต่อหน้าเรียงตาม yOffset N5 เหมือนเดิม, annotation ตาม createdAt) ให้ `SummaryExplorer` กรองใน memory; design "แฟ้มสำนวน" 2026-09-17 แถบหัวน้ำเงิน + การ์ดรายหน้าขอบ folio + รายการตามหน้าจุดไข่ปลา → **redesign 2026-09-24 ตามคำขอผู้ใช้เป็น explorer 2 คอลัมน์แบบหน้ากลุ่ม N9 โดยมิติแรกของตัวกรองเป็น "หน้า" แทน "ไฟล์"** — แถบหัวน้ำเงิน + action bar (กลับหน้าอ่าน — ปุ่มส่งออก CSV F16/ส่งออก Bookmark JSON N7 ถอด 2026-09-25 แทนด้วย N10 API) + empty state คงเดิม, ไฟล์ว่าง = ไม่ mount explorer เลย; generateMetadata ตั้งชื่อแท็บ browser ตามคดี; ตอนพิมพ์ header เป็นกรอบเส้นน้ำเงิน + แผงตัวกรอง/แถบ action ซ่อนด้วย print:hidden — กฎ @media print อยู่ใน globals.css) — อย่าถอด class `summary-page` ของ <main> (กฎ coarse 44px ใน globals.css ผูกอยู่กับ selector นี้ ครอบทั้ง `a` และ `button`)
|
||||||
app/wordaddin/layout.tsx + page.tsx ← taskpane ของ Word add-in (URL `/docedit/wordaddin` — nested layout แรกของ repo override title แท็บ browser; page เป็น server component บาง ๆ render WordAddinGate ทั้งหมดฝั่ง client)
|
app/wordaddin/layout.tsx + page.tsx ← taskpane ของ Word add-in (URL `/docedit/wordaddin` — nested layout แรกของ repo override title แท็บ browser; page เป็น server component บาง ๆ render WordAddinGate ทั้งหมดฝั่ง client)
|
||||||
|
app/wordaddin/manifest.xml/route.ts ← GET serve manifest ของ add-in พร้อม rewrite origin ตามโฮสต์ที่ขอ (template `public/wordaddin/manifest.template.xml` ค่า dev — dev/UAT/production โหลด manifest.xml ขอ environment ตัวเองแล้วใช้ได้เลย ไม่ต้อง rewrite มือ; origin จาก `utils/request-origin.ts` แชร์กับ deep link N10 — x-forwarded-proto/host ก่อน fallback host ตรง + บังคับ https โฮสต์ที่ไม่ใช่ localhost เพราะ Office รับ https เท่านั้น)
|
||||||
components/wordaddin/WordAddinGate.tsx ← client gate: ฉีด script office.js self-host (ผ่าน withBasePath "/wordaddin/office-js/office.js") → Office.onReady → mount TaskpaneApp · `?addindemo=untracked|tracking` (dev-only) ข้าม office.js + mock sign in + ตั้ง phase ตรง เพื่อดู UI ใน browser ธรรมดา — flow ที่แตะ Word จริงจะจบที่ error ตาม expected (FR-17: ใน browser ปกติ banner auto-save error โผล่หลัง ~30 วิ — ถูกต้อง)
|
components/wordaddin/WordAddinGate.tsx ← client gate: ฉีด script office.js self-host (ผ่าน withBasePath "/wordaddin/office-js/office.js") → Office.onReady → mount TaskpaneApp · `?addindemo=untracked|tracking` (dev-only) ข้าม office.js + mock sign in + ตั้ง phase ตรง เพื่อดู UI ใน browser ธรรมดา — flow ที่แตะ Word จริงจะจบที่ error ตาม expected (FR-17: ใน browser ปกติ banner auto-save error โผล่หลัง ~30 วิ — ถูกต้อง)
|
||||||
components/wordaddin/TaskpaneApp.tsx ← state machine checking/signed-out/untracked/tracking + handler ทุกตัว พอร์ตจาก App.tsx ต้นฉบับ (state/handlers คงเดิม เปลี่ยนแค่ JSX → Genesis) · SaveControls/MessageBar/Spinner/ConfirmVersionDialog(portal แบบ AnnotationSidebar)/VersionHistoryPanel
|
components/wordaddin/TaskpaneApp.tsx ← state machine checking/signed-out/untracked/tracking + handler ทุกตัว พอร์ตจาก App.tsx ต้นฉบับ (state/handlers คงเดิม เปลี่ยนแค่ JSX → Genesis) · SaveControls/MessageBar/Spinner/ConfirmVersionDialog(portal แบบ AnnotationSidebar)/VersionHistoryPanel
|
||||||
components/summary/SummaryExplorer.tsx ← client root ของหน้าสรุป (redesign 2026-09-24) — state ตัวกรอง `FacetFilterState` จากแผงกลาง explorer + กรองใน memory + facet count/static users derive เอง + มิติ "หน้า" เป็นตัวเลือกแบบ single-select เฉพาะหน้าที่มีรายการ ≥1 เรียงน้อย→มาก (`optionsScroll` ให้ scroll ภายในกลุ่ม max-h-64 กันหน้าเยอะดันกลุ่มอื่นออกจอ) + layout 2 คอลัมน์ (มือถือ < lg ตัวกรองหุบเป็น `<details>`, desktop aside sticky 15rem — pattern เดียวกับ GroupExplorer; คอลัมน์ content ครอบด้วย `<div>` ไม่ใช่ `<section>` เพราะกฎ print break-inside ของ `main.summary-page section` มุ่งการ์ดรายหน้าข้างใน)
|
components/summary/SummaryExplorer.tsx ← client root ของหน้าสรุป (redesign 2026-09-24) — state ตัวกรอง `FacetFilterState` จากแผงกลาง explorer + กรองใน memory + facet count/static users derive เอง + มิติ "หน้า" เป็นตัวเลือกแบบ single-select เฉพาะหน้าที่มีรายการ ≥1 เรียงน้อย→มาก (`optionsScroll` ให้ scroll ภายในกลุ่ม max-h-64 กันหน้าเยอะดันกลุ่มอื่นออกจอ) + layout 2 คอลัมน์ (มือถือ < lg ตัวกรองหุบเป็น `<details>`, desktop aside sticky 15rem — pattern เดียวกับ GroupExplorer; คอลัมน์ content ครอบด้วย `<div>` ไม่ใช่ `<section>` เพราะกฎ print break-inside ของ `main.summary-page section` มุ่งการ์ดรายหน้าข้างใน)
|
||||||
|
|
|
||||||
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 { files } from "@/db/schema";
|
||||||
import { externalApiError, requireExternalApiKey } from "@/lib/external-api-auth";
|
import { externalApiError, requireExternalApiKey } from "@/lib/external-api-auth";
|
||||||
import { loadDocumentBundles, parseQueryBody } from "@/lib/external-api";
|
import { loadDocumentBundles, parseQueryBody } from "@/lib/external-api";
|
||||||
|
import { requestOrigin } from "@/utils/request-origin";
|
||||||
|
|
||||||
// N10 (PLAN-API.md D2) — POST /api/note/external/documents
|
// N10 (PLAN-API.md D2) — POST /api/note/external/documents
|
||||||
// (เดิมชื่อ /api/external/query ถึงรอบสาม — เปลี่ยนชื่อตาม resource ที่คืน 2026-09-25)
|
// (เดิมชื่อ /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" };
|
const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" };
|
||||||
|
|
||||||
/** origin เต็มของ request นี้ (ใช้ประกอบ url เต็มใน response — คำขอผู้ใช้
|
// origin เต็มของ request สำหรับ url เต็มใน response — ถอดเป็น helper ร่วม
|
||||||
* 2026-09-25): เชื่อ header ของ reverse proxy ก่อน (x-forwarded-proto /
|
// (utils/request-origin.ts) ให้ route manifest ของ wordaddin ใช้เหมือนกัน (2026-10-16)
|
||||||
* 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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
const unauthorized = requireExternalApiKey(req);
|
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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -11,9 +11,10 @@ Taskpane ของ Word add-in "e-Court Documents Versioning" พอร์ตม
|
||||||
| ที่ | คือ |
|
| ที่ | คือ |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `app/wordaddin/page.tsx` + `components/wordaddin/WordAddinGate.tsx` | gate โหลด Office.js (self-host) → `Office.onReady` → mount `TaskpaneApp` |
|
| `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/signed-out/untracked/tracking` + ทุก handler |
|
| `components/wordaddin/TaskpaneApp.tsx` | state machine `checking/signed-out/untracked/tracking` + ทุก handler |
|
||||||
| `wordaddin/services/*` | ชั้น Office.js พอร์ตดิบจากต้นฉบับ + `authService`/`versioningService` เป็น mock |
|
| `wordaddin/services/*` | ชั้น Office.js พอร์ตดิบจากต้นฉบับ + `authService`/`versioningService` เป็น mock |
|
||||||
| `public/wordaddin/manifest.xml` | manifest sideload (URL เป็น absolute https ต่อ environment) |
|
| `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 ล้วน) |
|
| `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_*` (ตารางด้านล่าง) |
|
| `wordaddin/config.ts` | env `NEXT_PUBLIC_ADDIN_*` (ตารางด้านล่าง) |
|
||||||
|
|
||||||
|
|
@ -38,7 +39,7 @@ manifest ต้องชี้ https — 2 ทางเลือก:
|
||||||
3. Word: **File > Options > Trust Center > Trust Center Settings > Trusted Add-in Catalogs** เพิ่มโฟลเดอร์นั้น > restart Word
|
3. Word: **File > Options > Trust Center > Trust Center Settings > Trusted Add-in Catalogs** เพิ่มโฟลเดอร์นั้น > restart Word
|
||||||
4. **Insert > My Add-ins > SHARED FOLDER** > เปิด "e-Court Documents Versioning"
|
4. **Insert > My Add-ins > SHARED FOLDER** > เปิด "e-Court Documents Versioning"
|
||||||
|
|
||||||
Deploy จริง (intranet): rewrite URL ทุกตัวใน manifest จาก `https://localhost:3000/docedit/wordaddin` เป็น `https://<โฮสต์>/docedit/wordaddin` (และ AppDomain + icons ด้วย)
|
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 อยู่ดี
|
||||||
|
|
||||||
## ทดสอบใน browser ธรรมดา (ไม่ต้องเปิด Word)
|
## ทดสอบใน browser ธรรมดา (ไม่ต้องเปิด Word)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,12 @@
|
||||||
<!--
|
<!--
|
||||||
manifest ของ Word add-in "e-Court Documents Versioning" (พอร์ตจาก D:\word-addin-poc\addin\manifest.xml)
|
manifest ของ Word add-in "e-Court Documents Versioning" (พอร์ตจาก D:\word-addin-poc\addin\manifest.xml)
|
||||||
|
|
||||||
⚠️ URL ทุกตัวเป็น absolute https ตามข้อบังคับของ Office — ต้อง rewrite ต่อ environment:
|
ไฟล์นี้เป็น **template** — URL ทุกตัวเป็น absolute https ตามข้อบังคับของ Office
|
||||||
dev (default ด้านล่าง) = https://localhost:3000/docedit/wordaddin
|
โดยค่า default คือ dev (https://localhost:3000/docedit/wordaddin) และถูก serve
|
||||||
deploy จริง = https://<โฮสต์ intranet>/docedit/wordaddin
|
ผ่าน route `app/wordaddin/manifest.xml/route.ts` ที่ rewrite origin ให้ตรงกับ
|
||||||
(รายละเอียดวิธีรัน dev + sideload: docs/WORDADDIN-DEV.md)
|
โฮสต์ที่ขอไฟล์อัตโนมัติ (dev/UAT/production ใช้ template เดียวกัน — เข้าถึงที่
|
||||||
|
/docedit/wordaddin/manifest.xml ของ environment นั้น ๆ ได้เลย)
|
||||||
|
(รายละเอียดวิธีรัน dev + sideload: docs/WORDADDIN-DEV.md)
|
||||||
|
|
||||||
ผลต่างจากต้นฉบับ:
|
ผลต่างจากต้นฉบับ:
|
||||||
- taskpane ชี้ที่หน้า Next.js (/docedit/wordaddin) แทน taskpane.html ของ webpack
|
- taskpane ชี้ที่หน้า Next.js (/docedit/wordaddin) แทน taskpane.html ของ webpack
|
||||||
41
utils/request-origin.ts
Normal file
41
utils/request-origin.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
// origin เต็มของ request ที่เข้ามา — ใช้ประกอบ URL เต็มที่จะส่งกลับออกไปข้างนอก
|
||||||
|
// (deep link ของ N10 / manifest.xml ของ wordaddin) เพราะ request ที่เห็นฝั่ง app
|
||||||
|
// มักผ่าน reverse proxy หลัง (IIS ของทางศาล) จึงต้องดู header ของ proxy ก่อน
|
||||||
|
//
|
||||||
|
// ถอดมาจาก requestOrigin เดิมใน app/api/note/external/documents/route.ts
|
||||||
|
// (2026-10-16) ให้ route manifest ของ wordaddin ใช้ร่วม
|
||||||
|
|
||||||
|
/** origin เต็มของ request นี้: เชื่อ header ของ reverse proxy ก่อน
|
||||||
|
* (x-forwarded-proto / x-forwarded-host — proxy หลายชั้นส่งเป็น comma list
|
||||||
|
* ใช้ค่าแรก) แล้วค่อย fallback จาก host/protocol ของ request ตรง ๆ
|
||||||
|
*
|
||||||
|
* แล้วบังคับ https เมื่อโฮสต์ปลายทางไม่ใช่ localhost แต่ได้ proto เป็น http:
|
||||||
|
* hop ภายในหลัง proxy เป็น http ปกติ (อาจไม่มี x-forwarded-proto ให้ — Next
|
||||||
|
* server ก็ฉีด `x-forwarded-proto: http` ให้เองด้วย เชื่อ "ใครเป็นคนตั้ง" ไม่ได้)
|
||||||
|
* ส่วนผู้บริโภค URL พวกนี้ (Word สำหรับ manifest — รับ https เท่านั้น ยกเว้น
|
||||||
|
* localhost, browser สำหรับ deep link ของ N10) เข้าถึงโฮสต์จริงผ่าน https
|
||||||
|
* เสมอ — ค่า http://<โฮสต์ที่ไม่ใช่ localhost> ใช้ไม่ได้จริง จึงชี้ขาดจากตัว
|
||||||
|
* โฮสต์ปลายทางแทนการเดาว่า header มาจาก proxy หรือถูกฉีด */
|
||||||
|
export function requestOrigin(req: Request): string {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const host =
|
||||||
|
req.headers.get("x-forwarded-host")?.split(",")[0]?.trim() ??
|
||||||
|
req.headers.get("host") ??
|
||||||
|
url.host;
|
||||||
|
const forwardedProto =
|
||||||
|
req.headers.get("x-forwarded-proto")?.split(",")[0]?.trim() ??
|
||||||
|
url.protocol.replace(":", "");
|
||||||
|
// host อาจติด port มาด้วย (localhost:3000) — ถอดออกก่อนเทียบ (URL จัดการ
|
||||||
|
// IPv6 ในวงเล็บ [::1] ให้ด้วย เขียน split(":") เองจะพัง)
|
||||||
|
const hostname = (() => {
|
||||||
|
try {
|
||||||
|
return new URL(`http://${host}`).hostname;
|
||||||
|
} catch {
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
const isLocal =
|
||||||
|
hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
|
||||||
|
const proto = !isLocal && forwardedProto === "http" ? "https" : forwardedProto;
|
||||||
|
return `${proto}://${host}`;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue