feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
# PLAN-DOCVER — Document Versioning API (stand-alone)
|
|
|
|
|
|
|
2026-10-26 06:29:19 +07:00
|
|
|
|
> วันที่ตกลงแผน: 2026-10-20 · เพิ่ม auth ด้วย access_token: 2026-10-22 · สถานะ: implement
|
feat(addin): unmock versioning — real upload/download against docver API
Replace the in-memory versioningService with real calls to the docver
group (/api/documents/*) via apiClient with the ticket-issued Bearer;
keep the mock only for ?addindemo (authService.isDemoMode).
- uploadVersion: multipart POST /api/documents/versions (file, optional
pdf when non-empty, clientVersionId, fields JSON, trimmed message,
fileName) — identity fields are never sent (userId/userName/templateId
in the form = 400 per the 2026-10-22 contract; docId comes from the
fid claim)
- getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds
optional createdByUserName; history panel shows it with userId
fallback)
- getDownloadLink: GET presigned URL (+ ?format=pdf)
- apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin
through withBasePath (/docedit/api/...); the env is now only for a
split deployment (must include basePath, HTTPS)
- documentId/templateId params stay in signatures but are not sent
- tests: versioningService.test.ts covers both modes (real sign-in via
signInWithTicket, FormData contract assertions, DTO mapping, format
query, 401 -> AuthRequiredError, demo mock without network)
- docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section),
addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md
Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket ->
token -> upload docx / docx+pdf -> history -> presigned download of both
formats).
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 12:52:29 +07:00
|
|
|
|
> รอบแรก (API ก่อน) · **ปลด mock wordaddin แล้ว 2026-10-27** — `versioningService`
|
|
|
|
|
|
> ยิงกลุ่มนี้จริงผ่าน Bearer (mock เหลือเฉพาะ `?addindemo`)
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
|
|
|
|
|
|
## 0. บริบท/เป้าหมาย
|
|
|
|
|
|
|
|
|
|
|
|
ทำ backend จริงของฟีเจอร์ **"e-Court Documents Versioning"** (ตอนนี้ wordaddin ใช้ mock ใน
|
|
|
|
|
|
`wordaddin/services/versioningService.ts`):
|
|
|
|
|
|
|
2026-10-26 10:05:01 +07:00
|
|
|
|
- ตารางเอกสารคีย์ด้วย `docId` **ห้ามซ้ำ** (unique index) — **`docId` = claim `fid`
|
|
|
|
|
|
(string) จาก Bearer token ตรง ๆ** (2026-10-26 ถอด path param `{docId}` ออกจาก URL ทุก
|
|
|
|
|
|
endpoint — fileId ของ e-Court ใน token เป็นที่มาเดียวของรหัสเอกสาร)
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
- แต่ละ version อ้าง **object ใน MinIO** — เก็บทั้ง `.docx` และ `.pdf` (pdf เป็น
|
|
|
|
|
|
optional/best-effort ตาม contract ของ add-in — `pdf.size === 0` = ไม่มี, มี `hasPdf` บอกใน
|
|
|
|
|
|
response)
|
|
|
|
|
|
- เก็บ `userId`, `userName`, `createdAt` ต่อ version เพื่อรู้ว่าใครอัปโหลด
|
2026-10-26 10:05:01 +07:00
|
|
|
|
- **identity (`userId`/`userName`/`templateId`) และ `docId` แกะจาก Bearer token** (2026-10-22
|
|
|
|
|
|
— เดิมส่ง plain field ใน multipart): ขอ token ที่ `POST /api/documents/token` ด้วย header
|
2026-10-26 06:29:19 +07:00
|
|
|
|
`x-api-key` แล้วแนบ `Authorization: Bearer <token>` กับทุก request — ระบบ stand-alone
|
|
|
|
|
|
ไม่มี session ร่วมกับใคร (ผู้เรียกคือ add-in ใน Word หรือระบบภายนอก) token จึงเป็น
|
|
|
|
|
|
พาหะ identity เดียว
|
|
|
|
|
|
- **auth ทุก endpoint ของกลุ่มนี้** = Bearer JWT HS256 เซ็นด้วย `DOCVER_TOKEN_SECRET`
|
|
|
|
|
|
(ออก token ผ่าน `x-api-key` เทียบ env `DOCVER_API_KEY` — pattern `lib/external-api-auth.ts`
|
|
|
|
|
|
ของ N10) · ไม่มี CORS
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
|
|
|
|
|
|
ข้อตกลงกับผู้ใช้ (2026-10-20): ทำ API ก่อน **ไม่แต้ wordaddin รอบนี้** — contract ตรง surface
|
|
|
|
|
|
ที่ mock คงไว้เป๊ะ วันหน้าสลับของจริงแค่ตั้ง `NEXT_PUBLIC_ADDIN_API_BASE_URL` แล้วก้อบ body
|
|
|
|
|
|
ของต้นฉบับกลับมา (ดู `docs/WORDADDIN-DEV.md`) · MinIO dev รันผ่าน `docker-compose.yml`
|
|
|
|
|
|
(ผู้เรียกอัปโหลดทั้ง docx+pdf — ฝั่ง add-in ใช้ Office.js แปลง Word→PDF เอง)
|
|
|
|
|
|
|
|
|
|
|
|
Contract อ้างอิง: `docs/addin-ui-flow.md` §7 + `wordaddin/services/versioningService.ts` +
|
|
|
|
|
|
`wordaddin/models/versionRecord.ts` — **โค้ด server ห้าม import จาก `wordaddin/`**
|
|
|
|
|
|
(client-only zone) type define เองใน `db/schema.ts`/`lib`
|
|
|
|
|
|
|
|
|
|
|
|
## 1. Endpoint (URL เต็ม = `/docedit/api/documents/…` — prefix ใหม่ ได้รับยกเว้นจาก
|
|
|
|
|
|
middleware เหมือน `api/template`)
|
|
|
|
|
|
|
2026-10-26 06:29:19 +07:00
|
|
|
|
### `POST /api/documents/token` — ขอ access_token (2026-10-22)
|
|
|
|
|
|
|
|
|
|
|
|
auth: header `x-api-key` เทียบ env `DOCVER_API_KEY` (ไม่ตั้ง/ว่าง = 401 fail closed ·
|
|
|
|
|
|
ไม่ใส่/ผิด = 401 ข้อความเดียวกันเป๊ะ กัน enumerate) — ตรวจก่อนอ่าน body เสมอ
|
|
|
|
|
|
|
feat(addin): implement e-Court ticket auth (ticket_data + HmacSha256)
Replace the token/owner pass-through with the ticket flow from
docs/requirement/addin-workflow.md (full plan: docs/PLAN-ADDIN-AUTH.md):
- utils/docver-ticket.ts: shared pure ticket parse + hex digest helpers,
client/server inject their own crypto (Web Crypto / node:crypto)
- lib/addin-ticket.ts + POST /api/documents/token second entry: body
{ticket_data, HmacSha256} verified server-side (HMAC timing-safe + exp,
secret ADDIN_TICKET_SECRET -> SECRET -> "super_secret"); x-api-key path
unchanged
- wordaddin: ticketService (Web Crypto HMAC, localStorage session
wordaddin-auth keyed by fid, same-origin token request), authService
rewrite (7-step flow, silent renew), apiClient 401 renew-retry once,
metadataService reads ticket_data/HmacSha256, no-access messages per
cause
- scripts/make-ticket-docx.mjs: dev tool to stamp tickets onto any .docx
for testing in real Word
- app/wordaddin/error.tsx: error boundary showing the real error + stack
in the pane (webviews have no devtools)
- tests: docver-ticket + addin-ticket unit suites (21 cases) + temporary
crash repro suite for the ongoing client-crash debug session
- docs sync: PLAN-DOCVER, WORDADDIN-DEV, addin-ui-flow, openapi (two
request bodies for /token), CLAUDE.md, .env.example
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 11:21:15 +07:00
|
|
|
|
**ทางเข้าที่สอง (2026-10-27, add-in ใน Word — PLAN-ADDIN-AUTH.md):** ไม่มี header
|
|
|
|
|
|
`x-api-key` + body `{ticket_data, HmacSha256}` (ชื่อ field เหมือน custom properties
|
|
|
|
|
|
ในไฟล์) — server ตรวจ HMAC-SHA256(secret, ticket_data ดิบ) + exp ผ่าน
|
|
|
|
|
|
`lib/addin-ticket.ts` (env `ADDIN_TICKET_SECRET` → `SECRET` → default `"super_secret"`)
|
|
|
|
|
|
แล้วออก token จาก claims ใน ticket (`uid→userid`, `uname→username`, `fid`, `tid→template`)
|
|
|
|
|
|
· response/error รูปเดียวกับทางแรกทุกอย่าง (HMAC ผิด/หมดอายุ/ไม่ส่งมา = 401 ข้อความ
|
|
|
|
|
|
เดียวกัน · body ผิดรูป = 400)
|
|
|
|
|
|
|
2026-10-26 06:29:19 +07:00
|
|
|
|
JSON body (allowlist — key อื่น = 400 กัน typo):
|
|
|
|
|
|
|
|
|
|
|
|
| field | type | หมายเหตุ |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| `userid` | string | บังคับ — trim 1–100 (claim `userid` → column `user_id`) |
|
|
|
|
|
|
| `username` | string | บังคับ — trim 1–200 (claim `username` → column `user_name`) |
|
2026-10-26 10:05:01 +07:00
|
|
|
|
| `fid` | string | **บังคับ** — trim 1–200 · **fileId ของ e-Court** เป็น `docId` ของทุก operation ตรง ๆ · verify ไม่รับ token ที่ไม่มี fid |
|
2026-10-26 06:29:19 +07:00
|
|
|
|
| `template` | string | optional ≤100 (claim `template` → column `template_id` ตอน upload) |
|
|
|
|
|
|
|
|
|
|
|
|
- env `DOCVER_TOKEN_SECRET` ไม่ตั้ง/ว่าง = **500** "DOCVER_TOKEN_SECRET ยังไม่ได้ตั้งค่า"
|
|
|
|
|
|
(ความผิด config ฝั่ง server ไม่ใช่ความผิดของผู้เรียก — และห้าม log ค่า secret)
|
|
|
|
|
|
- response 200: `{ "access_token": string, "token_type": "Bearer", "expires_in": number }`
|
|
|
|
|
|
— JWT HS256 stateless มี `iat`/`exp` (TTL env `DOCVER_TOKEN_TTL_SECONDS` default 3600)
|
|
|
|
|
|
· verify บังคับ alg=HS256 (กัน alg-confusion) และเทียบ signature แบบ timing-safe
|
|
|
|
|
|
- **endpoint อื่นของกลุ่มนี้ทั้งหมดต้องแนบ `Authorization: Bearer <access_token>`** —
|
|
|
|
|
|
ไม่มี header / รูปผิด / signature ผิด / หมดอายุ / secret ไม่ตั้ง = 401 ข้อความไทย
|
|
|
|
|
|
เดียวกันทุกกรณี (ไม่เปิดให้ใช้ endpoint เป็น oracle รู้สาเหตุ)
|
|
|
|
|
|
|
2026-10-26 10:05:01 +07:00
|
|
|
|
### `POST /api/documents/versions` — อัปโหลด version ใหม่
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
|
2026-10-26 10:05:01 +07:00
|
|
|
|
auth: `Authorization: Bearer` (ดู /token ด้านบน) — identity/templateId ของ row ใหม่และ
|
|
|
|
|
|
`docId` (= `fid` — string) มาจาก claims ของ token ทั้งหมด ไม่รับจาก URL/form อีกต่อไป
|
2026-10-26 06:29:19 +07:00
|
|
|
|
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
multipart/form-data:
|
|
|
|
|
|
|
|
|
|
|
|
| field | type | หมายเหตุ |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| `file` | File (.docx) | บังคับ — ≤ 100MB, magic bytes `PK\x03\x04` |
|
|
|
|
|
|
| `pdf` | File (.pdf) | optional — `size === 0` = ถือว่าไม่มี (add-in แนบ blob เปล่าได้) |
|
|
|
|
|
|
| `clientVersionId` | string | optional ≤100 — GUID ที่ client generate เอง |
|
|
|
|
|
|
| `message` | string | optional — commit message ≤2048 (trim ว่าง = NULL) |
|
|
|
|
|
|
| `fileName` | string | optional — ชื่อไฟล์ดั้งเดิม (ผ่าน `decodeUploadFilename` แล้ว) |
|
|
|
|
|
|
|
2026-10-26 06:29:19 +07:00
|
|
|
|
- field ที่ไม่อยู่ในตาราง = **400 กัน typo เงียบ ๆ** (pattern `parseQueryBody` ของ N10) —
|
|
|
|
|
|
**breaking change 2026-10-22:** `userId`/`userName`/`templateId` ถอดออกจาก form แล้ว
|
|
|
|
|
|
(ส่งมา = 400) identity/template มาจาก Bearer token ของ request นั้นแทน
|
2026-10-31 10:21:17 +07:00
|
|
|
|
· **breaking change 2026-10-30:** `fields` ถอดออกจาก form เช่นกัน (ส่งมา = 400) —
|
|
|
|
|
|
`fields` ของ version ถูกแกะจากไฟล์ .docx ที่อัปโหลด**ฝั่ง server** เก็บ**ทั้งก้อน
|
|
|
|
|
|
เหมือน return ของ `/api/template/extract` เลย** (`{cc, bookmark, properties, verify}`
|
|
|
|
|
|
ผ่าน `extractDocumentSnapshot` ของ `lib/lib-tmpl.ts` ตัวเดียวกับ route นั้น — route
|
|
|
|
|
|
extract เรียก helper ตัวเดียวกัน จึงตรงกันเป๊ะโดยการก่อสร้าง) · `verify.HmacSha256`
|
|
|
|
|
|
ตรวจด้วย secret ของ add-in ticket ฝั่ง server (env chain `ADDIN_TICKET_SECRET` →
|
|
|
|
|
|
`SECRET` → `"super_secret"` ของ `lib/addin-ticket.ts` — ต่างจาก extract ที่ใช้ secret
|
|
|
|
|
|
ของผู้เรียกเพื่อทดสอบ) · เห็น content control ทุกชนิดเสมอ ไม่ขึ้นกับเวอร์ชัน WordApi
|
|
|
|
|
|
ของ host แบบตอน add-in อ่านเองผ่าน Office.js · ไฟล์ที่แกะไม่ได้ (ไม่มี
|
|
|
|
|
|
`word/document.xml`/ZIP เสีย/`ticket_data` ไม่ใช่ JSON) = 400 · snapshot เก็บทุกครั้ง
|
|
|
|
|
|
ที่แกะสำเร็จ (NULL เหลือเฉพาะ row เก่าก่อน 2026-10-30 ที่เก็บ map ดิบ)
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
- เอกสาร (`documents` row) สร้าง **แบบ implicit** ตอน upload version แรก — ไม่มี endpoint
|
2026-10-26 10:05:01 +07:00
|
|
|
|
"สร้างเอกสาร" แยก (เอกสารถูกระบุที่ fid ใน token ตั้งแต่ขอ token)
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
- response 200: `{ "hasPdf": boolean, "versionId": string, "versionNumber": number }`
|
|
|
|
|
|
(= `UploadVersionResult` เป๊ะ) — `versionNumber` เป็นค่า **authoritative จาก server**
|
|
|
|
|
|
(client เดา `latest+1` แล้วแก้ XML part ตัวเองเมื่อไม่ตรง — กรณีอัปโหลดชนกัน)
|
|
|
|
|
|
|
2026-10-26 10:05:01 +07:00
|
|
|
|
### `GET /api/documents/versions` — history (ของเอกสาร fid ใน token)
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
|
|
|
|
|
|
response: `DocumentVersionDTO[]` เรียง **ใหม่ → เก่า** (ตาม `VersionRecord[]` ของ add-in +
|
|
|
|
|
|
ส่วนเสริม `createdByUserName`):
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
{
|
|
|
|
|
|
versionId: string; // = id ของ row (uuid)
|
|
|
|
|
|
versionNumber: number;
|
|
|
|
|
|
createdAt: string; // ISO-8601
|
|
|
|
|
|
createdByUserId: string; // = user_id
|
|
|
|
|
|
createdByUserName: string; // = user_name (ส่วนเสริมนอก contract เดิม)
|
|
|
|
|
|
contentHash: string; // SHA-256 hex ของ docx (คำนวณฝั่ง server)
|
2026-10-31 10:21:17 +07:00
|
|
|
|
fields: { // ก้อนเดียวกับ return ของ /api/template/extract (2026-10-30)
|
|
|
|
|
|
cc: Record<string, string>; // ค่า Content Control { [tag]: text }
|
|
|
|
|
|
bookmark: Record<string, string>; // ข้อความใน bookmark { [name]: text }
|
|
|
|
|
|
properties: { // core/extended/custom props (Date → ISO string)
|
|
|
|
|
|
standard?: Record<string, string | undefined>;
|
|
|
|
|
|
extended?: Record<string, number | string | undefined>;
|
|
|
|
|
|
custom?: Record<string, string | number | boolean | null>;
|
|
|
|
|
|
};
|
|
|
|
|
|
verify: Record<string, unknown>; // exp (ฟอร์แมตไทย) + HmacSha256 (secret ฝั่ง server)
|
|
|
|
|
|
} | null; // null = row เก่าก่อน 2026-10-30 (map ดิบ) หรือไม่มีเลย
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
message: string | null;
|
|
|
|
|
|
hasPdf: boolean; // derive จาก pdf_object_key !== null ตัวเดียว
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-10-26 10:05:01 +07:00
|
|
|
|
- `docId` (จาก fid ใน token) ไม่รู้จัก (ยังไม่เคยมี version) = **404** (สัญญาณ REST ตรง ๆ แบบ
|
|
|
|
|
|
N10 — UI ของ add-in แสดง error + ปุ่มลองใหม่ ไม่พัง)
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
|
2026-10-26 10:05:01 +07:00
|
|
|
|
### `GET /api/documents/versions/{versionId}/download-link?format=docx|pdf`
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
|
|
|
|
|
|
- format ไม่ส่ง/"" = `docx` · ค่าอื่น = 400
|
2026-10-26 10:05:01 +07:00
|
|
|
|
- query version row **เช็ค docId (จาก fid ใน token) กับ versionId พร้อมกัน** (กันโยน id
|
|
|
|
|
|
ข้ามเอกสาร) — ไม่เจอ = 404
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
- `format=pdf` แต่ version นั้นไม่มี pdf = 404 "version นี้ไม่มีสำเนา PDF"
|
|
|
|
|
|
- response 200: `{ "url": string, "expiresAt": string }` — url เป็น **MinIO presigned GET**
|
|
|
|
|
|
(อายุตาม env `DOCVER_LINK_TTL_SECONDS` default 900 วิ) ให้ UI เปิดในเบราว์เซอร์จริงของ
|
|
|
|
|
|
ผู้ใช้ (webview ของ Word ทิ้ง blob download เงียบ ๆ — เหตุผลเดียวกับแผน add-in เดิม)
|
|
|
|
|
|
- URL อ้าง `MINIO_ENDPOINT:MINIO_PORT` โดยตรง — **โฮสต์นี้ต้องเข้าถึงได้จากเบราว์เซอร์
|
|
|
|
|
|
ของผู้ใช้** ไม่ใช่แค่จาก Next server (หมายเหตุนี้อยู่ใน `.env.example`/README ด้วย)
|
|
|
|
|
|
|
2026-10-31 11:09:42 +07:00
|
|
|
|
### `GET /api/documents/latest?type=docx|pdf&ticket_data=<json>` — ไฟล์ version ล่าสุด (2026-10-31)
|
|
|
|
|
|
|
|
|
|
|
|
ให้ backend e-Court ดึงไฟล์ **version ล่าสุด** กลับไปเปิดให้ผู้ใช้ (server-to-server —
|
|
|
|
|
|
ไม่ใช่ Bearer: auth ด้วย `x-api-key` อย่างเดียว) · `docId` = ฟิลด์ `fid` **ใน ticket_data**
|
|
|
|
|
|
ไม่ใช่ claim จาก token
|
|
|
|
|
|
|
|
|
|
|
|
- auth: header `x-api-key` เทียบ env **`DOCVER_API_KEY` หรือ `EXTERNAL_API_KEY` ตัวใด
|
|
|
|
|
|
ตัวหนึ่ง** (ต่างจาก /token ที่รับ `DOCVER_API_KEY` อย่างเดียว — ผู้เรียกถือ key สาย N10
|
|
|
|
|
|
หรือสาย docver ก็ได้) · ทั้งคู่ไม่ตั้ง/ว่าง = 401 fail closed · ไม่ใส่/ผิด = 401 ข้อความ
|
|
|
|
|
|
เดียวกันเป๊ะ (timing-safe เหมือน `requireDocverApiKey`)
|
|
|
|
|
|
- query (บังคับทั้งคู่):
|
|
|
|
|
|
- `type` = `docx` | `pdf` — **ไม่มี default** (ต่างจาก `format` ของ download-link)
|
|
|
|
|
|
- `ticket_data` = JSON string ของ ticket — **บังคับ `fid` + `uid` + `uname`** (`tid`
|
|
|
|
|
|
optional, `exp` ถ้าส่งต้องเป็นจำนวนเต็มแต่จะถูกเติมใหม่เสมอ) เพราะ
|
|
|
|
|
|
`parseDocverTicket` (`utils/docver-ticket.ts`) บังคับทั้งสามตอน add-in เอาไฟล์ไป
|
|
|
|
|
|
redeem ที่ /token ทาง ticket — validate ตั้งแต่รับเข้า กันคืนไฟล์ที่ add-in เปิด
|
|
|
|
|
|
ไม่ได้เงียบ ๆ · ⚠️ **หมายเหตุถึงฝั่ง e-Court:** `uid` ต้องเป็น*รหัส*ผู้ใช้ (ไม่ใช่
|
|
|
|
|
|
ชื่อ) และต้องส่ง `uname` (ชื่อแสดงผล) เช่น `{"fid":"DOC-2569-0001","uid":"123",
|
|
|
|
|
|
"uname":"สมชาย ใจดี"}`
|
|
|
|
|
|
- **type=pdf** → คืนไบต์ pdf ของ version ล่าสุดตรง ๆ (`Content-Type: application/pdf`) ·
|
|
|
|
|
|
version ล่าสุดไม่มี pdf = 404 (ข้อความเดียวกับ download-link)
|
|
|
|
|
|
- **type=docx** → ดึงไบต์ docx จาก MinIO มา **stamp ticket ใหม่บนสำเนาในหน่วยความจำ**
|
|
|
|
|
|
ก่อนคืน (`buildStampedTicketData` + `stampTicketProps` ของ `lib/lib-tmpl.ts` — recipe
|
|
|
|
|
|
เดียวกับ `applyTemplate` ของกลุ่ม template: เติม `exp = now + TICKET_SEC` → เรียง key
|
|
|
|
|
|
→ `HmacSha256` ด้วย secret จาก `addinTicketSecret()` (chain `ADDIN_TICKET_SECRET` →
|
|
|
|
|
|
`SECRET` → `"super_secret"` — ตรงฝั่ง verify ของ add-in//token ทาง ticket เป๊ะ) →
|
|
|
|
|
|
`Ed25519` เฉพาะเมื่อ `PRIVATE_KEY_HEX` ตั้ง (ไม่ตั้ง = ไม่ใส่ prop — chain ของ add-in
|
|
|
|
|
|
verify แค่ HMAC) → `setCustomDocProps` upsert เขียนทับ stamp เดิมได้) —
|
|
|
|
|
|
**ไม่เขียนกลับ MinIO ตาม invariant #1** (non-destructive) · stamp ล้ม = 500
|
|
|
|
|
|
(ห้ามกลืนเหมือน `applyTemplate` — ไฟล์ไม่มี stamp = add-in เปิดไม่ได้)
|
|
|
|
|
|
- ticket_data หลังเติม exp ยาวเกิน **255 ตัวอักษร = 400** (Word ตัดทอนค่า custom
|
|
|
|
|
|
property ที่ 255 — คืนไฟล์ที่โดนตัด = HMAC เสียเงียบ ๆ)
|
|
|
|
|
|
- response 200: ไบต์ไฟล์ + `Content-Type` ตามชนิด + `Content-Disposition` จาก
|
|
|
|
|
|
`fileName` ของ version (`contentDispositionValue`) + `Cache-Control: no-store`
|
|
|
|
|
|
· เช็ค magic bytes ของไบต์ที่อ่านได้ ไม่ตรง = 500 (ความผิดปกติของพื้นที่เก็บ
|
|
|
|
|
|
ไม่ใช่ความผิดของผู้เรียก)
|
|
|
|
|
|
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
### ตาราง error
|
|
|
|
|
|
|
|
|
|
|
|
| เหตุ | สถานะ |
|
|
|
|
|
|
|---|---|
|
2026-10-31 10:21:17 +07:00
|
|
|
|
| รูป versionId/format ผิด · field ไม่รู้จัก (รวม userId/userName/templateId/fields เก่า) · ไม่ใช่ multipart · magic bytes ผิด · แกะข้อมูลจากไฟล์ไม่ได้ · body ขอ /token ผิดกติกา (รวมขาด fid) | 400 |
|
2026-10-31 11:09:42 +07:00
|
|
|
|
| `type` ไม่ส่ง/ค่าอื่น · `ticket_data` ไม่ส่ง/ไม่ใช่ JSON object/field นอก allowlist/ขาด fid·uid·uname/เกิน cap/ยาว >2048 · ticket_data หลังเติม exp ยาว >255 (ที่ /latest เฉพาะ type=docx) — ทั้งหมดของ GET /latest | 400 |
|
|
|
|
|
|
| `x-api-key` ไม่ตั้ง/ไม่ใส่/ผิด (ที่ /token) · Bearer ขาด/เน่า/หมดอายุ/secret ไม่ตั้ง (ทุก endpoint อื่น) · `x-api-key` ไม่ตั้ง (ทั้งคู่)/ไม่ใส่/ไม่ตรงทั้ง `DOCVER_API_KEY`·`EXTERNAL_API_KEY` (ที่ /latest) | 401 |
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
| ไฟล์เกิน 100MB (เช็ค `file.size` ก่อน buffer เสมอ — route ไม่มี `bodySizeLimit` คุม) | 413 |
|
2026-10-31 11:09:42 +07:00
|
|
|
|
| เอกสาร/version ไม่มี · ขอ pdf ที่ version ไม่มี (รวม type=pdf ที่ /latest แต่ version ล่าสุดไม่มี pdf) | 404 |
|
|
|
|
|
|
| MinIO env ไม่ตั้ง/ล้ม · DB ล้ม (หลังเก็บกวาด object กำพร้า) · `DOCVER_TOKEN_SECRET` ไม่ตั้ง (ที่ /token) · stamp ticket ล้ม/magic bytes ของ object ไม่ตรง (ที่ /latest) | 500 (log เต็มฝั่ง server + ข้อความกลาง ๆ ไทย) |
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
|
|
|
|
|
|
## 2. ตาราง (db/schema.ts)
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
documents — id uuid PK · doc_id text UNIQUE NOT NULL · created_at timestamptz
|
|
|
|
|
|
document_versions — id uuid PK · document_id uuid FK→documents CASCADE
|
|
|
|
|
|
· version_number int NOT NULL (+ UNIQUE(document_id, version_number))
|
|
|
|
|
|
· client_version_id · content_hash NOT NULL · message · fields jsonb
|
|
|
|
|
|
· template_id · file_name · docx_object_key NOT NULL · pdf_object_key
|
|
|
|
|
|
· docx_size int NOT NULL · pdf_size int · user_id NOT NULL
|
|
|
|
|
|
· user_name NOT NULL · created_at
|
|
|
|
|
|
+ index (document_id, created_at)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
- **`documents` ≠ `files`** ของแอป note — ไม่มี FK ไม่มี field ร่วม ไม่มีหน้า note ไหนอ่าน
|
|
|
|
|
|
(ชื่อคล้ายกันแต่คนละโดเมน: ตารางนี้เป็นของกลุ่ม API นี้ล้วน ๆ)
|
|
|
|
|
|
- `content_hash`/`docx_size` เป็น NOT NULL เพราะ server buffer ไฟล์และคำนวณทั้งคู่ก่อน
|
|
|
|
|
|
insert เสมอ · `hasPdf` **ไม่มี column** — derive จาก `pdf_object_key !== null`
|
|
|
|
|
|
- object key ใน MinIO: `docver/<versionId>/document.docx` / `document.pdf` — **uuid ล้วน**
|
|
|
|
|
|
ไม่มี input ผู้ใช้ (เหตุผลเดียวกับ N4 ที่ห้ามชื่อผู้ใช้ตกแต่ง path) · `versionId` สร้าง
|
|
|
|
|
|
ด้วย `randomUUID()` ฝั่ง server แล้วส่งเป็น `id` ตอน insert (object เขียนไปก่อนแล้ว)
|
|
|
|
|
|
|
|
|
|
|
|
## 3. จ่ายเลข version ปลอดภัยเรียกคู่ขนาน
|
|
|
|
|
|
|
|
|
|
|
|
เลือก: **insert-on-conflict + `SELECT … FOR UPDATE` ใน transaction เดียว** (ใช้ `.for("update")`
|
|
|
|
|
|
ที่ repo ใช้อยู่แล้วใน `app/actions/annotations.ts`):
|
|
|
|
|
|
|
|
|
|
|
|
1. `INSERT INTO documents (doc_id) VALUES (…) ON CONFLICT DO NOTHING` — สร้างเอกสารถ้ายังไม่มี
|
|
|
|
|
|
2. `SELECT id FROM documents WHERE doc_id = … FOR UPDATE` — serialize ผู้จ่ายเลขรายเอกสาร
|
|
|
|
|
|
(unique `doc_id` → ได้ row เดียวเสมอ ไม่มี deadlock ข้ามเอกสาร)
|
|
|
|
|
|
3. `SELECT max(version_number) + 1` แล้ว insert row version
|
|
|
|
|
|
|
|
|
|
|
|
ทางเลือกที่ตัดทิ้ง: retry-on-23505 (ต้องเก็บกวาด object ซ้ำทุกรอบ ไม่มี ordering) ·
|
|
|
|
|
|
`pg_advisory_xact_lock` (ได้แต่ต้อง raw SQL และซ้ำซ้อนกับ row lock) — UNIQUE
|
|
|
|
|
|
`(document_id, version_number)` เก็บไว้เป็น safety net
|
|
|
|
|
|
|
|
|
|
|
|
## 4. ลำดับงานต่อ request (POST) + เก็บกวาด object กำพร้า
|
|
|
|
|
|
|
2026-10-31 10:21:17 +07:00
|
|
|
|
validate (text field ก่อนแตะไฟล์) → cap ขนาด → buffer → magic bytes → **แกะ fields snapshot
|
|
|
|
|
|
จากไฟล์ทั้งก้อนด้วย `extractDocumentSnapshot` ของ lib/lib-tmpl.ts (ตัวเดียวกับ
|
|
|
|
|
|
`/api/template/extract` — 2026-10-30)** → sha256 → `versionId`
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
→ **putObject MinIO** (docx ก่อน pdf; pdf ล้ม = ลบ docx ทิ้ง) → **insert DB (tx §3)** —
|
|
|
|
|
|
ล้ม = `removeObject` ทั้งคู่แล้วโยนต่อ (pattern เดียวกับ `unlink` + rethrow ของ N4)
|
|
|
|
|
|
|
|
|
|
|
|
- crash ระหว่าง putObject↔insert เหลือ object ไร้ row ชี้ (key เป็น uuid ไม่ชนใคร) —
|
|
|
|
|
|
**ไม่ทำ GC รอบนี้** บันทึกไว้เป็นงานอนาคต
|
|
|
|
|
|
- ไม่มี idempotency บน `clientVersionId` — retry หลัง network timeout เกิด version ใหม่
|
|
|
|
|
|
(client แก้เลขจาก response เองได้) ยอมรับได้
|
|
|
|
|
|
|
|
|
|
|
|
## 5. Carve-out จาก invariant #4 (CLAUDE.md)
|
|
|
|
|
|
|
|
|
|
|
|
กลุ่ม API นี้ **เขียน DB ผ่าน route handler** (ไม่ใช่ Server Action) และไม่ publish SSE
|
|
|
|
|
|
ไม่ `revalidatePath` — ตาราง `documents`/`document_versions` เป็นของกลุ่มนี้ล้วน ไม่มีหน้า
|
|
|
|
|
|
note ใดอ่าน จึงไม่มี Router Cache/SSE ที่ต้อง invalidate (นับเป็นข้อยกเว้นเดียวกับลักษณะ
|
|
|
|
|
|
read-only ของ N10 แต่ตัวนี้เขียนได้)
|
|
|
|
|
|
|
|
|
|
|
|
## 6. Env (อ่านตอนเรียก ไม่ใช่ตอน module load — ขาด/ล้ม = 500 ไทยกลาง ๆ + log เต็มฝั่ง server)
|
|
|
|
|
|
|
|
|
|
|
|
| env | default | หมายเหตุ |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| `MINIO_ENDPOINT` | — บังคับ | host อย่างเดียว ไม่มี scheme |
|
|
|
|
|
|
| `MINIO_PORT` | `9000` | dev compose แมพโฮสต์เป็น 19000 (9000/9001 ชน service อื่นในเครื่อง) |
|
|
|
|
|
|
| `MINIO_USE_SSL` | `false` | |
|
|
|
|
|
|
| `MINIO_ACCESS_KEY` / `MINIO_SECRET_KEY` | — บังคับ | dev compose = `minioadmin` |
|
|
|
|
|
|
| `MINIO_BUCKET` | `docver` | app สร้างเองตอน first use (`ensureBucket` — จับ race ข้าม process) |
|
|
|
|
|
|
| `DOCVER_LINK_TTL_SECONDS` | `900` | อายุ presigned URL |
|
2026-10-31 11:09:42 +07:00
|
|
|
|
| `DOCVER_API_KEY` | — บังคับ | API key ของ `POST /api/documents/token` (header `x-api-key`) — ไม่ตั้ง/ว่าง = 401 fail closed · เทียบ timing-safe · ห้าม log ค่าดิบ · GET /latest ใช้ตัวนี้**หรือ** `EXTERNAL_API_KEY` ตัวใดตัวหนึ่ง |
|
2026-10-26 06:29:19 +07:00
|
|
|
|
| `DOCVER_TOKEN_SECRET` | — บังคับ | secret เซ็น/ตรวจ JWT HS256 — ไม่ตั้ง: /token = 500 · endpoint อื่น = 401 · ห้าม log ค่าดิบ |
|
|
|
|
|
|
| `DOCVER_TOKEN_TTL_SECONDS` | `3600` | อายุ access_token (วินาที) |
|
2026-10-31 11:09:42 +07:00
|
|
|
|
| `TICKET_SEC` | `300` | TTL (วินาที) ของ `exp` ที่ stamp ตอน GET /latest type=docx — env เดียวกับกลุ่ม template (`app/api/template/*`) |
|
|
|
|
|
|
| `PRIVATE_KEY_HEX` | — optional | private key Ed25519 (hex seed) — ตั้งแล้ว GET /latest type=docx จะ stamp prop `Ed25519` เพิ่ม · ไม่ตั้ง = ไม่ใส่ (chain ของ add-in verify แค่ `HmacSha256`) · env เดียวกับกลุ่ม template |
|
|
|
|
|
|
| `ADDIN_TICKET_SECRET` | chain → `SECRET` → `"super_secret"` | secret ของ HMAC ที่ stamp ที่ /latest — ใช้ `addinTicketSecret()` ให้ตรงฝั่ง verify ของ /token ทาง ticket (ดู `docs/PLAN-ADDIN-AUTH.md`) |
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
|
|
|
|
|
|
`putObject` แนบ `Content-Type` + `Content-Disposition` (ชื่อไฟล์ไทยผ่าน `filename*=UTF-8''`
|
|
|
|
|
|
best-effort + ASCII fallback `document.docx`/`document.pdf`) — presigned GET ได้ชื่อไฟล์สวย
|
|
|
|
|
|
ตอนดาวน์โหลด
|
|
|
|
|
|
|
|
|
|
|
|
## 7. ไฟล์ของกลุ่มนี้
|
|
|
|
|
|
|
|
|
|
|
|
- `db/schema.ts` — ตาราง + `DocumentVersionDTO`/`toDocumentVersionDTO`
|
|
|
|
|
|
- `lib/document-versioning.ts` — pure validation (ห้าม import `@/db`/`minio` — unit test ได้
|
|
|
|
|
|
โดยไม่ต้องตั้ง env อะไรเลย ตาม pattern `lib/external-api-auth.ts`)
|
2026-10-26 06:29:19 +07:00
|
|
|
|
- `lib/docver-token.ts` — JWT HS256 issue/verify + `requireDocverApiKey`/`requireDocverBearer`
|
2026-10-31 11:09:42 +07:00
|
|
|
|
+ `parseTokenRequestBody` + `requireDocumentApiKey` (auth ของ GET /latest — สอง env)
|
|
|
|
|
|
(pure เหมือนกัน — ไม่ import `@/db`/`minio`)
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
- `lib/docver-store.ts` — MinIO client (เก็บบน `globalThis` ใน dev แบบ `db/index.ts`) +
|
2026-10-31 11:09:42 +07:00
|
|
|
|
`ensureBucket` + put/remove/presign + `getVersionObjectBytes` (อ่านไบต์ของ /latest) +
|
|
|
|
|
|
`insertVersionRecord` (tx) + query helpers (`findLatestVersionForDoc` ของ /latest)
|
2026-10-26 06:29:19 +07:00
|
|
|
|
- `app/api/documents/token/route.ts` — POST ขอ access_token (x-api-key)
|
2026-10-26 10:05:01 +07:00
|
|
|
|
- `app/api/documents/versions/route.ts` — POST + GET (docId = fid จาก token)
|
|
|
|
|
|
- `app/api/documents/versions/[versionId]/download-link/route.ts` — GET
|
2026-10-31 11:09:42 +07:00
|
|
|
|
- `app/api/documents/latest/route.ts` — GET ไฟล์ version ล่าสุด (2026-10-31 — docx
|
|
|
|
|
|
stamp ticket บนสำเนาในหน่วยความจำผ่าน `buildStampedTicketData`/`stampTicketProps`
|
|
|
|
|
|
ของ `lib/lib-tmpl.ts` + `parseTicketDataParam` ของ `utils/docver-ticket.ts` +
|
|
|
|
|
|
`parseLatestTypeParam` ของ `lib/document-versioning.ts`)
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
- `docker-compose.yml` — db (ชื่อ/วอลุ่มเดิมกับ `docker run` ใน README) + minio
|
|
|
|
|
|
(โฮสต์ 19000 API / 19001 console — ข้างใน container 9000/9001; image จาก quay.io)
|
2026-10-26 06:29:19 +07:00
|
|
|
|
- `tests/lib/document-versioning.test.ts` + `tests/lib/docver-token.test.ts` — unit test ส่วน pure
|
2026-10-31 11:09:42 +07:00
|
|
|
|
(+ `parseTicketDataParam` ใน `tests/utils/docver-ticket.test.ts` + stamp helpers ใน
|
|
|
|
|
|
`tests/lib/lib-tmpl.test.ts` ของ /latest)
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
|
|
|
|
|
|
## 8. Out of scope รอบนี้
|
|
|
|
|
|
|
feat(addin): unmock versioning — real upload/download against docver API
Replace the in-memory versioningService with real calls to the docver
group (/api/documents/*) via apiClient with the ticket-issued Bearer;
keep the mock only for ?addindemo (authService.isDemoMode).
- uploadVersion: multipart POST /api/documents/versions (file, optional
pdf when non-empty, clientVersionId, fields JSON, trimmed message,
fileName) — identity fields are never sent (userId/userName/templateId
in the form = 400 per the 2026-10-22 contract; docId comes from the
fid claim)
- getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds
optional createdByUserName; history panel shows it with userId
fallback)
- getDownloadLink: GET presigned URL (+ ?format=pdf)
- apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin
through withBasePath (/docedit/api/...); the env is now only for a
split deployment (must include basePath, HTTPS)
- documentId/templateId params stay in signatures but are not sent
- tests: versioningService.test.ts covers both modes (real sign-in via
signInWithTicket, FormData contract assertions, DTO mapping, format
query, 401 -> AuthRequiredError, demo mock without network)
- docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section),
addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md
Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket ->
token -> upload docx / docx+pdf -> history -> presigned download of both
formats).
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-27 12:52:29 +07:00
|
|
|
|
- ~~ไม่แต้ `wordaddin/` หรือ client ใด ๆ~~ (ปลด mock แล้ว 2026-10-27 — `wordaddin/services/versioningService.ts` ยิงกลุ่มนี้จริง same-origin ผ่าน Bearer; ดู `docs/WORDADDIN-DEV.md` หัว "การรันกับ backend จริง")
|
feat(docver): add document versioning API (docx+pdf in MinIO)
Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).
- POST versions (multipart): validates fields/magic bytes/100MB cap,
uploads to MinIO first, cleans up objects if the DB insert fails;
returns {hasPdf, versionId, versionNumber} with server-authoritative
numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
(docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)
wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 10:52:35 +07:00
|
|
|
|
- ไม่มีหน้า UI ดูรายการเอกสาร/version ของกลุ่มนี้ · ไม่มี GC object กำพร้า · ไม่มี idempotency
|
|
|
|
|
|
บน `clientVersionId` · ไม่มีการแปลง docx→pdf ฝั่ง server (Office.js ของ add-in ทำ)
|