feat(docver): derive docId from fid claim and drop [docId] URL segment
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m9s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 4s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m9s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 4s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s
- docId of every docver operation now comes from the Bearer token: the fid
claim (e-Court fileId) is used directly as docId; segment [docId] removed
from URLs — POST/GET /api/documents/versions and
GET /api/documents/versions/{versionId}/download-link
- fid is required and must be a string (trim 1-200): token issuance rejects
missing/blank/non-string fid with 400, verify rejects fid-less tokens with 401
- parseDocId removed from lib/document-versioning.ts (dead code, no callers)
- openapi spec: paths renamed, docId path params removed, fid required + string
- PLAN-DOCVER.md / CLAUDE.md / wordaddin versioningService comment updated
- tests: fid required/string cases added, parseDocId describe removed (256 pass)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
aad6f155fe
commit
0b2c9e6343
10 changed files with 122 additions and 158 deletions
|
|
@ -17,9 +17,9 @@ export const DOCX_CONTENT_TYPE =
|
|||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
export const PDF_CONTENT_TYPE = "application/pdf";
|
||||
|
||||
/** รหัสเอกสารจากผู้เรียก — URL-path-safe ล้วน (ไม่มี / .. ช่องว่าง ไทย) กันถูก
|
||||
* ฝังลง path/log แปลก ๆ และตัดขนาดให้จำกัด */
|
||||
const DOC_ID_RE = /^[A-Za-z0-9._:-]{1,200}$/;
|
||||
/** รหัสเอกสาร (docId) ไม่มี validator ที่นี่แล้ว — ตอนนี้มาจาก fid ใน Bearer token
|
||||
* (String(fid) — integer ที่ verify แล้ว ผ่าน lib/docver-token.ts) ไม่ใช่ path param
|
||||
* ของผู้เรียก · parseDocId เดิมถอดออกพร้อมการถอด segment [docId] ออกจาก URL */
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
|
|
@ -46,22 +46,6 @@ export function docverError(message: string, status: number): Response {
|
|||
});
|
||||
}
|
||||
|
||||
/** ตรวจ docId จาก path param — trim แล้วว่าง/เกิน 200/มีตัวอักษรนอก charset = ไม่ผ่าน
|
||||
* (+ ปฏิเสธ "." / ".." เป็นพิเศษแม้อยู่ใน charset — path-traversal specials) */
|
||||
export function parseDocId(raw: string):
|
||||
| { ok: true; docId: string }
|
||||
| DocverError {
|
||||
const docId = raw.trim();
|
||||
if (docId === "." || docId === ".." || !DOC_ID_RE.test(docId)) {
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
"รหัสเอกสาร (docId) ต้องยาว 1–200 ตัวอักษร และใช้ได้เฉพาะ A-Z a-z 0-9 . _ : -",
|
||||
};
|
||||
}
|
||||
return { ok: true, docId };
|
||||
}
|
||||
|
||||
/** magic bytes ของ .docx — OOXML เป็น ZIP จึงขึ้นต้น PK\x03\x04 (mirror รูปแบบ
|
||||
* matchesSignature ของ app/actions/files.ts — utils/file-kind.ts เป็นของ note files) */
|
||||
export function isDocxBytes(bytes: Buffer): boolean {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
// - token เซ็นด้วย env DOCVER_TOKEN_SECRET (มือเขียน HS256 ผ่าน node:crypto ล้วน —
|
||||
// ไม่เพิ่ม dependency) แล้วทุก endpoint อื่นของกลุ่มบังคับ Authorization: Bearer —
|
||||
// identity (userid/username) กับ templateId แกะจาก claims ไม่รับจาก payload
|
||||
// - claims "fid" = fileId ของ e-Court — ตาราง docver คีย์ด้วย docId (ไม่มีคอลัมน์
|
||||
// fileId ตัวเลข) จึงเก็บเป็น claim ไว้ก่อน ยังไม่มี operation ใดอ่าน/บังคับ
|
||||
// - claims "fid" = fileId ของ e-Court (string) — เป็น docId ของทุก operation ตรง ๆ
|
||||
// — บังคับตั้งแต่ขอ token และ verify ไม่รับ token ที่ไม่มี fid
|
||||
//
|
||||
// ห้าม import อะไรที่ลาก @/db หรือ minio มาด้วย — unit test (tests/lib/) ต้องรันได้
|
||||
// โดยไม่ต้องตั้ง DATABASE_URL/MINIO_* (pattern เดียวกับ lib/external-api-auth.ts)
|
||||
|
|
@ -19,7 +19,7 @@ import { docverError, type DocverError } from "@/lib/document-versioning";
|
|||
export type DocverTokenClaims = {
|
||||
userid: string;
|
||||
username: string;
|
||||
fid: number | null;
|
||||
fid: string;
|
||||
template: string | null;
|
||||
};
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ function claimsFromPayload(payload: Record<string, unknown>): DocverTokenClaims
|
|||
const { userid, username, fid, template } = payload;
|
||||
if (typeof userid !== "string" || userid === "") return null;
|
||||
if (typeof username !== "string" || username === "") return null;
|
||||
if (fid !== null && !(typeof fid === "number" && Number.isInteger(fid))) return null;
|
||||
if (typeof fid !== "string" || fid === "") return null;
|
||||
if (template !== null && typeof template !== "string") return null;
|
||||
return { userid, username, fid, template };
|
||||
}
|
||||
|
|
@ -190,9 +190,13 @@ export function parseTokenRequestBody(raw: unknown):
|
|||
return { ok: false, error: "username ยาวเกิน 200 ตัวอักษร" };
|
||||
}
|
||||
|
||||
const rawFid = record.fid ?? null;
|
||||
if (rawFid !== null && !(typeof rawFid === "number" && Number.isInteger(rawFid))) {
|
||||
return { ok: false, error: "fid ต้องเป็นตัวเลขจำนวนเต็ม" };
|
||||
const rawFid = record.fid;
|
||||
if (typeof rawFid !== "string" || rawFid.trim() === "") {
|
||||
return { ok: false, error: "ต้องระบุ fid (string)" };
|
||||
}
|
||||
const fid = rawFid.trim();
|
||||
if (fid.length > 200) {
|
||||
return { ok: false, error: "fid ยาวเกิน 200 ตัวอักษร" };
|
||||
}
|
||||
|
||||
const rawTemplate = record.template ?? null;
|
||||
|
|
@ -204,5 +208,5 @@ export function parseTokenRequestBody(raw: unknown):
|
|||
return { ok: false, error: "template ยาวเกิน 100 ตัวอักษร" };
|
||||
}
|
||||
|
||||
return { ok: true, claims: { userid, username, fid: rawFid, template } };
|
||||
return { ok: true, claims: { userid, username, fid, template } };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ export const openApiSpec = {
|
|||
"version เก็บ .docx + .pdf ใน MinIO พร้อมผู้อัปโหลด — auth ด้วย access_token " +
|
||||
"ของกลุ่มนี้: ขอที่ POST /api/documents/token (ยืนยัน x-api-key กับ env " +
|
||||
"DOCVER_API_KEY) แล้วแนบ Authorization: Bearer ทุก request — identity/template " +
|
||||
"แกะจาก claims ของ token ไม่รับใน multipart form อีก",
|
||||
"และ docId (= fid — fileId ของ e-Court, string) แกะจาก claims ของ token ทั้งหมด " +
|
||||
"ไม่รับจาก URL/multipart form อีก",
|
||||
},
|
||||
{
|
||||
name: "Judge's Note",
|
||||
|
|
@ -262,8 +263,9 @@ export const openApiSpec = {
|
|||
description:
|
||||
"ออก access_token ใช้กับ endpoint อื่นของกลุ่ม documents เท่านั้น — token เป็น " +
|
||||
"JWT HS256 ฝัง claims {userid, username, fid, template} ตามที่ส่งมา อายุตาม env " +
|
||||
"DOCVER_TOKEN_TTL_SECONDS (default 3600 วินาที) · identity (userId/userName) " +
|
||||
"และ templateId ตอนอัปโหลด version แกะจาก claims ของ token นี้",
|
||||
"DOCVER_TOKEN_TTL_SECONDS (default 3600 วินาที) · identity (userId/userName), " +
|
||||
"templateId และ docId (= fid) ตอนทำงานแกะจาก claims ของ token นี้ — " +
|
||||
"fid จึงบังคับ",
|
||||
security: [{ ApiKeyAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
|
|
@ -275,12 +277,12 @@ export const openApiSpec = {
|
|||
value: {
|
||||
userid: "123",
|
||||
username: "Sorawit Bholsithi",
|
||||
fid: 3,
|
||||
fid: "3",
|
||||
template: "T12",
|
||||
},
|
||||
},
|
||||
"ขั้นต่ำ (fid/template เป็นทางเลือก)": {
|
||||
value: { userid: "123", username: "Sorawit Bholsithi" },
|
||||
"ขั้นต่ำ (template เป็นทางเลือก)": {
|
||||
value: { userid: "123", username: "Sorawit Bholsithi", fid: "3" },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -308,27 +310,18 @@ export const openApiSpec = {
|
|||
},
|
||||
},
|
||||
},
|
||||
"/api/documents/{docId}/versions": {
|
||||
"/api/documents/versions": {
|
||||
post: {
|
||||
tags: ["documents"],
|
||||
summary: "อัปโหลด version ใหม่ของเอกสาร (docx + pdf)",
|
||||
description:
|
||||
"สร้างเอกสารอัตโนมัติเมื่อ docId ยังไม่เคยมี (docId ห้ามซ้ำ) แล้วเก็บไฟล์ " +
|
||||
".docx (+ .pdf ถ้าแนบมา) ลง MinIO พร้อมผู้อัปโหลด — versionNumber ที่ server จ่าย " +
|
||||
"คือค่าจริงเสมอ (client ที่เลขไม่ตรงต้องแก้ XML part ของตัวเอง) · pdf เป็น " +
|
||||
"optional: ไม่แนบ/ไฟล์เปล่า = version นี้ไม่มีสำเนา PDF (ดู hasPdf ใน response) · " +
|
||||
"ผู้อัปโหลด (userId/userName) และ templateId แกะจาก Bearer token ไม่รับใน form " +
|
||||
"(ส่ง field เหล่านั้นมา = 400)",
|
||||
"เอกสารถูกระบุด้วย fid ใน Bearer token (docId = fid) — สร้างเอกสาร " +
|
||||
"อัตโนมัติเมื่อ docId ยังไม่เคยมี แล้วเก็บไฟล์ .docx (+ .pdf ถ้าแนบมา) ลง MinIO " +
|
||||
"พร้อมผู้อัปโหลด — versionNumber ที่ server จ่ายคือค่าจริงเสมอ (client ที่เลขไม่ตรง " +
|
||||
"ต้องแก้ XML part ของตัวเอง) · pdf เป็น optional: ไม่แนบ/ไฟล์เปล่า = version นี้ไม่มี " +
|
||||
"สำเนา PDF (ดู hasPdf ใน response) · ผู้อัปโหลด (userId/userName) และ templateId " +
|
||||
"แกะจาก Bearer token ไม่รับใน form (ส่ง field เหล่านั้นมา = 400)",
|
||||
security: [{ DocverBearer: [] }],
|
||||
parameters: [
|
||||
{
|
||||
name: "docId",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
description: "รหัสเอกสารจากผู้เรียก (add-in GUID / รหัส e-Court) — [A-Za-z0-9._:-]",
|
||||
},
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
|
|
@ -397,18 +390,10 @@ export const openApiSpec = {
|
|||
tags: ["documents"],
|
||||
summary: "รายการ version ทั้งหมดของเอกสาร (ใหม่ → เก่า)",
|
||||
description:
|
||||
"version ทุกตัวของ docId นี้ พร้อมผู้อัปโหลด (userId/userName), เวลา, commit " +
|
||||
"message, ค่า fields และ hasPdf — ใช้เขียน UI version history ของ add-in",
|
||||
"เอกสารถูกระบุด้วย fid ใน Bearer token (docId = fid) — version ทุกตัว " +
|
||||
"ของเอกสารนั้น พร้อมผู้อัปโหลด (userId/userName), เวลา, commit message, ค่า fields " +
|
||||
"และ hasPdf — ใช้เขียน UI version history ของ add-in",
|
||||
security: [{ DocverBearer: [] }],
|
||||
parameters: [
|
||||
{
|
||||
name: "docId",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
description: "รหัสเอกสารจากผู้เรียก — [A-Za-z0-9._:-]",
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "รายการ version เรียงใหม่ → เก่า",
|
||||
|
|
@ -435,23 +420,17 @@ export const openApiSpec = {
|
|||
},
|
||||
},
|
||||
},
|
||||
"/api/documents/{docId}/versions/{versionId}/download-link": {
|
||||
"/api/documents/versions/{versionId}/download-link": {
|
||||
get: {
|
||||
tags: ["documents"],
|
||||
summary: "ขอ presigned URL ดาวน์โหลดไฟล์ของ version (docx/pdf)",
|
||||
description:
|
||||
"คืน URL อายุสั้น (env DOCVER_LINK_TTL_SECONDS, default 900 วินาที) ให้เปิดใน " +
|
||||
"เบราว์เซอร์/Word ของผู้ใช้ — URL อ้าง endpoint MinIO ตรง ๆ จึงต้องเป็นโฮสต์ที่ " +
|
||||
"ผู้ใช้เข้าถึงได้ · ขอ pdf ใน version ที่ไม่มีสำเนา = 404",
|
||||
"เอกสารถูกระบุด้วย fid ใน Bearer token (docId = fid) · คืน URL อายุสั้น " +
|
||||
"(env DOCVER_LINK_TTL_SECONDS, default 900 วินาที) ให้เปิดในเบราว์เซอร์/Word ของ " +
|
||||
"ผู้ใช้ — URL อ้าง endpoint MinIO ตรง ๆ จึงต้องเป็นโฮสต์ที่ผู้ใช้เข้าถึงได้ · ขอ pdf " +
|
||||
"ใน version ที่ไม่มีสำเนา = 404",
|
||||
security: [{ DocverBearer: [] }],
|
||||
parameters: [
|
||||
{
|
||||
name: "docId",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
description: "รหัสเอกสาร (ต้องตรงกับ version นี้ — โยน id ข้ามเอกสารไม่ได้)",
|
||||
},
|
||||
{
|
||||
name: "versionId",
|
||||
in: "path",
|
||||
|
|
@ -552,15 +531,14 @@ export const openApiSpec = {
|
|||
TokenRequest: {
|
||||
type: "object",
|
||||
description: "claims ที่จะฝังใน access_token ของกลุ่ม documents — key อื่น = 400",
|
||||
required: ["userid", "username"],
|
||||
required: ["userid", "username", "fid"],
|
||||
properties: {
|
||||
userid: { type: "string", description: "รหัสผู้ใช้ (บังคับ, ≤ 100 ตัวอักษร)" },
|
||||
username: { type: "string", description: "ชื่อผู้ใช้ (บังคับ, ≤ 200 ตัวอักษร)" },
|
||||
fid: {
|
||||
type: "integer",
|
||||
nullable: true,
|
||||
type: "string",
|
||||
description:
|
||||
"fileId ของ e-Court — เก็บเป็น claim ไว้ก่อน ยังไม่ถูก operation ของ docver อ่าน",
|
||||
"fileId ของ e-Court (บังคับ, ≤ 200 ตัวอักษร) — เป็น docId ของทุก operation ตรง ๆ",
|
||||
},
|
||||
template: {
|
||||
type: "string",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue