eCourt-note/lib/openapi.ts
Your Name d37bc2a3f0 feat(sso): switch note session from cookies to token
เปลี่ยน session ของแอป note จาก cookie คู่ oacCase/oacCaseSig มาเป็น
access_token แบบ add-in ตามคำขอผู้ใช้ (2026-10-30):

- POST /api/note/auth/session แลก credential ของลิงก์ e-Court
  (JWT ทาง A / ticket ทาง B) เป็น access_token purpose=login อายุ 8 ชม.
  พร้อมลงทะเบียนเอกสาร N8 แบบ trusted ฝั่ง server (fileId จาก fid)
- token เก็บ localStorage "note-auth" (lib/note-session.ts) + Bearer
  ผ่าน noteAuthFetch · Server Actions ทุกตัวรับ token เป็น param แรก
  และแกะ identity เอง (lib/note-token.ts) — ไม่เชื่อ userId จาก client
- หน้า protected เป็น shell + client gate (NoteHome bootstrap ที่ /note,
  AnnotateView + GET /api/note/files/[id]/bootstrap)
- ถอด: middleware.ts, lib/session.ts, utils/user.ts, action logout,
  GET /api/note/auth/e-court, entry /note/annotate?fileUrl=
- ทาง ticket ปฏิเสธ secret default "super_secret" เฉพาะ production
- GET /api/note/files/open: ลิงก์เปิดไฟล์ตัวอย่างแบบ /tmpl (path ใน
  เครื่อง /samples เท่านั้น)
- หน้า /note/dev: ลิงก์ A Link Get(new) ที่ mint credential แล้วยิง
  เข้า flow SSO จริง (JWT/ticket สลับได้) + section Flow + เรียบเรียง
  ภาษาให้อ่านง่าย · หน้า /note เป็นหน้าข้อมูลระบบ
- แก้ตาม code review: redo closure เรียก action ด้วย signature เก่า,
  bootstrap timeout + strip credential ที่ผิดรูป, แยก no-session ออก
  จาก network error, ห้ามอักขระ & = + % ใน identity, ตรวจ purpose
  ก่อน exp ของ login-token, audit log strip อักขระควบคุม
- เอกสาร: docs/PLAN-SSO.md §0.5 (ฉบับ token), CLAUDE.md, .env.example

ทดสอบ: tsc ผ่าน · vitest ผ่านทั้งชุด · ทดสอบจริงผ่าน Playwright
(SSO → viewer → เปลี่ยนสี annotation → undo → redo ครบวงจร)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-30 18:05:36 +07:00

1271 lines
64 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 เอกสาร)
// 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 เอกสาร 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)",
},
],
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 อ่านข้อมูล 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/note/auth/token": {
post: {
tags: ["Judge's Note"],
summary: "mint login-token สำหรับลิงก์ SSO เข้าแอป note (PLAN-SSO.md)",
description:
"ออก `access_token` ชนิด `purpose=login` ไว้ฝังเป็น param เดียว `t=` ของลิงก์ " +
"`/note?fileUrl=…&t=<access_token>` ที่ให้ผู้ใช้คลิกจากหน้า e-Court " +
"(ทางเข้า session ทางเดียวของแอป note — หน้าแรกแลก credential เป็น session " +
"token ที่ POST /api/note/auth/session แบบ add-in)\n\n" +
"- token ใช้ได้กับ**การเข้าแอป note เท่านั้น** — เอาไปยิง `/api/documents/*` " +
"ของกลุ่ม docver ไม่ได้ (401)\n" +
"- แนะนำให้ mint แบบ real-time ตอน render หน้า/ปุ่มให้ user ปัจจุบันเท่านั้น — " +
"**ห้ามเก็บ token ลง DB / ฝังอีเมล / แชท** (อายุสั้นตาม ttl_seconds)\n" +
"- `userid`/`username` = ค่าเดียวกับ `u_userid`/`u_userlogin` ของ e-Court " +
"(สิทธิ์เจ้าของ annotation ของแอป note ผูกกับ userid — ส่งผิดคน = ข้อมูลผูกคนผิด)\n" +
"- `fileUrl` ของลิงก์ปลายทางต้องผ่าน allowlist ของ N8 (admincourt.go.th + `EXTERNAL_FILE_HOSTS`)",
security: [{ ApiKeyAuth: [] }],
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["userid", "username", "fid"],
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 ต้องอายุสั้น)",
},
},
},
examples: {
"mint ให้ผู้ใช้ oom เปิดไฟล์ F123": {
value: { userid: "oom", username: "สรวิชญ์", fid: "F123", ttl_seconds: 300 },
},
},
},
},
},
responses: {
200: {
description: "login-token พร้อมฝังเป็น param t ของลิงก์ e-court",
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/token": {
post: {
tags: ["documents"],
summary: "ขอ access_token ของกลุ่ม documents (docver)",
description:
"ออก `access_token` (JWT HS256) ไว้เรียก endpoint อื่นของกลุ่ม documents — " +
"token ฝัง claims `{userid, username, fid, template}` อายุตาม env " +
"`DOCVER_TOKEN_TTL_SECONDS` (default 3600 วินาที)\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 เอง · ' +
"**ticket หมดอายุ** ยังออกให้ต่อเมื่อเอกสารนี้ (`fid`) เคยมี version ที่สร้างโดย `uid` เดียวกัน " +
"(ผู้ที่เคยทำงานกับเอกสารเดิมกลับมาใช้ต่อได้แม้ ticket หมดอายุ — ไม่เคยทำงาน = 401)\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: ticket ไม่ถูกต้อง/หมดอายุ หรือไม่ได้ส่งมา (ข้อความเดียวกันทุกสาเหตุ)",
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)**",
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 **ทุกตัว** ในเอกสาร รูปแบบ ' +
'`{ [tag]: ข้อความ }` (add-in อ่านมาตอนบันทึก version) ' +
'เช่น {"blackno":"คดีหมายเลขดำที่","detail1":"…"}',
},
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" },
},
},
},
},
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 <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 ดิบ)",
},
},
},
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 ทุกตัวตอนอัปโหลด รูปแบบ `{ [tag]: ข้อความ }` (ไม่ส่ง = 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;