eCourt-note/docs/PLAN-API.md

13 KiB
Raw Blame History

แผน N10 — API ดึงข้อมูลเอกสาร/กลุ่มให้ระบบภายนอก (API Key)

สถานะ: ร่างแผน (ยังไม่ implement) — เสนอ 2026-09-22 ตามคำขอผู้ใช้: "เพิ่ม API ดึงข้อมูลเอกสารทีละรายการ หรือทุกเอกสารที่อยู่ในกลุ่มเดียวกัน เอาทั้งหมดหรือเฉพาะ bookmark ใช้ API Key ในการ Authentication" Dependency: N8 (docs/PLAN-ECOURT.md — external_id สำหรับค้นด้วย fileId ของ e-Court) + N9 (docs/PLAN-DOCSET.md — files.type_id สำหรับค้นตามกลุ่ม) ตาม governance ของโปรเจกต์: implement ต้องเขียนสเปกลง docs/PLAN-FEAT.md §7 (N10) + ขยาย whitelist ใน CLAUDE.md ก่อน — เป็น task แรกของแผน แล้วรันผ่าน /implement-feat N10

Context

  • ผู้เรียกคือ backend ของ e-Court (server-to-server — ไม่มี browser ไม่มีหน้า UI) ต้องการดึงข้อมูลที่เกิดในระบบเรากลับไปใช้ต่อ: annotation/bookmark ของเอกสารหนึ่งฉบับ หรือของทุกเอกสารในกลุ่ม (typeId) เดียวกัน
  • ระบบเราเป็นฝ่ายถูกเรียกเท่านั้นในแผนนี้ (read-only) — ทิศทาง "เราเรียก e-Court" ไม่มี
  • middleware.ts ยกเว้น /api/* อยู่แล้ว (ไม่บังคับ login แบบ oacCase) — จึงตรวจ API Key ในตัว route เอง ไม่ชนกับระบบ login เดิม

การตัดสินใจ (ยืนยันกับผู้ใช้แล้ว 2026-09-22)

  1. API Key เดียวใน env EXTERNAL_API_KEY — ผู้เรียกคือ backend e-Court ตัวเดียว เปลี่ยน key = แก้ env แล้ว redeploy (ไม่ทำตาราง key ใน DB / UI จัดการ key)
  2. ระบุเอกสารด้วย fileId ของ e-Court (ค้น external_id = "ecourt:<fileId>") — ผู้เรียกรู้จัก id ของตัวเอง ไม่ต้องรู้ UUID ของเรา
  3. 2 endpoints ตาม pattern query-param ของบ้าน (เหมือน /api/annotations/export?fileId=) ไม่ใช้ path parameter
  4. include เลือกส่วนที่เอา: annotations, bookmarks คั่น comma — default = ทั้งสอง ("เอาทั้งหมด"), include=bookmarks = เฉพาะ bookmark — ส่วนที่ไม่เลือกคืนเป็น array ว่าง (contract คงที่ ง่ายต่อผู้บริโภค)

Design

D1 — Auth (lib/external-api-auth.ts ใหม่)

  • requireExternalApiKey(req: Request): Response | null — คืน null เมื่อผ่าน / คืน Response 401 เมื่อไม่ผ่าน (body JSON { "error": "<ข้อความไทย>" } + content-type: application/json; charset=utf-8)
  • อ่าน header x-api-key → เทียบกับ env EXTERNAL_API_KEY ด้วย SHA-256 ทั้งสองค่าแล้ว crypto.timingSafeEqual (ทั้งสองยาวเท่ากันหลัง hash — กัน timing + length leak; ค่า env ตั้งให้ยาวและสุ่ม เช่น 32+ ตัวอักษร)
  • env ไม่ตั้งหรือว่าง = ปฏิเสธทุก request (fail closed — 401 "API ยังไม่ได้ตั้งค่าคีย์") ไม่มี mode เปิดฟรี
  • ห้าม log ค่า key / header ดิบ (error log บอกแค่ "auth failed")

D2 — Endpoints (2 routes ใหม่)

URL เต็มฝั่งผู้เรียกอยู่ใต้ basePath: https://<host>/docedit/note/api/external/...

1) เอกสารเดียว — GET /api/external/document?fileId=<fileId ของ e-Court>&include=annotations,bookmarks

{
  "document": {                          // null = 404 (ไม่เคยเปิดในระบบ)
    "id": "<uuid ของเรา>",               // ใช้อ้างต่อใน DTO ข้างล่าง
    "externalFileId": "<fileId ของ e-Court>",
    "title": "…",
    "typeId": "…",                       // null ได้ (ไม่มีกลุ่ม)
    "createdAt": "<ISO string>"
  },
  "annotations": [ AnnotationDTO ตาม db/schema.ts พร้อม "replies": ReplyDTO[] ต่อรายการ ],
  "bookmarks":   [ BookmarkDTO ตาม db/schema.ts ]
}

2) ทุกเอกสารในกลุ่ม — GET /api/external/documents?typeId=<typeId>&include=…

{
  "typeId": "…",
  "documents": [ { "document": {…}, "annotations": […], "bookmarks": […] } ]  // รูปร่างเดียวกับ endpoint 1
}
  • ไม่มีเอกสารในกลุ่ม = 200 + documents: [] (ต่างจากหน้า UI /groups/[typeId] ที่ 404 — API คืน empty เป็นผลลัพธ์สมเหตุสมผลสำหรับ integration)
  • ลำดับ: documents ตาม files.createdAt (เหมือนหน้า /groups/[typeId]) · annotations ตาม (pageIndex, createdAt) เหมือนหน้าสรุป F15 · bookmarks ตาม N5 (pageIndex → yOffset → createdAt)
  • timestamp เป็น ISO string ทั้งหมด (ผ่าน mapper toDTO ที่มีอยู่ — invariant #5), data คือ JSONB ตาม discriminated union เดิม
  • error ทุกอันเป็น JSON { "error": "<ไทย>" }: 400 (param ขาด/include ค่าไม่รู้จัก), 401 (key), 404 (เอกสารเดียว: fileId ไม่เคยลงทะเบียน)

D3 — การ query / สิทธิ์ / ความปลอดภัย

  • read-only ทั้งหมด: ไม่เขียน DB ไม่ publish SSE ไม่ revalidatePath — snapshot ณ ตอนเรียก (คนละ context กับ Server Action ใน invariant #4)
  • ไม่ใส่ CORS headers (server-to-server — browser ไม่ควรเรียก; ไม่เปิด Access-Control-Allow-Origin)
  • ค้น fileId: where eq(files.externalId, "ecourt:" + fileId) — ไฟล์ที่ลงทะเบียนแบบ fallback "url:<href>" (ลิงก์ไม่มี fileId) ค้นด้วยวิธีนี้ไม่ได้ = 404 (ยอมรับ — สัญญาลิงก์ N8 แนะนำให้ส่ง fileId เสมอ)
  • REDACTION รวมใน annotations เมื่อเลือก include — ผู้ถือ key เห็นตำแหน่งกล่องปิดพราง (DB เก็บแค่พิกัด ไม่มีเนื้อหาใต้กล่อง) — ระบุไว้ในสเปกว่า key นี้เชื่อถือได้ระดับ backend e-Court เท่านั้น
  • annotation ทุกสถานะรวมหมด (resolved ดูจาก field resolved ได้) ทุกผู้ใช้ (userId/userName อยู่ใน DTO)
  • route ทั้งสองเรียก requireExternalApiKey เป็นอย่างแรกก่อนแตะ DB

Docs/rules sync (plan-first — ทำเป็น task แรก ก่อนแตะโค้ด)

ไฟล์ สิ่งที่แก้
docs/PLAN-FEAT.md §7 หัวข้อใหม่ N10 · API ดึงข้อมูลเอกสาร/กลุ่ม (API Key) (format ตาม N5–N7 + อ้างแผนฉบับเต็ม docs/PLAN-API.md)
CLAUDE.md whitelist เพิ่ม N10 + โครงสร้างไฟล์เพิ่ม lib/external-api-auth.ts, app/api/external/document/route.ts, app/api/external/documents/route.ts + บันทึก env EXTERNAL_API_KEY (README .env.example ถ้ามี)
.claude/rules/data-layer.md เพิ่ม: routes ของ N10 เป็น read-only ไม่ publish SSE/revalidatePath; env EXTERNAL_API_KEY ว่าง = fail closed
README.md สารบัญ env เพิ่ม EXTERNAL_API_KEY + ตัวอย่างการเรียก (curl)

ลำดับ task + verify (ทุก task: npx tsc --noEmit ผ่าน)

# งาน ไฟล์หลัก Verify เพิ่มเติม
1 Docs sync N10 ตารางด้านบน สเปก §7 ครบตาม format; README มี env + ตัวอย่าง
2 Auth helper lib/external-api-auth.ts unit test (vitest): ไม่ใส่ header / key ผิด / env ว่าง = 401; key ถูก = ผ่าน — ไม่มีข้อความบอกว่าขาดอะไร
3 Route เอกสารเดียว app/api/external/document/route.ts curl: 200 รูปร่างตาม D2 (replies ใน annotation, ISO timestamps), include=bookmarks → annotations: [], fileId ไม่รู้จัก = 404, include=foo = 400
4 Route กลุ่ม app/api/external/documents/route.ts curl: หลายไฟล์เรียง createdAt, typeId ไม่มี = documents: [], รูปร่าง document เหมือน endpoint 1 เป๊ะ
5 ปิดงาน — plan-reviewer + code-reviewer + vitest + tsc; ทดสอบร่วมกับ N8/N9 ด้วยข้อมูลจริงจากลิงก์ e-Court

Edge cases (ออกแบบไว้ล่วงหน้า)

  1. env EXTERNAL_API_KEY ไม่ตั้ง/ว่าง → 401 ทุก request (fail closed) — deploy ต้องตั้งก่อนเปิดใช้
  2. key ผิด → 401 ข้อความเดียวกับไม่ใส่ key (ไม่บอกว่าขาดอะไร — กัน enumerate)
  3. fileId ที่ e-Court ไม่เคยลิงก์มา → 404 "ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยเปิด)" — ไม่ใช่ 500
  4. ไฟล์ลงทะเบียนแบบ "url:<href>" (ลิงก์ไม่มี fileId) → ค้นไม่ได้ = 404 — เอกสารนี้ต้องเข้าผ่านลิงก์ที่มี fileId ก่อน
  5. include ซ้ำค่า / มี whitespace / ค่าไม่รู้จัก → trim + dedupe; มีค่านอก {annotations, bookmarks} สักตัว = 400 บอกค่าที่รับ
  6. ไม่มี param fileId/typeId เลย → 400 ข้อความไทยระบุ param ที่ขาด
  7. typeId ตัวเปล่า / include ตัวเปล่า → 400 (ไม่ปล่อยผ่านเป็น "ทั้งหมด")
  8. เอกสารไม่มี annotation/bookmark เลย → arrays ว่าง ไม่ใช่ 404 (ไฟล์เคยเปิด = ข้อมูลเอกสารมีค่าเสมอ)
  9. ไฟล์ไม่มีกลุ่ม → document.typeId: null (ไม่ใช่ string ว่าง)
  10. คนละรอบเรียกพร้อมกัน → read-only ไม่มีสถานะแชร์ — ไม่มี race (pool prepare: false ตาม invariant #11)

Out of scope (ไม่ทำในแผนนี้)

  • หลาย key / ตาราง key ใน DB / เพิกถอนราย key / UI จัดการ key
  • rate limiting / pagination / caching ของ response
  • write API (สร้าง/แก้ annotation ผ่าน API) — ระบบยังเขียนผ่าน Server Actions + login เท่านั้น
  • ดึงไฟล์ PDF/bytes เอกสารผ่าน API นี้ (/api/pdf มีอยู่แล้วแต่ไม่มี key — ความเสี่ยงที่รับไว้ตามข้อ 3 ของ plan-reviewer ใน PLAN-ECOURT.md)
  • webhook/event push แจ้งเตือนเมื่อมี annotation ใหม่, API รูปแบบอื่น (XML/CSV)
  • auth จริงเทียบระบบ e-Court (ยัง out of scope ของโปรเจกต์)