feat: add GET /api/documents/latest returning latest version file with stamped ticket
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m23s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 5s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 0s
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 4s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m23s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 5s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 0s
Server-to-server endpoint for the e-Court backend (x-api-key against DOCVER_API_KEY or EXTERNAL_API_KEY): - type=pdf returns the latest version's PDF bytes (404 when absent) - type=docx stamps fresh ticket custom properties (exp + HmacSha256 via addinTicketSecret(), Ed25519 when PRIVATE_KEY_HEX is set) into an in-memory copy before returning — MinIO object is never modified - docId comes from fid inside the ticket_data query param, which must include fid/uid/uname so the stamped file stays redeemable by the add-in at /api/documents/token (buildStampedTicketData drops null values because parseDocverTicket rejects tid:null) - adds parseTicketDataParam, parseLatestTypeParam, requireDocumentApiKey, findLatestVersionForDoc, getVersionObjectBytes + unit tests (320 pass) - syncs PLAN-DOCVER.md, OpenAPI spec and .env.example Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
5bc8d9a817
commit
0b6ee8cc84
12 changed files with 806 additions and 9 deletions
|
|
@ -104,6 +104,96 @@ export function parseDocverTicket(raw: string): DocverTicketParseResult {
|
|||
return { ok: true, ticket: { exp, fid, tid, uid, uname } };
|
||||
}
|
||||
|
||||
/** ticket ก่อนเติม exp ของ parseTicketDataParam — exp เติมตอน stamp เสมอ */
|
||||
export type TicketDataParam = {
|
||||
fid: string;
|
||||
uid: string;
|
||||
uname: string;
|
||||
tid: string | null;
|
||||
};
|
||||
|
||||
export type TicketDataParamResult =
|
||||
| { ok: true; ticket: TicketDataParam }
|
||||
| { ok: false; error: string };
|
||||
|
||||
/** cap ความยาวรวมของ query param ticket_data — เทียบ parseTicketTokenBody
|
||||
* ของ lib/addin-ticket.ts (body ทาง ticket ของ POST /api/documents/token) */
|
||||
const TICKET_DATA_PARAM_MAX = 2048;
|
||||
|
||||
/**
|
||||
* แกะ + ตรวจ query param `ticket_data` ของ GET /api/documents/latest —
|
||||
* JSON string ของ ticket ที่ยังไม่มี exp (server เติม exp ใหม่ตอน stamp เสมอ
|
||||
* จึงไม่บังคับ แต่ถ้าส่งมาต้องเป็นจำนวนเต็ม)
|
||||
*
|
||||
* บังคับ fid/uid/uname ครบตั้งแต่รับเข้า: parseDocverTicket ด้านบนบังคับทั้งสาม
|
||||
* ตอน add-in เอาไฟล์ไป redeem ที่ /api/documents/token — ปล่อย ticket ที่ขาด
|
||||
* field ผ่านไปเงียบ ๆ = คืนไฟล์ที่ add-in เปิดใช้ไม่ได้ภายหลัง
|
||||
*/
|
||||
export function parseTicketDataParam(raw: string | null): TicketDataParamResult {
|
||||
if (raw === null || raw.trim() === "") {
|
||||
return { ok: false, error: "ต้องระบุ ticket_data (JSON string ของ ticket)" };
|
||||
}
|
||||
if (raw.length > TICKET_DATA_PARAM_MAX) {
|
||||
return { ok: false, error: `ticket_data ยาวเกิน ${TICKET_DATA_PARAM_MAX} ตัวอักษร` };
|
||||
}
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
return { ok: false, error: "ticket_data ไม่ใช่ JSON ที่ถูกต้อง" };
|
||||
}
|
||||
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return { ok: false, error: "ticket_data ต้องเป็น JSON object" };
|
||||
}
|
||||
const record = parsed as Record<string, unknown>;
|
||||
for (const key of Object.keys(record)) {
|
||||
if (!(TICKET_FIELDS as readonly string[]).includes(key)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `ticket_data มี field ที่ไม่รู้จัก '${key}' (รับเฉพาะ ${TICKET_FIELDS.join(", ")})`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// exp ไม่บังคับ (server เติมใหม่ตอน stamp) — แต่ถ้าส่งมาต้องผิดรูปไม่ได้ กันเงียบ ๆ
|
||||
if (record.exp !== undefined &&
|
||||
(typeof record.exp !== "number" || !Number.isSafeInteger(record.exp))) {
|
||||
return { ok: false, error: "ticket_data ของ exp ต้องเป็นจำนวนเต็ม (unix วินาที)" };
|
||||
}
|
||||
|
||||
const field = (name: "uid" | "uname" | "fid"): string | null => {
|
||||
const value = record[name];
|
||||
if (typeof value !== "string" || value.trim() === "") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length <= TICKET_LIMITS[name] ? trimmed : null;
|
||||
};
|
||||
|
||||
const uid = field("uid");
|
||||
if (uid === null) return { ok: false, error: "ticket_data ต้องมี uid (string 1–100)" };
|
||||
const uname = field("uname");
|
||||
if (uname === null) return { ok: false, error: "ticket_data ต้องมี uname (string 1–200)" };
|
||||
const fid = field("fid");
|
||||
if (fid === null) return { ok: false, error: "ticket_data ต้องมี fid (string 1–200)" };
|
||||
|
||||
let tid: string | null = null;
|
||||
if (record.tid !== undefined) {
|
||||
if (typeof record.tid !== "string") {
|
||||
return { ok: false, error: "ticket_data ของ tid ต้องเป็น string" };
|
||||
}
|
||||
const trimmed = record.tid.trim();
|
||||
if (trimmed !== "") {
|
||||
if (trimmed.length > TICKET_LIMITS.tid) {
|
||||
return { ok: false, error: "ticket_data ของ tid ยาวเกิน 100 ตัวอักษร" };
|
||||
}
|
||||
tid = trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true, ticket: { fid, uid, uname, tid } };
|
||||
}
|
||||
|
||||
/** exp หมดอายุไหม — exp เท่ากับ now พอดีก็ถือว่าหมดแล้ว */
|
||||
export function isTicketExpired(exp: number, nowSec: number = Math.floor(Date.now() / 1000)): boolean {
|
||||
return exp <= nowSec;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue