feat(docver): add access token endpoint and Bearer auth on all endpoints
- POST /api/documents/token issues JWT HS256 (claims userid/username/fid/template) after x-api-key check against new env DOCVER_API_KEY (fail-closed, timing-safe, uniform 401) - all docver endpoints now require Authorization: Bearer, verified with env DOCVER_TOKEN_SECRET (TTL via DOCVER_TOKEN_TTL_SECONDS, default 3600); secret unset = 401 on verify / 500 on issue - identity (userId/userName) + templateId extracted from token claims instead of request payload; removed from multipart form (legacy fields now 400) - lib/docver-token.ts: hand-rolled JWT via node:crypto only — alg pinned to HS256 (anti alg-confusion), timing-safe compare with both sides hashed, exp enforced, identical 401 for all bearer failure modes - openapi spec: /api/documents/token path + DocverBearer scheme; PLAN-DOCVER.md / CLAUDE.md / .env.example updated - tests: new tests/lib/docver-token.test.ts + adjusted parseVersionUploadForm tests (258 pass, no DB env needed) - package-lock.json: sync minio (^8.0.7) that was missing from the earlier docver commit Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
280cc2da40
commit
98b370c925
13 changed files with 932 additions and 131 deletions
137
lib/openapi.ts
137
lib/openapi.ts
|
|
@ -39,8 +39,10 @@ export const openApiSpec = {
|
|||
name: "documents",
|
||||
description:
|
||||
"จัด version เอกสาร (docver — docs/PLAN-DOCVER.md): เอกสารเดียวต่อ docId แต่ละ " +
|
||||
"version เก็บ .docx + .pdf ใน MinIO พร้อมผู้อัปโหลด — ไม่มี auth (stand-alone): " +
|
||||
"ผู้เรียกบอก userId/userName เองใน multipart",
|
||||
"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 อีก",
|
||||
},
|
||||
{
|
||||
name: "Judge's Note",
|
||||
|
|
@ -225,6 +227,59 @@ export const openApiSpec = {
|
|||
},
|
||||
},
|
||||
},
|
||||
"/api/documents/token": {
|
||||
post: {
|
||||
tags: ["documents"],
|
||||
summary: "ขอ access_token ของกลุ่ม documents (docver)",
|
||||
description:
|
||||
"ออก access_token ใช้กับ endpoint อื่นของกลุ่ม documents เท่านั้น — token เป็น " +
|
||||
"JWT HS256 ฝัง claims {userid, username, fid, template} ตามที่ส่งมา อายุตาม env " +
|
||||
"DOCVER_TOKEN_TTL_SECONDS (default 3600 วินาที) · identity (userId/userName) " +
|
||||
"และ templateId ตอนอัปโหลด version แกะจาก claims ของ token นี้",
|
||||
security: [{ ApiKeyAuth: [] }],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/TokenRequest" },
|
||||
examples: {
|
||||
"ครบทุก claim": {
|
||||
value: {
|
||||
userid: "123",
|
||||
username: "Sorawit Bholsithi",
|
||||
fid: 3,
|
||||
template: "T12",
|
||||
},
|
||||
},
|
||||
"ขั้นต่ำ (fid/template เป็นทางเลือก)": {
|
||||
value: { userid: "123", username: "Sorawit Bholsithi" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "access_token พร้อมใช้ (แนบเป็น Authorization: Bearer ทุก request ถัดไป)",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/TokenResponse" },
|
||||
},
|
||||
},
|
||||
},
|
||||
400: { $ref: "#/components/responses/BadRequest" },
|
||||
401: {
|
||||
description: "ไม่ได้ส่ง header x-api-key หรือค่าไม่ถูกต้อง (env DOCVER_API_KEY)",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
500: { $ref: "#/components/responses/ServerError" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/documents/{docId}/versions": {
|
||||
post: {
|
||||
tags: ["documents"],
|
||||
|
|
@ -233,7 +288,10 @@ export const openApiSpec = {
|
|||
"สร้างเอกสารอัตโนมัติเมื่อ docId ยังไม่เคยมี (docId ห้ามซ้ำ) แล้วเก็บไฟล์ " +
|
||||
".docx (+ .pdf ถ้าแนบมา) ลง MinIO พร้อมผู้อัปโหลด — versionNumber ที่ server จ่าย " +
|
||||
"คือค่าจริงเสมอ (client ที่เลขไม่ตรงต้องแก้ XML part ของตัวเอง) · pdf เป็น " +
|
||||
"optional: ไม่แนบ/ไฟล์เปล่า = version นี้ไม่มีสำเนา PDF (ดู hasPdf ใน response)",
|
||||
"optional: ไม่แนบ/ไฟล์เปล่า = version นี้ไม่มีสำเนา PDF (ดู hasPdf ใน response) · " +
|
||||
"ผู้อัปโหลด (userId/userName) และ templateId แกะจาก Bearer token ไม่รับใน form " +
|
||||
"(ส่ง field เหล่านั้นมา = 400)",
|
||||
security: [{ DocverBearer: [] }],
|
||||
parameters: [
|
||||
{
|
||||
name: "docId",
|
||||
|
|
@ -249,7 +307,7 @@ export const openApiSpec = {
|
|||
"multipart/form-data": {
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["file", "userId", "userName"],
|
||||
required: ["file"],
|
||||
properties: {
|
||||
file: {
|
||||
type: "string",
|
||||
|
|
@ -262,14 +320,6 @@ export const openApiSpec = {
|
|||
description:
|
||||
"ไฟล์ .pdf ของ version นี้ (optional — ไม่แนบหรือไฟล์เปล่า = ไม่มี)",
|
||||
},
|
||||
userId: {
|
||||
type: "string",
|
||||
description: "รหัสผู้อัปโหลด (ระบบ stand-alone ไม่มี session)",
|
||||
},
|
||||
userName: {
|
||||
type: "string",
|
||||
description: "ชื่อผู้อัปโหลด (แสดงใน version history)",
|
||||
},
|
||||
clientVersionId: {
|
||||
type: "string",
|
||||
description: "GUID ที่ client generate ก่อนเรียก (เก็บไว้สืบสวน ไม่ใช่ identity)",
|
||||
|
|
@ -283,10 +333,6 @@ export const openApiSpec = {
|
|||
type: "string",
|
||||
description: "commit message ของ version (≤ 2048 ตัวอักษร)",
|
||||
},
|
||||
templateId: {
|
||||
type: "string",
|
||||
description: "รหัส template ที่ใช้สร้างเอกสาร (ถ้ามี)",
|
||||
},
|
||||
fileName: {
|
||||
type: "string",
|
||||
description:
|
||||
|
|
@ -307,6 +353,7 @@ export const openApiSpec = {
|
|||
},
|
||||
},
|
||||
400: { $ref: "#/components/responses/BadRequest" },
|
||||
401: { $ref: "#/components/responses/DocverUnauthorized" },
|
||||
413: {
|
||||
description: "ไฟล์แนบใหญ่เกิน 100MB",
|
||||
content: {
|
||||
|
|
@ -324,6 +371,7 @@ export const openApiSpec = {
|
|||
description:
|
||||
"version ทุกตัวของ docId นี้ พร้อมผู้อัปโหลด (userId/userName), เวลา, commit " +
|
||||
"message, ค่า fields และ hasPdf — ใช้เขียน UI version history ของ add-in",
|
||||
security: [{ DocverBearer: [] }],
|
||||
parameters: [
|
||||
{
|
||||
name: "docId",
|
||||
|
|
@ -346,6 +394,7 @@ export const openApiSpec = {
|
|||
},
|
||||
},
|
||||
400: { $ref: "#/components/responses/BadRequest" },
|
||||
401: { $ref: "#/components/responses/DocverUnauthorized" },
|
||||
404: {
|
||||
description: "ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยอัปโหลด version)",
|
||||
content: {
|
||||
|
|
@ -366,6 +415,7 @@ export const openApiSpec = {
|
|||
"คืน URL อายุสั้น (env DOCVER_LINK_TTL_SECONDS, default 900 วินาที) ให้เปิดใน " +
|
||||
"เบราว์เซอร์/Word ของผู้ใช้ — URL อ้าง endpoint MinIO ตรง ๆ จึงต้องเป็นโฮสต์ที่ " +
|
||||
"ผู้ใช้เข้าถึงได้ · ขอ pdf ใน version ที่ไม่มีสำเนา = 404",
|
||||
security: [{ DocverBearer: [] }],
|
||||
parameters: [
|
||||
{
|
||||
name: "docId",
|
||||
|
|
@ -399,6 +449,7 @@ export const openApiSpec = {
|
|||
},
|
||||
},
|
||||
400: { $ref: "#/components/responses/BadRequest" },
|
||||
401: { $ref: "#/components/responses/DocverUnauthorized" },
|
||||
404: {
|
||||
description:
|
||||
"ไม่พบ version นี้ของเอกสาร หรือขอ format=pdf ใน version ที่ไม่มีสำเนา PDF",
|
||||
|
|
@ -420,7 +471,17 @@ export const openApiSpec = {
|
|||
in: "header",
|
||||
name: "x-api-key",
|
||||
description:
|
||||
"ค่าเดียวกับ env EXTERNAL_API_KEY ของ server — กดปุ่ม Authorize ด้านบนเพื่อใส่ครั้งเดียวแล้วใช้ทุก request",
|
||||
"x-api-key ของ N10 = env EXTERNAL_API_KEY · ของการขอ token กลุ่ม documents = env " +
|
||||
"DOCVER_API_KEY (คนละค่ากับของ N10 — กดปุ่ม Authorize ใส่ครั้งเดียวแล้วใช้ได้ทุก " +
|
||||
"request ของกลุ่มที่ key ตรง)",
|
||||
},
|
||||
DocverBearer: {
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
description:
|
||||
"access_token จาก POST /api/documents/token — แนบเป็น Authorization: Bearer " +
|
||||
"กับ endpoint อื่นของกลุ่ม documents ทุกตัว",
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
|
|
@ -434,6 +495,15 @@ export const openApiSpec = {
|
|||
},
|
||||
},
|
||||
},
|
||||
DocverUnauthorized: {
|
||||
description:
|
||||
"ไม่ได้แนบ Authorization: Bearer หรือ token ไม่ถูกต้อง/หมดอายุ (กลุ่ม documents — ข้อความเดียวกันทุกสาเหตุ)",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
ServerError: {
|
||||
description: "เกิดข้อผิดพลาดของระบบ (เช่นไฟล์ .docx ไม่ถูกต้อง / DB ล้ม)",
|
||||
content: {
|
||||
|
|
@ -451,6 +521,39 @@ export const openApiSpec = {
|
|||
error: { type: "string" },
|
||||
},
|
||||
},
|
||||
TokenRequest: {
|
||||
type: "object",
|
||||
description: "claims ที่จะฝังใน access_token ของกลุ่ม documents — key อื่น = 400",
|
||||
required: ["userid", "username"],
|
||||
properties: {
|
||||
userid: { type: "string", description: "รหัสผู้ใช้ (บังคับ, ≤ 100 ตัวอักษร)" },
|
||||
username: { type: "string", description: "ชื่อผู้ใช้ (บังคับ, ≤ 200 ตัวอักษร)" },
|
||||
fid: {
|
||||
type: "integer",
|
||||
nullable: true,
|
||||
description:
|
||||
"fileId ของ e-Court — เก็บเป็น claim ไว้ก่อน ยังไม่ถูก operation ของ docver อ่าน",
|
||||
},
|
||||
template: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
description:
|
||||
"รหัส template ที่ใช้สร้างเอกสาร — กลายเป็น templateId ตอนอัปโหลด version",
|
||||
},
|
||||
},
|
||||
},
|
||||
TokenResponse: {
|
||||
type: "object",
|
||||
description: "access_token ของกลุ่ม documents (JWT HS256 อายุสั้น)",
|
||||
properties: {
|
||||
access_token: { type: "string", description: "JWT พร้อมใช้แนบเป็น Bearer" },
|
||||
token_type: { type: "string", example: "Bearer" },
|
||||
expires_in: {
|
||||
type: "integer",
|
||||
description: "อายุวินาที (env DOCVER_TOKEN_TTL_SECONDS, default 3600)",
|
||||
},
|
||||
},
|
||||
},
|
||||
ExtractResponse: {
|
||||
type: "object",
|
||||
properties: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue