feat(note): mock e-Court login form on dev page
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 1m16s
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

เพิ่มฟอร์ม "จำลอง e-Court — ขอ token ด้วย API key" บนหน้า
/note/dev (components/MockEcourtForm): ใส่ DOCVER_API_KEY +
userid/username + เลือกไฟล์ตัวอย่าง → ยิง POST /api/note/auth/token
(ทางเข้าเดียวกับที่ e-Court ใช้จริง) แล้ว redirect เข้า
/note?fileUrl=…&t=… ให้ NoteHome แลกเป็น session 8 ชม. ต่อ —
ใช้ได้ทั้ง dev และ production และไม่เป็นประตูหลัง (ต้องถือ
API key ที่ถูกต้อง)

พร้อมกันนี้เปลี่ยนเกณฑ์เปิดหน้า dev เป็น env ตัวเดียว
(คำขอผู้ใช้ 2026-11-02):

- devMintEnabled: ตัดเงื่อนไข NODE_ENV≠production ออก เหลือ
  NOTE_SSO_DEV_MINT === "1" อย่างเดียว — ตั้งไว้บน production
  (เครื่องทดสอบ) ก็ใช้ได้
- หน้า /note/dev: 0/ไม่ตั้ง = 404 ทั้งหน้า (notFound) — เดิมเป็น
  public ตลอดเหลือแต่ข้อความแนะนำ
- dev-link (GET/POST) เปิด/ปิดตามเกณฑ์เดียวกันโดยอัตโนมัติ
- sync CLAUDE.md + ปรับ unit test ให้ตรงเกณฑ์ใหม่

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-11-02 12:19:38 +07:00
parent 83a2cc3e5e
commit f7e84e3ee7
6 changed files with 160 additions and 23 deletions

View file

@ -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:<fileId>" ไม่เจอ 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` ของ <main> (กฎ coarse 44px ใน globals.css ผูกอยู่กับ selector นี้ ครอบทั้ง `a` และ `button`)