eCourt-note/lib/openapi.ts
Your Name 5121970f31
Some checks failed
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) Failing after 30s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Has been skipped
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Failing after 1s
refactor(auth): consolidate auth under /api/auth/* endpoints
- ยุบ route auth ที่เคย copy กันจน drift (flow N11 หายจาก /api/auth/session)
  เหลือโค้ด inline สองเส้นเดียว: POST /api/auth/token (ทางเข้า 3: x-api-key
  docver / x-api-key purpose:"login" mint ลิงก์ note / ticket ของ add-in)
  + POST /api/auth/session (แลก credential + register N8 + N11 fid-only)
- POST /api/documents/token คงไว้เป็น alias re-export (กลุ่ม docver/e-Court
  เดิม — contract เดิมเป๊ะ, มี test toBe กัน copy แยกชุดกลับมา)
- ถอดเส้นฝั่ง note ทั้งสอบ: /api/note/auth/token + /api/note/auth/session
  (e-Court ต้องย้ายมาทางเข้ารวม — แจ้งทีม e-Court ก่อน deploy)
- utils/auth-store.ts: primitives กลางฝั่ง client (jwtExp/isExpired/
  localStorage store + memory fallback) — lib/note-session ใช้ร่วม
  (expMs decode จาก JWT payload = นาฬิกา server) และวางไว้ให้ wordaddin
  ย้ายมาใช้รอบหน้าได้ (รอบนี้ไม่แต้ wordaddin ตามข้อตกลง)
- นโยบาย token หมดอายุกลาง session: PDFViewer reportActionError จับ
  auth-error ของ Server Action ทุกจุด → ล้าง session แล้วพาไป
  /note?sso=expired (จบที่ gate เดียวกับ bootstrap 401)
- openapi: ทางเข้า purpose:"login" + schema LoginTokenRequest + ลบ path
  เส้นที่ถอด + test กันเส้นตายหลุดค้างใน spec
- เอกสาร sync: PLAN-SSO §0.6, CLAUDE.md, PLAN-STAGED, .env.example, /note/dev
- tests: ย้าย/เพิ่มชุด auth (50 เคสกลุ่ม auth ผ่าน, ทั้งชุด 444 ผ่าน) +
  e2e ครบทั้ง API (mint/alias/ticket/404 เส้นเก่า/N11/D4) และ browser

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-09 14:30:29 +07:00

1798 lines
98 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// OpenAPI 3.0 spec ของ API ทดสอบทั้งหมดของ judes-note — เส้นเดียวกับที่ Swagger UI
// แสดงผล: กลุ่ม template (DOCX Content Control) + N10 external API + docver
// (จัด version เอกสาร) + auth (ทางเข้ารวม /api/auth/* — 2026-11-09 รอบสอง:
// /api/auth/token รวมทางเข้า mint login-token ด้วย purpose:"login" ·
// /api/note/auth/token กับ /api/note/auth/session ถอดแล้ว · /api/documents/token
// ของกลุ่ม docver คงไว้เป็น alias ของ /api/auth/token)
// route handler GET /api/swagger (app/api/swagger/route.ts) เป็นคนเดียวที่ใช้
// — inline เป็น JSON ลงใน HTML ตอน respond (ไม่มี endpoint spec แยก)
//
// URL ใน paths เป็น path เต็มรวม /api/… ตาม invariant #12 (URL จริง
// /docedit/api/… — servers เติมแค่ basePath ไม่มี /note ซ้อน)
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const ANNOTATION_TYPE_ENUM = [
"HIGHLIGHT",
"DRAW",
"COMMENT",
"SHAPE",
"REDACTION",
"TEXTBOX",
"STAMP",
] as const;
export const openApiSpec = {
openapi: "3.0.3",
info: {
title: "Document Template & External API ของระบบ Judge's Note",
version: "1.0.0",
description:
"API ทดสอบของระบบ Judge's Note — กลุ่ม Template (DOCX Content Control), " +
"API ภายนอกสำหรับ backend e-Court (N10), API จัด version เอกสาร (docver) " +
"และทางเข้า auth แบบ path สั้น (/api/auth/*)",
},
servers: [{ url: BASE_PATH, description: "origin เดียวกับหน้านี้ (basePath /docedit)" }],
tags: [
{
name: "template",
description:
"Next Generation of Template : เป็นเทมเพลต MS Word(docx) รองรับรูปแบบข้อมูลที่ซับซ้อนได้ ",
},
{
name: "documents",
description:
"จัด version เอกสาร Word (.docx + .pdf) — ใช้งานจริงโดย Word add-in " +
'"e-Court Documents Versioning" และระบบภายนอกที่ต้องการดึงไฟล์\n\n' +
"**แนวคิด**\n" +
"- เอกสารหนึ่งฉบับถูกระบุด้วย `docId` ตัวเดียว ซึ่งก็คือ `fid` (fileId ของ e-Court)\n" +
"- ผู้ใช้กดบันทึกใน Word แต่ละครั้ง = เพิ่ม 1 version ที่เก็บไฟล์ .docx " +
"(+ สำเนา .pdf ถ้ามี) พร้อมชื่อผู้อัปโหลดและเวลา — ไฟล์เก่าของ version ก่อนหน้าไม่ถูกแก้\n\n" +
"**วิธีเรียกใช้ 3 ขั้น**\n" +
"1. ขอ `access_token` ที่ `POST /api/documents/token`\n" +
"2. แนบ `Authorization: Bearer <access_token>` กับทุก request ถัดไปของกลุ่มนี้\n" +
"3. เรียก endpoint ที่ต้องการ: อัปโหลด version ใหม่ / ดูประวัติ version / ขอลิงก์ดาวน์โหลด\n\n" +
"**ข้อควรรู้**\n" +
"- identity (userId/userName), templateId และ docId อ่านจาก claims ใน token ทั้งหมด — " +
"ไม่รับจาก URL หรือ form (ส่งมา = 400)\n" +
"- อัปโหลด version แรกของ docId ที่ยังไม่เคยมี = ระบบสร้างเอกสารให้อัตโนมัติ",
},
{
name: "Judge's Note",
description:
"API ภายนอกของแอป Judge's Note — N10: backend e-Court เรียกแบบ " +
"server-to-server ดึง annotations/bookmarks ของเอกสารที่เคยเปิดในระบบนี้ · " +
"SSO: เส้น mint login-token สำหรับประกอบลิงก์เข้าสู่ระบบ (PLAN-SSO.md)",
},
{
name: "auth",
description:
"**ทางเข้า auth รวม** `/api/auth/*` (2026-11-09 รอบสอง — เส้นของฝั่ง note " +
"กลุ่ม `note/auth/*` ทั้งหมดถอดแล้ว ผู้เรียกเก่าต้องย้ายมาที่นี่):\n" +
"- `POST /api/auth/token` — ออก access_token: x-api-key + body เดิม = " +
"docver-token 7 วัน · x-api-key + `purpose:\"login\"` = login-token อายุสั้น " +
"(mint ลิงก์ `/note?t=`) · ticket ของ add-in\n" +
"- `POST /api/auth/session` — แลก credential ของลิงก์ e-Court เป็น session token " +
"ของแอป note\n" +
"- `POST /api/documents/token` — alias ของกลุ่ม docver (e-Court เดิม คงไว้ — " +
"function เดียวกับ `/api/auth/token`)",
},
],
paths: {
"/api/template/test": {
get: {
tags: ["template"],
summary: "ตรวจสอบว่า service มีชีวิต",
responses: {
200: {
description: "service พร้อมใช้งาน",
content: {
"application/json": {
schema: {
type: "object",
properties: {
ok: { type: "boolean" },
service: { type: "string" },
},
},
},
},
},
},
},
},
"/api/template/render": {
post: {
tags: ["template"],
summary: "สร้างเอกสารจาก template (.docx) + data (JSON) ใช้สำหรับนักพัฒนาเท่านั้น",
description:
"แทนค่า JSON ลงใน Content Control ของ template แล้วคืนไฟล์ .docx ใหม่ " +
'(filename ตอน download = generated-document.docx) — ส่ง data ได้ 2 ทาง: ' +
'field "data" เป็นไฟล์ JSON หรือ field "dataJson" เป็น string ตรง ๆ (เลือกอย่างใดอย่างหนึ่ง)<br/>'+
'<i>สำหรับนักพัฒนา Template เท่านั้น ไม่ต้อง ใช้ API Key ต้องตั้งค่า secret และ private key เองไม่ใช้ของระบบของระบบ</i>',
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: {
type: "object",
required: ["template"],
properties: {
template: {
type: "string",
format: "binary",
description: "ไฟล์ .docx ที่เป็น template (มี Content Control)",
},
data: {
type: "string",
format: "binary",
description: 'ไฟล์ JSON เช่น { "blackno": "คดีหมายเลขดำที่", … }',
},
dataJson: {
type: "string",
description:
"JSON เป็น string ตรง ๆ (ทางเลือกแทนการแนบไฟล์ data)<br/>{\"info:\"{\"ticket_data\":\"JSON String\"},\"dc:\"{..},\"dc:\"{\"blackyear\": \"๙๐/๒๕๖๙\",...}}",
example:
'',
},
secret: {
type: "string",
description:
"ค่า secret สำหรับทดสอบการทำ HMAC-SHA256",
example:
'super_secret',
},
private_key: {
type: "string",
description:
"Private key ในรูปแบบ HEX <br/> openssl genpkey -algorithm ED25519 -out private.pem <br/> openssl pkey -in private.pem -outform DER | tail -c 32 | xxd -p -c 32 > private.hex <br/>openssl pkey -in private.pem -pubout -outform DER | tail -c 32 | xxd -p -c 32 > public.hex",
example:
'8f2077b57b39399b6a7828f35da03a63ade8e3a36a6bf437b091a8bdb3d18ee1',
},
},
},
},
},
},
responses: {
200: {
description: "ไฟล์ .docx ที่กรอกค่าแล้ว (binary)",
content: {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
schema: { type: "string", format: "binary" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/template/extract": {
post: {
tags: ["template"],
summary: "ดึงค่า Content Control ทั้งหมดออกมาจากไฟล์ .docx ใช้สำหรับนักพัฒนาเท่านั้น",
description:
"อ่าน Content Control, bookmark , property ของ MS Word(docx) ออกมาแสดง" +
"ค่าของ Content Control จะเป็น Plain Text ไม่มี markup ซึ่งเหมาะกับการเอาไปใส่ในฐานข้อมูล"+
'<i>สำหรับนักพัฒนา Template เท่านั้น ไม่ต้อง ใช้ API Key ต้องตั้งค่า secret และ Public key เองไม่ใช้ของระบบของระบบ</i>',
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: {
type: "object",
required: ["file"],
properties: {
file: {
type: "string",
format: "binary",
description: "ไฟล์ .docx ที่มี Content Control",
},
secret: {
type: "string",
description:
"ค่า secret สำหรับ validate HMAC-SHA256 กับข้อมูลใน custom property",
example:
"super_secret",
},
public_key: {
type: "string",
description:
"Public key ในรูปแบบ HEX <br/> openssl genpkey -algorithm ED25519 -out private.pem <br/> openssl pkey -in private.pem -outform DER | tail -c 32 | xxd -p -c 32 > private.hex <br/>openssl pkey -in private.pem -pubout -outform DER | tail -c 32 | xxd -p -c 32 > public.hex",
example:
"66b09b4a1b66271d5728c592c2409657e1c070a7bf371bf9f6ed457855270546",
},
},
},
},
},
},
responses: {
200: {
description: "รายการแท็กพร้อมข้อความ",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ExtractResponse" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/template/generate": {
post: {
tags: ["template"],
summary: "สร้างเอกสาร DOCX จากข้อมูล JSON และ template ที่กำหนด",
description:
"เทมเพลตมีแท็กแบบพิเศษ(รองรับตัวแปร ลูป เงื่อนไข), Content Control เมื่อรวมกับข้อมูล(JSON) "+
"จะได้เอกสาร รูปแบบฟอร์มที่กรอกค่าแล้วดึงค่ากลับมาใช้งานต่อได้เช่นอัปเดตฐานข้อมูล<br><ul>"+
"<li>JSON payload ต้องมี info.ticket_data เป็น JSON string รูปแบบถูกต้อวมีค่าครบ(tid, fid และ uid)</li>" +
"<li>ไฟล์เทมเพลต(tid)ในระบบ + ข้อมูล(payload) เพื่อสร้างเอกสาร </li>" +
"<li>ticket_data จะทำการ hash/sign แบบอายุสั้นแล้วนำไปฝังไว้ใน custom property เพื่อนำไปใช้ร่วมกับระบบ Versioning</li>"+
"</ul>",
security: [{ ApiKeyAuth: [] }],
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["info"],
properties: {
info: {
type: "object",
required: ["ticket_data"],
properties: {
ticket_data: {
type: "string",
description:
"JSON string ที่ต้อง parse ได้ และต้องมี tid, fid และ uid",
example:
'{"tid":"T12","fid":"DOC-2569-0001","uid":"123"}',
},
},
additionalProperties: true,
},
},
additionalProperties: true,
},
example: {
"info": {
"ticket_data": "{\"fid\":\"T12_b2_IN\",\"tid\":\"T12_X\",\"uid\":\"oom\",\"uname\":\"สรวิชญ์\"}"
},
"dc": {
"Interpleader": true,
"docname": "",
"docabbr": "(ต. ๑๒)",
"blackno": "คดีหมายเลขดำที่",
"redno": "คดีหมายเลขแดงที่",
"court": "ศาลปกครองนครราชสีมา",
"panels": [
{
"pname": "นายวิรุฬห์ จินตนะกุล",
"pmname": "ตุลาการศาลปกครองกลาง",
"pm": "ตุลาการเจ้าของสำนวน"
},
{
"pname": "นายสัมฤทธิ์ อ่อนคำ",
"pmname": "ตุลาการศาลปกครองสูงสุด"
}
],
"pnameT": "",
"pmnameT": "",
"pmT": "",
"acc_name": ""
},
"cc": {
"blackyear": "๙๐/๒๕๖๙",
"redyear": "",
"curdate": "วันที่ เดือน สิงหาคม พุทธศักราช ๒๕๖๙",
"accuser": "นายธีรพล ร่มเริง",
"acc1": "ผู้ฟ้องคดี",
"accused": "รร",
"acc2": "ผู้ถูกฟ้องคดี",
"interpleader": "สมชาย ใจดี",
"acc3": "ผู้ร้องสอด",
"detail1": "ตุลาการเจ้าของสำนวนได้พิจารณาคำฟ้องและเอกสารประกอบคำฟ้องแล้วเห็นว่าคดีนี้เป็นคดีพิพาทซึ่งไม่อยู่ในอำนาจศาลปกครองสูงสุด ผู้ฟ้องเป็นผู้มีสิทธิฟ้องคดี คดีนี้มีกฎหมายกำหนดให้ต้องดำเนินการแก้ไขความเดือดร้อนหรือเสียหายก่อนฟ้องคดี และผู้ฟ้องคดีได้ดำเนินการครบถ้วน โดยเป็นการยื่นฟ้องคดีภายในระยะเวลาการฟ้องคดีตาม พรบ. จัดตั้งศาลปกครองและวิธีพิจารณาคดีปกครองฯ คดีนี้มีค่าธรรมเนียมที่ต้องชำระ\nโดยผู้ฟ้องคดีได้ชำระค่าธรรมเนียมครบถ้วนแล้ว รวมทั้งคดีนี้ไม่เป็นการฟ้องซ้อน การดำเนินกระบวนพิจารณาซ้ำ และการฟ้องซ้ำ\n\nจึงมีคำสั่งรับคำฟ้องไว้พิจารณาและสั่งให้ส่งสำเนาคำฟ้องพร้อมทั้งสำเนาพยานหลักฐานไปยังผู้ถูกฟ้องคดีเพื่อให้ผู้ถูกฟ้องคดีทำคำให้การยื่นต่อศาลภายใน ๓๐ วัน นับแต่วันที่ได้รับคำสั่ง"
}
},
},
},
},
responses: {
200: {
description: "ไฟล์ DOCX ที่สร้างสำเร็จ",
headers: {
"Content-Disposition": {
description: "ชื่อไฟล์สำหรับดาวน์โหลด",
schema: { type: "string" },
},
"Cache-Control": {
description: "ป้องกันการ cache response",
schema: { type: "string", example: "no-store, max-age=0" },
},
},
content: {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
schema: { type: "string", format: "binary" },
},
},
},
400: {
description:
"JSON ไม่มี info.ticket_data, ticket_data ไม่ใช่ string, JSON string ไม่ถูกต้อง หรือไม่มี tid/fid/uid",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
examples: {
missingTicketData: {
value: {
error:
"template/generate json.info.ticket_data not avaliable",
},
},
missingIdentifiers: {
value: {
error:
"template/generate require json.info.ticket_data.(fid|tid|uid)",
},
},
},
},
},
},
401: {
description: "ไม่ได้ส่ง x-api-key หรือ API key ไม่ถูกต้อง",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
example: { error: "Unauthorized: Invalid API Key" },
},
},
},
404: {
description: "ไม่สามารถดึงข้อมูลขึ้นมาได้",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
example: { error: "NotFound: Invalid template ID or incorrect storage path" },
},
},
},
500: {
description:
"เกิดข้อผิดพลาดระหว่างอ่าน template หรือสร้างเอกสาร",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
example: { error: "Failed to generate document payload" },
},
},
},
},
},
},
"/api/template/download-docx": {
get: {
tags: ["template"],
summary: "ดาวน์โหลดเอกสาร DOCX ผ่าน Query Parameters (GET) ใช้เพื่อเป็นตัวอย่างจะถูกเอาออกในภายหลัง",
description:
"รับค่า fid, tid, uid, uname จาก Query Parameter "+
"ตรวจสอบรุ่นล่าสุดใน /api/document/latest ถ้าไม่พบ ต้องหาจากฐานข้อมูลแล้วส่งไปประมวลผลต่อที่ /api/template/generate "+
"เพื่อสร้างและคืนค่าเป็นไฟล์ DOCX stream ใช้เพื่อเป็นตัวอย่างสำหรับนักพัฒนาเท่านั้น ไม่มีการตรวจสอบ user session ต้องนำออกก่อน production",
parameters: [
{
name: "fid",
in: "query",
required: true,
schema: { type: "string" },
description: "ID เอกสาร (File ID)",
example: "T_no_warning",
},
{
name: "tid",
in: "query",
required: true,
schema: { type: "string" },
description: "ID เทมเพลต (Template ID)",
example: "T_X",
},
{
name: "uid",
in: "query",
required: true,
schema: { type: "string" },
description: "รหัสผู้ใช้งาน (User ID)",
example: "123",
},
{
name: "uname",
in: "query",
required: true,
schema: { type: "string" },
description: "ชื่อผู้ใช้งาน (User Name)",
example: "สมชาย ใจดี",
},
],
responses: {
200: {
description: "ไฟล์ DOCX stream ที่สร้างสำเร็จ",
headers: {
"Content-Disposition": {
description: "ชื่อไฟล์สำหรับการดาวน์โหลด (attachment)",
schema: { type: "string", example: 'attachment; filename="T_no_warning_123_xxx.docx"' },
},
"Cache-Control": {
description: "การควบคุม Cache (no-store)",
schema: { type: "string", example: "no-store, max-age=0" },
},
},
content: {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
schema: { type: "string", format: "binary" },
},
},
},
400: {
description: "พารามิเตอร์ไม่ครบถ้วน หรือรูปแบบไฟล์ JSON ไม่ถูกต้อง",
content: {
"text/plain": {
schema: { type: "string" },
examples: {
missingParams: { value: "Missing fid,tid,uid or uname" },
invalidData: { value: "Invalid data format" },
},
},
},
},
404: {
description: "ไม่พบไฟล์ข้อมูล JSON ใน TMPL_PATH",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
example: { error: "Data not found: /path/to/TMPL_PATH/T_no_warning.json" },
},
},
},
502: {
description: "เกิดข้อผิดพลาดในการเรียกไปยังบริการ Docedit /api/template/generate",
content: {
"text/plain": {
schema: { type: "string" },
example: "Error from docedit generate: Expected docx stream but received text/html (Status 500)",
},
},
},
500: {
description: "เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์",
content: {
"text/plain": {
schema: { type: "string" },
example: "Internal Server Error",
},
},
},
},
},
post: {
tags: ["template"],
summary: "ดาวน์โหลดเอกสาร DOCX ผ่าน HTML Form / Multipart Form Data (POST) เป็นตัวอย่างเท่านั้น จะถูกเอาออกในภายหลัง",
description:
"รับค่า fid, tid, uid, uname จาก Multipart Form Data หรือ Form-urlencoded "+
"ตรวจสอบรุ่นล่าสุดใน /api/document/latest ถ้าไม่พบ ต้องหาจากฐานข้อมูลแล้วส่งไปประมวลผลต่อที่ /api/template/generate "+
"เพื่อสร้างและคืนค่าเป็นไฟล์ DOCX stream ใช้เพื่อเป็นตัวอย่างสำหรับนักพัฒนาเท่านั้น ไม่มีการตรวจสอบ user session ต้องนำออกก่อน production",
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: {
type: "object",
required: ["fid", "tid", "uid", "uname"],
properties: {
fid: { type: "string", description: "ID เอกสาร (File ID)",example:"T12_b2_IN" },
tid: { type: "string", description: "ID เทมเพลต (Template ID)",example:"T12_X" },
uid: { type: "string", description: "รหัสผู้ใช้งาน (User ID)", example:"john" },
uname: { type: "string", description: "ชื่อผู้ใช้งาน (User Name)", example:"John Doe"},
},
},
},
"application/x-www-form-urlencoded": {
schema: {
type: "object",
required: ["fid", "tid", "uid", "uname"],
properties: {
fid: { type: "string", description: "ID เอกสาร (File ID)" },
tid: { type: "string", description: "ID เทมเพลต (Template ID)" },
uid: { type: "string", description: "รหัสผู้ใช้งาน (User ID)" },
uname: { type: "string", description: "ชื่อผู้ใช้งาน (User Name)" },
},
},
},
},
},
responses: {
200: {
description: "ไฟล์ DOCX stream ที่สร้างสำเร็จ",
headers: {
"Content-Disposition": {
description: "ชื่อไฟล์สำหรับการดาวน์โหลด (attachment)",
schema: { type: "string", example: 'attachment; filename="T12_b2_IN_john_xxx.docx"' },
},
"Cache-Control": {
description: "การควบคุม Cache (no-store)",
schema: { type: "string", example: "no-store, max-age=0" },
},
},
content: {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
schema: { type: "string", format: "binary" },
},
},
},
400: {
description: "พารามิเตอร์ไม่ครบถ้วน หรือรูปแบบไฟล์ JSON ไม่ถูกต้อง",
content: {
"text/plain": {
schema: { type: "string" },
examples: {
missingParams: { value: "Missing fid,tid,uid or uname" },
invalidData: { value: "Invalid data format" },
},
},
},
},
404: {
description: "ไม่พบไฟล์ข้อมูล JSON ใน TMPL_PATH",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
example: { error: "Data not found: /path/to/TMPL_PATH/T12_b2_IN.json" },
},
},
},
502: {
description: "เกิดข้อผิดพลาดในการเรียกไปยังบริการ Docedit /api/template/generate",
content: {
"text/plain": {
schema: { type: "string" },
example: "Error from docedit generate: Expected docx stream but received text/html (Status 500)",
},
},
},
500: {
description: "เกิดข้อผิดพลาดภายในเซิร์ฟเวอร์",
content: {
"text/plain": {
schema: { type: "string" },
example: "Internal Server Error",
},
},
},
},
},
},
"/api/note/external/documents": {
post: {
tags: ["external"],
summary: "ดึง annotations/bookmarks ของเอกสาร (N10)",
description:
"ถามด้วย fileId (เอกสารเดียว — id ของ e-Court ที่ลิงก์มา) และ/หรือ typeId " +
"(ทุกเอกสารในกลุ่ม) — ส่งทั้งคู่ = AND เหลือเฉพาะเอกสารเดียวที่ตรงทั้งสองเงื่อนไข " +
"· response รูปเดียวเสมอ { documents: […] } (กลุ่มว่าง = array ว่าง) " +
"· read-only: snapshot ข้อมูล ณ ตอนเรียก · แต่ละรายการมี url deep link กลับหน้าอ่านของระบบนี้",
security: [{ ApiKeyAuth: [] }],
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ExternalQuery" },
examples: {
"ถามด้วย fileId (เอกสารเดียว)": {
value: { fileId: "DOC-2569-0001" },
},
"ทุกเอกสารในกลุ่ม": {
value: { typeId: "GROUP-01" },
},
"กลุ่ม + เฉพาะ bookmark ของคนใดคนหนึ่ง": {
value: {
typeId: "GROUP-01",
filter: { type: "bookmarks", userId: "u0001" },
},
},
},
},
},
},
responses: {
200: {
description: "bundle ของเอกสารที่ตรงเงื่อนไข",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/DocumentsResponse" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: {
description: "ไม่ได้ส่ง header x-api-key หรือค่าไม่ถูกต้อง",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
404: {
description: "ไม่พบเอกสาร (ยังไม่เคยเปิดในระบบ หรือไม่อยู่ในกลุ่ม typeId ที่ระบุ)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/note/files/stage": {
post: {
tags: ["Judge's Note"],
summary: "ส่งเนื้อไฟล์เอกสาร (PDF/รูป) เข้า RAM staging ด้วย base64 (N11)",
description:
"ทางเข้าเอกสารที่ e-Court **ไม่มี URL ให้ดึง** (เอกสารภายใน/สร้างสด — ต่างจาก " +
"N8 ที่ส่ง `fileUrl`) — ส่งเนื้อไฟล์มาเป็น base64 ระบบเก็บใน **RAM ชั่วคราว** " +
"(TTL 8 ชม. นับจากการเข้าใช้ล่าสุด — ห้ามเก็บไฟล์ถาวรทุกรูปแบบตามโจทย์) " +
"annotation/bookmark เก็บถาวรใน DB ตามระบบเดิม\n\n" +
"ได้ `{fileId}` แล้วเอา `fid` **เดิม** ไป mint login token ที่ " +
"`POST /api/auth/token` แบบ `purpose:\"login\"` → ลิงก์ `/note?t=<access_token>` " +
"(ไม่ต้องมี `fileUrl`) ให้ผู้ใช้คลิกตามเส้นทางเดิม\n\n" +
"- auth: header `x-api-key` (env `DOCVER_API_KEY` — ตัวเดียวกับทางเข้า mint)\n" +
"- `data`: **base64 มาตรฐานต่อเนื่อง** — ห้าม whitespace/line break และไม่รับ " +
"base64url (`-`/`_`)\n" +
"- ขนาด: เนื้อไฟล์ ≤ 100MB (base64 ยาว ~134MB) — ระบบตรวจ magic bytes เอง " +
"(PDF/JPEG/PNG/TIFF) และใช้ผลตรวจเป็นชนิดไฟล์ ไม่เชื่อค่าที่ประกาศ\n" +
"- การ stage ด้วย `fid` เดิมซ้ำ = **replace เนื้อล่าสุด** (row เดิม " +
"annotation ครบ — พิกัดอาจคลาดถ้าเลย์เอาต์ขยับ เช่นเดียวกับ N8)\n\n" +
"**คู่มือ retry:** `408`/`429`/`5xx` = **ยิงซ้ำได้** (429 ตาม header " +
"`Retry-After`, ที่เหลือ backoff) และการ stage ซ้ำ **ปลอดภัย** — `fid` เดิม " +
"แทนที่ของเดิม ไม่เกิดรายการซ้ำ ไม่เสียอะไร · `400`/`413` = แก้ request " +
"ก่อนยิงใหม่ · อัปโหลดช้าถูกตัดที่ 60 วิว่างเปล่า/10 นาทีรวม (408) — " +
"ลองส่งใหม่บนเครือข่ายที่ดีกว่า",
security: [{ ApiKeyAuth: [] }],
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["fid", "data"],
properties: {
fid: {
type: "string",
description:
"รหัสเอกสารของ e-Court (1–200 ตัวอักษร, ห้ามอักขระ & = + %) — ใช้ mint login token " +
"ต่อด้วย fid เดิม · ฝั่งระบบนี้เก็บเป็น hash (ไม่มี fid ดิบใน DB) แต่ผู้เรียกยังส่ง " +
"fid ดิบมาก่อน hash ทุกเส้น — contract ไม่เปลี่ยน",
},
data: {
type: "string",
description:
"เนื้อไฟล์ (.pdf/.jpg/.png/.tif) เข้ารหัส base64 มาตรฐานต่อเนื่อง — เนื้อไฟล์ ≤ 100MB",
},
typeId: {
type: "string",
description:
"รหัสกลุ่มเอกสาร N9 (ทางเลือก) — ส่งมา = ย้ายกลุ่มตามค่าล่าสุด ไม่ส่ง = คงเดิม · " +
"⚠️ ของเอกสาร staging รับทางเส้นนี้**ทางเดียว** — typeId ที่แนบมากับ body ของ " +
"`POST /api/auth/session` (ตอนแลกลิงก์ fid-only) ไม่ถูกใช้กับกลุ่มของไฟล์",
},
title: {
type: "string",
description:
"ชื่อแสดง (ทางเลือก, ≤ 200 ตัวอักษร) — อัปเดตได้เฉพาะเมื่อชื่อปัจจุบันยังเป็นค่า default " +
'"เอกสารจาก e-Court" (ครั้งแรกที่ส่งชื่อจริงชนะแล้วล็อก)',
},
},
},
examples: {
"stage คำสั่งศาล PDF": {
value: {
fid: "F123",
data: "JVBERi0xLjQK…",
title: "คำสั่งศาลที่ 2501/2569",
typeId: "GROUP-01",
},
},
},
},
},
},
responses: {
200: {
description:
"ลงทะเบียนสำเร็จ — `fileId` คือ id ของ row เอกสาร (ผู้ใช้เข้าผ่านลิงก์ fid ตามปกติ)",
content: {
"application/json": {
schema: {
type: "object",
properties: {
fileId: {
type: "string",
description: "uuid ของ row เอกสาร (sentinel staged:<uuid>.<ext> ใน pdf_url)",
},
},
},
},
},
},
400: {
description:
"body ผิด (field ที่ไม่รู้จัก / base64 เสีย / magic bytes ไม่ใช่ PDF หรือรูปที่รองรับ / fid มีอักขระ & = + %)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
401: {
description:
"ไม่ได้ส่ง header x-api-key หรือค่าไม่ถูกต้อง (env DOCVER_API_KEY — ข้อความเดียวกันทุกสาเหตุ)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
408: {
description:
"อ่าน body ช้าเกินกำหนด (ไม่มี chunk 60 วิ / เกิน 10 นาทีรวม) หรือการเชื่อมต่อขาด — ยิงซ้ำได้",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
413: {
description:
"ไฟล์ใหญ่เกินขนาดที่รองรับ (เนื้อไฟล์เกิน 100MB / base64 เกิน ~140MB) หรือใหญ่เกินโควตารวม — แก้ไฟล์ก่อนส่งใหม่",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
429: {
description:
"มีการ stage เอกสารอื่นกำลังดำเนินการ (จำกัด concurrent) — ยิงซ้ำตาม header Retry-After",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/auth/token": {
post: {
tags: ["auth"],
summary: "ขอ access_token — ทางเข้ารวมของทั้งระบบ (docver / login ลิงก์ note / ticket add-in)",
description:
"**ทางเข้า token รวมของทั้งระบบ** (2026-11-09 รอบสอง — ถอดเส้น mint เดิมของฝั่ง note " +
"ทาง mint login-token จึงรวมอยู่ที่นี่ทางเดียวผ่าน `purpose:\"login\"` · " +
"`/api/documents/token` ของกลุ่ม docver เป็น alias ของเส้นนี้ — function เดียวกัน)\n\n" +
"ออก `access_token` (JWT HS256) — token ฝัง claims `{userid, username, fid, " +
"template}` อายุตามชนิดของ token (D4 แยกสิทธิ์ด้วย claim `purpose`)\n\n" +
"**ทางเข้า 3 แบบ** — เลือกตามว่าใครเป็นคนเรียก (ดูตัวอย่างใน Request body):\n\n" +
"1. **ระบบภายนอก (server-to-server)** — แนบ header `x-api-key` (ค่าจาก env " +
"`DOCVER_API_KEY`) แล้วส่ง claims ตรง ๆ ใน body ตาม schema `TokenRequest` → " +
"docver-token อายุตาม env `DOCVER_TOKEN_TTL_SECONDS` (default 604800 วินาที = " +
"7 วัน — token อายุยาวแทนระบบ refresh/renew ของ add-in) ใช้กับ endpoint กลุ่ม " +
"documents\n" +
"2. **e-Court mint ลิงก์ SSO ของแอป note** — `x-api-key` เช่นเดียวกัน + body ตาม " +
'schema `LoginTokenRequest` (บอกด้วย `purpose:"login"` + `ttl_seconds` 60–600 ' +
"default 300) → login-token อายุสั้นไว้ฝังเป็น param `t=` ของลิงก์ `/note?t=…` " +
"(credential อยู่ใน URL ต้องอายุสั้น — D5) · ไม่ส่ง `purpose` = ทางเข้า 1 ตามเดิม " +
"พอดี\n" +
"3. **add-in ใน Word** — ไม่ต้องมี `x-api-key` ส่ง body `{ticket_data, HmacSha256}` " +
"ตาม schema `TicketTokenRequest` (คู่ค่าที่ e-Court stamp ไว้ใน custom properties " +
"ของไฟล์ .docx) — server ตรวจลายเซ็น HMAC-SHA256 ด้วย env `ADDIN_TICKET_SECRET` " +
'(ไม่ตั้ง → env `SECRET` → `"super_secret"`) แล้วออก token จาก claims ที่อยู่ในตัว ticket เอง ' +
"(contract 2026-11-04: ฟอร์แม็ตพัง/ไม่ครบ = 400 · ลายเซ็นไม่ถูก = 401 · " +
"**ลายเซ็นถูกแต่ ticket หมดอายุ = 403** — add-in ที่มี access_token เดิมของ `fid` " +
"นี้ใน session ยังใช้ต่อได้ ไม่มี = ขอไฟล์ใหม่จาก e-Court)\n\n" +
"ได้ token แล้วแนบเป็น `Authorization: Bearer <token>` กับทุก request ถัดไป",
security: [{ ApiKeyAuth: [] }],
requestBody: {
required: true,
content: {
"application/json": {
schema: {
oneOf: [
{ $ref: "#/components/schemas/TokenRequest" },
{ $ref: "#/components/schemas/LoginTokenRequest" },
{ $ref: "#/components/schemas/TicketTokenRequest" },
],
},
examples: {
"server-to-server ด้วย x-api-key (ครบทุก claim)": {
value: {
userid: "123",
username: "Sorawit Bholsithi",
fid: "3",
template: "T12",
},
},
"server-to-server ขั้นต่ำ (template เป็นทางเลือก)": {
value: { userid: "123", username: "Sorawit Bholsithi", fid: "3" },
},
"mint login-token ลิงก์ SSO ของแอป note (purpose=login)": {
value: { userid: "oom", username: "สรวิชญ์", fid: "F123", ttl_seconds: 300, purpose: "login" },
description:
"ออก login-token อายุสั้น — ฝังเป็น t= ของลิงก์ /note?fileUrl=…&t=…",
},
"add-in ด้วย ticket จากไฟล์ (ไม่ต้องมี x-api-key)": {
value: {
ticket_data:
'{"exp":1793067433,"fid":"T12_FD155_3s-long_ZZZ","tid":"T12","uid":"oom","uname":"สรวิชญ์"}',
HmacSha256:
"1dc53fbd48410845c895ddb841a8fe4ad93da413126fae9c688202e57f2baaa9",
},
},
},
},
},
},
responses: {
200: {
description: "access_token พร้อมใช้ (แนบเป็น Authorization: Bearer ทุก request ถัดไป)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/TokenResponse" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: {
description:
"ทางเข้า x-api-key: ไม่ได้ส่ง header หรือค่าไม่ถูกต้อง (env DOCVER_API_KEY) · " +
"ทางเข้า ticket: ลายเซ็น HMAC ไม่ถูก (ข้อความเดียวกันทุกสาเหตุ)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
403: {
description:
"เฉพาะทางเข้า ticket: ลายเซ็นถูกแต่ ticket หมดอายุ — ใช้ access_token เดิมของ " +
"fid นี้ใน session ต่อ หรือขอไฟล์ใหม่จาก e-Court (contract 2026-11-04)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/auth/session": {
post: {
tags: ["auth"],
summary: "แลก credential ของลิงก์ e-Court เป็น session token ของแอป note (SSO)",
description:
"**ทางเข้าหลัก (และเดียว) ของแอป note** (2026-11-09 รอบสอง — เส้น session เดิมของฝั่ง note " +
"ถอดแล้ว ผู้เรียกเก่าต้องย้ายมาเส้นนี้) · flow SSO ของแอป note " +
"(PLAN-SSO.md §3.2 ฉบับ token): ลิงก์ e-Court พา browser มาหน้า `/note` พร้อม " +
"credential → client เรียกเส้นนี้แลกเป็น access_token เก็บ localStorage " +
'("note-auth") แล้วแนบ `Authorization: Bearer` กับทุก API/Server Action ของแอป\n\n' +
"- ส่ง credential **ทางเดียว**: `t` (login-token จาก `POST /api/auth/token` " +
'แบบ `purpose:"login"`) หรือ `ticket_data`+`HmacSha256` (คู่ค่าชุดเดียวกับที่ e-Court ' +
"stamp ใน custom properties ของไฟล์ .docx ของ add-in) — สองชุดพร้อมกัน = 400 (D12)\n" +
"- token ที่ได้เป็นชนิด `purpose=login` อายุ 8 ชม. (28800 วินาที) — ใช้ได้กับแอป " +
"note เท่านั้น เอาไปยิง `/api/documents/*` ของกลุ่ม docver ไม่ได้ (401) · หมด " +
"กลาง session แล้ว client ล้างทิ้งแล้วพากลับ e-Court (token ที่ตายแล้วไม่มีทางฟื้น)\n" +
"- ส่ง `fileUrl` มาด้วย = ลงทะเบียนเอกสาร N8 แบบ trusted ฝั่ง server ทันที " +
"(fid จาก credential ที่ verify แล้ว) — ล้มก็ยังได้ token พร้อม `fileError` " +
"ให้หน้าแรกแสดงสาเหตุ · `fileUrl` ต้องผ่าน allowlist ของ N8 " +
"(admincourt.go.th + env `EXTERNAL_FILE_HOSTS`) หรือเป็น path `/samples/…`\n" +
"- **ไม่ส่ง `fileUrl` (ลิงก์ fid-only — เอกสาร staging ของ N11)**: ค้นเอกสารของ " +
"`fid` ตั้งแต่ตอนแลก token — staging ที่ยังมีเนื้อ (RAM) หรือเอกสาร N8 ที่รู้จัก " +
"แล้ว = ได้ `fileId` เข้า viewer ได้เลย · ไม่เจอ/พ้นช่วงจัดเตรียม = `fileError` " +
"ให้กลับ e-Court กดลิงก์ใหม่ (ผู้ใช้ยังได้ token ตามปกติทุกกรณี)\n" +
"- **ทาง ticket ใน production**: secret ต้องตั้งจริง (env `ADDIN_TICKET_SECRET` " +
'→ `SECRET`) — ค่า default `"super_secret"` ที่รู้กันทั้งระบบถูกปฏิเสธ (500)',
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/SessionExchangeRequest" },
examples: {
"ทาง JWT (param t ของลิงก์ e-Court)": {
value: {
t: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwdXJwb3NlIjoibG9naW4iLCJ1c2VyaWQiOiJvb20iLCJ1c2VybmFtZSI6IuC4geC4m+C4tOC5iOC4quC4lOC5iCIsImZpZCI6IkYxMjMifQ.….….…",
fileUrl: "https://service.admincourt.go.th/uploads/F123.pdf",
typeId: "T12",
},
},
"ทาง ticket (คู่ค่าเดียวกับไฟล์ .docx ของ add-in)": {
value: {
ticket_data:
'{"exp":1793067433,"fid":"T12_FD155_3s-long_ZZZ","tid":"T12","uid":"oom","uname":"สรวิชญ์"}',
HmacSha256:
"1dc53fbd48410845c895ddb841a8fe4ad93da413126fae9c688202e57f2baaa9",
},
},
},
},
},
},
responses: {
200: {
description:
"session token ของแอป note (พร้อม fileId ถ้าลงทะเบียน fileUrl สำเร็จ)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/SessionResponse" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: {
description:
"credential หมดอายุหรือไม่ถูกต้อง — ทาง JWT แยก `reason: expired` (หมดอายุ) กับ " +
"`invalid` · ทาง ticket รวมเป็น `invalid` ทั้งลายเซ็นผิดและหมดอายุ",
content: {
"application/json": {
schema: {
allOf: [
{ $ref: "#/components/schemas/Error" },
{
type: "object",
properties: {
reason: {
type: "string",
enum: ["expired", "invalid"],
},
},
},
],
},
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/documents/token": {
post: {
tags: ["documents"],
summary: "ขอ access_token ของกลุ่ม documents (docver)",
description:
"**alias ของ `POST /api/auth/token`** — function เดียวกันเป๊ะ (re-export · " +
"2026-11-09: คงไว้ตามคำขอผู้ใช้เพราะเป็นทางเข้าของกลุ่ม docver/e-Court เดิม; " +
"contract เดิมคงทั้งหมด — body ไม่ส่ง `purpose` = docver-token)\n\n" +
"ออก `access_token` (JWT HS256) ไว้เรียก endpoint อื่นของกลุ่ม documents — " +
"token ฝัง claims `{userid, username, fid, template}` อายุตาม env " +
"`DOCVER_TOKEN_TTL_SECONDS` (default 604800 วินาที = 7 วัน — token อายุยาวแทน" +
"ระบบ refresh/renew ของ add-in)\n\n" +
"**ทางเข้า 2 แบบ** — เลือกตามว่าใครเป็นคนเรียก (ดูตัวอย่างใน Request body ได้ทั้งคู่):\n\n" +
"1. **ระบบภายนอก (server-to-server)** — แนบ header `x-api-key` (ค่าจาก env " +
"`DOCVER_API_KEY`) แล้วส่ง claims ตรง ๆ ใน body ตาม schema `TokenRequest`\n" +
"2. **add-in ใน Word** — ไม่ต้องมี `x-api-key` ส่ง body `{ticket_data, HmacSha256}` " +
"ตาม schema `TicketTokenRequest` (คู่ค่าที่ e-Court stamp ไว้ใน custom properties " +
"ของไฟล์ .docx) — server ตรวจลายเซ็น HMAC-SHA256 ด้วย env `ADDIN_TICKET_SECRET` " +
'(ไม่ตั้ง → env `SECRET` → `"super_secret"`) แล้วออก token จาก claims ที่อยู่ในตัว ticket เอง ' +
"(contract 2026-11-04: ฟอร์แม็ตพัง/ไม่ครบ = 400 · ลายเซ็นไม่ถูก = 401 · " +
"**ลายเซ็นถูกแต่ ticket หมดอายุ = 403** — add-in ที่มี access_token เดิมของ `fid` " +
"นี้ใน session ยังใช้ต่อได้ ไม่มี = ขอไฟล์ใหม่จาก e-Court)\n\n" +
"ได้ token แล้วแนบเป็น `Authorization: Bearer <token>` กับทุก request ถัดไป",
security: [{ ApiKeyAuth: [] }],
requestBody: {
required: true,
content: {
"application/json": {
schema: {
oneOf: [
{ $ref: "#/components/schemas/TokenRequest" },
{ $ref: "#/components/schemas/TicketTokenRequest" },
],
},
examples: {
"server-to-server ด้วย x-api-key (ครบทุก claim)": {
value: {
userid: "123",
username: "Sorawit Bholsithi",
fid: "3",
template: "T12",
},
},
"server-to-server ขั้นต่ำ (template เป็นทางเลือก)": {
value: { userid: "123", username: "Sorawit Bholsithi", fid: "3" },
},
"add-in ด้วย ticket จากไฟล์ (ไม่ต้องมี x-api-key)": {
value: {
ticket_data:
'{"exp":1793067433,"fid":"T12_FD155_3s-long_ZZZ","tid":"T12","uid":"oom","uname":"สรวิชญ์"}',
HmacSha256:
"1dc53fbd48410845c895ddb841a8fe4ad93da413126fae9c688202e57f2baaa9",
},
},
},
},
},
},
responses: {
200: {
description: "access_token พร้อมใช้ (แนบเป็น Authorization: Bearer ทุก request ถัดไป)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/TokenResponse" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: {
description:
"ทางเข้า x-api-key: ไม่ได้ส่ง header หรือค่าไม่ถูกต้อง (env DOCVER_API_KEY) · " +
"ทางเข้า ticket: ลายเซ็น HMAC ไม่ถูก (ข้อความเดียวกันทุกสาเหตุ)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
403: {
description:
"เฉพาะทางเข้า ticket: ลายเซ็นถูกแต่ ticket หมดอายุ — ใช้ access_token เดิมของ " +
"fid นี้ใน session ต่อ หรือขอไฟล์ใหม่จาก e-Court (contract 2026-11-04)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/documents/versions": {
post: {
tags: ["documents"],
summary: "อัปโหลด version ใหม่ของเอกสาร (docx + pdf)",
description:
"บันทึกไฟล์ปัจจุบันของเอกสารเป็น **version ใหม่** — เอกสารถูกระบุด้วย `fid` " +
"ใน Bearer token ถ้า docId นี้ยังไม่เคยมีในระบบ จะสร้างเอกสารให้อัตโนมัติ (version แรก = 1)\n\n" +
"- ไฟล์เก็บใน MinIO แยก object ต่อ version พร้อมบันทึกผู้อัปโหลดจาก claims ใน token\n" +
"- `pdf` เป็น optional — ไม่แนบหรือแนบไฟล์เปล่า = version นี้ไม่มีสำเนา PDF " +
"(ดู `hasPdf` ใน response)\n" +
"- `versionNumber` ใน response คือเลขจริงที่ server จ่าย (ปลอดภัยแม้อัปโหลดพร้อมกัน " +
"หลาย client) — ถ้าต่างจากที่ client คาดไว้ ให้ใช้เลขจาก response เสมอ\n" +
"- ผู้อัปโหลด (userId/userName) และ templateId อ่านจาก token — " +
"**ห้ามส่ง field เหล่านี้ใน form (ส่งมา = 400)**\n" +
"- `fields` ของ version ถูก server แกะจากไฟล์ .docx ที่อัปโหลดเอง เก็บ**ทั้งก้อนเหมือน " +
"return ของ `/api/template/extract`** (`{cc, bookmark, properties, verify}` — ผ่าน helper " +
"ตัวเดียวกัน) โดย `verify.HmacSha256` ตรวจด้วย secret ของ ticket ฝั่ง server — " +
"**ห้ามส่ง field `fields` ใน form (ส่งมา = 400)**",
security: [{ DocverBearer: [] }],
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: {
type: "object",
required: ["file"],
properties: {
file: {
type: "string",
format: "binary",
description: "ไฟล์ .docx ของ version นี้ (บังคับ)",
},
pdf: {
type: "string",
format: "binary",
description:
"ไฟล์ .pdf ของ version นี้ (optional — ไม่แนบหรือไฟล์เปล่า = ไม่มี)",
},
clientVersionId: {
type: "string",
description: "GUID ที่ client generate ก่อนเรียก (เก็บไว้สืบสวน ไม่ใช่ identity)",
},
message: {
type: "string",
description: "commit message ของ version (≤ 2048 ตัวอักษร)",
},
fileName: {
type: "string",
description:
"ชื่อไฟล์สำหรับแสดงตอนดาวน์โหลด (ไม่ส่ง = ใช้ชื่อไฟล์ที่แนบ)",
},
},
},
},
},
},
responses: {
200: {
description: "version ถูกบันทึกแล้ว",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/UploadVersionResult" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: { $ref: "#/components/responses/DocverUnauthorized" },
413: {
description: "ไฟล์แนบใหญ่เกิน 100MB",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
get: {
tags: ["documents"],
summary: "รายการ version ทั้งหมดของเอกสาร (ใหม่ → เก่า)",
description:
"version ทุกตัวของเอกสารที่ระบุด้วย `fid` ใน Bearer token เรียง **ใหม่ → เก่า** — " +
"แต่ละรายการมีผู้อัปโหลด, เวลา, commit message, ค่า Content Control ตอนอัปโหลด " +
"(`fields`) และว่ามีสำเนา PDF ไหม (`hasPdf`) — ใช้เขียน UI version history ของ add-in\n\n" +
"docId นี้ยังไม่เคยมี version เลย = 404 (สำหรับ add-in ถือเหมือน list ว่าง — " +
"กดบันทึกเพื่อสร้าง version 1 ได้ทันที)",
security: [{ DocverBearer: [] }],
responses: {
200: {
description: "รายการ version เรียงใหม่ → เก่า",
content: {
"application/json": {
schema: {
type: "array",
items: { $ref: "#/components/schemas/VersionRecord" },
},
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: { $ref: "#/components/responses/DocverUnauthorized" },
404: {
description: "ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยอัปโหลด version)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/documents/versions/{versionId}/download-link": {
get: {
tags: ["documents"],
summary: "ขอ presigned URL ดาวน์โหลดไฟล์ของ version (docx/pdf)",
description:
"ขอ **presigned URL อายุสั้น** สำหรับดาวน์โหลดไฟล์ของ version หนึ่ง — URL ชี้ที่ " +
"MinIO ตรง ๆ ให้เปิดในเบราว์เซอร์/Word ของผู้ใช้\n\n" +
"- อายุ URL ตาม env `DOCVER_LINK_TTL_SECONDS` (default 900 วินาที) — " +
"หมดอายุแล้วขอใหม่ได้เรื่อย ๆ\n" +
"- ต้องเป็น version ของเอกสารเดียวกับ `fid` ใน token (โยน versionId ข้ามเอกสารไม่ได้)\n" +
"- ขอ `format=pdf` ใน version ที่ไม่มีสำเนา PDF = 404",
security: [{ DocverBearer: [] }],
parameters: [
{
name: "versionId",
in: "path",
required: true,
schema: { type: "string", format: "uuid" },
description: "id ของ version (field versionId ใน VersionRecord)",
},
{
name: "format",
in: "query",
required: false,
schema: { type: "string", enum: ["docx", "pdf"], default: "docx" },
description: "ชนิดไฟล์ที่จะดาวน์โหลด (ไม่ส่ง = docx)",
},
],
responses: {
200: {
description: "presigned URL พร้อมเวลาหมดอายุ",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/DownloadLink" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: { $ref: "#/components/responses/DocverUnauthorized" },
404: {
description:
"ไม่พบ version นี้ของเอกสาร หรือขอ format=pdf ใน version ที่ไม่มีสำเนา PDF",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/documents/latest": {
get: {
tags: ["documents"],
summary:
"ดาวน์โหลดไฟล์ของ version ล่าสุด (docx พร้อม stamp ticket ใหม่ / pdf ตรง ๆ)",
description:
"ให้ backend e-Court ดึงไฟล์ **version ล่าสุด** ของเอกสารกลับไปเปิดให้ผู้ใช้ " +
"(server-to-server — ไม่ใช้ Bearer token)\n\n" +
"- **auth**: header `x-api-key` เทียบ env `DOCVER_API_KEY` **หรือ** " +
"`EXTERNAL_API_KEY` ตัวใดตัวหนึ่ง (ต่างจาก /token ที่รับ DOCVER_API_KEY " +
"อย่างเดียว) · ทั้งคู่ไม่ตั้ง = 401 fail closed\n" +
"- เอกสารถูกระบุด้วย `fid` **ใน ticket_data** (ไม่ใช่ token)\n" +
"- `type=pdf` → คืนไบต์ pdf ตรง ๆ (version ล่าสุดไม่มีสำเนา PDF = 404)\n" +
"- `type=docx` → server เติม `exp` ใหม่ (now + `TICKET_SEC` default 300 วิ) + " +
"คำนวณ `HmacSha256` (secret ตาม chain `ADDIN_TICKET_SECRET` → `SECRET` → " +
"`\"super_secret\"` ให้ตรงฝั่ง verify ของ server — ทาง ticket ของ /token) และ `Ed25519` (เฉพาะเมื่อ " +
"`PRIVATE_KEY_HEX` ตั้ง) แล้วเขียน custom properties ลง**สำเนาในหน่วยความจำ** " +
"ก่อนคืนไฟล์ — object ใน MinIO ไม่ถูกแต่ (non-destructive) และเขียนทับ stamp " +
"เดิมของ template ได้\n" +
"- **ticket_data บังคับ `fid` + `uid` + `uname`** (`tid` optional · `exp` ถ้าส่ง " +
"ต้องเป็นจำนวนเต็ม แต่จะถูกเติมใหม่เสมอ) เพราะ parser ฝั่ง add-in บังคับทั้งสาม — " +
"⚠️ `uid` ต้องเป็น*รหัส*ผู้ใช้ (ไม่ใช่ชื่อ) และต้องส่ง `uname` (ชื่อแสดงผล)\n" +
"- ticket_data หลังเติม exp ยาวเกิน 255 ตัวอักษร = 400 (ข้อจำกัด custom property " +
"ของ Word)",
security: [{ ApiKeyAuth: [] }],
parameters: [
{
name: "type",
in: "query",
required: true,
schema: { type: "string", enum: ["docx", "pdf"] },
description: "ชนิดไฟล์ที่จะดาวน์โหลด (บังคับระบุ — ไม่มีค่า default)",
},
{
name: "ticket_data",
in: "query",
required: true,
schema: { type: "string" },
description:
"JSON string ของ ticket เช่น " +
"`{\"fid\":\"DOC-2569-0001\",\"uid\":\"123\",\"uname\":\"สมชาย ใจดี\"}` " +
"(ต้อง URL-encode — ใช้ URLSearchParams ของฝั่งเรียกก็ได้)",
example:
"{\"fid\":\"DOC-2569-0001\",\"uid\":\"123\",\"uname\":\"สมชาย ใจดี\"}",
},
],
responses: {
200: {
description:
"ไบต์ของไฟล์ (docx = ผ่านการ stamp ticket ใหม่ · pdf = ตรงจาก version ล่าสุด)",
headers: {
"Content-Disposition": {
description: "attachment พร้อมชื่อไฟล์ของ version (ชื่อไทยผ่าน filename*)",
schema: { type: "string" },
},
"Cache-Control": {
description: "no-store — ไฟล์ผูกกับ ticket/exp ของ request นั้น",
schema: { type: "string" },
},
},
content: {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
schema: { type: "string", format: "binary" },
},
"application/pdf": {
schema: { type: "string", format: "binary" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: {
description:
"x-api-key ไม่ตรงทั้ง DOCVER_API_KEY และ EXTERNAL_API_KEY หรือทั้งคู่ไม่ตั้งค่า " +
"(ไม่ใส่ header กับ key ผิดได้ข้อความเดียวกัน)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
404: {
description:
"ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยอัปโหลด version) หรือ type=pdf แต่ version " +
"ล่าสุดไม่มีสำเนา PDF",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
},
components: {
securitySchemes: {
ApiKeyAuth: {
type: "apiKey",
in: "header",
name: "x-api-key",
description:
"x-api-key ของ N10 = env EXTERNAL_API_KEY · ของการขอ token กลุ่ม documents = env " +
"DOCVER_API_KEY · ของกลุ่ม Judge's Note (mint login-token + stage เอกสาร base64) " +
"= env DOCVER_API_KEY เช่นกัน (คนละค่ากับของ N10 — กดปุ่ม Authorize ใส่ครั้งเดียว " +
"แล้วใช้ได้ทุก request ของกลุ่มที่ key ตรง) · GET /api/documents/latest พิเศษ: " +
"รับได้ทั้งสองค่า (DOCVER_API_KEY **หรือ** EXTERNAL_API_KEY ตัวใดตัวหนึ่งตรง = ผ่าน)",
},
DocverBearer: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
description:
"access_token จาก POST /api/documents/token — แนบเป็น " +
"`Authorization: Bearer <token>` กับ endpoint อื่นของกลุ่ม documents ทุกตัว\n" +
"(ตอนกรอกใน Swagger: ใส่เฉพาะค่า token — คำว่า Bearer ไม่ต้องพิมพ์เอง)",
},
},
responses: {
BadRequest: {
description:
"request ไม่ถูกต้อง (field หาย / ชนิดไฟล์ผิด / ค่า enum ไม่รู้จัก / " +
"JSON ผิด format / ไฟล์แนบใหญ่เกิน 100MB)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
DocverUnauthorized: {
description:
"ไม่ได้แนบ Authorization: Bearer หรือ token ไม่ถูกต้อง/หมดอายุ (กลุ่ม documents — ข้อความเดียวกันทุกสาเหตุ)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
ServerError: {
description: "เกิดข้อผิดพลาดของระบบ (เช่นไฟล์ .docx ไม่ถูกต้อง / DB ล้ม)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
},
schemas: {
Error: {
type: "object",
properties: {
error: { type: "string" },
},
},
TokenRequest: {
type: "object",
description:
"claims ที่จะฝังใน access_token (ทางเข้า x-api-key) — key อื่น = 400",
required: ["userid", "username", "fid"],
properties: {
userid: { type: "string", description: "รหัสผู้ใช้ (บังคับ, ≤ 100 ตัวอักษร)" },
username: { type: "string", description: "ชื่อผู้ใช้ (บังคับ, ≤ 200 ตัวอักษร)" },
fid: {
type: "string",
description:
"fileId ของ e-Court (บังคับ, ≤ 200 ตัวอักษร) — เป็น docId ของทุก operation ตรง ๆ",
},
template: {
type: "string",
nullable: true,
description:
"รหัส template ที่ใช้สร้างเอกสาร — กลายเป็น templateId ตอนอัปโหลด version",
},
},
},
TicketTokenRequest: {
type: "object",
description:
"ทางเข้าของ add-in (ไม่ต้องมี x-api-key) — ส่ง ticket ที่ e-Court stamp มากับ " +
"ไฟล์ .docx ใน custom properties ตามเป๊ะ · key อื่น = 400 · server ตรวจ " +
"HMAC-SHA256(ลับ, ticket_data ดิบ) เทียบ HmacSha256 + exp ของ ticket — " +
"ไม่ผ่าน = 401 ข้อความเดียวกันทุกสาเหตุ",
required: ["ticket_data", "HmacSha256"],
properties: {
ticket_data: {
type: "string",
description:
'JSON string ของ ticket เช่น {"exp":1793067433,"fid":"…","tid":"…","uid":"…","uname":"…"} ' +
"(exp = unix วินาที · tid optional · ส่งตรง ๆ ไม่แก้ string — HMAC ผูกกับค่าดิบ)",
},
HmacSha256: {
type: "string",
description: "hex 64 ตัว ของ HMAC-SHA256(secret, ticket_data ดิบ)",
},
},
},
LoginTokenRequest: {
type: "object",
description:
"ทางเข้า mint login-token ของแอป note (x-api-key เช่นเดียวกับ TokenRequest — " +
"บอกความต้องการด้วย purpose:\"login\") · ไม่ส่ง purpose = ทางเข้า docver ของ " +
"TokenRequest ข้อ 1 · key อื่นนอกจาก 5 ตัวนี้ = 400 · token ที่ได้เป็น " +
"purpose=login ใช้ได้กับการเข้าแอป note เท่านั้น (D4)",
required: ["userid", "username", "fid", "purpose"],
properties: {
userid: { type: "string", description: "u_userid ของ e-Court (1–100 ตัวอักษร)" },
username: { type: "string", description: "u_userlogin ของ e-Court (1–200 ตัวอักษร)" },
fid: {
type: "string",
description:
"fileId ของ e-Court — ใช้เป็น fileId ของ N8 เสมอ (1–200 ตัวอักษร)",
},
ttl_seconds: {
type: "integer",
minimum: 60,
maximum: 600,
default: 300,
description: "อายุ token ทางเลือกช่วง 60–600 วินาที (credential อยู่ใน URL ต้องอายุสั้น)",
},
purpose: {
type: "string",
enum: ["login"],
description: 'ค่าเดียวที่รับ — "login" (ค่าอื่น = 400)',
},
},
},
TokenResponse: {
type: "object",
description: "access_token ของกลุ่ม documents (JWT HS256)",
properties: {
access_token: { type: "string", description: "JWT พร้อมใช้แนบเป็น Bearer" },
token_type: { type: "string", example: "Bearer" },
expires_in: {
type: "integer",
description: "อายุวินาที (env DOCVER_TOKEN_TTL_SECONDS, default 604800 = 7 วัน)",
},
},
},
SessionExchangeRequest: {
type: "object",
description:
"credential ของลิงก์ e-Court ที่จะแลกเป็น session token — ส่งทางเดียว: `t` " +
"**หรือ** `ticket_data`+`HmacSha256` (สองชุดพร้อมกัน = 400 ตาม D12) · " +
"allowlist 5 field เป๊ะ — ส่ง field อื่น = 400",
properties: {
t: {
type: "string",
description:
'login-token (JWT purpose=login) จาก POST /api/auth/token แบบ purpose:"login" — ≤ 8192 ตัวอักษร',
},
ticket_data: {
type: "string",
description:
"JSON string ของ ticket ชุดเดียวกับที่ e-Court stamp ในไฟล์ .docx — " +
"≤ 2048 ตัวอักษร (ส่งตรง ๆ ไม่แก้ string — HMAC ผูกกับค่าดิบ)",
},
HmacSha256: {
type: "string",
description: "hex 64 ตัว ของ HMAC-SHA256(secret, ticket_data ดิบ) — ≤ 128 ตัวอักษร",
},
fileUrl: {
type: "string",
description:
"URL เอกสารบน e-Court — ต้องผ่าน allowlist ของ N8 (admincourt.go.th + env " +
"EXTERNAL_FILE_HOSTS) · ส่งมา = ลงทะเบียนเอกสารตอนแลก token (≤ 2048 ตัวอักษร)",
},
typeId: {
type: "string",
description: "รหัสกลุ่มเอกสารของ e-Court (≤ 200 ตัวอักษร)",
},
},
},
SessionResponse: {
type: "object",
description:
"session ของแอป note — client เก็บใน localStorage (\"note-auth\") แล้วแนบ " +
"Authorization: Bearer กับทุก API/Server Action ของแอป",
properties: {
access_token: {
type: "string",
description: "JWT purpose=login อายุ 8 ชม. — ใช้ได้กับแอป note เท่านั้น",
},
token_type: { type: "string", example: "Bearer" },
expires_in: {
type: "integer",
description: "อายุวินาที (NOTE_SESSION_TTL = 28800 = 8 ชม.)",
example: 28800,
},
userid: { type: "string", description: "รหัสผู้ใช้จาก credential (u_userid ของ e-Court)" },
username: { type: "string", description: "ชื่อแสดงผลจาก credential" },
fileId: {
type: "string",
format: "uuid",
description:
"มีเมื่อส่ง fileUrl และลงทะเบียนสำเร็จ หรือ (ลิงก์ fid-only ของ N11) เจอเอกสารของ " +
"fid ที่พร้อมใช้ — id ของเอกสารในระบบนี้ (ใช้เข้าหน้าอ่าน /note/annotate/<fileId>)",
},
fileError: {
type: "string",
description:
"มีเมื่อลงทะเบียน fileUrl ไม่สำเร็จ (ไม่ผ่าน allowlist ฯลฯ) หรือลิงก์ fid-only " +
"ไม่เจอเอกสาร/พ้นช่วงจัดเตรียม — token ยังออกให้ หน้าแรกแสดงสาเหตุ",
},
},
},
ExtractResponse: {
type: "object",
properties: {
data: {
type: "object",
description: "ชื่อแท็ก Content Control → ข้อความในนั้น (หลายบรรทัดคั่นด้วย \\n)",
additionalProperties: { type: "string" },
example: {
"cc": {
"blackyear": "๙๐/๒๕๖๙",
"redyear": "xxx",
"curdate": "วันที่ เดือน สิงหาคม พุทธศักราช ๒๕๖๙ ",
"accuser": "นายธีรพล ร่มเริง",
"detail1": "ตุลาการเจ้าของสำนวนได้พิจารณาคำฟ้อง...",
},
"bookmark": {
"signature": ""
},
"properties": {
"standard": {
"creator": "mam",
"created": "2026-10-22T02:58:00.000Z",
"modified": "2026-10-22T07:08:00.000Z"
},
"extended": {
"template": "TS_P2_IN.dotx",
"totalTime": 172,
"pages": 2,
"words": 124,
},
"custom": {
"ticket_data": "{\"exp\":1793013726,\"fid\":\"T_no_warning\",\"tid\":\"T_X\",\"uid\":\"john\",\"uname\":\"John Doe\"}",
"HmacSha256": "1dc53fbd48410845c895ddb841a8fe4ad93da413126fae9c688202e57f2baaa9",
"Ed25519": "95ea0ca6d81df885c22a2334ed89b2fc3c75a1a309fb5843e0deb0a16c9d05a45b345fb28fd606f3b152285e70560ad43a6389534794aa0d6f06d13f1307b005"
}
},
"verify": {
"exp": "26/10/2569 18:22:06",
"HmacSha256": true,
"Ed25519": true
}
},
},
},
},
ExternalQuery: {
type: "object",
description:
"ต้องมี fileId หรือ typeId อย่างน้อยหนึ่งอย่าง (ส่งทั้งคู่ได้ = AND เหลือเอกสารเดียว)",
properties: {
fileId: {
type: "string",
description: "id เอกสารของ e-Court — ตรงกับที่ส่งมากับลิงก์ ?fileId= ตอนเปิดเอกสาร",
},
typeId: {
type: "string",
description: "รหัสกลุ่มเอกสาร — คืนทุกไฟล์ในกลุ่มนี้ (เรียงตามวันที่สร้าง)",
},
filter: {
type: "object",
properties: {
type: {
type: "string",
enum: ["bookmarks", "annotations"],
description: "เลือกเฉพาะชนิดรายการ (ไม่ส่ง = ทั้งสอง ส่วนที่ไม่เลือกคืนเป็น array ว่าง)",
},
userId: {
description: "กรองตามเจ้าของรายการ (string เดี่ยวหรือ array)",
oneOf: [
{ type: "string" },
{ type: "array", items: { type: "string" } },
],
},
annotationTypes: {
description: "กรองชนิดของ annotation — มีผลเฉพาะเมื่อรวม annotations",
oneOf: [
{ type: "string", enum: [...ANNOTATION_TYPE_ENUM] },
{
type: "array",
items: { type: "string", enum: [...ANNOTATION_TYPE_ENUM] },
},
],
},
},
},
},
},
DocumentsResponse: {
type: "object",
properties: {
documents: {
type: "array",
items: { $ref: "#/components/schemas/DocumentBundle" },
},
},
},
DocumentBundle: {
type: "object",
description: "field ของเอกสารอยู่ระดับเดียวกับ annotations/bookmarks (bundle ยุบชั้น)",
properties: {
id: { type: "string", format: "uuid" },
externalFileId: {
type: "string",
nullable: true,
description:
"ค่า hash ของ fileId (HMAC-SHA256 ทางเดียว — แปลงกลับเป็น fid เดิมไม่ได้ " +
"2026-11-04: ระบบเก็บ hash แทน fid ดิบ; ค่านี้คงเดิมทุกครั้งที่ถามด้วย fid เดียวกัน " +
"จึงใช้เทียบความเหมือนได้อย่างเดียว · null = ไฟล์ url:-fallback หรืออัปโหลดในกลุ่ม)",
},
title: { type: "string" },
typeId: { type: "string", nullable: true },
createdAt: { type: "string", format: "date-time" },
annotations: {
type: "array",
items: { $ref: "#/components/schemas/ExternalAnnotation" },
},
bookmarks: {
type: "array",
items: { $ref: "#/components/schemas/ExternalBookmark" },
},
},
},
ExternalAnnotation: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
fileId: { type: "string", format: "uuid" },
pageIndex: { type: "integer", description: "0-based" },
userId: { type: "string" },
userName: { type: "string" },
type: { type: "string", enum: [...ANNOTATION_TYPE_ENUM] },
data: {
type: "object",
description:
"ข้อมูลเฉพาะชนิด (พิกัดเป็น % ของขนาดหน้า) เช่น HIGHLIGHT { text, color, rects }, COMMENT { message, position }",
},
createdAt: { type: "string", format: "date-time" },
updatedAt: { type: "string", format: "date-time" },
resolved: { type: "boolean" },
resolvedAt: { type: "string", format: "date-time", nullable: true },
url: {
type: "string",
description: "deep link กลับหน้าอ่าน พร้อม ?ann= ของรายการนี้",
},
replies: {
type: "array",
items: { $ref: "#/components/schemas/ExternalReply" },
},
},
},
ExternalBookmark: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
fileId: { type: "string", format: "uuid" },
pageIndex: { type: "integer", description: "0-based" },
userId: { type: "string" },
userName: { type: "string" },
title: { type: "string" },
parentId: {
type: "string",
nullable: true,
description: "null = ระดับบนสุด (มีค่า = sub ของ bookmark นั้น)",
},
note: { type: "string", nullable: true },
yOffset: { type: "number", description: "ตำแหน่งบนหน้า (% ของความสูง)" },
createdAt: { type: "string", format: "date-time" },
url: {
type: "string",
description: "deep link กลับหน้าอ่าน พร้อม ?page= 1-based ของหน้านี้",
},
},
},
ExternalReply: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
annotationId: { type: "string", format: "uuid" },
userId: { type: "string" },
userName: { type: "string" },
message: { type: "string" },
createdAt: { type: "string", format: "date-time" },
},
},
UploadVersionResult: {
type: "object",
description: "ตอบกลับหลังอัปโหลด version สำเร็จ",
properties: {
hasPdf: { type: "boolean", description: "true = version นี้มีสำเนา PDF" },
versionId: { type: "string", format: "uuid" },
versionNumber: {
type: "integer",
description: "เลข version ที่ server จ่าย (ค่าจริง — ไม่ใช่ที่ client คาด)",
},
},
},
ExtractSnapshot: {
type: "object",
description:
"ก้อนข้อมูลแกะจากไฟล์ .docx — รูปเดียวกับ return ของ POST /api/template/extract " +
"(ใช้ helper ตัวเดียวกันใน lib/lib-tmpl.ts)",
properties: {
cc: {
type: "object",
description: "ค่า Content Control ทุกตัว รูปแบบ `{ [tag]: ข้อความ }`",
additionalProperties: { type: "string" },
},
bookmark: {
type: "object",
description: "ข้อความใน bookmark รูปแบบ `{ [ชื่อ bookmark]: ข้อความ }`",
additionalProperties: { type: "string" },
},
properties: {
type: "object",
description: "document properties — standard (core) / extended (app) / custom",
properties: {
standard: { type: "object", additionalProperties: { type: "string" } },
extended: { type: "object", additionalProperties: { type: "string" } },
custom: { type: "object", additionalProperties: {} },
},
},
verify: {
type: "object",
description:
"ผลตรวจ ticket ที่ stamp มากับไฟล์ — exp (วันที่ไทย +07:00) และ HmacSha256 " +
"(boolean — ตรวจด้วย secret ของ ticket ฝั่ง server)",
additionalProperties: {},
},
},
},
VersionRecord: {
type: "object",
description: "รายการ version หนึ่งตัว (GET /versions คืนเป็น array ใหม่ → เก่า)",
properties: {
versionId: { type: "string", format: "uuid" },
versionNumber: { type: "integer" },
createdAt: { type: "string", format: "date-time" },
createdByUserId: { type: "string" },
createdByUserName: { type: "string" },
contentHash: {
type: "string",
description: "SHA-256 hex ของไฟล์ .docx (คำนวณฝั่ง server เสมอ)",
},
fields: {
nullable: true,
description:
"snapshot ตอนอัปโหลด — **ก้อนเดียวกับ return ของ /api/template/extract** (server " +
"แกะจากไฟล์ .docx เอง; null = version เก่าก่อน 2026-10-30)",
allOf: [{ $ref: "#/components/schemas/ExtractSnapshot" }],
},
message: { type: "string", nullable: true, description: "commit message" },
hasPdf: { type: "boolean" },
},
},
DownloadLink: {
type: "object",
properties: {
url: { type: "string", description: "presigned URL ของ MinIO (GET โดยตรง)" },
expiresAt: { type: "string", format: "date-time" },
},
},
},
},
} as const;