diff --git a/CLAUDE.md b/CLAUDE.md index ac82bd6..7f6d8db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,7 +85,7 @@ app/api/note/auth/token/route.ts ← **SSO เส้น mint login-token** (PLAN app/api/note/auth/session/route.ts ← **แลก credential ของลิงก์ e-Court เป็น access_token** (PLAN-SSO.md §3.2 ฉบับ token 2026-10-30 — แทน GET e-court ที่ set cookie ซึ่งถอดแล้ว): body {t | ticket_data+HmacSha256, fileUrl?, typeId?} → verify (JWT: verifyDocverLoginToken แยก expired/invalid / ticket: verifyAddinTicket + **ปฏิเสธค่า default "super_secret" เฉพาะ production** — ค่ารู้กันทั้ง repo = ปลอม session ได้ (dev ยอมรับ default ให้ทดสอบได้; กลุ่ม docver/add-in ใช้ default ได้ตาม requirement) → registerExternalFile trusted (fid จาก credential) → คืน {access_token (8 ชม.), userid, username, fileId?, fileError?} · audit log เฉพาะ userid (strip อักขระควบคุม) ห้าม log token ดิบ · ไม่ publish SSE app/api/note/files/[id]/bootstrap/route.ts ← ข้อมูลเริ่มต้นของ viewer (file + annotations + bookmarks + replies + user เป็น DTO) ให้ AnnotateView ดึงหลังมี token — Bearer บังคับ (401), query ชุดเดียวกับหน้า annotate สมัย server component app/api/note/files/open/route.ts ← **GET** /api/note/files/open?fileUrl=/samples/… — ทางเปิดเอกสารตัวอย่างของหน้า /note/dev แบบ "A Link Get(new)" ของ /tmpl (2026-10-30): ลิงก์เปล่ายิง GET ไม่มี Bearer (navigation ใส่ header ไม่ได้) จึง**รับเฉพาะ path ในเครื่องขึ้นต้น "/" เท่านั้น** (https เด็ดขาด + ห้าม ..) → registerExternalFile trusted:false (แถว url:… เท่านั้น — ไม่รับ fileId แถว ecourt: เกิดไม่ได้) → 302 เข้า viewer · ผิด = 302 /note?fileError=… ให้หน้าแรกแสดงสาเหตุ -app/api/note/auth/dev-link/route.ts ← **dev-only mint ลิงก์ทดสอบจากหน้า /note** (คำขอผู้ใช้ 2026-10-29 — เปิดด้วย env `NOTE_SSO_DEV_MINT=1` **และ NODE_ENV≠production** เท่านั้น ตาม `devMintEnabled` ของ lib/note-auth; ปิด = 404 + ฟอร์มหายจากหน้า): **POST** parseDevLinkBody (delegate เกณฑ์ identity/ttl ให้ parseLoginTokenBody) → jwt ใช้ DOCVER_TOKEN_SECRET / ticket ใช้ addinTicketSecret + ticket_data เรียง key ตามตัว stamp → คืน `{url}` เต็มจาก origin ของ request — secret ไม่ผ่านหน้าเบราว์เซอร์ · **GET ?file=<ไฟล์ตัวอย่าง>&mode=jwt|ticket (2026-10-30 รอบสอง)** — ลิงก์ "A Link Get(new)" ของหน้า /note/dev: mint credential (identity dev คงที่ oom/สรวิชญ์ + `fid=dev-<ไฟล์>` ให้แต่ละตัวอย่างมี row ecourt: ของตัวเอง) แล้ว 302 เข้า `/note?fileUrl=…&t=…` ให้ bootstrap แลก token ต่อ — flow เดียวกับลิงก์จริงของ e-Court ทุกขั้น · ไม่ใช่ทางเข้าจริง (แผน D7) · dev server บน LAN เปิดค้าง = คนในวง mint ได้ +app/api/note/auth/dev-link/route.ts ← **mint ลิงก์ทดสอบจากหน้า /note** (คำขอผู้ใช้ 2026-10-29 — **2026-11-02 เกณฑ์เหลือ env `NOTE_SSO_DEV_MINT=1` อย่างเดียว** ตาม `devMintEnabled` ของ lib/note-auth ใช้ได้ทุก environment รวม production ที่ตั้ง env ไว้ (เครื่องทดสอบ .74) — เดิม gate ด้วย NODE_ENV≠production ด้วย; ปิด = 404 + ฟอร์มหายจากหน้า): **POST** parseDevLinkBody (delegate เกณฑ์ identity/ttl ให้ parseLoginTokenBody) → jwt ใช้ DOCVER_TOKEN_SECRET / ticket ใช้ addinTicketSecret + ticket_data เรียง key ตามตัว stamp → คืน `{url}` เต็มจาก origin ของ request — secret ไม่ผ่านหน้าเบราว์เซอร์ · **GET ?file=<ไฟล์ตัวอย่าง>&mode=jwt|ticket (2026-10-30 รอบสอง)** — ลิงก์ "A Link Get(new)" ของหน้า /note/dev: mint credential (identity dev คงที่ oom/สรวิชญ์ + `fid=dev-<ไฟล์>` ให้แต่ละตัวอย่างมี row ecourt: ของตัวเอง) แล้ว 302 เข้า `/note?fileUrl=…&t=…` ให้ bootstrap แลก token ต่อ — flow เดียวกับลิงก์จริงของ e-Court ทุกขั้น · ไม่ใช่ทางเข้าจริง (แผน D7) · เครื่องที่ตั้ง env นี้ค้าง = ใครรู้ URL ก็ mint ได้ (ความเสี่ยงตกที่ผู้ตั้ง env) app/api/note/annotations/stream/route.ts ← SSE endpoint (GET ?fileId=) app/api/note/annotations/export/route.ts ← export PDF ฝัง annotation (pdf-lib, F8) app/api/note/external/documents/route.ts ← N10 API ภายนอกเส้นเดียว (POST + JSON body {fileId และ/หรือ typeId, filter:{type, userId, annotationTypes}} — auth ผ่าน requireExternalApiKey ก่อนแตะ DB, ถาม fileId = ค้น files.external_id = "ecourt:" ไม่เจอ 404 แม้ส่ง typeId มาด้วย / ถาม typeId = ทุกไฟล์ในกลุ่ม กลุ่มว่าง = 200 documents:[] / ส่งทั้งคู่ = AND กรอง externalId+typeId ใน query เดียว เหลือเอกสารเดียว ไม่ตรงคู่ = 404 แยกสาเหตุในข้อความ (2026-09-25 เดิมเคยเป็น OR แล้วเป็น 400); response รูปเดียว {documents:[bundle…]} ผ่าน loadDocumentBundles ทั้งสอง mode; ครอบ try/catch ทุก DB call — ล้ม = log ฝั่ง server + 500 JSON ตาม contract ไม่ปล่อยเป็น 500 ของ framework; read-only ไม่ publish SSE ไม่ revalidatePath — snapshot ณ ตอนเรียก) @@ -94,7 +94,7 @@ app/api/template/{test,extract,render}/route.ts ← กลุ่ม API docx tem app/api/documents/token/route.ts + versions/route.ts + versions/[versionId]/download-link/route.ts ← **กลุ่ม API docver จัด version เอกสาร** (docs/PLAN-DOCVER.md — URL /docedit/api/documents/…, auth ของกลุ่มเอง 2026-10-22: POST token ขอ access_token ด้วย header `x-api-key` + env `DOCVER_API_KEY` (lib/docver-token.ts — fail closed 401 ไม่ตั้ง/ว่าง เหมือน N10) แล้ว **endpoint ที่เหลือทุกตัวบังคับ `Authorization: Bearer`** JWT HS256 เซ็นด้วย env `DOCVER_TOKEN_SECRET` (TTL `DOCVER_TOKEN_TTL_SECONDS` default 3600) — identity userid/username + templateId แกะจาก claims ไม่รับจาก request อีกต่อไป; ส่ง userId/userName/templateId ใน form = 400 ไม่รู้จัก field · **2026-10-27 POST token มีทางเข้าที่สองสำหรับ add-in ใน Word** (PLAN-ADDIN-AUTH.md): ไม่มี x-api-key + body `{ticket_data, HmacSha256}` — lib/addin-ticket.ts ตรวจ HMAC+exp (secret env ADDIN_TICKET_SECRET → SECRET → default "super_secret") แล้วออก token จาก claims ของ ticket เอง · **2026-10-26 ถอด segment `[docId]` ออกจาก URL ทุก endpoint — docId = claim `fid` (string) จาก token ตรง ๆ (บังคับตั้งแต่ขอ token)**): POST versions = multipart `file`(.docx บังคับ ≤100MB เช็ค size ก่อน buffer → 413)+`pdf`(optional ไฟล์เปล่า = ไม่มี)+message/clientVersionId/fileName (**form field `fields` ยกเลิก 2026-10-30 ส่งมา = 400 — fields ของ version แกะจากไฟล์ .docx ฝั่ง server เก็บทั้งก้อนเหมือน return ของ `/api/template/extract` เลย** (`{cc, bookmark, properties, verify}` ผ่าน `extractDocumentSnapshot` ของ lib/lib-tmpl.ts ตัวเดียวกับ route นั้น · verify.HmacSha256 ตรวจด้วย secret ของ add-in ticket ฝั่ง server ผ่าน `addinTicketSecret()` · เห็น CC ทุกชนิดเสมอไม่ขึ้นกับ WordApi ของ host แบบตอน add-in อ่านเอง; แกะไม่ได้ = 400 · row เก่าก่อน 2026-10-30 เก็บ map ดิบ — `FieldsSnapshot`/`readSnapshotField` ของ add-in แยกรูปเอง) → สร้างเอกสารอัตโนมัติถ้า docId ใหม่, MinIO ก่อน DB (DB ล้มหลัง put = เก็บกวาด object แบบ N4) → `{hasPdf, versionId, versionNumber}` (เลขจาก server เป็นค่าจริง) / GET versions = DocumentVersionDTO[] ใหม่→เก่า (ไม่เคยมี docId = 404) / GET download-link?format=docx|pdf = presigned URL อายุสั้น (เช็ค docId+versionId คู่กันกันโยน id ข้ามเอกสาร; ขอ pdf ที่ version ไม่มี = 404) — carve-out invariant #4: ไม่ publish SSE ไม่ revalidatePath app/note/uploads/[name]/route.ts ← เสิร์ฟไฟล์อัปโหลดจาก public/note/uploads/ (N4 — Next production เห็นไฟล์ static เฉพาะที่มีตอน startup ไฟล์ที่เขียน runtime ต้องเสิร์ฟผ่าน route) app/note/page.tsx ← **หน้าแรก = ข้อมูลเกี่ยวกับระบบ + client bootstrap ของลิงก์ e-Court** (2026-10-30 รอบสอง ตามคำขอผู้ใช้ — server shell บาง ๆ เนื้อหาทั้งหมดอยู่ใน `components/NoteHome.tsx`): "ระบบนี้ทำอะไรได้" (การ์ด 4 ใบ: มาร์ก/วาด, ความเห็น/ทำงานร่วม, คั่นหน้า/สรุป, เอกสารจาก e-Court) + วิธีเข้าใช้ (ลิงก์ e-Court, session 8 ชม.) + ลิงก์ /note/dev · มี credential ใน URL (t/ticket) = แลก token ที่ POST /api/note/auth/session → เก็บ localStorage → ตัด credential ทิ้ง → มี fileId พาเข้า viewer · แบนเนอร์ sso/fileError · public -app/note/dev/page.tsx ← **เครื่องมือทดสอบ** (ย้ายมาจากหน้าแรก 2026-10-30): เปิดเอกสารตัวอย่าง (PLAN-SSO.md D14 2026-10-29 — dropdown 2 ระดับกลุ่ม→ไฟล์ รายการ fixed จาก public/note/samples/ ไม่ query DB + รายการลิงก์ GET แบบ /tmpl "A Link Get(new)" ชี้ `/api/note/files/open?fileUrl=/samples/…` + section Flow ของแอป — 2026-10-30 รอบสอง) + **คู่มือทางเข้า SSO 2 แบบ + flow แต่ละแบบ** (การ์ดทาง A JWT/ทาง B ticket + กล่อง dev `make-login-link` — ตัวอย่างเป็นรูปแบบ+ขั้นตอนเท่านั้น **ห้าม mint จริงจากหน้า public**) + ฟอร์ม mint ลิงก์ `DevLoginLinkForm` (เปิดด้วย env `NOTE_SSO_DEV_MINT=1` เท่านั้น) · **public เหมือนหน้าแรก** (ต้อง mint ลิงก์ตอนยังไม่มี session — ฉบับ token 2026-10-30 ไม่มี middleware ทุกหน้าเป็น shell public อยู่แล้ว) · แบนเนอร์ `?sso=`/`?fileUrl=` ไม่อยู่ที่หน้านี้ (คงอยู่ที่ `/note` ตาม contract) +app/note/dev/page.tsx ← **เครื่องมือทดสอบ** (ย้ายมาจากหน้าแรก 2026-10-30): เปิดเอกสารตัวอย่าง (PLAN-SSO.md D14 2026-10-29 — dropdown 2 ระดับกลุ่ม→ไฟล์ รายการ fixed จาก public/note/samples/ ไม่ query DB + รายการลิงก์ GET แบบ /tmpl "A Link Get(new)" ชี้ `/api/note/files/open?fileUrl=/samples/…` เมื่อ mint ปิด / dev-link เมื่อ mint เปิด + section Flow ของแอป — 2026-10-30 รอบสอง) + **ฟอร์ม `MockEcourtForm` "จำลอง e-Court — ขอ token ด้วย API key" (2026-11-02, แสดงทุก environment)** — ใส่ `DOCVER_API_KEY` + uid/uname + เลือกไฟล์ → ยิง `POST /api/note/auth/token` (ทางเข้าจริงเหมือน e-Court) → redirect เข้า `/note?fileUrl=…&t=…` ให้ bootstrap แลก session ต่อ · **คู่มือทางเข้า SSO 2 แบบ + flow แต่ละแบบ** (การ์ดทาง A JWT/ทาง B ticket + กล่อง dev `make-login-link` — ตัวอย่างเป็นรูปแบบ+ขั้นตอนเท่านั้น **ห้าม mint จริงจากหน้า public**) + ฟอร์ม mint ลิงก์ `DevLoginLinkForm` · **ทางเข้าหน้าคุมด้วย env `NOTE_SSO_DEV_MINT=1` อย่างเดียว (2026-11-02 — ใช้ได้ทุก environment รวม production ที่ตั้ง env ไว้) · 0/ไม่ตั้ง = `notFound()` 404 ทั้งหน้า ไม่ใช่หน้า public อีกต่อไป** · แบนเนอร์ `?sso=`/`?fileUrl=` ไม่อยู่ที่หน้านี้ (คงอยู่ที่ `/note` ตาม contract) app/note/groups/[typeId]/page.tsx ← N9 หน้าเอกสารในกลุ่ม (server component force-dynamic — files where type_id orderBy createdAt, ไม่มี = 404; ~~หมายเหตุ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้"~~ ถอดออกตามคำขอผู้ใช้ 2026-09-24; **redesign 2026-09-24 ตามคำขอผู้ใช้: ไม่มี navbar** — explorer 2 คอลัมน์ ตัวกรองซ้าย + list เดียวรวม bookmark+annotation ทั้งกลุ่มไม่แยกไฟล์: query ทั้งหมดฝั่ง server แล้วส่ง `GroupFileSummary[]` + `GroupListItem[]` (เรียง ไฟล์ตาม createdAt → หน้า → bookmark ก่อน annotation → createdAt) ให้ `GroupExplorer` กรองใน memory, แถวรายการ deep link `?ann=`/`?page=` เหมือนหน้าสรุป F12 + ไฟล์ที่มาต้น meta line) app/note/annotate/[id]/page.tsx ← shell บาง ๆ (ตรวจ UUID → `components/pdf/AnnotateView`) — ฉบับ token 2026-10-30: client gate ตรวจ token ใน localStorage แล้วดึง initial data ผ่าน GET /api/note/files/[id]/bootstrap (Bearer) → PDFViewer (prop token); ไม่มี token/401 = หน้า "กลับ e-Court กดลิงก์ใหม่" 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` ของ
(กฎ coarse 44px ใน globals.css ผูกอยู่กับ selector นี้ ครอบทั้ง `a` และ `button`) diff --git a/app/api/note/auth/dev-link/route.ts b/app/api/note/auth/dev-link/route.ts index e8fa16a..fa5a84b 100644 --- a/app/api/note/auth/dev-link/route.ts +++ b/app/api/note/auth/dev-link/route.ts @@ -1,6 +1,7 @@ -// dev-only (NOTE_SSO_DEV_MINT=1 + ไม่ใช่ production) — mint ลิงก์ SSO จากหน้า /note/dev -// ให้ทดสอบ flow จริงจาก browser ได้โดยไม่ต้องรัน script: คำนวณ token/ticket ฝั่ง -// server ด้วย secret ของตัวเอง (ไม่มีค่าใดหลุดไปหน้า public) แล้วคืน URL เต็มเดียว +// เปิดด้วย env NOTE_SSO_DEV_MINT=1 อย่างเดียว (2026-11-02 — ใช้ได้ทุก environment +// รวม production ที่ตั้ง env ไว้, เดิม gate ด้วย NODE_ENV ด้วย) — mint ลิงก์ SSO +// จากหน้า /note/dev ให้ทดสอบ flow จริงจาก browser ได้โดยไม่ต้องรัน script: +// คำนวณ token/ticket ฝั่ง server ด้วย secret ของตัวเอง (ไม่มีค่าใดหลุดไปหน้า public) แล้วคืน URL เต็มเดียว // พร้อมคลิก — ปิดอยู่ = 404 เหมือนไม่มีเส้นนี้ · ไม่ import @/db · ห้าม log ค่าดิบ // · GET ?file=<ชื่อไฟล์ตัวอย่าง>&mode=jwt|ticket (2026-10-30 รอบสอง) — ลิงก์ "A Link // Get(new)" ของ /note/dev: mint แล้ว 302 เข้า /note?fileUrl=…&t=… ให้ bootstrap diff --git a/app/note/dev/page.tsx b/app/note/dev/page.tsx index 5b5a637..e8dbcd9 100644 --- a/app/note/dev/page.tsx +++ b/app/note/dev/page.tsx @@ -1,7 +1,9 @@ import Link from "next/link"; +import { notFound } from "next/navigation"; import AppNav from "@/components/AppNav"; import DevLoginLinkForm from "@/components/DevLoginLinkForm"; +import MockEcourtForm from "@/components/MockEcourtForm"; import { devMintEnabled } from "@/lib/note-auth"; export const dynamic = "force-dynamic"; @@ -9,11 +11,10 @@ export const dynamic = "force-dynamic"; /** * /note/dev — เครื่องมือทดสอบ (ย้ายมาจากหน้าแรก 2026-10-30 ตอน /note เปลี่ยนเป็น * landing เรียบ): เปิดเอกสารตัวอย่าง (D14) + คู่มือทางเข้า SSO 2 แบบ + ตัวช่วย mint - * ลิงก์ทดสอบ · **public** (ต้อง mint ลิงก์ได้ตอนยังไม่มี session ไม่งั้นตายก่อนเริ่ม - * — ฉบับ token 2026-10-30: ไม่มี middleware แล้ว ทุกหน้าเป็น shell public ส่วน - * session ตรวจฝั่ง client ผ่าน localStorage) · ตัวอย่างลิงก์เป็น "รูปแบบ + ขั้นตอน" - * เท่านั้น ไม่ mint จริงจากหน้า (secret ต้องอยู่ฝั่ง server) · ไม่ query DB - * · แบนเนอร์ ?sso= และ bootstrap ของ N8 ?fileUrl= อยู่ที่หน้าแรก /note + * ลิงก์ทดสอบ · **เปิดด้วย env NOTE_SSO_DEV_MINT=1 อย่างเดียว — 0/ไม่ตั้ง = 404 + * ทั้งหน้า** (คำขอผู้ใช้ 2026-11-02, ใช้ได้ทุก environment รวม production ที่ตั้ง env + * ไว้) · ตัวอย่างลิงก์เป็น "รูปแบบ + ขั้นตอน" เท่านั้น ไม่มี secret ในหน้า + * · ไม่ query DB · แบนเนอร์ ?sso= และ bootstrap ของ N8 ?fileUrl= อยู่ที่หน้าแรก /note */ /** รายการเอกสาร fixed ในโค้ด (D14) — จาก PDF ตัวอย่างที่มีอยู่จริงของแอป @@ -52,6 +53,9 @@ export default async function DevToolsPage({ }: { searchParams: Record; }) { + // ทางเข้าหน้านี้คุมด้วย env ตัวเดียวกับ dev-link (2026-11-02) — ปิด = 404 + // เหมือนไม่มีหน้านี้ ไม่เหลือแค่ข้อความแนะนำ + if (!devMintEnabled()) notFound(); // โหมด credential ของลิงก์ตัวอย่าง — jwt (default) | ticket สลับผ่าน ?sampleMode= const sampleMode = searchParams.sampleMode === "ticket" ? "ticket" : "jwt"; const mintOn = devMintEnabled(); @@ -98,9 +102,10 @@ export default async function DevToolsPage({ ) : (

- ตอนนี้ลิงก์ด้านล่างเปิดเอกสารแบบธรรมดา (ไม่ได้จำลองการเข้าระบบ) — - เพิ่ม NOTE_SSO_DEV_MINT=1 ในไฟล์ .env แล้วรีสตาร์ท - dev server จะกดแล้วผ่านขั้นตอนเข้าระบบเหมือนของจริงทุกอย่าง + ลิงก์ด้านล่างเปิดเอกสารด้วย session ที่มีอยู่เท่านั้น — ยังไม่มี session + ใช้ฟอร์ม "จำลอง e-Court" ด้านล่างแทน · dev server ที่ต้องการ mint + ให้อัตโนมัติ (ไม่ต้องใส่ key) เพิ่ม NOTE_SSO_DEV_MINT=1{" "} + แล้วรีสตาร์ท

)}
@@ -127,6 +132,23 @@ export default async function DevToolsPage({
))} + + {/* จำลอง e-Court ด้วย API key — แสดงทุก environment (คำขอผู้ใช้ 2026-11-02: + ใช้ทางเข้าจริงเหมือน e-Court ต้องถือ DOCVER_API_KEY จึงไม่เป็นประตูหลัง) */} +
+

+ จำลอง e-Court — ขอ token ด้วย API key +

+

+ ใส่ API key (DOCVER_API_KEY) เพื่อขอ token ผ่าน{" "} + POST /api/note/auth/token แบบเดียวกับที่ e-Court ทำจริง + แล้วระบบจะพาเข้าอ่านเอกสารที่เลือกเลย — ได้ session 8 ชม. + (ใช้ได้ทั้ง dev และ production) +

+
+ +
+
{/* ── Flow ─────────────────────────────────────────────────────────── */} diff --git a/components/MockEcourtForm.tsx b/components/MockEcourtForm.tsx new file mode 100644 index 0000000..b8f5983 --- /dev/null +++ b/components/MockEcourtForm.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useState, type FormEvent } from "react"; + +import { withBasePath } from "@/utils/base-path"; + +/** + * ฟอร์ม "จำลอง e-Court" บนหน้า /note/dev สำหรับตอน mint อัตโนมัติปิด + * (production เสมอตาม devMintEnabled) — ผู้ทดสอบใส่ DOCVER_API_KEY แล้วยิง + * POST /api/note/auth/token (เส้นเดียวกับที่ backend e-Court ใช้จริง) ได้ + * access_token มาแล้วพาเข้า /note?fileUrl=…&t=… ให้ NoteHome แลกเป็น session + * ต่อ — ไม่เป็นประตูหลังเพราะต้องถือ key ที่ถูกต้องอยู่แล้ว + * (คำขอผู้ใช้ 2026-11-02: กดเข้าเอกสารจากหน้า /note/dev บน production ให้ได้) + */ +export default function MockEcourtForm({ files }: { files: string[] }) { + const [apiKey, setApiKey] = useState(""); + const [userid, setUserid] = useState("oom"); + const [username, setUsername] = useState("สรวิชญ์"); + const [file, setFile] = useState(files[0] ?? ""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + setBusy(true); + setError(null); + try { + const res = await fetch(withBasePath("/api/note/auth/token"), { + method: "POST", + headers: { "x-api-key": apiKey, "content-type": "application/json" }, + // fid ผูกไฟล์ที่เลือกแบบเดียวกับ dev-link (dev-) — แต่ละไฟล์ + // ตัวอย่างมี row ecourt: ของตัวเอง ไม่ปนกัน + body: JSON.stringify({ + userid: userid.trim(), + username: username.trim(), + fid: `dev-${file}`, + ttl_seconds: 600, + }), + }); + const data = (await res.json().catch(() => null)) as + | { access_token?: string; error?: string } + | null; + if (!res.ok || data?.access_token === undefined) { + throw new Error(data?.error ?? `ขอ token ไม่สำเร็จ (HTTP ${res.status})`); + } + // withBasePath("") = /docedit/note — ห้ามส่ง "/note" เข้า withBasePath (D11) + const params = new URLSearchParams({ fileUrl: `/samples/${file}`, t: data.access_token }); + window.location.href = `${withBasePath("")}?${params.toString()}`; + } catch (err) { + setError(err instanceof Error ? err.message : "ขอ token ไม่สำเร็จ"); + setBusy(false); + } + }; + + const inputCls = + "genesis-input h-9 px-3 py-1.5 disabled:bg-canvas disabled:text-ink-faint"; + const labelCls = "text-xs font-medium text-ink-soft"; + + return ( +
+
+ + setApiKey(e.target.value)} + className={inputCls} + autoComplete="off" + required + /> +
+
+ + setUserid(e.target.value)} className={inputCls} required /> +
+
+ + setUsername(e.target.value)} className={inputCls} required /> +
+
+ + +
+ +
+ +
+ + {error && ( +

+ {error} +

+ )} +
+ ); +} diff --git a/lib/note-auth.ts b/lib/note-auth.ts index 8dc6ce0..466d872 100644 --- a/lib/note-auth.ts +++ b/lib/note-auth.ts @@ -239,17 +239,16 @@ export function parseLoginTokenBody(raw: unknown): LoginTokenBody { // --------------------------------------------------------------------------- // dev-link — mint ลิงก์ SSO จากหน้า /note/dev (คำขอผู้ใช้ 2026-10-29 "ใช้ได้ใน note" -// — ย้ายตามหน้า 2026-10-30) เปิดด้วย env NOTE_SSO_DEV_MINT=1 เท่านั้น และตายเสมอ -// ใน production build — ไม่ใช่ทางเข้าจริง (แผน D7) แค่ทางทดสอบของ dev server +// — ย้ายตามหน้า 2026-10-30) · 2026-11-02 คำขอผู้ใช้: เปิดด้วย env +// NOTE_SSO_DEV_MINT=1 อย่างเดียว — ใช้ได้ทุก environment (รวม production ที่ตั้ง +// env ไว้ เช่น เครื่องทดสอบ) 0/ไม่ตั้ง = หน้า /note/dev และ route dev-link เป็น 404 +// — ความเสี่ยง mint จากหน้า public ตกอยู่ที่ผู้ตั้ง env บนเครื่องนั้น ไม่ใช่ทางเข้าจริง (D7) // --------------------------------------------------------------------------- -/** เปิดตัวช่วย mint ลิงก์ทดสอบหรือไม่ — env NOTE_SSO_DEV_MINT === "1" และไม่ใช่ - * production (dev server บน LAN จึงคุมด้วยการตั้ง/ถอด env เอง) */ +/** เปิดหน้า /note/dev + ตัวช่วย mint ลิงก์ทดสอบหรือไม่ — env NOTE_SSO_DEV_MINT === "1" + * ตัวเดียว (ไม่ดู NODE_ENV — 2026-11-02) แต่ละเครื่องคุมด้วยการตั้ง/ถอด env เอง */ export function devMintEnabled(): boolean { - return ( - process.env.NOTE_SSO_DEV_MINT?.trim() === "1" && - process.env.NODE_ENV !== "production" - ); + return process.env.NOTE_SSO_DEV_MINT?.trim() === "1"; } const DEV_LINK_BODY_FIELDS = ["userid", "username", "fid", "ttl_seconds", "fileUrl", "typeId", "mode"] as const; diff --git a/tests/lib/note-auth.test.ts b/tests/lib/note-auth.test.ts index fd13f45..59e8a31 100644 --- a/tests/lib/note-auth.test.ts +++ b/tests/lib/note-auth.test.ts @@ -184,14 +184,15 @@ describe("parseDevLinkBody / devMintEnabled (dev-link)", () => { expect(parseDevLinkBody({ userid: "u", username: "n", fid: "F", oops: 1 }).ok).toBe(false); }); - it("devMintEnabled: ต้องเป็น 1 เป๊ะและไม่ใช่ production — ค่าอื่น/production = false", () => { + it("devMintEnabled: ต้องเป็น 1 เป๊ะ — ค่าอื่น = false · ใช้ได้ทุก NODE_ENV (2026-11-02)", () => { const orig = process.env.NODE_ENV; const writable = process.env as Record; process.env.NOTE_SSO_DEV_MINT = "1"; writable.NODE_ENV = "development"; expect(devMintEnabled()).toBe(true); + // production ที่ตั้ง env ไว้ก็เปิด (เครื่องทดสอบ) — gate เหลือ env ตัวเดียว writable.NODE_ENV = "production"; - expect(devMintEnabled()).toBe(false); + expect(devMintEnabled()).toBe(true); writable.NODE_ENV = orig ?? "test"; for (const value of [undefined, "", "0", "true", " yes "]) { if (value === undefined) delete process.env.NOTE_SSO_DEV_MINT;