// OpenAPI 3.0 spec ของ API ทดสอบทั้งหมดของ judes-note — เส้นเดียวกับที่ Swagger UI // แสดงผล: กลุ่ม template (DOCX Content Control) + N10 external API + docver // (จัด version เอกสาร) // 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)", }, servers: [{ url: BASE_PATH, description: "origin เดียวกับหน้านี้ (basePath /docedit)" }], tags: [ { name: "template", description: "Next Generation of Template : เป็นเทมเพลต MS Word(docx) รองรับรูปแบบข้อมูลที่ซับซ้อนได้ ", }, { name: "documents", description: "จัด version เอกสาร (docver): เอกสารเดียวต่อ docId แต่ละ " + "version เก็บ .docx + .pdf ใน MinIO พร้อมผู้อัปโหลด — auth ด้วย access_token " + "ของกลุ่มนี้: ขอที่ POST /api/documents/token (ยืนยัน x-api-key กับ env " + "DOCVER_API_KEY) แล้วแนบ Authorization: Bearer ทุก request — identity/template " + "และ docId (= fid — fileId ของ e-Court, string) แกะจาก claims ของ token ทั้งหมด " + "ไม่รับจาก URL/multipart form อีก", }, { name: "Judge's Note", description: "API ภายนอก (N10) — backend e-Court เรียกแบบ server-to-server ของ (Judge's Note)" + "ดึง annotations/bookmarks ของเอกสารที่เคยเปิดในระบบนี้", }, ], 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 ตรง ๆ (เลือกอย่างใดอย่างหนึ่ง)
'+ 'สำหรับนักพัฒนา Template เท่านั้น ไม่ต้อง ใช้ API Key ต้องตั้งค่า secret และ private key เองไม่ใช้ของระบบของระบบ', 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)
{\"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
openssl genpkey -algorithm ED25519 -out private.pem
openssl pkey -in private.pem -outform DER | tail -c 32 | xxd -p -c 32 > private.hex
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 ซึ่งเหมาะกับการเอาไปใส่ในฐานข้อมูล"+ 'สำหรับนักพัฒนา Template เท่านั้น ไม่ต้อง ใช้ API Key ต้องตั้งค่า secret และ Public key เองไม่ใช้ของระบบของระบบ', 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
openssl genpkey -algorithm ED25519 -out private.pem
openssl pkey -in private.pem -outform DER | tail -c 32 | xxd -p -c 32 > private.hex
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) "+ "จะได้เอกสาร รูปแบบฟอร์มที่กรอกค่าแล้วดึงค่ากลับมาใช้งานต่อได้เช่นอัปเดตฐานข้อมูล
", 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 อ่านข้อมูล JSON ประจำไฟล์ fid จาก TMPL_PATH แล้วส่งไปประมวลผลต่อที่ /api/template/generate เพื่อสร้างและคืนค่าเป็นไฟล์ DOCX stream ใช้เพื่อเป็นตัวอย่างสำหรับนักพัฒนา ต้องนำออกก่อน 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 อ่านข้อมูล JSON และประมวลผลสร้างไฟล์ DOCX stream คืนกลับไป "+ "จะเรียก generate ในเบื้องหลัง ใช้เพื่อเป็นตัวอย่างสำหรับนักพัฒนา ต้องนำออกก่อน 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/documents/token": { post: { tags: ["documents"], summary: "ขอ access_token ของกลุ่ม documents (docver)", description: "ออก access_token ใช้กับ endpoint อื่นของกลุ่ม documents เท่านั้น — token เป็น " + "JWT HS256 ฝัง claims {userid, username, fid, template} ตามที่ส่งมา อายุตาม env " + "DOCVER_TOKEN_TTL_SECONDS (default 3600 วินาที) · identity (userId/userName), " + "templateId และ docId (= fid) ตอนทำงานแกะจาก claims ของ token นี้ — " + "fid จึงบังคับ", security: [{ ApiKeyAuth: [] }], requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/TokenRequest" }, examples: { "ครบทุก claim": { value: { userid: "123", username: "Sorawit Bholsithi", fid: "3", template: "T12", }, }, "ขั้นต่ำ (template เป็นทางเลือก)": { value: { userid: "123", username: "Sorawit Bholsithi", fid: "3" }, }, }, }, }, }, responses: { 200: { description: "access_token พร้อมใช้ (แนบเป็น Authorization: Bearer ทุก request ถัดไป)", content: { "application/json": { schema: { $ref: "#/components/schemas/TokenResponse" }, }, }, }, 400: { $ref: "#/components/responses/BadRequest" }, 401: { description: "ไม่ได้ส่ง header x-api-key หรือค่าไม่ถูกต้อง (env DOCVER_API_KEY)", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" }, }, }, }, 500: { $ref: "#/components/responses/ServerError" }, }, }, }, "/api/documents/versions": { post: { tags: ["documents"], summary: "อัปโหลด version ใหม่ของเอกสาร (docx + pdf)", description: "เอกสารถูกระบุด้วย fid ใน Bearer token (docId = fid) — สร้างเอกสาร " + "อัตโนมัติเมื่อ docId ยังไม่เคยมี แล้วเก็บไฟล์ .docx (+ .pdf ถ้าแนบมา) ลง MinIO " + "พร้อมผู้อัปโหลด — versionNumber ที่ server จ่ายคือค่าจริงเสมอ (client ที่เลขไม่ตรง " + "ต้องแก้ XML part ของตัวเอง) · pdf เป็น optional: ไม่แนบ/ไฟล์เปล่า = version นี้ไม่มี " + "สำเนา PDF (ดู hasPdf ใน response) · ผู้อัปโหลด (userId/userName) และ templateId " + "แกะจาก Bearer token ไม่รับใน form (ส่ง field เหล่านั้นมา = 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)", }, fields: { type: "string", description: 'JSON string ของค่า Content Control เช่น {"blackno":"คดีหมายเลขดำที่"}', }, 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: "เอกสารถูกระบุด้วย fid ใน Bearer token (docId = fid) — version ทุกตัว " + "ของเอกสารนั้น พร้อมผู้อัปโหลด (userId/userName), เวลา, commit message, ค่า fields " + "และ hasPdf — ใช้เขียน UI version history ของ add-in", 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: "เอกสารถูกระบุด้วย fid ใน Bearer token (docId = fid) · คืน URL อายุสั้น " + "(env DOCVER_LINK_TTL_SECONDS, default 900 วินาที) ให้เปิดในเบราว์เซอร์/Word ของ " + "ผู้ใช้ — URL อ้าง endpoint MinIO ตรง ๆ จึงต้องเป็นโฮสต์ที่ผู้ใช้เข้าถึงได้ · ขอ pdf " + "ใน version ที่ไม่มีสำเนา = 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" }, }, }, }, }, 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 (คนละค่ากับของ N10 — กดปุ่ม Authorize ใส่ครั้งเดียวแล้วใช้ได้ทุก " + "request ของกลุ่มที่ key ตรง)", }, DocverBearer: { type: "http", scheme: "bearer", bearerFormat: "JWT", description: "access_token จาก POST /api/documents/token — แนบเป็น Authorization: Bearer " + "กับ endpoint อื่นของกลุ่ม documents ทุกตัว", }, }, 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 ของกลุ่ม documents — 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", }, }, }, 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 3600)", }, }, }, 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: "id ของ e-Court (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 คาด)", }, }, }, 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: { type: "object", nullable: true, description: "ค่า Content Control ตอนอัปโหลด (ไม่ส่ง = null)", additionalProperties: { type: "string" }, }, 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;